pi-extmgr 0.1.20 → 0.1.22
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/commands/registry.ts +12 -8
- package/src/constants.ts +48 -17
- package/src/packages/extensions.ts +252 -31
- package/src/packages/install.ts +17 -28
- package/src/packages/management.ts +2 -58
- package/src/types/index.ts +7 -6
- package/src/ui/footer.ts +67 -0
- package/src/ui/help.ts +1 -0
- package/src/ui/package-config.ts +361 -0
- package/src/ui/unified.ts +87 -163
- package/src/utils/auto-update.ts +10 -18
- package/src/utils/format.ts +31 -16
- package/src/utils/history.ts +22 -16
- package/src/utils/settings.ts +15 -23
- package/src/utils/timer.ts +36 -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.22",
|
|
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/commands/registry.ts
CHANGED
|
@@ -132,16 +132,20 @@ const COMMAND_DEFINITIONS: Record<CommandId, CommandDefinition> = {
|
|
|
132
132
|
},
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
function buildCommandAliasMap(
|
|
136
|
+
definitions: Record<CommandId, CommandDefinition>
|
|
137
|
+
): Record<string, CommandId> {
|
|
138
|
+
const map: Record<string, CommandId> = {};
|
|
139
|
+
for (const def of Object.values(definitions)) {
|
|
140
|
+
map[def.id] = def.id;
|
|
138
141
|
for (const alias of def.aliases ?? []) {
|
|
139
|
-
|
|
142
|
+
map[alias] = def.id;
|
|
140
143
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
}
|
|
145
|
+
return map;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const COMMAND_ALIAS_TO_ID: Record<string, CommandId> = buildCommandAliasMap(COMMAND_DEFINITIONS);
|
|
145
149
|
|
|
146
150
|
export function resolveCommand(tokens: string[]): { id: CommandId; args: string[] } | undefined {
|
|
147
151
|
if (tokens.length === 0) {
|
package/src/constants.ts
CHANGED
|
@@ -1,48 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Constants for pi-extmgr
|
|
3
|
+
*
|
|
4
|
+
* All time values are in milliseconds unless otherwise noted.
|
|
3
5
|
*/
|
|
4
6
|
|
|
7
|
+
/** File extension suffix used to disable extensions (e.g., `extension.ts.disabled`) */
|
|
5
8
|
export const DISABLED_SUFFIX = ".disabled";
|
|
9
|
+
|
|
10
|
+
/** Number of items to display per page in paginated views */
|
|
6
11
|
export const PAGE_SIZE = 20;
|
|
7
|
-
|
|
12
|
+
|
|
13
|
+
/** Default cache time-to-live: 5 minutes */
|
|
14
|
+
export const CACHE_TTL = 5 * 60 * 1000;
|
|
8
15
|
|
|
9
16
|
/**
|
|
10
17
|
* Timeout values for various operations (in milliseconds)
|
|
18
|
+
*
|
|
19
|
+
* These values balance user experience with reliability.
|
|
20
|
+
* Network operations get shorter timeouts, file operations get longer ones.
|
|
11
21
|
*/
|
|
12
22
|
export const TIMEOUTS = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
/** npm registry search timeout */
|
|
24
|
+
npmSearch: 20_000,
|
|
25
|
+
/** npm package metadata lookup timeout */
|
|
26
|
+
npmView: 10_000,
|
|
27
|
+
/** Full package installation timeout (3 minutes) */
|
|
28
|
+
packageInstall: 180_000,
|
|
29
|
+
/** Package update timeout (2 minutes) */
|
|
30
|
+
packageUpdate: 120_000,
|
|
31
|
+
/** Bulk package update timeout (5 minutes) */
|
|
32
|
+
packageUpdateAll: 300_000,
|
|
33
|
+
/** Package removal timeout (1 minute) */
|
|
34
|
+
packageRemove: 60_000,
|
|
35
|
+
/** Package listing timeout */
|
|
36
|
+
listPackages: 10_000,
|
|
37
|
+
/** Package metadata fetch timeout */
|
|
38
|
+
fetchPackageInfo: 30_000,
|
|
39
|
+
/** Package extraction timeout */
|
|
40
|
+
extractPackage: 30_000,
|
|
41
|
+
/** Weekly download stats timeout */
|
|
42
|
+
weeklyDownloads: 5_000,
|
|
22
43
|
} as const;
|
|
23
44
|
|
|
24
45
|
export type TimeoutKey = keyof typeof TIMEOUTS;
|
|
25
46
|
|
|
26
47
|
/**
|
|
27
|
-
* Cache limits (in milliseconds or count)
|
|
48
|
+
* Cache limits and TTL values (in milliseconds or count)
|
|
28
49
|
*/
|
|
29
50
|
export const CACHE_LIMITS = {
|
|
51
|
+
/** Maximum number of package info entries to cache */
|
|
30
52
|
packageInfoMaxSize: 100,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
/** Metadata cache TTL: 24 hours */
|
|
54
|
+
metadataTTL: 24 * 60 * 60 * 1000,
|
|
55
|
+
/** Search results cache TTL: 15 minutes */
|
|
56
|
+
searchTTL: 15 * 60 * 1000,
|
|
57
|
+
/** Package info cache TTL: 6 hours */
|
|
58
|
+
packageInfoTTL: 6 * 60 * 60 * 1000,
|
|
34
59
|
} as const;
|
|
35
60
|
|
|
36
61
|
export type CacheLimitKey = keyof typeof CACHE_LIMITS;
|
|
37
62
|
|
|
38
63
|
/**
|
|
39
64
|
* UI Constants
|
|
65
|
+
*
|
|
66
|
+
* These values control the user interface behavior and appearance.
|
|
40
67
|
*/
|
|
41
68
|
export const UI = {
|
|
69
|
+
/** Maximum height for scrollable lists in terminal rows */
|
|
42
70
|
maxListHeight: 16,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
/** Minimum number of items before enabling search functionality */
|
|
72
|
+
searchThreshold: 8,
|
|
73
|
+
/** Default confirmation dialog timeout: 30 seconds */
|
|
74
|
+
confirmTimeout: 30_000,
|
|
75
|
+
/** Extended confirmation timeout for destructive operations: 1 minute */
|
|
76
|
+
longConfirmTimeout: 60_000,
|
|
46
77
|
} as const;
|
|
47
78
|
|
|
48
79
|
export type UIKey = keyof typeof UI;
|
|
@@ -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";
|
|
@@ -48,18 +49,20 @@ function getExtensionInstallDir(ctx: ExtensionCommandContext, scope: InstallScop
|
|
|
48
49
|
return join(homedir(), ".pi", "agent", "extensions");
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
interface GithubUrlInfo {
|
|
53
|
+
owner: string;
|
|
54
|
+
repo: string;
|
|
55
|
+
branch: string;
|
|
56
|
+
filePath: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
/**
|
|
52
60
|
* Safely extracts regex match groups with validation
|
|
53
61
|
*/
|
|
54
|
-
function safeExtractGithubMatch(
|
|
55
|
-
match: RegExpMatchArray | null
|
|
56
|
-
): { owner: string; repo: string; branch: string; filePath: string } | undefined {
|
|
62
|
+
function safeExtractGithubMatch(match: RegExpMatchArray | null): GithubUrlInfo | undefined {
|
|
57
63
|
if (!match) return undefined;
|
|
58
64
|
|
|
59
|
-
const owner = match
|
|
60
|
-
const repo = match[2];
|
|
61
|
-
const branch = match[3];
|
|
62
|
-
const filePath = match[4];
|
|
65
|
+
const [, owner, repo, branch, filePath] = match;
|
|
63
66
|
|
|
64
67
|
if (!owner || !repo || !branch || !filePath) {
|
|
65
68
|
return undefined;
|
|
@@ -68,29 +71,15 @@ function safeExtractGithubMatch(
|
|
|
68
71
|
return { owner, repo, branch, filePath };
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
function normalizeRelativePath(value: string): string {
|
|
72
|
-
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
74
|
async function hasStandaloneEntrypoint(packageRoot: string): Promise<boolean> {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (Array.isArray(declared) && declared.length > 0) {
|
|
83
|
-
for (const entry of declared) {
|
|
84
|
-
if (typeof entry !== "string" || !entry.trim()) continue;
|
|
85
|
-
const candidate = join(packageRoot, normalizeRelativePath(entry));
|
|
86
|
-
if (await fileExists(candidate)) {
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
75
|
+
const declared = await resolveManifestExtensionEntrypoints(packageRoot);
|
|
76
|
+
if (declared !== undefined) {
|
|
77
|
+
for (const path of declared) {
|
|
78
|
+
if (await fileExists(join(packageRoot, path))) {
|
|
79
|
+
return true;
|
|
89
80
|
}
|
|
90
|
-
return false;
|
|
91
81
|
}
|
|
92
|
-
|
|
93
|
-
// Ignore invalid/missing manifest and fall back to conventional entrypoints.
|
|
82
|
+
return false;
|
|
94
83
|
}
|
|
95
84
|
|
|
96
85
|
return (
|