pi-extmgr 0.1.26 → 0.1.28

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 CHANGED
@@ -6,6 +6,7 @@
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
8
  A better way to manage Pi extensions. Browse, install, enable/disable, and remove extensions from one place.
9
+ Built on top of Pi's native package install, update, and config flows, so extmgr stays aligned with current upstream behavior.
9
10
 
10
11
  **🌐 [pi-extmgr landing page](https://ayagmar.github.io/pi-extmgr)**
11
12
 
@@ -15,9 +16,9 @@ A better way to manage Pi extensions. Browse, install, enable/disable, and remov
15
16
  pi install npm:pi-extmgr
16
17
  ```
17
18
 
18
- Then reload Pi.
19
+ If Pi is already running, use `/reload`.
19
20
 
20
- Requires Node.js `>=22.5.0`.
21
+ Requires Node.js `>=22`.
21
22
 
22
23
  ## Features
23
24
 
@@ -38,10 +39,11 @@ Requires Node.js `>=22.5.0`.
38
39
  - npm search/browse with pagination
39
40
  - Install by source (`npm:`, `git:`, `https://`, `ssh://`, `git@...`, local path)
40
41
  - Supports direct GitHub `.ts` installs and standalone local install for self-contained packages
42
+ - Long-running discovery/detail screens now show dedicated loading UI, and cancellable reads can be aborted with `Esc`
41
43
  - **Auto-update**
42
44
  - Interactive wizard (`t` in manager, or `/extensions auto-update`)
43
45
  - Persistent schedule restored on startup and session switch
44
- - Background checks + status bar updates for installed npm packages
46
+ - Background checks + status bar updates for installed npm + git packages
45
47
  - **Operational visibility**
46
48
  - Session history (`/extensions history`)
47
49
  - Cache controls (`/extensions clear-cache` clears persistent + runtime extmgr caches)
@@ -154,12 +156,12 @@ Examples:
154
156
 
155
157
  - **Staged local changes**: Toggle local extensions on/off, then press `S` to apply all at once.
156
158
  - **Package extension config**: Select a package and press `c` (or Enter/A → Configure) to enable/disable individual package entrypoints.
157
- - After saving package extension config, restart pi to fully apply changes.
159
+ - After saving package extension config, run /reload to apply changes.
158
160
  - **Two install modes**:
159
161
  - **Managed** (npm): Auto-updates with `pi update`, stored in pi's package cache, supports Pi package manifest/convention loading
160
162
  - **Local** (standalone): Copies to `~/.pi/agent/extensions/{package}/`, so it only accepts runnable standalone layouts (manifest-declared/root entrypoints), requires `tar` on `PATH`, and rejects packages whose runtime `dependencies` are not already bundled with the package contents
161
163
  - **Auto-update schedule is persistent**: `/extensions auto-update 1d` stays active across future Pi sessions and is restored when switching sessions.
162
- - **Auto-update coverage is npm-only today**: extmgr checks update availability for managed npm packages; git/local installs are not included in the background update badge yet.
164
+ - **Auto-update/update badges cover npm + git packages**: extmgr now uses pi's package manager APIs for structured update detection instead of parsing `pi list` output.
163
165
  - **Settings/cache writes are hardened**: extmgr serializes writes and uses safe file replacement to reduce JSON corruption issues.
164
166
  - **Invalid JSON is handled safely**: malformed `auto-update.json` / metadata cache files are backed up and reset; invalid `.pi/settings.json` is not overwritten during package-extension toggles.
165
167
  - **Reload is built-in**: When extmgr asks to reload, it calls `ctx.reload()` directly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extmgr",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Enhanced UX for managing local Pi extensions and community packages",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -19,14 +19,14 @@
19
19
  "README.md"
20
20
  ],
21
21
  "scripts": {
22
- "lint": "eslint . --max-warnings=0",
23
- "lint:fix": "eslint . --fix",
24
- "format": "prettier --write .",
25
- "format:check": "prettier --check .",
22
+ "lint": "biome lint . --error-on-warnings",
23
+ "lint:fix": "biome check --write .",
24
+ "format": "biome format --write .",
25
+ "format:check": "biome format .",
26
26
  "typecheck": "tsc --noEmit -p tsconfig.json",
27
27
  "smoke-test": "node --import=tsx ./scripts/smoke-test.mjs",
28
28
  "test": "node --import=tsx --test ./test/*.test.ts",
29
- "check": "pnpm run typecheck && pnpm run smoke-test && pnpm run test && pnpm run lint && pnpm run format:check",
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
30
  "prepublishOnly": "pnpm run check",
31
31
  "prepare": "husky"
32
32
  },
@@ -42,22 +42,19 @@
42
42
  "@mariozechner/pi-tui": "*"
43
43
  },
44
44
  "devDependencies": {
45
- "@mariozechner/pi-coding-agent": "^0.52.6",
46
- "@mariozechner/pi-tui": "^0.52.6",
47
- "@types/node": "^22.13.10",
48
- "@typescript-eslint/eslint-plugin": "^8.42.0",
49
- "@typescript-eslint/parser": "^8.42.0",
50
- "eslint": "^9.38.0",
51
- "eslint-config-prettier": "^10.1.8",
45
+ "@biomejs/biome": "^2.4.9",
46
+ "@mariozechner/pi-coding-agent": "^0.63.1",
47
+ "@mariozechner/pi-tui": "^0.63.1",
48
+ "@types/node": "^22.19.10",
52
49
  "husky": "^9.1.7",
53
- "prettier": "^3.8.1",
54
- "tsx": "^4.19.3",
50
+ "tsx": "^4.21.0",
55
51
  "typescript": "^5.9.3"
56
52
  },
57
53
  "author": "ayagmar",
58
54
  "license": "MIT",
55
+ "packageManager": "pnpm@10.33.0",
59
56
  "engines": {
60
- "node": ">=22.5.0"
57
+ "node": ">=22"
61
58
  },
62
59
  "repository": {
63
60
  "type": "git",
@@ -1,7 +1,7 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionCommandContext,
4
- ExtensionContext,
1
+ import {
2
+ type ExtensionAPI,
3
+ type ExtensionCommandContext,
4
+ type ExtensionContext,
5
5
  } from "@mariozechner/pi-coding-agent";
6
6
  import {
7
7
  disableAutoUpdate,
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
1
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
2
  import { clearSearchCache } from "../packages/discovery.js";
3
3
  import { clearRemotePackageInfoCache } from "../ui/remote.js";
4
4
  import { clearCache } from "../utils/cache.js";
@@ -1,10 +1,10 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
1
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
2
  import {
3
+ type ChangeAction,
3
4
  formatChangeEntry,
5
+ type HistoryFilters,
4
6
  queryGlobalHistory,
5
7
  querySessionChanges,
6
- type ChangeAction,
7
- type HistoryFilters,
8
8
  } from "../utils/history.js";
9
9
  import { notify } from "../utils/notify.js";
10
10
  import { formatListOutput } from "../utils/ui-helpers.js";
@@ -1,5 +1,5 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
- import { installPackage, type InstallScope } from "../packages/install.js";
1
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import { type InstallScope, installPackage } from "../packages/install.js";
3
3
  import { notify } from "../utils/notify.js";
4
4
 
5
5
  export const INSTALL_USAGE = "Usage: /extensions install <source> [--project|--global]";
@@ -1,7 +1,5 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
- import type { AutocompleteItem } from "@mariozechner/pi-tui";
3
- import { showInteractive, showInstalledPackagesLegacy, showListOnly } from "../ui/unified.js";
4
- import { showRemote } from "../ui/remote.js";
1
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import { type AutocompleteItem } from "@mariozechner/pi-tui";
5
3
  import {
6
4
  promptRemove,
7
5
  removePackage,
@@ -9,12 +7,14 @@ import {
9
7
  updatePackage,
10
8
  updatePackages,
11
9
  } from "../packages/management.js";
10
+ import { showRemote } from "../ui/remote.js";
11
+ import { showInstalledPackagesLegacy, showInteractive, showListOnly } from "../ui/unified.js";
12
12
  import { notify } from "../utils/notify.js";
13
- import { handleInstallSubcommand, INSTALL_USAGE } from "./install.js";
14
- import { handleHistorySubcommand } from "./history.js";
15
13
  import { handleAutoUpdateSubcommand } from "./auto-update.js";
16
14
  import { clearMetadataCacheCommand } from "./cache.js";
17
- import type { CommandDefinition, CommandId } from "./types.js";
15
+ import { handleHistorySubcommand } from "./history.js";
16
+ import { handleInstallSubcommand, INSTALL_USAGE } from "./install.js";
17
+ import { type CommandDefinition, type CommandId } from "./types.js";
18
18
 
19
19
  const REMOVE_USAGE = "Usage: /extensions remove <npm:package|git:url|path>";
20
20
 
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
1
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
2
 
3
3
  export type CommandId =
4
4
  | "local"
@@ -4,12 +4,13 @@
4
4
  * This module handles discovery and management of local Pi extensions
5
5
  * in both global (~/.pi/agent/extensions) and project (.pi/extensions) scopes.
6
6
  */
7
+
8
+ import { type Dirent } from "node:fs";
7
9
  import { readdir, rename, rm } from "node:fs/promises";
8
- import { basename, dirname, join, relative } from "node:path";
9
10
  import { homedir } from "node:os";
10
- import type { Dirent } from "node:fs";
11
- import type { ExtensionEntry, Scope, State } from "../types/index.js";
11
+ import { basename, dirname, join, relative } from "node:path";
12
12
  import { DISABLED_SUFFIX } from "../constants.js";
13
+ import { type ExtensionEntry, type Scope, type State } from "../types/index.js";
13
14
  import { fileExists, readSummary } from "../utils/fs.js";
14
15
 
15
16
  interface RootConfig {
package/src/index.ts CHANGED
@@ -3,21 +3,12 @@
3
3
  *
4
4
  * Entry point - exports the main extension function
5
5
  */
6
- import type {
7
- ExtensionAPI,
8
- ExtensionCommandContext,
9
- ExtensionContext,
10
- } from "@mariozechner/pi-coding-agent";
11
- import { isPackageSource } from "./utils/format.js";
12
- import { installPackage } from "./packages/install.js";
13
- import { tokenizeArgs } from "./utils/command.js";
14
- import { updateExtmgrStatus } from "./utils/status.js";
15
6
  import {
16
- startAutoUpdateTimer,
17
- stopAutoUpdateTimer,
18
- type ContextProvider,
19
- } from "./utils/auto-update.js";
20
- import { hydrateAutoUpdateConfig, getAutoUpdateConfig } from "./utils/settings.js";
7
+ type ExtensionAPI,
8
+ type ExtensionCommandContext,
9
+ type ExtensionContext,
10
+ } from "@mariozechner/pi-coding-agent";
11
+ import { createAutoUpdateNotificationHandler } from "./commands/auto-update.js";
21
12
  import {
22
13
  getExtensionsAutocompleteItems,
23
14
  resolveCommand,
@@ -25,7 +16,16 @@ import {
25
16
  showNonInteractiveHelp,
26
17
  showUnknownCommandMessage,
27
18
  } from "./commands/registry.js";
28
- import { createAutoUpdateNotificationHandler } from "./commands/auto-update.js";
19
+ import { installPackage } from "./packages/install.js";
20
+ import {
21
+ type ContextProvider,
22
+ startAutoUpdateTimer,
23
+ stopAutoUpdateTimer,
24
+ } from "./utils/auto-update.js";
25
+ import { tokenizeArgs } from "./utils/command.js";
26
+ import { isPackageSource } from "./utils/format.js";
27
+ import { getAutoUpdateConfig, hydrateAutoUpdateConfig } from "./utils/settings.js";
28
+ import { updateExtmgrStatus } from "./utils/status.js";
29
29
 
30
30
  async function executeExtensionsCommand(
31
31
  args: string,
@@ -0,0 +1,163 @@
1
+ import {
2
+ DefaultPackageManager,
3
+ getAgentDir,
4
+ type PackageSource,
5
+ type ProgressEvent,
6
+ SettingsManager,
7
+ } from "@mariozechner/pi-coding-agent";
8
+ import { type InstalledPackage, type Scope } from "../types/index.js";
9
+ import { normalizePackageIdentity, parsePackageNameAndVersion } from "../utils/package-source.js";
10
+
11
+ type PiScope = "user" | "project";
12
+ type PiPackageUpdate = Awaited<
13
+ ReturnType<DefaultPackageManager["checkForAvailableUpdates"]>
14
+ >[number];
15
+
16
+ export interface AvailablePackageUpdate {
17
+ source: string;
18
+ displayName: string;
19
+ type: "npm" | "git";
20
+ scope: Scope;
21
+ }
22
+
23
+ export interface PackageCatalog {
24
+ listInstalledPackages(options?: { dedupe?: boolean }): Promise<InstalledPackage[]>;
25
+ checkForAvailableUpdates(): Promise<AvailablePackageUpdate[]>;
26
+ install(source: string, scope: Scope, onProgress?: (event: ProgressEvent) => void): Promise<void>;
27
+ remove(source: string, scope: Scope, onProgress?: (event: ProgressEvent) => void): Promise<void>;
28
+ update(source?: string, onProgress?: (event: ProgressEvent) => void): Promise<void>;
29
+ }
30
+
31
+ type PackageCatalogFactory = (cwd: string) => PackageCatalog;
32
+
33
+ let packageCatalogFactory: PackageCatalogFactory = createDefaultPackageCatalog;
34
+
35
+ function toScope(scope: PiScope): Scope {
36
+ return scope === "project" ? "project" : "global";
37
+ }
38
+
39
+ function getPackageSource(pkg: PackageSource): string {
40
+ return typeof pkg === "string" ? pkg : pkg.source;
41
+ }
42
+
43
+ function createPackageRecord(
44
+ source: string,
45
+ scope: PiScope,
46
+ packageManager: DefaultPackageManager
47
+ ): InstalledPackage {
48
+ const resolvedPath = packageManager.getInstalledPath(source, scope);
49
+ const { name, version } = parsePackageNameAndVersion(source);
50
+
51
+ return {
52
+ source,
53
+ name,
54
+ scope: toScope(scope),
55
+ ...(version ? { version } : {}),
56
+ ...(resolvedPath ? { resolvedPath } : {}),
57
+ };
58
+ }
59
+
60
+ function dedupeInstalledPackages(packages: InstalledPackage[], cwd: string): InstalledPackage[] {
61
+ const byIdentity = new Map<string, InstalledPackage>();
62
+
63
+ for (const pkg of packages) {
64
+ const baseCwd = pkg.scope === "project" ? cwd : getAgentDir();
65
+ const identity = normalizePackageIdentity(pkg.source, {
66
+ ...(pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : {}),
67
+ cwd: baseCwd,
68
+ });
69
+
70
+ if (!byIdentity.has(identity)) {
71
+ byIdentity.set(identity, pkg);
72
+ }
73
+ }
74
+
75
+ return [...byIdentity.values()];
76
+ }
77
+
78
+ function setProgressCallback(
79
+ packageManager: DefaultPackageManager,
80
+ onProgress?: (event: ProgressEvent) => void
81
+ ): void {
82
+ packageManager.setProgressCallback(onProgress);
83
+ }
84
+
85
+ function createDefaultPackageCatalog(cwd: string): PackageCatalog {
86
+ const agentDir = getAgentDir();
87
+ const settingsManager = SettingsManager.create(cwd, agentDir);
88
+ const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
89
+
90
+ return {
91
+ listInstalledPackages(options) {
92
+ const projectPackages = (settingsManager.getProjectSettings().packages ?? []).map((pkg) =>
93
+ createPackageRecord(getPackageSource(pkg), "project", packageManager)
94
+ );
95
+ const globalPackages = (settingsManager.getGlobalSettings().packages ?? []).map((pkg) =>
96
+ createPackageRecord(getPackageSource(pkg), "user", packageManager)
97
+ );
98
+
99
+ const installed = [...projectPackages, ...globalPackages];
100
+ return Promise.resolve(
101
+ options?.dedupe === false ? installed : dedupeInstalledPackages(installed, cwd)
102
+ );
103
+ },
104
+
105
+ async checkForAvailableUpdates() {
106
+ const updates = await packageManager.checkForAvailableUpdates();
107
+ return updates.map((update: PiPackageUpdate) => ({
108
+ source: update.source,
109
+ displayName: update.displayName,
110
+ type: update.type,
111
+ scope: toScope(update.scope),
112
+ }));
113
+ },
114
+
115
+ async install(source, scope, onProgress) {
116
+ setProgressCallback(packageManager, onProgress);
117
+
118
+ try {
119
+ await packageManager.install(source, { local: scope === "project" });
120
+ packageManager.addSourceToSettings(source, { local: scope === "project" });
121
+ await settingsManager.flush();
122
+ } finally {
123
+ setProgressCallback(packageManager, undefined);
124
+ }
125
+ },
126
+
127
+ async remove(source, scope, onProgress) {
128
+ setProgressCallback(packageManager, onProgress);
129
+
130
+ try {
131
+ await packageManager.remove(source, { local: scope === "project" });
132
+ const removed = packageManager.removeSourceFromSettings(source, {
133
+ local: scope === "project",
134
+ });
135
+ await settingsManager.flush();
136
+
137
+ if (!removed) {
138
+ throw new Error(`No matching package found for ${source}`);
139
+ }
140
+ } finally {
141
+ setProgressCallback(packageManager, undefined);
142
+ }
143
+ },
144
+
145
+ async update(source, onProgress) {
146
+ setProgressCallback(packageManager, onProgress);
147
+
148
+ try {
149
+ await packageManager.update(source);
150
+ } finally {
151
+ setProgressCallback(packageManager, undefined);
152
+ }
153
+ },
154
+ };
155
+ }
156
+
157
+ export function getPackageCatalog(cwd: string): PackageCatalog {
158
+ return packageCatalogFactory(cwd);
159
+ }
160
+
161
+ export function setPackageCatalogFactory(factory?: PackageCatalogFactory): void {
162
+ packageCatalogFactory = factory ?? createDefaultPackageCatalog;
163
+ }