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.
Files changed (41) hide show
  1. package/README.md +21 -10
  2. package/package.json +21 -16
  3. package/src/commands/auto-update.ts +5 -5
  4. package/src/commands/cache.ts +1 -1
  5. package/src/commands/history.ts +5 -34
  6. package/src/commands/install.ts +2 -2
  7. package/src/commands/registry.ts +7 -7
  8. package/src/commands/types.ts +1 -1
  9. package/src/constants.ts +0 -8
  10. package/src/extensions/discovery.ts +125 -42
  11. package/src/index.ts +15 -15
  12. package/src/packages/catalog.ts +9 -8
  13. package/src/packages/discovery.ts +56 -19
  14. package/src/packages/extensions.ts +65 -103
  15. package/src/packages/install.ts +104 -74
  16. package/src/packages/management.ts +78 -65
  17. package/src/types/index.ts +20 -11
  18. package/src/ui/async-task.ts +101 -65
  19. package/src/ui/footer.ts +47 -31
  20. package/src/ui/help.ts +17 -13
  21. package/src/ui/package-config.ts +36 -48
  22. package/src/ui/remote.ts +714 -119
  23. package/src/ui/theme.ts +2 -2
  24. package/src/ui/unified.ts +964 -371
  25. package/src/utils/auto-update.ts +44 -39
  26. package/src/utils/cache.ts +208 -37
  27. package/src/utils/command.ts +1 -1
  28. package/src/utils/duration.ts +132 -0
  29. package/src/utils/format.ts +4 -33
  30. package/src/utils/fs.ts +8 -4
  31. package/src/utils/history.ts +47 -9
  32. package/src/utils/mode.ts +2 -2
  33. package/src/utils/notify.ts +1 -15
  34. package/src/utils/npm-exec.ts +1 -1
  35. package/src/utils/package-source.ts +35 -7
  36. package/src/utils/path-identity.ts +7 -0
  37. package/src/utils/relative-path-selection.ts +100 -0
  38. package/src/utils/settings.ts +11 -61
  39. package/src/utils/status.ts +12 -10
  40. package/src/utils/ui-helpers.ts +2 -2
  41. 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 type {
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 { readSummary } from "../utils/fs.js";
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
- return normalizePackageIdentity(
206
- pkg.source,
207
- pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : undefined
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 type { InstalledPackage, PackageExtensionEntry, Scope, State } from "../types/index.js";
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, extensions: [] };
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
- extensions: Array.isArray(existing.extensions) ? [...existing.extensions] : [],
247
+ ...(Array.isArray(existing.extensions) ? { extensions: [...existing.extensions] } : {}),
243
248
  };
244
249
  }
245
250
 
246
- return { source: packageSource, extensions: [] };
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
- nextTokens.push(`${target === "enabled" ? "+" : "-"}${extensionPath}`);
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(packageEntry);
338
+ packages.push(normalizedPackageEntry);
322
339
  } else {
323
- packages[index] = packageEntry;
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 getPackageExtensionState(
414
- packageSource: string,
415
- extensionPath: string,
414
+ async function readPackageFilterMap(
416
415
  scope: Scope,
417
416
  cwd: string
418
- ): Promise<State> {
419
- const settingsPath = getSettingsPath(scope, cwd);
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 normalizedSource = normalizeSource(packageSource);
420
+ const filterMap = new Map<string, string[] | undefined>();
423
421
 
424
- const entry = packages.find((pkg) => {
425
- if (typeof pkg === "string") {
426
- return normalizeSource(pkg) === normalizedSource;
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
- if (!entry || typeof entry === "string") {
432
- return "enabled";
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 getPackageFilterState(entry.extensions, extensionPath);
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
- for (const rawToken of entries) {
507
- const token = rawToken.trim();
508
- if (!token) continue;
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 = await getPackageExtensionState(pkg.source, normalizedPath, pkg.scope, cwd);
589
+ const state = getPackageFilterState(packageFilters, normalizedPath);
628
590
 
629
591
  entries.push({
630
592
  id: `pkg-ext:${pkg.scope}:${pkg.source}:${normalizedPath}`,