pi-extmgr 0.1.28 → 0.2.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 +18 -8
- package/package.json +10 -2
- package/src/commands/auto-update.ts +1 -1
- package/src/commands/history.ts +2 -31
- package/src/constants.ts +0 -8
- package/src/extensions/discovery.ts +121 -39
- package/src/packages/discovery.ts +34 -0
- package/src/packages/extensions.ts +55 -98
- package/src/packages/install.ts +85 -56
- package/src/packages/management.ts +25 -38
- package/src/types/index.ts +4 -2
- package/src/ui/footer.ts +49 -29
- package/src/ui/help.ts +15 -11
- package/src/ui/remote.ts +704 -112
- package/src/ui/unified.ts +922 -311
- package/src/utils/auto-update.ts +34 -29
- package/src/utils/cache.ts +205 -34
- package/src/utils/duration.ts +132 -0
- package/src/utils/format.ts +0 -30
- package/src/utils/fs.ts +8 -4
- package/src/utils/history.ts +43 -7
- package/src/utils/mode.ts +1 -1
- package/src/utils/notify.ts +0 -14
- package/src/utils/package-source.ts +2 -5
- package/src/utils/path-identity.ts +7 -0
- package/src/utils/relative-path-selection.ts +100 -0
- package/src/utils/settings.ts +4 -63
- package/src/utils/retry.ts +0 -49
package/README.md
CHANGED
|
@@ -18,25 +18,29 @@ pi install npm:pi-extmgr
|
|
|
18
18
|
|
|
19
19
|
If Pi is already running, use `/reload`.
|
|
20
20
|
|
|
21
|
-
Requires Node.js `>=22`.
|
|
21
|
+
Requires Node.js `>=22.20.0`.
|
|
22
22
|
|
|
23
23
|
## Features
|
|
24
24
|
|
|
25
25
|
- **Unified manager UI**
|
|
26
26
|
- Local extensions (`~/.pi/agent/extensions`, `.pi/extensions`) and installed packages in one list
|
|
27
|
-
-
|
|
27
|
+
- Grouped sections for local extensions vs installed packages
|
|
28
|
+
- Compact rows with selected-item details below the list, so large extension sets stay scannable
|
|
29
|
+
- Built-in search and filter shortcuts for large extension sets
|
|
30
|
+
- Scope indicators (global/project), status indicators, update badges, and package sizes when known
|
|
28
31
|
- **Package extension configuration panel**
|
|
29
32
|
- Configure individual extension entrypoints inside an installed package (`c` on package row)
|
|
30
33
|
- Works with manifest-declared entrypoints and conventional `extensions/` package layouts
|
|
31
34
|
- Persists to package filters in `settings.json` (no manual JSON editing)
|
|
32
35
|
- **Safe staged local extension toggles**
|
|
33
|
-
- Toggle with `Space
|
|
36
|
+
- Toggle with `Space`, apply with `S`
|
|
34
37
|
- Unsaved-change guard when leaving (save/discard/stay)
|
|
35
38
|
- **Package management**
|
|
36
39
|
- Install, update, remove from UI and command line
|
|
37
40
|
- Quick actions (`A`, `u`, `X`) and bulk update (`U`)
|
|
38
41
|
- **Remote discovery and install**
|
|
39
|
-
- npm search/browse with pagination
|
|
42
|
+
- npm search/browse with pagination, inline browse search, and keyboard page navigation
|
|
43
|
+
- Path- and git-like queries are handled explicitly instead of surfacing unrelated npm results
|
|
40
44
|
- Install by source (`npm:`, `git:`, `https://`, `ssh://`, `git@...`, local path)
|
|
41
45
|
- Supports direct GitHub `.ts` installs and standalone local install for self-contained packages
|
|
42
46
|
- Long-running discovery/detail screens now show dedicated loading UI, and cancellable reads can be aborted with `Esc`
|
|
@@ -66,20 +70,26 @@ Open the manager:
|
|
|
66
70
|
| Key | Action |
|
|
67
71
|
| ------------- | ----------------------------------------------------- |
|
|
68
72
|
| `↑↓` | Navigate |
|
|
69
|
-
| `
|
|
73
|
+
| `PageUp/Down` | Jump through longer lists |
|
|
74
|
+
| `Home/End` | Jump to top or bottom |
|
|
75
|
+
| `Space` | Toggle selected local extension on/off |
|
|
70
76
|
| `S` | Save local extension changes |
|
|
71
|
-
| `Enter` / `A` | Actions on selected
|
|
77
|
+
| `Enter` / `A` | Actions on selected item |
|
|
78
|
+
| `/` / `Ctrl+F`| Search visible items |
|
|
79
|
+
| `Tab` / `Shift+Tab` | Cycle filters |
|
|
80
|
+
| `1-5` | Filters: All / Local / Packages / Updates / Disabled |
|
|
72
81
|
| `c` | Configure selected package extensions |
|
|
73
82
|
| `u` | Update selected package directly |
|
|
83
|
+
| `V` | View full details for selected item |
|
|
74
84
|
| `X` | Remove selected item (package/local extension) |
|
|
75
85
|
| `i` | Quick install by source |
|
|
76
|
-
| `f` |
|
|
86
|
+
| `f` | Remote package search |
|
|
77
87
|
| `U` | Update all packages |
|
|
78
88
|
| `t` | Auto-update wizard |
|
|
79
89
|
| `P` / `M` | Quick actions palette |
|
|
80
90
|
| `R` | Browse remote packages |
|
|
81
91
|
| `?` / `H` | Help |
|
|
82
|
-
| `Esc` |
|
|
92
|
+
| `Esc` | Clear search or exit |
|
|
83
93
|
|
|
84
94
|
### Commands
|
|
85
95
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extmgr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Enhanced UX for managing local Pi extensions and community packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
"smoke-test": "node --import=tsx ./scripts/smoke-test.mjs",
|
|
28
28
|
"test": "node --import=tsx --test ./test/*.test.ts",
|
|
29
29
|
"check": "tsc --noEmit -p tsconfig.json && node --import=tsx ./scripts/smoke-test.mjs && node --import=tsx --test ./test/*.test.ts && pnpm run lint && pnpm run format:check",
|
|
30
|
+
"release": "release-it",
|
|
31
|
+
"release:patch": "release-it patch",
|
|
32
|
+
"release:minor": "release-it minor",
|
|
33
|
+
"release:major": "release-it major",
|
|
34
|
+
"release:first": "release-it --first-release",
|
|
35
|
+
"release:ci": "node --import=tsx ./scripts/release-ci.ts",
|
|
30
36
|
"prepublishOnly": "pnpm run check",
|
|
31
37
|
"prepare": "husky"
|
|
32
38
|
},
|
|
@@ -45,8 +51,10 @@
|
|
|
45
51
|
"@biomejs/biome": "^2.4.9",
|
|
46
52
|
"@mariozechner/pi-coding-agent": "^0.63.1",
|
|
47
53
|
"@mariozechner/pi-tui": "^0.63.1",
|
|
54
|
+
"@release-it/conventional-changelog": "^10.0.5",
|
|
48
55
|
"@types/node": "^22.19.10",
|
|
49
56
|
"husky": "^9.1.7",
|
|
57
|
+
"release-it": "^19.2.4",
|
|
50
58
|
"tsx": "^4.21.0",
|
|
51
59
|
"typescript": "^5.9.3"
|
|
52
60
|
},
|
|
@@ -54,7 +62,7 @@
|
|
|
54
62
|
"license": "MIT",
|
|
55
63
|
"packageManager": "pnpm@10.33.0",
|
|
56
64
|
"engines": {
|
|
57
|
-
"node": ">=22"
|
|
65
|
+
"node": ">=22.20.0"
|
|
58
66
|
},
|
|
59
67
|
"repository": {
|
|
60
68
|
"type": "git",
|
|
@@ -54,7 +54,7 @@ export async function handleAutoUpdateSubcommand(
|
|
|
54
54
|
" 3d - Check every 3 days",
|
|
55
55
|
" 1w - Check weekly",
|
|
56
56
|
" 2w - Check every 2 weeks",
|
|
57
|
-
" 1mo - Check monthly
|
|
57
|
+
" 1mo - Check monthly",
|
|
58
58
|
" daily - Check daily (alias)",
|
|
59
59
|
" weekly - Check weekly (alias)",
|
|
60
60
|
];
|
package/src/commands/history.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
queryGlobalHistory,
|
|
7
7
|
querySessionChanges,
|
|
8
8
|
} from "../utils/history.js";
|
|
9
|
+
import { parseLookbackDuration } from "../utils/duration.js";
|
|
9
10
|
import { notify } from "../utils/notify.js";
|
|
10
11
|
import { formatListOutput } from "../utils/ui-helpers.js";
|
|
11
12
|
|
|
@@ -37,36 +38,6 @@ type HistoryOptionHandler = (tokens: string[], index: number, state: HistoryPars
|
|
|
37
38
|
|
|
38
39
|
const HISTORY_ACTION_SET = new Set<ChangeAction>(HISTORY_ACTIONS);
|
|
39
40
|
|
|
40
|
-
function parseHistorySinceDuration(input: string): number | undefined {
|
|
41
|
-
const normalized = input.toLowerCase().trim();
|
|
42
|
-
const match = normalized.match(
|
|
43
|
-
/^(\d+)\s*(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|wk|wks|week|weeks|mo|mos|month|months)$/
|
|
44
|
-
);
|
|
45
|
-
if (!match) return undefined;
|
|
46
|
-
|
|
47
|
-
const value = Number.parseInt(match[1] ?? "", 10);
|
|
48
|
-
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
49
|
-
|
|
50
|
-
const unit = match[2] ?? "";
|
|
51
|
-
if (unit.startsWith("m") && !unit.startsWith("mo")) {
|
|
52
|
-
return value * 60 * 1000;
|
|
53
|
-
}
|
|
54
|
-
if (unit.startsWith("h")) {
|
|
55
|
-
return value * 60 * 60 * 1000;
|
|
56
|
-
}
|
|
57
|
-
if (unit.startsWith("d")) {
|
|
58
|
-
return value * 24 * 60 * 60 * 1000;
|
|
59
|
-
}
|
|
60
|
-
if (unit.startsWith("w")) {
|
|
61
|
-
return value * 7 * 24 * 60 * 60 * 1000;
|
|
62
|
-
}
|
|
63
|
-
if (unit.startsWith("mo")) {
|
|
64
|
-
return value * 30 * 24 * 60 * 60 * 1000;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
41
|
const HISTORY_OPTION_HANDLERS: Record<string, HistoryOptionHandler> = {
|
|
71
42
|
"--help": (_tokens, _index, state) => {
|
|
72
43
|
state.showHelp = true;
|
|
@@ -142,7 +113,7 @@ const HISTORY_OPTION_HANDLERS: Record<string, HistoryOptionHandler> = {
|
|
|
142
113
|
return 0;
|
|
143
114
|
}
|
|
144
115
|
|
|
145
|
-
const ms =
|
|
116
|
+
const ms = parseLookbackDuration(value);
|
|
146
117
|
if (!ms) {
|
|
147
118
|
state.errors.push(`Invalid --since duration: ${value}`);
|
|
148
119
|
} else {
|
package/src/constants.ts
CHANGED
|
@@ -26,14 +26,6 @@ export const TIMEOUTS = {
|
|
|
26
26
|
npmView: 10_000,
|
|
27
27
|
/** Full package installation timeout (3 minutes) */
|
|
28
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
29
|
/** Package metadata fetch timeout */
|
|
38
30
|
fetchPackageInfo: 30_000,
|
|
39
31
|
/** Package extraction timeout */
|
|
@@ -10,8 +10,13 @@ import { readdir, rename, rm } from "node:fs/promises";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { basename, dirname, join, relative } from "node:path";
|
|
12
12
|
import { DISABLED_SUFFIX } from "../constants.js";
|
|
13
|
+
import { readPackageManifest } from "../packages/extensions.js";
|
|
13
14
|
import { type ExtensionEntry, type Scope, type State } from "../types/index.js";
|
|
14
15
|
import { fileExists, readSummary } from "../utils/fs.js";
|
|
16
|
+
import {
|
|
17
|
+
normalizeRelativePath,
|
|
18
|
+
resolveRelativePathSelection,
|
|
19
|
+
} from "../utils/relative-path-selection.js";
|
|
15
20
|
|
|
16
21
|
interface RootConfig {
|
|
17
22
|
root: string;
|
|
@@ -94,8 +99,7 @@ async function discoverInRoot(
|
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
if (item.isDirectory()) {
|
|
97
|
-
|
|
98
|
-
if (entry) found.push(entry);
|
|
102
|
+
found.push(...(await parseDirectoryExtensions(root, label, scope, name)));
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
|
|
@@ -141,53 +145,131 @@ async function parseTopLevelFile(
|
|
|
141
145
|
};
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
function stripDisabledSuffix(path: string): string {
|
|
149
|
+
return path.replace(/\.(ts|js)\.disabled$/i, ".$1");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isExtensionEntrypointPath(path: string): boolean {
|
|
153
|
+
return /\.(ts|js)$/i.test(path);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isLocalExtensionFile(path: string): boolean {
|
|
157
|
+
return /\.(ts|js)(?:\.disabled)?$/i.test(path);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function collectLocalExtensionFiles(rootDir: string, startDir: string): Promise<string[]> {
|
|
161
|
+
const collected: string[] = [];
|
|
162
|
+
|
|
163
|
+
let entries: Dirent[];
|
|
164
|
+
try {
|
|
165
|
+
entries = await readdir(startDir, { withFileTypes: true });
|
|
166
|
+
} catch {
|
|
167
|
+
return collected;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
if (entry.name.startsWith(".")) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const absolutePath = join(startDir, entry.name);
|
|
176
|
+
if (entry.isDirectory()) {
|
|
177
|
+
collected.push(...(await collectLocalExtensionFiles(rootDir, absolutePath)));
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!entry.isFile()) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const relativePath = normalizeRelativePath(relative(rootDir, absolutePath));
|
|
186
|
+
if (isLocalExtensionFile(relativePath)) {
|
|
187
|
+
collected.push(stripDisabledSuffix(relativePath));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return collected;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function resolveManifestLocalEntrypoints(dir: string): Promise<string[] | undefined> {
|
|
195
|
+
const manifest = await readPackageManifest(dir);
|
|
196
|
+
const extensions = manifest?.pi?.extensions;
|
|
197
|
+
if (!Array.isArray(extensions)) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const entries = extensions.filter((value): value is string => typeof value === "string");
|
|
202
|
+
const allFiles = await collectLocalExtensionFiles(dir, dir);
|
|
203
|
+
return resolveRelativePathSelection(
|
|
204
|
+
allFiles,
|
|
205
|
+
entries,
|
|
206
|
+
(path, files) => isExtensionEntrypointPath(path) && files.includes(path)
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function toDirectoryExtensionEntry(
|
|
211
|
+
root: string,
|
|
212
|
+
label: string,
|
|
213
|
+
scope: Scope,
|
|
214
|
+
dir: string,
|
|
215
|
+
extensionPath: string
|
|
216
|
+
): Promise<ExtensionEntry | undefined> {
|
|
217
|
+
const normalizedPath = normalizeRelativePath(extensionPath);
|
|
218
|
+
const activePath = join(dir, normalizedPath);
|
|
219
|
+
const disabledPath = `${activePath}${DISABLED_SUFFIX}`;
|
|
220
|
+
|
|
221
|
+
let state: State;
|
|
222
|
+
let summaryPath: string;
|
|
223
|
+
if (await fileExists(activePath)) {
|
|
224
|
+
state = "enabled";
|
|
225
|
+
summaryPath = activePath;
|
|
226
|
+
} else if (await fileExists(disabledPath)) {
|
|
227
|
+
state = "disabled";
|
|
228
|
+
summaryPath = disabledPath;
|
|
229
|
+
} else {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
id: `${scope}:${activePath}`,
|
|
235
|
+
scope,
|
|
236
|
+
state,
|
|
237
|
+
activePath,
|
|
238
|
+
disabledPath,
|
|
239
|
+
displayName: `${label}/${normalizeRelativePath(relative(root, activePath))}`,
|
|
240
|
+
summary: await readSummary(summaryPath),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
144
244
|
/**
|
|
145
|
-
* Parse a directory containing
|
|
146
|
-
*
|
|
147
|
-
* @param root - Root directory path
|
|
148
|
-
* @param label - Display label for the root
|
|
149
|
-
* @param scope - "global" or "project"
|
|
150
|
-
* @param dirName - Name of the directory to parse
|
|
151
|
-
* @returns ExtensionEntry if index file found, undefined otherwise
|
|
245
|
+
* Parse a directory containing a manifest-declared entrypoint or index.ts/js file as one or more
|
|
246
|
+
* extension entries.
|
|
152
247
|
*/
|
|
153
|
-
async function
|
|
248
|
+
async function parseDirectoryExtensions(
|
|
154
249
|
root: string,
|
|
155
250
|
label: string,
|
|
156
251
|
scope: Scope,
|
|
157
252
|
dirName: string
|
|
158
|
-
): Promise<ExtensionEntry
|
|
253
|
+
): Promise<ExtensionEntry[]> {
|
|
159
254
|
const dir = join(root, dirName);
|
|
255
|
+
const manifestEntrypoints = await resolveManifestLocalEntrypoints(dir);
|
|
160
256
|
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
scope,
|
|
169
|
-
state: "enabled",
|
|
170
|
-
activePath,
|
|
171
|
-
disabledPath,
|
|
172
|
-
displayName: `${label}/${dirName}/index${ext}`,
|
|
173
|
-
summary: await readSummary(activePath),
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (await fileExists(disabledPath)) {
|
|
178
|
-
return {
|
|
179
|
-
id: `${scope}:${activePath}`,
|
|
180
|
-
scope,
|
|
181
|
-
state: "disabled",
|
|
182
|
-
activePath,
|
|
183
|
-
disabledPath,
|
|
184
|
-
displayName: `${label}/${dirName}/index${ext}`,
|
|
185
|
-
summary: await readSummary(disabledPath),
|
|
186
|
-
};
|
|
187
|
-
}
|
|
257
|
+
if (manifestEntrypoints !== undefined) {
|
|
258
|
+
const entries = await Promise.all(
|
|
259
|
+
manifestEntrypoints.map((extensionPath) =>
|
|
260
|
+
toDirectoryExtensionEntry(root, label, scope, dir, extensionPath)
|
|
261
|
+
)
|
|
262
|
+
);
|
|
263
|
+
return entries.filter((entry): entry is ExtensionEntry => Boolean(entry));
|
|
188
264
|
}
|
|
189
265
|
|
|
190
|
-
|
|
266
|
+
const fallbackEntries = await Promise.all(
|
|
267
|
+
["index.ts", "index.js"].map((extensionPath) =>
|
|
268
|
+
toDirectoryExtensionEntry(root, label, scope, dir, extensionPath)
|
|
269
|
+
)
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
return fallbackEntries.filter((entry): entry is ExtensionEntry => Boolean(entry)).slice(0, 1);
|
|
191
273
|
}
|
|
192
274
|
|
|
193
275
|
/**
|
|
@@ -28,6 +28,14 @@ interface NpmSearchResultObject {
|
|
|
28
28
|
description?: string;
|
|
29
29
|
keywords?: string[];
|
|
30
30
|
date?: string;
|
|
31
|
+
publisher?: {
|
|
32
|
+
username?: string;
|
|
33
|
+
email?: string;
|
|
34
|
+
};
|
|
35
|
+
maintainers?: Array<{
|
|
36
|
+
username?: string;
|
|
37
|
+
email?: string;
|
|
38
|
+
}>;
|
|
31
39
|
};
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -79,6 +87,31 @@ import {
|
|
|
79
87
|
setCachedSearch,
|
|
80
88
|
} from "../utils/cache.js";
|
|
81
89
|
|
|
90
|
+
function getNpmPackageAuthor(
|
|
91
|
+
pkg: NonNullable<NpmSearchResultObject["package"]>
|
|
92
|
+
): string | undefined {
|
|
93
|
+
const publisher = pkg.publisher;
|
|
94
|
+
if (publisher?.username?.trim()) {
|
|
95
|
+
return publisher.username.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (publisher?.email?.trim()) {
|
|
99
|
+
return publisher.email.trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const maintainerWithUsername = pkg.maintainers?.find((entry) => entry.username?.trim());
|
|
103
|
+
if (maintainerWithUsername?.username?.trim()) {
|
|
104
|
+
return maintainerWithUsername.username.trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const maintainerWithEmail = pkg.maintainers?.find((entry) => entry.email?.trim());
|
|
108
|
+
if (maintainerWithEmail?.email?.trim()) {
|
|
109
|
+
return maintainerWithEmail.email.trim();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
82
115
|
function toNpmPackage(entry: NpmSearchResultObject): NpmPackage | undefined {
|
|
83
116
|
const pkg = entry.package;
|
|
84
117
|
if (!pkg) return undefined;
|
|
@@ -90,6 +123,7 @@ function toNpmPackage(entry: NpmSearchResultObject): NpmPackage | undefined {
|
|
|
90
123
|
name,
|
|
91
124
|
version: pkg.version,
|
|
92
125
|
description: pkg.description,
|
|
126
|
+
author: getNpmPackageAuthor(pkg),
|
|
93
127
|
keywords: Array.isArray(pkg.keywords) ? pkg.keywords : undefined,
|
|
94
128
|
date: pkg.date,
|
|
95
129
|
};
|
|
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { type Dirent } from "node:fs";
|
|
3
3
|
import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { dirname, join,
|
|
5
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
@@ -14,6 +14,11 @@ import {
|
|
|
14
14
|
} from "../types/index.js";
|
|
15
15
|
import { parseNpmSource } from "../utils/format.js";
|
|
16
16
|
import { fileExists, readSummary } from "../utils/fs.js";
|
|
17
|
+
import {
|
|
18
|
+
matchesFilterPattern,
|
|
19
|
+
normalizeRelativePath,
|
|
20
|
+
resolveRelativePathSelection,
|
|
21
|
+
} from "../utils/relative-path-selection.js";
|
|
17
22
|
import { resolveNpmCommand } from "../utils/npm-exec.js";
|
|
18
23
|
|
|
19
24
|
interface PackageSettingsObject {
|
|
@@ -36,11 +41,6 @@ export interface PackageManifest {
|
|
|
36
41
|
const execFileAsync = promisify(execFile);
|
|
37
42
|
let globalNpmRootCache: string | null | undefined;
|
|
38
43
|
|
|
39
|
-
function normalizeRelativePath(value: string): string {
|
|
40
|
-
const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
41
|
-
return normalized;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
44
|
function normalizeSource(source: string): string {
|
|
45
45
|
return source
|
|
46
46
|
.trim()
|
|
@@ -238,17 +238,17 @@ function toPackageSettingsObject(
|
|
|
238
238
|
packageSource: string
|
|
239
239
|
): PackageSettingsObject {
|
|
240
240
|
if (typeof existing === "string") {
|
|
241
|
-
return { source: existing
|
|
241
|
+
return { source: existing };
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
if (existing && typeof existing.source === "string") {
|
|
245
245
|
return {
|
|
246
246
|
source: existing.source,
|
|
247
|
-
|
|
247
|
+
...(Array.isArray(existing.extensions) ? { extensions: [...existing.extensions] } : {}),
|
|
248
248
|
};
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
return { source: packageSource
|
|
251
|
+
return { source: packageSource };
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
function updateExtensionMarkers(
|
|
@@ -276,7 +276,16 @@ function updateExtensionMarkers(
|
|
|
276
276
|
for (const [extensionPath, target] of Array.from(changes.entries()).sort((a, b) =>
|
|
277
277
|
a[0].localeCompare(b[0])
|
|
278
278
|
)) {
|
|
279
|
-
|
|
279
|
+
const baseFilters =
|
|
280
|
+
nextTokens.length > 0
|
|
281
|
+
? nextTokens
|
|
282
|
+
: existingTokens && existingTokens.length === 0
|
|
283
|
+
? []
|
|
284
|
+
: undefined;
|
|
285
|
+
const baseState = getPackageFilterState(baseFilters, extensionPath);
|
|
286
|
+
if (target !== baseState) {
|
|
287
|
+
nextTokens.push(`${target === "enabled" ? "+" : "-"}${extensionPath}`);
|
|
288
|
+
}
|
|
280
289
|
}
|
|
281
290
|
|
|
282
291
|
return nextTokens;
|
|
@@ -322,10 +331,13 @@ export async function applyPackageExtensionStateChanges(
|
|
|
322
331
|
|
|
323
332
|
packageEntry.extensions = updateExtensionMarkers(packageEntry.extensions, normalizedChanges);
|
|
324
333
|
|
|
334
|
+
const normalizedPackageEntry =
|
|
335
|
+
packageEntry.extensions.length > 0 ? packageEntry : packageEntry.source;
|
|
336
|
+
|
|
325
337
|
if (index === -1) {
|
|
326
|
-
packages.push(
|
|
338
|
+
packages.push(normalizedPackageEntry);
|
|
327
339
|
} else {
|
|
328
|
-
packages[index] =
|
|
340
|
+
packages[index] = normalizedPackageEntry;
|
|
329
341
|
}
|
|
330
342
|
|
|
331
343
|
settings.packages = packages;
|
|
@@ -340,22 +352,6 @@ export async function applyPackageExtensionStateChanges(
|
|
|
340
352
|
}
|
|
341
353
|
}
|
|
342
354
|
|
|
343
|
-
function safeMatchesGlob(targetPath: string, pattern: string): boolean {
|
|
344
|
-
try {
|
|
345
|
-
return matchesGlob(targetPath, pattern);
|
|
346
|
-
} catch {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function matchesFilterPattern(targetPath: string, pattern: string): boolean {
|
|
352
|
-
const normalizedPattern = normalizeRelativePath(pattern.trim());
|
|
353
|
-
if (!normalizedPattern) return false;
|
|
354
|
-
if (targetPath === normalizedPattern) return true;
|
|
355
|
-
|
|
356
|
-
return safeMatchesGlob(targetPath, normalizedPattern);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
355
|
function getPackageFilterState(filters: string[] | undefined, extensionPath: string): State {
|
|
360
356
|
// Omitted key => all enabled (pi default).
|
|
361
357
|
if (filters === undefined) {
|
|
@@ -415,58 +411,37 @@ function getPackageFilterState(filters: string[] | undefined, extensionPath: str
|
|
|
415
411
|
return enabled ? "enabled" : "disabled";
|
|
416
412
|
}
|
|
417
413
|
|
|
418
|
-
async function
|
|
419
|
-
packageSource: string,
|
|
420
|
-
extensionPath: string,
|
|
414
|
+
async function readPackageFilterMap(
|
|
421
415
|
scope: Scope,
|
|
422
416
|
cwd: string
|
|
423
|
-
): Promise<
|
|
424
|
-
const
|
|
425
|
-
const settings = await readSettingsFile(settingsPath);
|
|
417
|
+
): Promise<Map<string, string[] | undefined>> {
|
|
418
|
+
const settings = await readSettingsFile(getSettingsPath(scope, cwd));
|
|
426
419
|
const packages = settings.packages ?? [];
|
|
427
|
-
const
|
|
420
|
+
const filterMap = new Map<string, string[] | undefined>();
|
|
428
421
|
|
|
429
|
-
const entry
|
|
430
|
-
if (typeof
|
|
431
|
-
|
|
422
|
+
for (const entry of packages) {
|
|
423
|
+
if (typeof entry === "string") {
|
|
424
|
+
filterMap.set(normalizeSource(entry), undefined);
|
|
425
|
+
continue;
|
|
432
426
|
}
|
|
433
|
-
return normalizeSource(pkg.source) === normalizedSource;
|
|
434
|
-
});
|
|
435
427
|
|
|
436
|
-
|
|
437
|
-
|
|
428
|
+
if (typeof entry.source !== "string") {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
filterMap.set(
|
|
433
|
+
normalizeSource(entry.source),
|
|
434
|
+
Array.isArray(entry.extensions) ? entry.extensions : undefined
|
|
435
|
+
);
|
|
438
436
|
}
|
|
439
437
|
|
|
440
|
-
return
|
|
438
|
+
return filterMap;
|
|
441
439
|
}
|
|
442
440
|
|
|
443
441
|
function isExtensionEntrypointPath(path: string): boolean {
|
|
444
442
|
return /\.(ts|js)$/i.test(path);
|
|
445
443
|
}
|
|
446
444
|
|
|
447
|
-
function hasGlobMagic(path: string): boolean {
|
|
448
|
-
return /[*?{}[\]]/.test(path);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function isSafeRelativePath(path: string): boolean {
|
|
452
|
-
return path !== "" && path !== ".." && !path.startsWith("../") && !path.includes("/../");
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function selectDirectoryFiles(allFiles: string[], directoryPath: string): string[] {
|
|
456
|
-
const prefix = `${directoryPath}/`;
|
|
457
|
-
return allFiles.filter((file) => file.startsWith(prefix));
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function applySelection(selected: Set<string>, files: Iterable<string>, exclude: boolean): void {
|
|
461
|
-
for (const file of files) {
|
|
462
|
-
if (exclude) {
|
|
463
|
-
selected.delete(file);
|
|
464
|
-
} else {
|
|
465
|
-
selected.add(file);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
445
|
async function collectExtensionFilesFromDir(
|
|
471
446
|
packageRoot: string,
|
|
472
447
|
startDir: string
|
|
@@ -505,38 +480,12 @@ async function resolveManifestExtensionEntries(
|
|
|
505
480
|
packageRoot: string,
|
|
506
481
|
entries: string[]
|
|
507
482
|
): Promise<string[]> {
|
|
508
|
-
const selected = new Set<string>();
|
|
509
483
|
const allFiles = await collectExtensionFilesFromDir(packageRoot, packageRoot);
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const exclude = token.startsWith("!");
|
|
516
|
-
const normalizedToken = normalizeRelativePath(exclude ? token.slice(1) : token);
|
|
517
|
-
const pattern = normalizedToken.replace(/[\\/]+$/g, "");
|
|
518
|
-
if (!isSafeRelativePath(pattern)) {
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
if (hasGlobMagic(pattern)) {
|
|
523
|
-
const matchedFiles = allFiles.filter((file) => matchesFilterPattern(file, pattern));
|
|
524
|
-
applySelection(selected, matchedFiles, exclude);
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const directoryFiles = selectDirectoryFiles(allFiles, pattern);
|
|
529
|
-
if (directoryFiles.length > 0) {
|
|
530
|
-
applySelection(selected, directoryFiles, exclude);
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (isExtensionEntrypointPath(pattern)) {
|
|
535
|
-
applySelection(selected, [pattern], exclude);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return Array.from(selected).sort((a, b) => a.localeCompare(b));
|
|
484
|
+
return resolveRelativePathSelection(
|
|
485
|
+
allFiles,
|
|
486
|
+
entries,
|
|
487
|
+
(path, files) => isExtensionEntrypointPath(path) && files.includes(path)
|
|
488
|
+
);
|
|
540
489
|
}
|
|
541
490
|
|
|
542
491
|
export async function readPackageManifest(
|
|
@@ -617,11 +566,19 @@ export async function discoverPackageExtensions(
|
|
|
617
566
|
cwd: string
|
|
618
567
|
): Promise<PackageExtensionEntry[]> {
|
|
619
568
|
const entries: PackageExtensionEntry[] = [];
|
|
569
|
+
const [globalFilterMap, projectFilterMap] = await Promise.all([
|
|
570
|
+
readPackageFilterMap("global", cwd),
|
|
571
|
+
readPackageFilterMap("project", cwd),
|
|
572
|
+
]);
|
|
620
573
|
|
|
621
574
|
for (const pkg of packages) {
|
|
622
575
|
const packageRoot = await toPackageRoot(pkg, cwd);
|
|
623
576
|
if (!packageRoot) continue;
|
|
624
577
|
|
|
578
|
+
const packageFilters =
|
|
579
|
+
(pkg.scope === "global" ? globalFilterMap : projectFilterMap).get(
|
|
580
|
+
normalizeSource(pkg.source)
|
|
581
|
+
) ?? undefined;
|
|
625
582
|
const extensionPaths = await discoverPackageExtensionEntrypoints(packageRoot);
|
|
626
583
|
for (const extensionPath of extensionPaths) {
|
|
627
584
|
const normalizedPath = normalizeRelativePath(extensionPath);
|
|
@@ -629,7 +586,7 @@ export async function discoverPackageExtensions(
|
|
|
629
586
|
const summary = (await fileExists(absolutePath))
|
|
630
587
|
? await readSummary(absolutePath)
|
|
631
588
|
: "package extension";
|
|
632
|
-
const state =
|
|
589
|
+
const state = getPackageFilterState(packageFilters, normalizedPath);
|
|
633
590
|
|
|
634
591
|
entries.push({
|
|
635
592
|
id: `pkg-ext:${pkg.scope}:${pkg.source}:${normalizedPath}`,
|