pi-extmgr 0.1.27 → 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 +21 -10
- package/package.json +21 -16
- package/src/commands/auto-update.ts +5 -5
- package/src/commands/cache.ts +1 -1
- package/src/commands/history.ts +5 -34
- package/src/commands/install.ts +2 -2
- package/src/commands/registry.ts +7 -7
- package/src/commands/types.ts +1 -1
- package/src/constants.ts +0 -8
- package/src/extensions/discovery.ts +125 -42
- package/src/index.ts +15 -15
- package/src/packages/catalog.ts +9 -8
- package/src/packages/discovery.ts +56 -19
- package/src/packages/extensions.ts +65 -103
- package/src/packages/install.ts +104 -74
- package/src/packages/management.ts +78 -65
- package/src/types/index.ts +20 -11
- package/src/ui/async-task.ts +101 -65
- package/src/ui/footer.ts +47 -31
- package/src/ui/help.ts +17 -13
- package/src/ui/package-config.ts +36 -48
- package/src/ui/remote.ts +714 -119
- package/src/ui/theme.ts +2 -2
- package/src/ui/unified.ts +964 -371
- package/src/utils/auto-update.ts +44 -39
- package/src/utils/cache.ts +208 -37
- package/src/utils/command.ts +1 -1
- package/src/utils/duration.ts +132 -0
- package/src/utils/format.ts +4 -33
- package/src/utils/fs.ts +8 -4
- package/src/utils/history.ts +47 -9
- package/src/utils/mode.ts +2 -2
- package/src/utils/notify.ts +1 -15
- package/src/utils/npm-exec.ts +1 -1
- package/src/utils/package-source.ts +35 -7
- package/src/utils/path-identity.ts +7 -0
- package/src/utils/relative-path-selection.ts +100 -0
- package/src/utils/settings.ts +11 -61
- package/src/utils/status.ts +12 -10
- package/src/utils/ui-helpers.ts +2 -2
- package/src/utils/retry.ts +0 -49
|
@@ -3,19 +3,20 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import
|
|
7
|
-
ExtensionAPI,
|
|
8
|
-
ExtensionCommandContext,
|
|
9
|
-
ExtensionContext,
|
|
6
|
+
import {
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionCommandContext,
|
|
9
|
+
type ExtensionContext,
|
|
10
|
+
getAgentDir,
|
|
10
11
|
} from "@mariozechner/pi-coding-agent";
|
|
11
|
-
import type { InstalledPackage, NpmPackage, SearchCache } from "../types/index.js";
|
|
12
12
|
import { CACHE_TTL, TIMEOUTS } from "../constants.js";
|
|
13
|
-
import {
|
|
13
|
+
import { type InstalledPackage, type NpmPackage, type SearchCache } from "../types/index.js";
|
|
14
14
|
import { parseNpmSource } from "../utils/format.js";
|
|
15
|
+
import { readSummary } from "../utils/fs.js";
|
|
16
|
+
import { fetchWithTimeout } from "../utils/network.js";
|
|
17
|
+
import { execNpm } from "../utils/npm-exec.js";
|
|
15
18
|
import { normalizePackageIdentity } from "../utils/package-source.js";
|
|
16
19
|
import { getPackageCatalog } from "./catalog.js";
|
|
17
|
-
import { execNpm } from "../utils/npm-exec.js";
|
|
18
|
-
import { fetchWithTimeout } from "../utils/network.js";
|
|
19
20
|
|
|
20
21
|
const NPM_SEARCH_API = "https://registry.npmjs.org/-/v1/search";
|
|
21
22
|
const NPM_SEARCH_PAGE_SIZE = 250;
|
|
@@ -27,6 +28,14 @@ interface NpmSearchResultObject {
|
|
|
27
28
|
description?: string;
|
|
28
29
|
keywords?: string[];
|
|
29
30
|
date?: string;
|
|
31
|
+
publisher?: {
|
|
32
|
+
username?: string;
|
|
33
|
+
email?: string;
|
|
34
|
+
};
|
|
35
|
+
maintainers?: Array<{
|
|
36
|
+
username?: string;
|
|
37
|
+
email?: string;
|
|
38
|
+
}>;
|
|
30
39
|
};
|
|
31
40
|
}
|
|
32
41
|
|
|
@@ -69,15 +78,40 @@ export function isCacheValid(query: string): boolean {
|
|
|
69
78
|
|
|
70
79
|
// Import persistent cache
|
|
71
80
|
import {
|
|
72
|
-
getCachedSearch,
|
|
73
|
-
setCachedSearch,
|
|
74
81
|
getCachedPackage,
|
|
75
|
-
setCachedPackage,
|
|
76
|
-
getPackageDescriptions,
|
|
77
82
|
getCachedPackageSize,
|
|
83
|
+
getCachedSearch,
|
|
84
|
+
getPackageDescriptions,
|
|
85
|
+
setCachedPackage,
|
|
78
86
|
setCachedPackageSize,
|
|
87
|
+
setCachedSearch,
|
|
79
88
|
} from "../utils/cache.js";
|
|
80
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
|
+
|
|
81
115
|
function toNpmPackage(entry: NpmSearchResultObject): NpmPackage | undefined {
|
|
82
116
|
const pkg = entry.package;
|
|
83
117
|
if (!pkg) return undefined;
|
|
@@ -89,6 +123,7 @@ function toNpmPackage(entry: NpmSearchResultObject): NpmPackage | undefined {
|
|
|
89
123
|
name,
|
|
90
124
|
version: pkg.version,
|
|
91
125
|
description: pkg.description,
|
|
126
|
+
author: getNpmPackageAuthor(pkg),
|
|
92
127
|
keywords: Array.isArray(pkg.keywords) ? pkg.keywords : undefined,
|
|
93
128
|
date: pkg.date,
|
|
94
129
|
};
|
|
@@ -201,11 +236,13 @@ export async function getInstalledPackages(
|
|
|
201
236
|
return packages;
|
|
202
237
|
}
|
|
203
238
|
|
|
204
|
-
function getInstalledPackageIdentity(pkg: InstalledPackage): string {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
239
|
+
function getInstalledPackageIdentity(pkg: InstalledPackage, options?: { cwd?: string }): string {
|
|
240
|
+
const baseCwd = pkg.scope === "project" ? options?.cwd : getAgentDir();
|
|
241
|
+
|
|
242
|
+
return normalizePackageIdentity(pkg.source, {
|
|
243
|
+
...(pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : {}),
|
|
244
|
+
...(baseCwd ? { cwd: baseCwd } : {}),
|
|
245
|
+
});
|
|
209
246
|
}
|
|
210
247
|
|
|
211
248
|
export async function isSourceInstalled(
|
|
@@ -214,10 +251,10 @@ export async function isSourceInstalled(
|
|
|
214
251
|
options?: { scope?: "global" | "project" }
|
|
215
252
|
): Promise<boolean> {
|
|
216
253
|
const installed = await getPackageCatalog(ctx.cwd).listInstalledPackages({ dedupe: false });
|
|
217
|
-
const expected = normalizePackageIdentity(source);
|
|
254
|
+
const expected = normalizePackageIdentity(source, { cwd: ctx.cwd });
|
|
218
255
|
|
|
219
256
|
return installed.some((pkg) => {
|
|
220
|
-
if (getInstalledPackageIdentity(pkg) !== expected) {
|
|
257
|
+
if (getInstalledPackageIdentity(pkg, { cwd: ctx.cwd }) !== expected) {
|
|
221
258
|
return false;
|
|
222
259
|
}
|
|
223
260
|
return options?.scope ? pkg.scope === options.scope : true;
|
|
@@ -1,14 +1,24 @@
|
|
|
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";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { type Dirent } from "node:fs";
|
|
3
|
+
import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
type InstalledPackage,
|
|
11
|
+
type PackageExtensionEntry,
|
|
12
|
+
type Scope,
|
|
13
|
+
type State,
|
|
14
|
+
} from "../types/index.js";
|
|
10
15
|
import { parseNpmSource } from "../utils/format.js";
|
|
11
16
|
import { fileExists, readSummary } from "../utils/fs.js";
|
|
17
|
+
import {
|
|
18
|
+
matchesFilterPattern,
|
|
19
|
+
normalizeRelativePath,
|
|
20
|
+
resolveRelativePathSelection,
|
|
21
|
+
} from "../utils/relative-path-selection.js";
|
|
12
22
|
import { resolveNpmCommand } from "../utils/npm-exec.js";
|
|
13
23
|
|
|
14
24
|
interface PackageSettingsObject {
|
|
@@ -31,11 +41,6 @@ export interface PackageManifest {
|
|
|
31
41
|
const execFileAsync = promisify(execFile);
|
|
32
42
|
let globalNpmRootCache: string | null | undefined;
|
|
33
43
|
|
|
34
|
-
function normalizeRelativePath(value: string): string {
|
|
35
|
-
const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
36
|
-
return normalized;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
44
|
function normalizeSource(source: string): string {
|
|
40
45
|
return source
|
|
41
46
|
.trim()
|
|
@@ -233,17 +238,17 @@ function toPackageSettingsObject(
|
|
|
233
238
|
packageSource: string
|
|
234
239
|
): PackageSettingsObject {
|
|
235
240
|
if (typeof existing === "string") {
|
|
236
|
-
return { source: existing
|
|
241
|
+
return { source: existing };
|
|
237
242
|
}
|
|
238
243
|
|
|
239
244
|
if (existing && typeof existing.source === "string") {
|
|
240
245
|
return {
|
|
241
246
|
source: existing.source,
|
|
242
|
-
|
|
247
|
+
...(Array.isArray(existing.extensions) ? { extensions: [...existing.extensions] } : {}),
|
|
243
248
|
};
|
|
244
249
|
}
|
|
245
250
|
|
|
246
|
-
return { source: packageSource
|
|
251
|
+
return { source: packageSource };
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
function updateExtensionMarkers(
|
|
@@ -271,7 +276,16 @@ function updateExtensionMarkers(
|
|
|
271
276
|
for (const [extensionPath, target] of Array.from(changes.entries()).sort((a, b) =>
|
|
272
277
|
a[0].localeCompare(b[0])
|
|
273
278
|
)) {
|
|
274
|
-
|
|
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
|
+
}
|
|
275
289
|
}
|
|
276
290
|
|
|
277
291
|
return nextTokens;
|
|
@@ -317,10 +331,13 @@ export async function applyPackageExtensionStateChanges(
|
|
|
317
331
|
|
|
318
332
|
packageEntry.extensions = updateExtensionMarkers(packageEntry.extensions, normalizedChanges);
|
|
319
333
|
|
|
334
|
+
const normalizedPackageEntry =
|
|
335
|
+
packageEntry.extensions.length > 0 ? packageEntry : packageEntry.source;
|
|
336
|
+
|
|
320
337
|
if (index === -1) {
|
|
321
|
-
packages.push(
|
|
338
|
+
packages.push(normalizedPackageEntry);
|
|
322
339
|
} else {
|
|
323
|
-
packages[index] =
|
|
340
|
+
packages[index] = normalizedPackageEntry;
|
|
324
341
|
}
|
|
325
342
|
|
|
326
343
|
settings.packages = packages;
|
|
@@ -335,22 +352,6 @@ export async function applyPackageExtensionStateChanges(
|
|
|
335
352
|
}
|
|
336
353
|
}
|
|
337
354
|
|
|
338
|
-
function safeMatchesGlob(targetPath: string, pattern: string): boolean {
|
|
339
|
-
try {
|
|
340
|
-
return matchesGlob(targetPath, pattern);
|
|
341
|
-
} catch {
|
|
342
|
-
return false;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function matchesFilterPattern(targetPath: string, pattern: string): boolean {
|
|
347
|
-
const normalizedPattern = normalizeRelativePath(pattern.trim());
|
|
348
|
-
if (!normalizedPattern) return false;
|
|
349
|
-
if (targetPath === normalizedPattern) return true;
|
|
350
|
-
|
|
351
|
-
return safeMatchesGlob(targetPath, normalizedPattern);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
355
|
function getPackageFilterState(filters: string[] | undefined, extensionPath: string): State {
|
|
355
356
|
// Omitted key => all enabled (pi default).
|
|
356
357
|
if (filters === undefined) {
|
|
@@ -410,58 +411,37 @@ function getPackageFilterState(filters: string[] | undefined, extensionPath: str
|
|
|
410
411
|
return enabled ? "enabled" : "disabled";
|
|
411
412
|
}
|
|
412
413
|
|
|
413
|
-
async function
|
|
414
|
-
packageSource: string,
|
|
415
|
-
extensionPath: string,
|
|
414
|
+
async function readPackageFilterMap(
|
|
416
415
|
scope: Scope,
|
|
417
416
|
cwd: string
|
|
418
|
-
): Promise<
|
|
419
|
-
const
|
|
420
|
-
const settings = await readSettingsFile(settingsPath);
|
|
417
|
+
): Promise<Map<string, string[] | undefined>> {
|
|
418
|
+
const settings = await readSettingsFile(getSettingsPath(scope, cwd));
|
|
421
419
|
const packages = settings.packages ?? [];
|
|
422
|
-
const
|
|
420
|
+
const filterMap = new Map<string, string[] | undefined>();
|
|
423
421
|
|
|
424
|
-
const entry
|
|
425
|
-
if (typeof
|
|
426
|
-
|
|
422
|
+
for (const entry of packages) {
|
|
423
|
+
if (typeof entry === "string") {
|
|
424
|
+
filterMap.set(normalizeSource(entry), undefined);
|
|
425
|
+
continue;
|
|
427
426
|
}
|
|
428
|
-
return normalizeSource(pkg.source) === normalizedSource;
|
|
429
|
-
});
|
|
430
427
|
|
|
431
|
-
|
|
432
|
-
|
|
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
|
+
);
|
|
433
436
|
}
|
|
434
437
|
|
|
435
|
-
return
|
|
438
|
+
return filterMap;
|
|
436
439
|
}
|
|
437
440
|
|
|
438
441
|
function isExtensionEntrypointPath(path: string): boolean {
|
|
439
442
|
return /\.(ts|js)$/i.test(path);
|
|
440
443
|
}
|
|
441
444
|
|
|
442
|
-
function hasGlobMagic(path: string): boolean {
|
|
443
|
-
return /[*?{}[\]]/.test(path);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function isSafeRelativePath(path: string): boolean {
|
|
447
|
-
return path !== "" && path !== ".." && !path.startsWith("../") && !path.includes("/../");
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function selectDirectoryFiles(allFiles: string[], directoryPath: string): string[] {
|
|
451
|
-
const prefix = `${directoryPath}/`;
|
|
452
|
-
return allFiles.filter((file) => file.startsWith(prefix));
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function applySelection(selected: Set<string>, files: Iterable<string>, exclude: boolean): void {
|
|
456
|
-
for (const file of files) {
|
|
457
|
-
if (exclude) {
|
|
458
|
-
selected.delete(file);
|
|
459
|
-
} else {
|
|
460
|
-
selected.add(file);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
445
|
async function collectExtensionFilesFromDir(
|
|
466
446
|
packageRoot: string,
|
|
467
447
|
startDir: string
|
|
@@ -500,38 +480,12 @@ async function resolveManifestExtensionEntries(
|
|
|
500
480
|
packageRoot: string,
|
|
501
481
|
entries: string[]
|
|
502
482
|
): Promise<string[]> {
|
|
503
|
-
const selected = new Set<string>();
|
|
504
483
|
const allFiles = await collectExtensionFilesFromDir(packageRoot, packageRoot);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const exclude = token.startsWith("!");
|
|
511
|
-
const normalizedToken = normalizeRelativePath(exclude ? token.slice(1) : token);
|
|
512
|
-
const pattern = normalizedToken.replace(/[\\/]+$/g, "");
|
|
513
|
-
if (!isSafeRelativePath(pattern)) {
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (hasGlobMagic(pattern)) {
|
|
518
|
-
const matchedFiles = allFiles.filter((file) => matchesFilterPattern(file, pattern));
|
|
519
|
-
applySelection(selected, matchedFiles, exclude);
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const directoryFiles = selectDirectoryFiles(allFiles, pattern);
|
|
524
|
-
if (directoryFiles.length > 0) {
|
|
525
|
-
applySelection(selected, directoryFiles, exclude);
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (isExtensionEntrypointPath(pattern)) {
|
|
530
|
-
applySelection(selected, [pattern], exclude);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
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
|
+
);
|
|
535
489
|
}
|
|
536
490
|
|
|
537
491
|
export async function readPackageManifest(
|
|
@@ -612,11 +566,19 @@ export async function discoverPackageExtensions(
|
|
|
612
566
|
cwd: string
|
|
613
567
|
): Promise<PackageExtensionEntry[]> {
|
|
614
568
|
const entries: PackageExtensionEntry[] = [];
|
|
569
|
+
const [globalFilterMap, projectFilterMap] = await Promise.all([
|
|
570
|
+
readPackageFilterMap("global", cwd),
|
|
571
|
+
readPackageFilterMap("project", cwd),
|
|
572
|
+
]);
|
|
615
573
|
|
|
616
574
|
for (const pkg of packages) {
|
|
617
575
|
const packageRoot = await toPackageRoot(pkg, cwd);
|
|
618
576
|
if (!packageRoot) continue;
|
|
619
577
|
|
|
578
|
+
const packageFilters =
|
|
579
|
+
(pkg.scope === "global" ? globalFilterMap : projectFilterMap).get(
|
|
580
|
+
normalizeSource(pkg.source)
|
|
581
|
+
) ?? undefined;
|
|
620
582
|
const extensionPaths = await discoverPackageExtensionEntrypoints(packageRoot);
|
|
621
583
|
for (const extensionPath of extensionPaths) {
|
|
622
584
|
const normalizedPath = normalizeRelativePath(extensionPath);
|
|
@@ -624,7 +586,7 @@ export async function discoverPackageExtensions(
|
|
|
624
586
|
const summary = (await fileExists(absolutePath))
|
|
625
587
|
? await readSummary(absolutePath)
|
|
626
588
|
: "package extension";
|
|
627
|
-
const state =
|
|
589
|
+
const state = getPackageFilterState(packageFilters, normalizedPath);
|
|
628
590
|
|
|
629
591
|
entries.push({
|
|
630
592
|
id: `pkg-ext:${pkg.scope}:${pkg.source}:${normalizedPath}`,
|