pi-extmgr 0.1.21 → 0.1.23
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 +25 -17
- package/package.json +2 -2
- package/src/index.ts +6 -1
- package/src/packages/discovery.ts +17 -8
- package/src/packages/extensions.ts +252 -31
- package/src/packages/install.ts +10 -23
- package/src/packages/management.ts +60 -70
- package/src/types/index.ts +7 -6
- package/src/ui/footer.ts +3 -6
- package/src/ui/help.ts +1 -0
- package/src/ui/package-config.ts +361 -0
- package/src/ui/remote.ts +2 -2
- package/src/ui/unified.ts +88 -111
- package/src/utils/auto-update.ts +2 -2
- package/src/utils/command.ts +77 -1
- package/src/utils/format.ts +23 -3
- package/src/utils/npm-exec.ts +47 -0
- package/src/utils/package-source.ts +12 -0
package/README.md
CHANGED
|
@@ -17,11 +17,16 @@ pi install npm:pi-extmgr
|
|
|
17
17
|
|
|
18
18
|
Then reload Pi.
|
|
19
19
|
|
|
20
|
+
Requires Node.js `>=22.5.0`.
|
|
21
|
+
|
|
20
22
|
## Features
|
|
21
23
|
|
|
22
24
|
- **Unified manager UI**
|
|
23
25
|
- Local extensions (`~/.pi/agent/extensions`, `.pi/extensions`) and installed packages in one list
|
|
24
26
|
- Scope indicators (global/project), status indicators, update badges
|
|
27
|
+
- **Package extension configuration panel**
|
|
28
|
+
- Configure individual extension entrypoints inside an installed package (`c` on package row)
|
|
29
|
+
- Persists to package filters in `settings.json` (no manual JSON editing)
|
|
25
30
|
- **Safe staged local extension toggles**
|
|
26
31
|
- Toggle with `Space/Enter`, apply with `S`
|
|
27
32
|
- Unsaved-change guard when leaving (save/discard/stay)
|
|
@@ -54,22 +59,23 @@ Open the manager:
|
|
|
54
59
|
|
|
55
60
|
### In the manager
|
|
56
61
|
|
|
57
|
-
| Key | Action
|
|
58
|
-
| ------------- |
|
|
59
|
-
| `↑↓` | Navigate
|
|
60
|
-
| `Space/Enter` | Toggle local
|
|
61
|
-
| `S` | Save changes
|
|
62
|
-
| `Enter` / `A` | Actions on selected package (update/remove
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
|
|
|
72
|
-
| `
|
|
62
|
+
| Key | Action |
|
|
63
|
+
| ------------- | ----------------------------------------------------- |
|
|
64
|
+
| `↑↓` | Navigate |
|
|
65
|
+
| `Space/Enter` | Toggle local extension on/off |
|
|
66
|
+
| `S` | Save local extension changes |
|
|
67
|
+
| `Enter` / `A` | Actions on selected package (configure/update/remove) |
|
|
68
|
+
| `c` | Configure selected package extensions |
|
|
69
|
+
| `u` | Update selected package directly |
|
|
70
|
+
| `X` | Remove selected item (package/local extension) |
|
|
71
|
+
| `i` | Quick install by source |
|
|
72
|
+
| `f` | Quick search |
|
|
73
|
+
| `U` | Update all packages |
|
|
74
|
+
| `t` | Auto-update wizard |
|
|
75
|
+
| `P` / `M` | Quick actions palette |
|
|
76
|
+
| `R` | Browse remote packages |
|
|
77
|
+
| `?` / `H` | Help |
|
|
78
|
+
| `Esc` | Exit |
|
|
73
79
|
|
|
74
80
|
### Commands
|
|
75
81
|
|
|
@@ -134,7 +140,9 @@ Examples:
|
|
|
134
140
|
|
|
135
141
|
## Tips
|
|
136
142
|
|
|
137
|
-
- **Staged changes**: Toggle extensions on/off, then press `S` to apply all at once.
|
|
143
|
+
- **Staged local changes**: Toggle local extensions on/off, then press `S` to apply all at once.
|
|
144
|
+
- **Package extension config**: Select a package and press `c` (or Enter/A → Configure) to enable/disable individual package entrypoints.
|
|
145
|
+
- After saving package extension config, restart pi to fully apply changes.
|
|
138
146
|
- **Two install modes**:
|
|
139
147
|
- **Managed** (npm): Auto-updates with `pi update`, stored in pi's package cache
|
|
140
148
|
- **Local** (standalone): Copies to `~/.pi/agent/extensions/{package}/`, supports multi-file extensions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extmgr",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Enhanced UX for managing local Pi extensions and community packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"author": "ayagmar",
|
|
58
58
|
"license": "MIT",
|
|
59
59
|
"engines": {
|
|
60
|
-
"node": ">=22"
|
|
60
|
+
"node": ">=22.5.0"
|
|
61
61
|
},
|
|
62
62
|
"repository": {
|
|
63
63
|
"type": "git",
|
package/src/index.ts
CHANGED
|
@@ -70,12 +70,17 @@ export default function extensionsManager(pi: ExtensionAPI) {
|
|
|
70
70
|
// Restore persisted auto-update config into session entries so sync lookups are valid.
|
|
71
71
|
await hydrateAutoUpdateConfig(pi, ctx);
|
|
72
72
|
|
|
73
|
-
if (!ctx.hasUI)
|
|
73
|
+
if (!ctx.hasUI) {
|
|
74
|
+
stopAutoUpdateTimer();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
74
77
|
|
|
75
78
|
const config = getAutoUpdateConfig(ctx);
|
|
76
79
|
if (config.enabled && config.intervalMs > 0) {
|
|
77
80
|
const getCtx: ContextProvider = () => ctx;
|
|
78
81
|
startAutoUpdateTimer(pi, getCtx, createAutoUpdateNotificationHandler(ctx));
|
|
82
|
+
} else {
|
|
83
|
+
stopAutoUpdateTimer();
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
setImmediate(() => {
|
|
@@ -12,7 +12,12 @@ import type { InstalledPackage, NpmPackage, SearchCache } from "../types/index.j
|
|
|
12
12
|
import { CACHE_TTL, TIMEOUTS } from "../constants.js";
|
|
13
13
|
import { readSummary } from "../utils/fs.js";
|
|
14
14
|
import { parseNpmSource } from "../utils/format.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
getPackageSourceKind,
|
|
17
|
+
normalizeLocalSourceIdentity,
|
|
18
|
+
splitGitRepoAndRef,
|
|
19
|
+
} from "../utils/package-source.js";
|
|
20
|
+
import { execNpm } from "../utils/npm-exec.js";
|
|
16
21
|
|
|
17
22
|
let searchCache: SearchCache | null = null;
|
|
18
23
|
|
|
@@ -67,9 +72,8 @@ export async function searchNpmPackages(
|
|
|
67
72
|
ctx.ui.notify(`Searching npm for "${query}"...`, "info");
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
const res = await pi
|
|
75
|
+
const res = await execNpm(pi, ["search", "--json", `--searchlimit=${searchLimit}`, query], ctx, {
|
|
71
76
|
timeout: TIMEOUTS.npmSearch,
|
|
72
|
-
cwd: ctx.cwd,
|
|
73
77
|
});
|
|
74
78
|
|
|
75
79
|
if (res.code !== 0) {
|
|
@@ -118,7 +122,14 @@ function sanitizeListSourceSuffix(source: string): string {
|
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
function normalizeSourceIdentity(source: string): string {
|
|
121
|
-
|
|
125
|
+
const sanitized = sanitizeListSourceSuffix(source);
|
|
126
|
+
const kind = getPackageSourceKind(sanitized);
|
|
127
|
+
|
|
128
|
+
if (kind === "local") {
|
|
129
|
+
return normalizeLocalSourceIdentity(sanitized);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return sanitized.replace(/\\/g, "/").toLowerCase();
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
function isScopeHeader(lowerTrimmed: string, scope: "global" | "project"): boolean {
|
|
@@ -375,9 +386,8 @@ async function fetchPackageSize(
|
|
|
375
386
|
|
|
376
387
|
try {
|
|
377
388
|
// Try to get unpacked size from npm view
|
|
378
|
-
const res = await pi
|
|
389
|
+
const res = await execNpm(pi, ["view", pkgName, "dist.unpackedSize", "--json"], ctx, {
|
|
379
390
|
timeout: TIMEOUTS.npmView,
|
|
380
|
-
cwd: ctx.cwd,
|
|
381
391
|
});
|
|
382
392
|
if (res.code === 0) {
|
|
383
393
|
try {
|
|
@@ -441,9 +451,8 @@ async function addPackageMetadata(
|
|
|
441
451
|
if (cached?.description) {
|
|
442
452
|
pkg.description = cached.description;
|
|
443
453
|
} else {
|
|
444
|
-
const res = await pi
|
|
454
|
+
const res = await execNpm(pi, ["view", pkgName, "description", "--json"], ctx, {
|
|
445
455
|
timeout: TIMEOUTS.npmView,
|
|
446
|
-
cwd: ctx.cwd,
|
|
447
456
|
});
|
|
448
457
|
if (res.code === 0) {
|
|
449
458
|
try {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile, rename, rm } from "node:fs/promises";
|
|
2
|
-
import {
|
|
1
|
+
import { mkdir, readFile, writeFile, rename, rm, readdir } from "node:fs/promises";
|
|
2
|
+
import type { Dirent } from "node:fs";
|
|
3
|
+
import { dirname, join, matchesGlob, relative, resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { homedir } from "node:os";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
5
8
|
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
6
9
|
import type { InstalledPackage, PackageExtensionEntry, Scope, State } from "../types/index.js";
|
|
10
|
+
import { parseNpmSource } from "../utils/format.js";
|
|
7
11
|
import { fileExists, readSummary } from "../utils/fs.js";
|
|
8
12
|
|
|
9
13
|
interface PackageSettingsObject {
|
|
@@ -15,6 +19,9 @@ interface SettingsFile {
|
|
|
15
19
|
packages?: (string | PackageSettingsObject)[];
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
const execFileAsync = promisify(execFile);
|
|
23
|
+
let globalNpmRootCache: string | null | undefined;
|
|
24
|
+
|
|
18
25
|
function normalizeRelativePath(value: string): string {
|
|
19
26
|
const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
20
27
|
return normalized;
|
|
@@ -37,11 +44,69 @@ function normalizePackageRootCandidate(candidate: string): string {
|
|
|
37
44
|
return resolved;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
function
|
|
47
|
+
async function getGlobalNpmRoot(): Promise<string | undefined> {
|
|
48
|
+
if (globalNpmRootCache !== undefined) {
|
|
49
|
+
return globalNpmRootCache ?? undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const { stdout } = await execFileAsync("npm", ["root", "-g"], {
|
|
54
|
+
timeout: 2_000,
|
|
55
|
+
windowsHide: true,
|
|
56
|
+
});
|
|
57
|
+
const root = stdout.trim();
|
|
58
|
+
globalNpmRootCache = root || null;
|
|
59
|
+
} catch {
|
|
60
|
+
globalNpmRootCache = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return globalNpmRootCache ?? undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function resolveNpmPackageRoot(
|
|
67
|
+
pkg: InstalledPackage,
|
|
68
|
+
cwd: string
|
|
69
|
+
): Promise<string | undefined> {
|
|
70
|
+
const parsed = parseNpmSource(pkg.source);
|
|
71
|
+
if (!parsed?.name) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const packageName = parsed.name;
|
|
76
|
+
const projectCandidates = [
|
|
77
|
+
join(cwd, ".pi", "npm", "node_modules", packageName),
|
|
78
|
+
join(cwd, "node_modules", packageName),
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const packageDir = process.env.PI_PACKAGE_DIR || join(homedir(), ".pi", "agent");
|
|
82
|
+
const globalCandidates = [join(packageDir, "npm", "node_modules", packageName)];
|
|
83
|
+
|
|
84
|
+
const npmGlobalRoot = await getGlobalNpmRoot();
|
|
85
|
+
if (npmGlobalRoot) {
|
|
86
|
+
globalCandidates.unshift(join(npmGlobalRoot, packageName));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const candidates =
|
|
90
|
+
pkg.scope === "project" ? projectCandidates : [...globalCandidates, ...projectCandidates];
|
|
91
|
+
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
if (await fileExists(join(candidate, "package.json"))) {
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function toPackageRoot(pkg: InstalledPackage, cwd: string): Promise<string | undefined> {
|
|
41
102
|
if (pkg.resolvedPath) {
|
|
42
103
|
return normalizePackageRootCandidate(pkg.resolvedPath);
|
|
43
104
|
}
|
|
44
105
|
|
|
106
|
+
if (pkg.source.startsWith("npm:")) {
|
|
107
|
+
return resolveNpmPackageRoot(pkg, cwd);
|
|
108
|
+
}
|
|
109
|
+
|
|
45
110
|
if (pkg.source.startsWith("file://")) {
|
|
46
111
|
try {
|
|
47
112
|
return normalizePackageRootCandidate(fileURLToPath(pkg.source));
|
|
@@ -141,23 +206,79 @@ async function writeSettingsFile(path: string, settings: SettingsFile): Promise<
|
|
|
141
206
|
}
|
|
142
207
|
}
|
|
143
208
|
|
|
209
|
+
function safeMatchesGlob(targetPath: string, pattern: string): boolean {
|
|
210
|
+
try {
|
|
211
|
+
return matchesGlob(targetPath, pattern);
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function matchesFilterPattern(targetPath: string, pattern: string): boolean {
|
|
218
|
+
const normalizedPattern = normalizeRelativePath(pattern.trim());
|
|
219
|
+
if (!normalizedPattern) return false;
|
|
220
|
+
if (targetPath === normalizedPattern) return true;
|
|
221
|
+
|
|
222
|
+
return safeMatchesGlob(targetPath, normalizedPattern);
|
|
223
|
+
}
|
|
224
|
+
|
|
144
225
|
function getPackageFilterState(filters: string[] | undefined, extensionPath: string): State {
|
|
145
|
-
|
|
226
|
+
// Omitted key => all enabled (pi default).
|
|
227
|
+
if (filters === undefined) {
|
|
146
228
|
return "enabled";
|
|
147
229
|
}
|
|
148
230
|
|
|
231
|
+
// Explicit empty array => load none.
|
|
232
|
+
if (filters.length === 0) {
|
|
233
|
+
return "disabled";
|
|
234
|
+
}
|
|
235
|
+
|
|
149
236
|
const normalizedTarget = normalizeRelativePath(extensionPath);
|
|
150
|
-
|
|
237
|
+
const includePatterns: string[] = [];
|
|
238
|
+
const excludePatterns: string[] = [];
|
|
239
|
+
let markerOverride: State | undefined;
|
|
240
|
+
|
|
241
|
+
for (const rawToken of filters) {
|
|
242
|
+
const token = rawToken.trim();
|
|
243
|
+
if (!token) continue;
|
|
244
|
+
|
|
245
|
+
const prefix = token[0];
|
|
246
|
+
|
|
247
|
+
if (prefix === "+" || prefix === "-") {
|
|
248
|
+
const markerPath = normalizeRelativePath(token.slice(1));
|
|
249
|
+
if (markerPath === normalizedTarget) {
|
|
250
|
+
markerOverride = prefix === "+" ? "enabled" : "disabled";
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (prefix === "!") {
|
|
256
|
+
const pattern = normalizeRelativePath(token.slice(1));
|
|
257
|
+
if (pattern) {
|
|
258
|
+
excludePatterns.push(pattern);
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const include = normalizeRelativePath(token);
|
|
264
|
+
if (include) {
|
|
265
|
+
includePatterns.push(include);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let enabled =
|
|
270
|
+
includePatterns.length === 0 ||
|
|
271
|
+
includePatterns.some((p) => matchesFilterPattern(normalizedTarget, p));
|
|
272
|
+
|
|
273
|
+
if (enabled && excludePatterns.some((p) => matchesFilterPattern(normalizedTarget, p))) {
|
|
274
|
+
enabled = false;
|
|
275
|
+
}
|
|
151
276
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const sign = token[0];
|
|
155
|
-
const path = normalizeRelativePath(token.slice(1));
|
|
156
|
-
if (path !== normalizedTarget) continue;
|
|
157
|
-
state = sign === "+" ? "enabled" : "disabled";
|
|
277
|
+
if (markerOverride !== undefined) {
|
|
278
|
+
enabled = markerOverride === "enabled";
|
|
158
279
|
}
|
|
159
280
|
|
|
160
|
-
return
|
|
281
|
+
return enabled ? "enabled" : "disabled";
|
|
161
282
|
}
|
|
162
283
|
|
|
163
284
|
async function getPackageExtensionState(
|
|
@@ -185,26 +306,131 @@ async function getPackageExtensionState(
|
|
|
185
306
|
return getPackageFilterState(entry.extensions, extensionPath);
|
|
186
307
|
}
|
|
187
308
|
|
|
188
|
-
|
|
309
|
+
function isExtensionEntrypointPath(path: string): boolean {
|
|
310
|
+
return /\.(ts|js)$/i.test(path);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function hasGlobMagic(path: string): boolean {
|
|
314
|
+
return /[*?{}[\]]/.test(path);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isSafeRelativePath(path: string): boolean {
|
|
318
|
+
return path !== "" && path !== ".." && !path.startsWith("../") && !path.includes("/../");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function selectDirectoryFiles(allFiles: string[], directoryPath: string): string[] {
|
|
322
|
+
const prefix = `${directoryPath}/`;
|
|
323
|
+
return allFiles.filter((file) => file.startsWith(prefix));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function applySelection(selected: Set<string>, files: Iterable<string>, exclude: boolean): void {
|
|
327
|
+
for (const file of files) {
|
|
328
|
+
if (exclude) {
|
|
329
|
+
selected.delete(file);
|
|
330
|
+
} else {
|
|
331
|
+
selected.add(file);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function collectExtensionFilesFromDir(
|
|
337
|
+
packageRoot: string,
|
|
338
|
+
startDir: string
|
|
339
|
+
): Promise<string[]> {
|
|
340
|
+
const collected: string[] = [];
|
|
341
|
+
|
|
342
|
+
let entries: Dirent[];
|
|
343
|
+
try {
|
|
344
|
+
entries = await readdir(startDir, { withFileTypes: true });
|
|
345
|
+
} catch {
|
|
346
|
+
return collected;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
const absolutePath = join(startDir, entry.name);
|
|
351
|
+
|
|
352
|
+
if (entry.isDirectory()) {
|
|
353
|
+
collected.push(...(await collectExtensionFilesFromDir(packageRoot, absolutePath)));
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!entry.isFile()) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const relativePath = normalizeRelativePath(relative(packageRoot, absolutePath));
|
|
362
|
+
if (isExtensionEntrypointPath(relativePath)) {
|
|
363
|
+
collected.push(relativePath);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return collected;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function resolveManifestExtensionEntries(
|
|
371
|
+
packageRoot: string,
|
|
372
|
+
entries: string[]
|
|
373
|
+
): Promise<string[]> {
|
|
374
|
+
const selected = new Set<string>();
|
|
375
|
+
const allFiles = await collectExtensionFilesFromDir(packageRoot, packageRoot);
|
|
376
|
+
|
|
377
|
+
for (const rawToken of entries) {
|
|
378
|
+
const token = rawToken.trim();
|
|
379
|
+
if (!token) continue;
|
|
380
|
+
|
|
381
|
+
const exclude = token.startsWith("!");
|
|
382
|
+
const normalizedToken = normalizeRelativePath(exclude ? token.slice(1) : token);
|
|
383
|
+
const pattern = normalizedToken.replace(/[\\/]+$/g, "");
|
|
384
|
+
if (!isSafeRelativePath(pattern)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (hasGlobMagic(pattern)) {
|
|
389
|
+
const matchedFiles = allFiles.filter((file) => matchesFilterPattern(file, pattern));
|
|
390
|
+
applySelection(selected, matchedFiles, exclude);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const directoryFiles = selectDirectoryFiles(allFiles, pattern);
|
|
395
|
+
if (directoryFiles.length > 0) {
|
|
396
|
+
applySelection(selected, directoryFiles, exclude);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (isExtensionEntrypointPath(pattern)) {
|
|
401
|
+
applySelection(selected, [pattern], exclude);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return Array.from(selected).sort((a, b) => a.localeCompare(b));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export async function resolveManifestExtensionEntrypoints(
|
|
409
|
+
packageRoot: string
|
|
410
|
+
): Promise<string[] | undefined> {
|
|
189
411
|
const packageJsonPath = join(packageRoot, "package.json");
|
|
190
|
-
let manifestExtensions: string[] | undefined;
|
|
191
412
|
|
|
413
|
+
let parsed: { pi?: { extensions?: unknown } };
|
|
192
414
|
try {
|
|
193
415
|
const raw = await readFile(packageJsonPath, "utf8");
|
|
194
|
-
|
|
195
|
-
const ext = parsed.pi?.extensions;
|
|
196
|
-
if (Array.isArray(ext)) {
|
|
197
|
-
const entries = ext.filter((value): value is string => typeof value === "string");
|
|
198
|
-
if (entries.length > 0) {
|
|
199
|
-
manifestExtensions = entries;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
416
|
+
parsed = JSON.parse(raw) as { pi?: { extensions?: unknown } };
|
|
202
417
|
} catch {
|
|
203
|
-
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const extensions = parsed.pi?.extensions;
|
|
422
|
+
if (!Array.isArray(extensions)) {
|
|
423
|
+
return undefined;
|
|
204
424
|
}
|
|
205
425
|
|
|
206
|
-
|
|
207
|
-
|
|
426
|
+
const entries = extensions.filter((value): value is string => typeof value === "string");
|
|
427
|
+
return resolveManifestExtensionEntries(packageRoot, entries);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function discoverEntrypoints(packageRoot: string): Promise<string[]> {
|
|
431
|
+
const manifestEntrypoints = await resolveManifestExtensionEntrypoints(packageRoot);
|
|
432
|
+
if (manifestEntrypoints !== undefined) {
|
|
433
|
+
return manifestEntrypoints;
|
|
208
434
|
}
|
|
209
435
|
|
|
210
436
|
const indexTs = join(packageRoot, "index.ts");
|
|
@@ -227,7 +453,7 @@ export async function discoverPackageExtensions(
|
|
|
227
453
|
const entries: PackageExtensionEntry[] = [];
|
|
228
454
|
|
|
229
455
|
for (const pkg of packages) {
|
|
230
|
-
const packageRoot = toPackageRoot(pkg, cwd);
|
|
456
|
+
const packageRoot = await toPackageRoot(pkg, cwd);
|
|
231
457
|
if (!packageRoot) continue;
|
|
232
458
|
|
|
233
459
|
const extensionPaths = await discoverEntrypoints(packageRoot);
|
|
@@ -319,8 +545,3 @@ export async function setPackageExtensionState(
|
|
|
319
545
|
};
|
|
320
546
|
}
|
|
321
547
|
}
|
|
322
|
-
|
|
323
|
-
export function toProjectRelativePath(path: string, cwd: string): string {
|
|
324
|
-
const rel = relative(cwd, path);
|
|
325
|
-
return rel.startsWith("..") ? path : normalizeRelativePath(rel);
|
|
326
|
-
}
|
package/src/packages/install.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package installation logic
|
|
3
3
|
*/
|
|
4
|
-
import { mkdir, rm, writeFile, cp
|
|
4
|
+
import { mkdir, rm, writeFile, cp } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
8
8
|
import { normalizePackageSource } from "../utils/format.js";
|
|
9
9
|
import { fileExists } from "../utils/fs.js";
|
|
10
10
|
import { clearSearchCache, isSourceInstalled } from "./discovery.js";
|
|
11
|
+
import { resolveManifestExtensionEntrypoints } from "./extensions.js";
|
|
11
12
|
import { waitForCondition } from "../utils/retry.js";
|
|
12
13
|
import { logPackageInstall } from "../utils/history.js";
|
|
13
14
|
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
@@ -15,6 +16,7 @@ import { notify, error as notifyError, success } from "../utils/notify.js";
|
|
|
15
16
|
import { confirmAction, confirmReload, showProgress } from "../utils/ui-helpers.js";
|
|
16
17
|
import { tryOperation } from "../utils/mode.js";
|
|
17
18
|
import { updateExtmgrStatus } from "../utils/status.js";
|
|
19
|
+
import { execNpm } from "../utils/npm-exec.js";
|
|
18
20
|
import { TIMEOUTS } from "../constants.js";
|
|
19
21
|
|
|
20
22
|
export type InstallScope = "global" | "project";
|
|
@@ -70,29 +72,15 @@ function safeExtractGithubMatch(match: RegExpMatchArray | null): GithubUrlInfo |
|
|
|
70
72
|
return { owner, repo, branch, filePath };
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
function normalizeRelativePath(value: string): string {
|
|
74
|
-
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
75
|
async function hasStandaloneEntrypoint(packageRoot: string): Promise<boolean> {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (Array.isArray(declared) && declared.length > 0) {
|
|
85
|
-
for (const entry of declared) {
|
|
86
|
-
if (typeof entry !== "string" || !entry.trim()) continue;
|
|
87
|
-
const candidate = join(packageRoot, normalizeRelativePath(entry));
|
|
88
|
-
if (await fileExists(candidate)) {
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
76
|
+
const declared = await resolveManifestExtensionEntrypoints(packageRoot);
|
|
77
|
+
if (declared !== undefined) {
|
|
78
|
+
for (const path of declared) {
|
|
79
|
+
if (await fileExists(join(packageRoot, path))) {
|
|
80
|
+
return true;
|
|
91
81
|
}
|
|
92
|
-
return false;
|
|
93
82
|
}
|
|
94
|
-
|
|
95
|
-
// Ignore invalid/missing manifest and fall back to conventional entrypoints.
|
|
83
|
+
return false;
|
|
96
84
|
}
|
|
97
85
|
|
|
98
86
|
return (
|
|
@@ -304,9 +292,8 @@ export async function installPackageLocally(
|
|
|
304
292
|
await mkdir(extensionDir, { recursive: true });
|
|
305
293
|
showProgress(ctx, "Fetching", packageName);
|
|
306
294
|
|
|
307
|
-
const viewRes = await pi
|
|
295
|
+
const viewRes = await execNpm(pi, ["view", packageName, "--json"], ctx, {
|
|
308
296
|
timeout: TIMEOUTS.fetchPackageInfo,
|
|
309
|
-
cwd: ctx.cwd,
|
|
310
297
|
});
|
|
311
298
|
|
|
312
299
|
if (viewRes.code !== 0) {
|