pi-extmgr 0.0.5 → 0.1.1
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 +64 -3
- package/package.json +4 -4
- package/src/constants.ts +7 -0
- package/src/extensions/discovery.ts +176 -0
- package/src/index.ts +261 -0
- package/src/packages/discovery.ts +324 -0
- package/src/packages/install.ts +389 -0
- package/src/packages/management.ts +277 -0
- package/src/types/index.ts +79 -0
- package/src/ui/help.ts +46 -0
- package/src/ui/remote.ts +264 -0
- package/src/ui/theme.ts +131 -0
- package/src/ui/unified.ts +469 -0
- package/src/utils/cache.ts +284 -0
- package/src/utils/format.ts +52 -0
- package/src/utils/fs.ts +70 -0
- package/src/utils/history.ts +211 -0
- package/index.ts +0 -2082
package/README.md
CHANGED
|
@@ -27,16 +27,17 @@
|
|
|
27
27
|
All your extensions in one place:
|
|
28
28
|
|
|
29
29
|
- **Local extensions**: `● enabled` / `○ disabled` with `[G]` global or `[P]` project scope
|
|
30
|
-
- **Installed packages**:
|
|
30
|
+
- **Installed packages**: `◆` npm / `◇` git icon with name@version and size info
|
|
31
31
|
- **Visual distinction** between toggle-able locals and action-based packages
|
|
32
32
|
- **Smart deduplication** - packages already managed as local extensions are hidden
|
|
33
|
+
- **Theme adaptive UI** - Works consistently in dark and light themes
|
|
33
34
|
|
|
34
35
|
### 🔍 Smart Package Discovery
|
|
35
36
|
|
|
36
37
|
- **Browse community packages** with pagination (20 per page)
|
|
37
|
-
- **Cached search results** for
|
|
38
|
+
- **Cached search results** - npm metadata cached for 24 hours for fast navigation
|
|
38
39
|
- **Keyword filtering** - automatically shows `pi-package` tagged npm packages
|
|
39
|
-
- **Detailed package info** - view version, author, homepage
|
|
40
|
+
- **Detailed package info** - view version, author, homepage, and install size
|
|
40
41
|
|
|
41
42
|
### 📦 Flexible Installation
|
|
42
43
|
|
|
@@ -56,6 +57,15 @@ All your extensions in one place:
|
|
|
56
57
|
- **Bulk operations** - update all packages at once
|
|
57
58
|
- **Scope indicators**: Global (G) vs Project (P) for all items
|
|
58
59
|
|
|
60
|
+
### 📊 Extension Change History
|
|
61
|
+
|
|
62
|
+
Track all your extension management actions:
|
|
63
|
+
|
|
64
|
+
- **Automatic logging** - Every toggle, install, update, and remove is recorded
|
|
65
|
+
- **Session persistence** - View change history with `/extensions history`
|
|
66
|
+
- **Statistics** - View totals by action type with `/extensions stats`
|
|
67
|
+
- **Error tracking** - Failed operations are logged with error details
|
|
68
|
+
|
|
59
69
|
### 🎯 Quality of Life
|
|
60
70
|
|
|
61
71
|
- **Tab autocomplete** for all subcommands
|
|
@@ -63,6 +73,7 @@ All your extensions in one place:
|
|
|
63
73
|
- **Keyboard shortcut**: `Ctrl+Shift+E` opens extension manager
|
|
64
74
|
- **Non-interactive mode** - works in scripts and CI
|
|
65
75
|
- **Parallel data loading** - local extensions and packages fetched simultaneously
|
|
76
|
+
- **Metadata caching** - npm package info cached for 24 hours for faster browsing
|
|
66
77
|
|
|
67
78
|
## 🚀 Installation
|
|
68
79
|
|
|
@@ -145,6 +156,11 @@ Browse and install from npm:
|
|
|
145
156
|
/extensions install <source> # Install from npm/git/path
|
|
146
157
|
/extensions remove [source] # Remove package (interactive if no source)
|
|
147
158
|
/extensions uninstall [source]# Alias for remove
|
|
159
|
+
|
|
160
|
+
# History & Stats
|
|
161
|
+
/extensions history # View recent extension changes
|
|
162
|
+
/extensions stats # View extension manager statistics
|
|
163
|
+
/extensions clear-cache # Clear metadata cache
|
|
148
164
|
```
|
|
149
165
|
|
|
150
166
|
### Install Sources
|
|
@@ -299,6 +315,16 @@ export default function myTheme(pi: ExtensionAPI) {
|
|
|
299
315
|
|
|
300
316
|
The extension remains installed but won't load until re-enabled.
|
|
301
317
|
|
|
318
|
+
### Removing a Package
|
|
319
|
+
|
|
320
|
+
1. Type `/extensions` to open unified manager
|
|
321
|
+
2. Navigate to the installed package with `↑↓`
|
|
322
|
+
3. Press `A` for actions
|
|
323
|
+
4. Select "Remove package"
|
|
324
|
+
5. Confirm removal
|
|
325
|
+
|
|
326
|
+
**Important**: Unlike install/update, removing a package requires a **full restart** of pi (not just `/reload`). The extension files are deleted, but the extension remains loaded in memory until you exit and restart pi.
|
|
327
|
+
|
|
302
328
|
### Updating All Packages
|
|
303
329
|
|
|
304
330
|
1. Type `/extensions` to open unified manager
|
|
@@ -307,6 +333,32 @@ The extension remains installed but won't load until re-enabled.
|
|
|
307
333
|
4. Select "Update package"
|
|
308
334
|
5. Or use command: `/extensions install npm:pi-extmgr` then select "[Update all packages]"
|
|
309
335
|
|
|
336
|
+
### Viewing Change History
|
|
337
|
+
|
|
338
|
+
See what you've done in the current session:
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
/extensions history
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Output shows recent actions like:
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
[12:34:56] ✓ global:my-extension: enabled → disabled
|
|
348
|
+
[12:35:10] ✓ Installed pi-some-package@1.2.3
|
|
349
|
+
[12:36:22] ✓ Updated pi-other-package → @2.0.0
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Viewing Statistics
|
|
353
|
+
|
|
354
|
+
Get a summary of your extension management activity:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
/extensions stats
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Shows installed package count, total changes, and breakdown by action type.
|
|
361
|
+
|
|
310
362
|
## 🐛 Troubleshooting
|
|
311
363
|
|
|
312
364
|
### Commands not showing after install
|
|
@@ -330,6 +382,15 @@ Check that the file has a `.ts` or `.js` extension and is in one of the discover
|
|
|
330
382
|
- For git installs, ensure git is available
|
|
331
383
|
- Verify the package has the `pi-package` keyword for browsing
|
|
332
384
|
|
|
385
|
+
### Commands still work after removing package
|
|
386
|
+
|
|
387
|
+
This is expected behavior. When you remove a package:
|
|
388
|
+
|
|
389
|
+
1. The files are deleted from disk
|
|
390
|
+
2. The extension remains loaded in pi's memory
|
|
391
|
+
3. Commands continue to work until you **restart pi**\n
|
|
392
|
+
A full restart (Ctrl+C or `/quit`) is required to fully unload removed extensions. `/reload` is only sufficient for install/update operations.
|
|
393
|
+
|
|
333
394
|
### Back to manager closes everything
|
|
334
395
|
|
|
335
396
|
Fixed! Pressing "Back to manager" now correctly returns to the unified view instead of closing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extmgr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Enhanced UX for managing local Pi extensions and community packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"pi-extension-manager"
|
|
10
10
|
],
|
|
11
11
|
"type": "module",
|
|
12
|
-
"main": "./index.ts",
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
13
|
"files": [
|
|
14
|
-
"
|
|
14
|
+
"src/",
|
|
15
15
|
"README.md"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"pi": {
|
|
29
29
|
"extensions": [
|
|
30
|
-
"./index.ts"
|
|
30
|
+
"./src/index.ts"
|
|
31
31
|
],
|
|
32
32
|
"video": "https://github.com/ayagmar/pi-extmgr/releases/download/v0.0.2/Screencast_20260207_013142.mp4",
|
|
33
33
|
"image": "https://i.imgur.com/nP5rJPC.png"
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local extension discovery
|
|
3
|
+
*/
|
|
4
|
+
import { readdir, rename } from "node:fs/promises";
|
|
5
|
+
import { join, relative } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import type { Dirent } from "node:fs";
|
|
8
|
+
import type { ExtensionEntry, Scope, State } from "../types/index.js";
|
|
9
|
+
import { DISABLED_SUFFIX } from "../constants.js";
|
|
10
|
+
import { fileExists, readSummary } from "../utils/fs.js";
|
|
11
|
+
|
|
12
|
+
interface RootConfig {
|
|
13
|
+
root: string;
|
|
14
|
+
scope: Scope;
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function discoverExtensions(cwd: string): Promise<ExtensionEntry[]> {
|
|
19
|
+
const roots: RootConfig[] = [
|
|
20
|
+
{
|
|
21
|
+
root: join(homedir(), ".pi", "agent", "extensions"),
|
|
22
|
+
scope: "global",
|
|
23
|
+
label: "~/.pi/agent/extensions",
|
|
24
|
+
},
|
|
25
|
+
{ root: join(cwd, ".pi", "extensions"), scope: "project", label: ".pi/extensions" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const all: ExtensionEntry[] = [];
|
|
29
|
+
for (const root of roots) {
|
|
30
|
+
all.push(...(await discoverInRoot(root.root, root.scope, root.label)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
all.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
34
|
+
return dedupeExtensions(all);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function discoverInRoot(
|
|
38
|
+
root: string,
|
|
39
|
+
scope: Scope,
|
|
40
|
+
label: string
|
|
41
|
+
): Promise<ExtensionEntry[]> {
|
|
42
|
+
let dirEntries: Dirent[];
|
|
43
|
+
try {
|
|
44
|
+
dirEntries = await readdir(root, { withFileTypes: true });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Silently ignore ENOENT (directory doesn't exist) - this is expected
|
|
47
|
+
// for project scope when .pi/extensions doesn't exist yet
|
|
48
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
// Log other errors for debugging
|
|
52
|
+
console.error(`[extensions-manager] Error reading ${root}:`, error);
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const found: ExtensionEntry[] = [];
|
|
57
|
+
|
|
58
|
+
for (const item of dirEntries) {
|
|
59
|
+
const name = item.name;
|
|
60
|
+
|
|
61
|
+
// Skip hidden files and directories (e.g., .temp, .git, etc.)
|
|
62
|
+
if (name.startsWith(".")) continue;
|
|
63
|
+
|
|
64
|
+
if (item.isFile()) {
|
|
65
|
+
const entry = await parseTopLevelFile(root, label, scope, name);
|
|
66
|
+
if (entry) found.push(entry);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (item.isDirectory()) {
|
|
71
|
+
const entry = await parseDirectoryIndex(root, label, scope, name);
|
|
72
|
+
if (entry) found.push(entry);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return found;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function parseTopLevelFile(
|
|
80
|
+
root: string,
|
|
81
|
+
label: string,
|
|
82
|
+
scope: Scope,
|
|
83
|
+
fileName: string
|
|
84
|
+
): Promise<ExtensionEntry | undefined> {
|
|
85
|
+
const isEnabledTsJs = /\.(ts|js)$/i.test(fileName) && !fileName.endsWith(DISABLED_SUFFIX);
|
|
86
|
+
const isDisabledTsJs = /\.(ts|js)\.disabled$/i.test(fileName);
|
|
87
|
+
|
|
88
|
+
if (!isEnabledTsJs && !isDisabledTsJs) return undefined;
|
|
89
|
+
|
|
90
|
+
const currentPath = join(root, fileName);
|
|
91
|
+
const activePath = isDisabledTsJs ? currentPath.slice(0, -DISABLED_SUFFIX.length) : currentPath;
|
|
92
|
+
const disabledPath = `${activePath}${DISABLED_SUFFIX}`;
|
|
93
|
+
const state: State = isDisabledTsJs ? "disabled" : "enabled";
|
|
94
|
+
const summary = await readSummary(state === "enabled" ? activePath : disabledPath);
|
|
95
|
+
|
|
96
|
+
const relativePath = relative(root, activePath).replace(/\.disabled$/i, "");
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
id: `${scope}:${activePath}`,
|
|
100
|
+
scope,
|
|
101
|
+
state,
|
|
102
|
+
activePath,
|
|
103
|
+
disabledPath,
|
|
104
|
+
displayName: `${label}/${relativePath}`,
|
|
105
|
+
summary,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function parseDirectoryIndex(
|
|
110
|
+
root: string,
|
|
111
|
+
label: string,
|
|
112
|
+
scope: Scope,
|
|
113
|
+
dirName: string
|
|
114
|
+
): Promise<ExtensionEntry | undefined> {
|
|
115
|
+
const dir = join(root, dirName);
|
|
116
|
+
|
|
117
|
+
for (const ext of [".ts", ".js"]) {
|
|
118
|
+
const activePath = join(dir, `index${ext}`);
|
|
119
|
+
const disabledPath = `${activePath}${DISABLED_SUFFIX}`;
|
|
120
|
+
|
|
121
|
+
if (await fileExists(activePath)) {
|
|
122
|
+
return {
|
|
123
|
+
id: `${scope}:${activePath}`,
|
|
124
|
+
scope,
|
|
125
|
+
state: "enabled",
|
|
126
|
+
activePath,
|
|
127
|
+
disabledPath,
|
|
128
|
+
displayName: `${label}/${dirName}/index${ext}`,
|
|
129
|
+
summary: await readSummary(activePath),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (await fileExists(disabledPath)) {
|
|
134
|
+
return {
|
|
135
|
+
id: `${scope}:${activePath}`,
|
|
136
|
+
scope,
|
|
137
|
+
state: "disabled",
|
|
138
|
+
activePath,
|
|
139
|
+
disabledPath,
|
|
140
|
+
displayName: `${label}/${dirName}/index${ext}`,
|
|
141
|
+
summary: await readSummary(disabledPath),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function dedupeExtensions(entries: ExtensionEntry[]): ExtensionEntry[] {
|
|
150
|
+
const byId = new Map<string, ExtensionEntry>();
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!byId.has(entry.id)) {
|
|
153
|
+
byId.set(entry.id, entry);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return Array.from(byId.values());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function setExtensionState(
|
|
160
|
+
entry: Pick<ExtensionEntry, "activePath" | "disabledPath">,
|
|
161
|
+
target: State
|
|
162
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
163
|
+
try {
|
|
164
|
+
if (!entry.activePath || !entry.disabledPath) {
|
|
165
|
+
return { ok: false, error: "Missing paths" };
|
|
166
|
+
}
|
|
167
|
+
if (target === "enabled") {
|
|
168
|
+
await rename(entry.disabledPath, entry.activePath);
|
|
169
|
+
} else {
|
|
170
|
+
await rename(entry.activePath, entry.disabledPath);
|
|
171
|
+
}
|
|
172
|
+
return { ok: true };
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
175
|
+
}
|
|
176
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extensions Manager - Enhanced UI/UX for managing Pi extensions and packages
|
|
3
|
+
*
|
|
4
|
+
* Entry point - exports the main extension function
|
|
5
|
+
*/
|
|
6
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import type { AutocompleteItem } from "@mariozechner/pi-tui";
|
|
8
|
+
import { isPackageSource } from "./utils/format.js";
|
|
9
|
+
import { showInteractive, showListOnly, showInstalledPackagesLegacy } from "./ui/unified.js";
|
|
10
|
+
import { showRemote } from "./ui/remote.js";
|
|
11
|
+
import { showHelp } from "./ui/help.js";
|
|
12
|
+
import { installPackage } from "./packages/install.js";
|
|
13
|
+
import { removePackage, promptRemove, showInstalledPackagesList } from "./packages/management.js";
|
|
14
|
+
import { getInstalledPackages } from "./packages/discovery.js";
|
|
15
|
+
import { getRecentChanges, formatChangeEntry, getChangeStats } from "./utils/history.js";
|
|
16
|
+
import { clearCache } from "./utils/cache.js";
|
|
17
|
+
|
|
18
|
+
export default function extensionsManager(pi: ExtensionAPI) {
|
|
19
|
+
pi.registerCommand("extensions", {
|
|
20
|
+
description: "Manage local extensions and browse/install community packages",
|
|
21
|
+
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
|
|
22
|
+
const commands = [
|
|
23
|
+
{ value: "list", description: "List local extensions" },
|
|
24
|
+
{ value: "local", description: "Open interactive manager (default)" },
|
|
25
|
+
{ value: "remote", description: "Browse community packages" },
|
|
26
|
+
{ value: "packages", description: "Browse community packages (alias)" },
|
|
27
|
+
{ value: "installed", description: "List installed packages" },
|
|
28
|
+
{ value: "search", description: "Search npm for packages" },
|
|
29
|
+
{ value: "install", description: "Install a package" },
|
|
30
|
+
{ value: "remove", description: "Remove an installed package" },
|
|
31
|
+
{ value: "uninstall", description: "Remove an installed package (alias)" },
|
|
32
|
+
{ value: "history", description: "View extension change history" },
|
|
33
|
+
{ value: "stats", description: "View extension manager statistics" },
|
|
34
|
+
{ value: "clear-cache", description: "Clear metadata cache" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const safePrefix = (prefix ?? "").toLowerCase();
|
|
38
|
+
const filtered = commands.filter(
|
|
39
|
+
(c) => c.value.startsWith(safePrefix) || c.description.toLowerCase().includes(safePrefix)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return filtered.length > 0
|
|
43
|
+
? filtered.map((c) => ({ value: c.value, label: `${c.value} - ${c.description}` }))
|
|
44
|
+
: null;
|
|
45
|
+
},
|
|
46
|
+
handler: async (args, ctx) => {
|
|
47
|
+
// Check if we have UI support
|
|
48
|
+
if (!ctx.hasUI) {
|
|
49
|
+
await handleNonInteractive(args, ctx, pi);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const input = args.trim();
|
|
54
|
+
const [subcommand, ...rest] = input.split(/\s+/).filter(Boolean);
|
|
55
|
+
const sub = (subcommand ?? "").toLowerCase();
|
|
56
|
+
|
|
57
|
+
switch (sub) {
|
|
58
|
+
case "":
|
|
59
|
+
case "local":
|
|
60
|
+
await showInteractive(ctx, pi);
|
|
61
|
+
break;
|
|
62
|
+
case "list":
|
|
63
|
+
await showListOnly(ctx);
|
|
64
|
+
break;
|
|
65
|
+
case "remote":
|
|
66
|
+
case "packages":
|
|
67
|
+
await showRemote(rest.join(" "), ctx, pi);
|
|
68
|
+
break;
|
|
69
|
+
case "installed":
|
|
70
|
+
await showInstalledPackagesLegacy(ctx, pi);
|
|
71
|
+
break;
|
|
72
|
+
case "search":
|
|
73
|
+
await showRemote(`search ${rest.join(" ")}`, ctx, pi);
|
|
74
|
+
break;
|
|
75
|
+
case "install":
|
|
76
|
+
if (rest.length > 0) {
|
|
77
|
+
await installPackage(rest.join(" "), ctx, pi);
|
|
78
|
+
} else {
|
|
79
|
+
await showRemote("install", ctx, pi);
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
case "remove":
|
|
83
|
+
case "uninstall":
|
|
84
|
+
if (rest.length > 0) {
|
|
85
|
+
await removePackage(rest.join(" "), ctx, pi);
|
|
86
|
+
} else {
|
|
87
|
+
await promptRemove(ctx, pi);
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
case "history":
|
|
91
|
+
showHistory(ctx, pi);
|
|
92
|
+
break;
|
|
93
|
+
case "stats":
|
|
94
|
+
await showStats(ctx, pi);
|
|
95
|
+
break;
|
|
96
|
+
case "clear-cache":
|
|
97
|
+
await clearMetadataCache(ctx, pi);
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
// If it looks like a package source, try to install it
|
|
101
|
+
if (subcommand && isPackageSource(subcommand)) {
|
|
102
|
+
await installPackage(input, ctx, pi);
|
|
103
|
+
} else {
|
|
104
|
+
ctx.ui.notify(
|
|
105
|
+
`Unknown command: ${subcommand ?? "(empty)"}. Try: local, remote, installed, search, install, remove`,
|
|
106
|
+
"warning"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Status bar integration - show installed package count
|
|
114
|
+
pi.on("session_start", (_event, ctx) => {
|
|
115
|
+
if (!ctx.hasUI) return;
|
|
116
|
+
|
|
117
|
+
// Defer status update to avoid interfering with extension loading lifecycle
|
|
118
|
+
// This prevents race conditions during reload where commands might not appear
|
|
119
|
+
setImmediate(() => {
|
|
120
|
+
void (async () => {
|
|
121
|
+
try {
|
|
122
|
+
const packages = await getInstalledPackages(ctx, pi);
|
|
123
|
+
if (packages.length > 0) {
|
|
124
|
+
ctx.ui.setStatus(
|
|
125
|
+
"extmgr",
|
|
126
|
+
ctx.ui.theme.fg("dim", `${packages.length} pkg${packages.length === 1 ? "" : "s"}`)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Silently ignore status bar errors
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function handleNonInteractive(
|
|
138
|
+
args: string,
|
|
139
|
+
ctx: ExtensionCommandContext,
|
|
140
|
+
pi: ExtensionAPI
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
const input = args.trim();
|
|
143
|
+
const [subcommand, ...rest] = input.split(/\s+/).filter(Boolean);
|
|
144
|
+
const sub = (subcommand ?? "").toLowerCase();
|
|
145
|
+
|
|
146
|
+
switch (sub) {
|
|
147
|
+
case "list":
|
|
148
|
+
await showListOnly(ctx);
|
|
149
|
+
break;
|
|
150
|
+
case "installed":
|
|
151
|
+
await showInstalledPackagesList(ctx, pi);
|
|
152
|
+
break;
|
|
153
|
+
case "remote":
|
|
154
|
+
case "packages":
|
|
155
|
+
// Non-interactive: just show help
|
|
156
|
+
console.log("Extensions Manager (non-interactive mode)");
|
|
157
|
+
console.log("Remote package browsing requires interactive mode.");
|
|
158
|
+
console.log("");
|
|
159
|
+
console.log("Available commands:");
|
|
160
|
+
console.log(" /extensions list - List local extensions");
|
|
161
|
+
console.log(" /extensions installed - List installed packages");
|
|
162
|
+
console.log(" /extensions install <source> - Install a package");
|
|
163
|
+
console.log(" /extensions remove <source> - Remove a package");
|
|
164
|
+
break;
|
|
165
|
+
case "search":
|
|
166
|
+
console.log("Search requires interactive mode.");
|
|
167
|
+
break;
|
|
168
|
+
case "install":
|
|
169
|
+
if (rest.length > 0) {
|
|
170
|
+
await installPackage(rest.join(" "), ctx, pi);
|
|
171
|
+
} else {
|
|
172
|
+
console.log("Usage: /extensions install <npm:package|git:url|path>");
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
case "remove":
|
|
176
|
+
case "uninstall":
|
|
177
|
+
if (rest.length > 0) {
|
|
178
|
+
await removePackage(rest.join(" "), ctx, pi);
|
|
179
|
+
} else {
|
|
180
|
+
console.log("Usage: /extensions remove <npm:package|git:url|path>");
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
default:
|
|
184
|
+
// If it looks like a package source, try to install it
|
|
185
|
+
if (subcommand && isPackageSource(subcommand)) {
|
|
186
|
+
await installPackage(input, ctx, pi);
|
|
187
|
+
} else {
|
|
188
|
+
console.log("Extensions Manager (non-interactive mode)");
|
|
189
|
+
console.log("");
|
|
190
|
+
console.log("Commands:");
|
|
191
|
+
console.log(" /extensions list - List local extensions");
|
|
192
|
+
console.log(" /extensions installed - List installed packages");
|
|
193
|
+
console.log("");
|
|
194
|
+
console.log("For full functionality, run in interactive mode.");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export { showHelp };
|
|
200
|
+
|
|
201
|
+
function showHistory(ctx: ExtensionCommandContext, _pi: ExtensionAPI): void {
|
|
202
|
+
const changes = getRecentChanges(ctx, 20);
|
|
203
|
+
|
|
204
|
+
if (changes.length === 0) {
|
|
205
|
+
const msg = "No extension changes recorded in this session.";
|
|
206
|
+
if (ctx.hasUI) {
|
|
207
|
+
ctx.ui.notify(msg, "info");
|
|
208
|
+
} else {
|
|
209
|
+
console.log(msg);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const lines = changes.map(formatChangeEntry);
|
|
215
|
+
const output = ["Extension Change History (recent 20):", "", ...lines].join("\n");
|
|
216
|
+
|
|
217
|
+
if (ctx.hasUI) {
|
|
218
|
+
ctx.ui.notify(output, "info");
|
|
219
|
+
} else {
|
|
220
|
+
console.log(output);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function showStats(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
225
|
+
const stats = getChangeStats(ctx);
|
|
226
|
+
const packages = await getInstalledPackages(ctx, pi);
|
|
227
|
+
|
|
228
|
+
const lines = [
|
|
229
|
+
"Extension Manager Statistics",
|
|
230
|
+
"",
|
|
231
|
+
`Installed packages: ${packages.length}`,
|
|
232
|
+
`Session changes: ${stats.total}`,
|
|
233
|
+
` - Successful: ${stats.successful}`,
|
|
234
|
+
` - Failed: ${stats.failed}`,
|
|
235
|
+
"",
|
|
236
|
+
"Changes by type:",
|
|
237
|
+
` - Extension toggles: ${stats.byAction.extension_toggle}`,
|
|
238
|
+
` - Package installs: ${stats.byAction.package_install}`,
|
|
239
|
+
` - Package updates: ${stats.byAction.package_update}`,
|
|
240
|
+
` - Package removals: ${stats.byAction.package_remove}`,
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const output = lines.join("\n");
|
|
244
|
+
|
|
245
|
+
if (ctx.hasUI) {
|
|
246
|
+
ctx.ui.notify(output, "info");
|
|
247
|
+
} else {
|
|
248
|
+
console.log(output);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function clearMetadataCache(ctx: ExtensionCommandContext, _pi: ExtensionAPI): Promise<void> {
|
|
253
|
+
await clearCache();
|
|
254
|
+
|
|
255
|
+
const msg = "Metadata cache cleared.";
|
|
256
|
+
if (ctx.hasUI) {
|
|
257
|
+
ctx.ui.notify(msg, "info");
|
|
258
|
+
} else {
|
|
259
|
+
console.log(msg);
|
|
260
|
+
}
|
|
261
|
+
}
|