pi-extmgr 0.0.2 → 0.0.4
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 +95 -47
- package/index.ts +308 -121
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -17,10 +17,20 @@
|
|
|
17
17
|
### 🎨 Interactive TUI Interface
|
|
18
18
|
|
|
19
19
|
- **Beautiful themed interface** with color-coded status indicators
|
|
20
|
+
- **Unified view** - local extensions and npm/git packages in one screen
|
|
20
21
|
- **Keyboard-driven navigation** - fast and efficient
|
|
21
22
|
- **Real-time previews** with package descriptions
|
|
22
23
|
- **Context-aware help** - press `?` anywhere for shortcuts
|
|
23
24
|
|
|
25
|
+
### 📋 Unified Extension Manager
|
|
26
|
+
|
|
27
|
+
All your extensions in one place:
|
|
28
|
+
|
|
29
|
+
- **Local extensions**: `● enabled` / `○ disabled` with `[G]` global or `[P]` project scope
|
|
30
|
+
- **Installed packages**: `📦` icon with name@version
|
|
31
|
+
- **Visual distinction** between toggle-able locals and action-based packages
|
|
32
|
+
- **Smart deduplication** - packages already managed as local extensions are hidden
|
|
33
|
+
|
|
24
34
|
### 🔍 Smart Package Discovery
|
|
25
35
|
|
|
26
36
|
- **Browse community packages** with pagination (20 per page)
|
|
@@ -41,9 +51,10 @@
|
|
|
41
51
|
### ⚡ Quick Extension Management
|
|
42
52
|
|
|
43
53
|
- **Enable/disable extensions** with staging (preview before applying)
|
|
44
|
-
- **
|
|
54
|
+
- **Package actions** - update/remove/view details without leaving the manager
|
|
55
|
+
- **Visual change indicators** (`*`) show pending modifications
|
|
45
56
|
- **Bulk operations** - update all packages at once
|
|
46
|
-
- **Scope indicators**: Global (G) vs Project (P)
|
|
57
|
+
- **Scope indicators**: Global (G) vs Project (P) for all items
|
|
47
58
|
|
|
48
59
|
### 🎯 Quality of Life
|
|
49
60
|
|
|
@@ -51,7 +62,7 @@
|
|
|
51
62
|
- **Status bar integration** - shows installed package count
|
|
52
63
|
- **Keyboard shortcut**: `Ctrl+Shift+E` opens extension manager
|
|
53
64
|
- **Non-interactive mode** - works in scripts and CI
|
|
54
|
-
- **
|
|
65
|
+
- **Parallel data loading** - local extensions and packages fetched simultaneously
|
|
55
66
|
|
|
56
67
|
## 🚀 Installation
|
|
57
68
|
|
|
@@ -70,26 +81,38 @@ Then reload Pi:
|
|
|
70
81
|
### Interactive Mode (Recommended)
|
|
71
82
|
|
|
72
83
|
```
|
|
73
|
-
/extensions # Open
|
|
84
|
+
/extensions # Open unified interactive manager
|
|
74
85
|
```
|
|
75
86
|
|
|
76
|
-
|
|
87
|
+
The unified view displays:
|
|
77
88
|
|
|
78
|
-
|
|
89
|
+
- **Local extensions** first (toggle-able)
|
|
90
|
+
- **Installed packages** second (action-based)
|
|
91
|
+
- Sorted alphabetically within each group
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
|
|
|
89
|
-
| `
|
|
93
|
+
#### Keyboard Shortcuts
|
|
94
|
+
|
|
95
|
+
| Key | Action |
|
|
96
|
+
| ------------- | --------------------------------------------------- |
|
|
97
|
+
| `↑↓` | Navigate items |
|
|
98
|
+
| `Space/Enter` | Toggle local extension on/off |
|
|
99
|
+
| `S` | Save changes to local extensions |
|
|
100
|
+
| `A` | Actions on selected package (update/remove/details) |
|
|
101
|
+
| `R` | Browse remote packages |
|
|
102
|
+
| `?` / `H` | Show help |
|
|
103
|
+
| `Esc` | Cancel / Exit |
|
|
90
104
|
|
|
91
105
|
**Staged Changes**: Toggle extensions on/off without immediate effect. Press `S` to apply all changes at once. Pending changes show `*` next to the extension name.
|
|
92
106
|
|
|
107
|
+
#### Package Actions
|
|
108
|
+
|
|
109
|
+
When a package is selected, press `A` to:
|
|
110
|
+
|
|
111
|
+
- **Update package** - fetch latest version
|
|
112
|
+
- **Remove package** - uninstall completely
|
|
113
|
+
- **View details** - see version, source, scope
|
|
114
|
+
- **Back to manager** - return to unified view
|
|
115
|
+
|
|
93
116
|
#### Community Package Browser
|
|
94
117
|
|
|
95
118
|
Browse and install from npm:
|
|
@@ -101,15 +124,17 @@ Browse and install from npm:
|
|
|
101
124
|
| `N` | Next page |
|
|
102
125
|
| `P` | Previous page |
|
|
103
126
|
| `R` | Refresh search |
|
|
104
|
-
| `M` | Back to menu |
|
|
105
127
|
| `Esc` | Cancel |
|
|
106
128
|
|
|
107
129
|
### Command Reference
|
|
108
130
|
|
|
109
131
|
```bash
|
|
110
|
-
#
|
|
132
|
+
# Unified Manager (Recommended)
|
|
133
|
+
/extensions # Open unified interactive manager
|
|
134
|
+
|
|
135
|
+
# Legacy Commands
|
|
111
136
|
/extensions list # List local extensions (text output)
|
|
112
|
-
/extensions
|
|
137
|
+
/extensions installed # Redirects to unified view
|
|
113
138
|
|
|
114
139
|
# Package Discovery
|
|
115
140
|
/extensions remote # Browse community packages
|
|
@@ -117,7 +142,6 @@ Browse and install from npm:
|
|
|
117
142
|
/extensions search <query> # Search npm for packages
|
|
118
143
|
|
|
119
144
|
# Package Management
|
|
120
|
-
/extensions installed # List installed packages with actions
|
|
121
145
|
/extensions install <source> # Install from npm/git/path
|
|
122
146
|
/extensions remove [source] # Remove package (interactive if no source)
|
|
123
147
|
/extensions uninstall [source]# Alias for remove
|
|
@@ -162,41 +186,46 @@ All commands work in non-interactive environments (CI, scripts):
|
|
|
162
186
|
|
|
163
187
|
- `Ctrl+Shift+E` - Open Extensions Manager
|
|
164
188
|
|
|
165
|
-
### In
|
|
189
|
+
### In Unified Manager
|
|
166
190
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
191
|
+
| Key | Action |
|
|
192
|
+
| -------------- | ------------------------------- |
|
|
193
|
+
| `↑/↓` or `K/J` | Navigate |
|
|
194
|
+
| `Enter/Space` | Toggle local extension |
|
|
195
|
+
| `S` | Save changes |
|
|
196
|
+
| `A` | Package actions (update/remove) |
|
|
197
|
+
| `R` | Browse remote packages |
|
|
198
|
+
| `?` / `H` | Help |
|
|
199
|
+
| `Esc` | Cancel / Exit |
|
|
175
200
|
|
|
176
201
|
## 🏗️ Extension Discovery
|
|
177
202
|
|
|
178
|
-
pi-extmgr discovers extensions from
|
|
203
|
+
pi-extmgr discovers extensions from multiple sources:
|
|
179
204
|
|
|
180
|
-
###
|
|
205
|
+
### Local Extensions
|
|
181
206
|
|
|
182
207
|
```
|
|
183
|
-
~/.pi/agent/extensions/
|
|
208
|
+
~/.pi/agent/extensions/ # Global
|
|
184
209
|
├── my-extension.ts
|
|
185
210
|
├── disabled-extension.ts.disabled
|
|
186
211
|
└── my-extension/
|
|
187
212
|
└── index.ts
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### Project Extensions
|
|
191
213
|
|
|
192
|
-
|
|
193
|
-
./.pi/extensions/
|
|
214
|
+
./.pi/extensions/ # Project
|
|
194
215
|
├── project-tool.ts
|
|
195
216
|
└── local-helper/
|
|
196
217
|
└── index.ts
|
|
197
218
|
```
|
|
198
219
|
|
|
199
|
-
|
|
220
|
+
### Installed Packages
|
|
221
|
+
|
|
222
|
+
Managed by `pi install`:
|
|
223
|
+
|
|
224
|
+
- npm packages (`npm:package@version`)
|
|
225
|
+
- git packages (`git:https://...`)
|
|
226
|
+
- Stored in pi's package cache
|
|
227
|
+
|
|
228
|
+
**Naming**: Append `.disabled` to disable a local extension without removing it.
|
|
200
229
|
|
|
201
230
|
## 🔧 Configuration
|
|
202
231
|
|
|
@@ -220,9 +249,19 @@ export default function myTheme(pi: ExtensionAPI) {
|
|
|
220
249
|
|
|
221
250
|
## 📝 Example Workflows
|
|
222
251
|
|
|
223
|
-
###
|
|
252
|
+
### Managing All Extensions
|
|
224
253
|
|
|
225
254
|
1. Press `Ctrl+Shift+E` or type `/extensions`
|
|
255
|
+
2. See all local extensions and installed packages in one view
|
|
256
|
+
3. Navigate with `↑↓`
|
|
257
|
+
4. For local extensions: press `Space` to toggle on/off
|
|
258
|
+
5. For packages: press `A` for actions (update/remove)
|
|
259
|
+
6. Press `S` to save any changes to local extensions
|
|
260
|
+
7. Confirm reload to apply changes
|
|
261
|
+
|
|
262
|
+
### Installing a New Extension
|
|
263
|
+
|
|
264
|
+
1. Type `/extensions` to open manager
|
|
226
265
|
2. Press `R` for remote packages
|
|
227
266
|
3. Browse or search for the extension
|
|
228
267
|
4. Press `Enter` on the desired package
|
|
@@ -242,10 +281,18 @@ export default function myTheme(pi: ExtensionAPI) {
|
|
|
242
281
|
└── package.json # Original package.json preserved
|
|
243
282
|
```
|
|
244
283
|
|
|
245
|
-
###
|
|
284
|
+
### Updating a Package
|
|
285
|
+
|
|
286
|
+
1. Type `/extensions` to open unified manager
|
|
287
|
+
2. Navigate to the installed package
|
|
288
|
+
3. Press `A` for actions
|
|
289
|
+
4. Select "Update package"
|
|
290
|
+
5. Confirm reload if updated
|
|
291
|
+
|
|
292
|
+
### Disabling a Local Extension Temporarily
|
|
246
293
|
|
|
247
294
|
1. Type `/extensions` to open manager
|
|
248
|
-
2. Navigate to the extension with `↑↓`
|
|
295
|
+
2. Navigate to the local extension with `↑↓`
|
|
249
296
|
3. Press `Space` to toggle it off
|
|
250
297
|
4. Press `S` to save
|
|
251
298
|
5. Confirm reload
|
|
@@ -254,10 +301,11 @@ The extension remains installed but won't load until re-enabled.
|
|
|
254
301
|
|
|
255
302
|
### Updating All Packages
|
|
256
303
|
|
|
257
|
-
1. Type `/extensions
|
|
258
|
-
2.
|
|
259
|
-
3.
|
|
260
|
-
4.
|
|
304
|
+
1. Type `/extensions` to open unified manager
|
|
305
|
+
2. Navigate to any installed package
|
|
306
|
+
3. Press `A` for actions
|
|
307
|
+
4. Select "Update package"
|
|
308
|
+
5. Or use command: `/extensions install npm:pi-extmgr` then select "[Update all packages]"
|
|
261
309
|
|
|
262
310
|
## 🐛 Troubleshooting
|
|
263
311
|
|
|
@@ -282,9 +330,9 @@ Check that the file has a `.ts` or `.js` extension and is in one of the discover
|
|
|
282
330
|
- For git installs, ensure git is available
|
|
283
331
|
- Verify the package has the `pi-package` keyword for browsing
|
|
284
332
|
|
|
285
|
-
###
|
|
333
|
+
### Back to manager closes everything
|
|
286
334
|
|
|
287
|
-
|
|
335
|
+
Fixed! Pressing "Back to manager" now correctly returns to the unified view instead of closing.
|
|
288
336
|
|
|
289
337
|
## 🤝 Contributing
|
|
290
338
|
|
package/index.ts
CHANGED
|
@@ -118,7 +118,7 @@ export default function extensionsManager(pi: ExtensionAPI) {
|
|
|
118
118
|
await showRemote(rest.join(" "), ctx, pi);
|
|
119
119
|
break;
|
|
120
120
|
case "installed":
|
|
121
|
-
await
|
|
121
|
+
await showInstalledPackagesLegacy(ctx, pi);
|
|
122
122
|
break;
|
|
123
123
|
case "search":
|
|
124
124
|
await searchPackages(rest.join(" "), ctx, pi);
|
|
@@ -187,7 +187,8 @@ async function handleNonInteractive(args: string, ctx: ExtensionCommandContext,
|
|
|
187
187
|
await showListOnly(ctx);
|
|
188
188
|
break;
|
|
189
189
|
case "installed":
|
|
190
|
-
|
|
190
|
+
// Legacy: show package list in non-interactive mode
|
|
191
|
+
await showInstalledPackagesList(ctx, pi);
|
|
191
192
|
break;
|
|
192
193
|
default:
|
|
193
194
|
console.log("Extensions Manager (non-interactive mode)");
|
|
@@ -232,72 +233,163 @@ async function showInteractive(ctx: ExtensionCommandContext, pi: ExtensionAPI) {
|
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
// Unified item type for local extensions and installed packages
|
|
237
|
+
interface UnifiedItem {
|
|
238
|
+
type: "local" | "package";
|
|
239
|
+
id: string;
|
|
240
|
+
displayName: string;
|
|
241
|
+
summary: string;
|
|
242
|
+
scope: Scope | "global" | "project";
|
|
243
|
+
// Local extension fields
|
|
244
|
+
state?: State;
|
|
245
|
+
activePath?: string;
|
|
246
|
+
disabledPath?: string;
|
|
247
|
+
originalState?: State;
|
|
248
|
+
// Package fields
|
|
249
|
+
source?: string;
|
|
250
|
+
version?: string | undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
235
253
|
async function showInteractiveOnce(
|
|
236
254
|
ctx: ExtensionCommandContext,
|
|
237
255
|
pi: ExtensionAPI
|
|
238
256
|
): Promise<boolean> {
|
|
239
|
-
|
|
257
|
+
// Load both local extensions and installed packages in parallel for performance
|
|
258
|
+
const [localEntries, installedPackages] = await Promise.all([
|
|
259
|
+
discoverExtensions(ctx.cwd),
|
|
260
|
+
getInstalledPackages(ctx, pi),
|
|
261
|
+
]);
|
|
240
262
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
263
|
+
// Build unified items list
|
|
264
|
+
const items: UnifiedItem[] = [];
|
|
265
|
+
|
|
266
|
+
// Add local extensions
|
|
267
|
+
for (const entry of localEntries) {
|
|
268
|
+
items.push({
|
|
269
|
+
type: "local",
|
|
270
|
+
id: entry.id,
|
|
271
|
+
displayName: entry.displayName,
|
|
272
|
+
summary: entry.summary,
|
|
273
|
+
scope: entry.scope,
|
|
274
|
+
state: entry.state,
|
|
275
|
+
activePath: entry.activePath,
|
|
276
|
+
disabledPath: entry.disabledPath,
|
|
277
|
+
originalState: entry.state,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Add installed packages (filter out duplicates that exist as local extensions)
|
|
282
|
+
const localPaths = new Set(localEntries.map((e) => e.activePath?.toLowerCase()));
|
|
283
|
+
for (const pkg of installedPackages) {
|
|
284
|
+
// Skip if this package is already managed as a local extension
|
|
285
|
+
const pkgPath = pkg.source.toLowerCase();
|
|
286
|
+
if (localPaths.has(pkgPath)) continue;
|
|
287
|
+
|
|
288
|
+
items.push({
|
|
289
|
+
type: "package",
|
|
290
|
+
id: `pkg:${pkg.source}`,
|
|
291
|
+
displayName: pkg.name,
|
|
292
|
+
summary: `${pkg.source} (${pkg.scope})`,
|
|
293
|
+
scope: pkg.scope,
|
|
294
|
+
source: pkg.source,
|
|
295
|
+
version: pkg.version,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Sort: locals first, then packages, both alphabetically
|
|
300
|
+
items.sort((a, b) => {
|
|
301
|
+
if (a.type !== b.type) return a.type === "local" ? -1 : 1;
|
|
302
|
+
return a.displayName.localeCompare(b.displayName);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// If nothing found, show quick actions
|
|
306
|
+
if (items.length === 0) {
|
|
307
|
+
const choice = await ctx.ui.select("No extensions or packages found", [
|
|
244
308
|
"Browse community packages",
|
|
245
|
-
"List installed packages",
|
|
246
309
|
"Cancel",
|
|
247
310
|
]);
|
|
248
311
|
|
|
249
312
|
if (choice === "Browse community packages") {
|
|
250
313
|
await browseRemotePackages(ctx, "keywords:pi-package", pi);
|
|
251
|
-
return false;
|
|
252
|
-
} else if (choice === "List installed packages") {
|
|
253
|
-
await showInstalledPackages(ctx, pi);
|
|
254
|
-
return false; // Return to main menu
|
|
314
|
+
return false;
|
|
255
315
|
}
|
|
256
|
-
return true;
|
|
316
|
+
return true;
|
|
257
317
|
}
|
|
258
318
|
|
|
259
|
-
// Staged changes tracking
|
|
260
|
-
const staged = new Map
|
|
261
|
-
const byId = new Map(
|
|
319
|
+
// Staged changes tracking for local extensions
|
|
320
|
+
const staged = new Map<string, State>();
|
|
321
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
262
322
|
|
|
263
|
-
type Action =
|
|
323
|
+
type Action =
|
|
324
|
+
| { type: "cancel" }
|
|
325
|
+
| { type: "apply" }
|
|
326
|
+
| { type: "remote" }
|
|
327
|
+
| { type: "help" }
|
|
328
|
+
| { type: "menu" }
|
|
329
|
+
| { type: "action"; itemId: string };
|
|
264
330
|
|
|
265
331
|
const result = await ctx.ui.custom<Action>((tui, theme, _keybindings, done) => {
|
|
266
332
|
const container = new Container();
|
|
333
|
+
const hasLocals = items.some((i) => i.type === "local");
|
|
334
|
+
const hasPackages = items.some((i) => i.type === "package");
|
|
267
335
|
|
|
268
336
|
// Header
|
|
269
337
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
270
|
-
container.addChild(new Text(theme.fg("accent", theme.bold("
|
|
338
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Extensions Manager")), 2, 0));
|
|
271
339
|
container.addChild(
|
|
272
|
-
new Text(
|
|
340
|
+
new Text(
|
|
341
|
+
theme.fg(
|
|
342
|
+
"muted",
|
|
343
|
+
`${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter to toggle local extensions, A for actions on packages`
|
|
344
|
+
),
|
|
345
|
+
2,
|
|
346
|
+
0
|
|
347
|
+
)
|
|
273
348
|
);
|
|
274
349
|
container.addChild(new Spacer(1));
|
|
275
350
|
|
|
276
|
-
//
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
351
|
+
// Build settings items
|
|
352
|
+
const settingsItems: SettingItem[] = items.map((item) => {
|
|
353
|
+
if (item.type === "local") {
|
|
354
|
+
const currentState = staged.get(item.id) ?? item.state!;
|
|
355
|
+
const changed = staged.has(item.id) && staged.get(item.id) !== item.originalState;
|
|
356
|
+
return {
|
|
357
|
+
id: item.id,
|
|
358
|
+
label: formatUnifiedItemLabel(item, currentState, theme, changed),
|
|
359
|
+
currentValue: currentState,
|
|
360
|
+
values: ["enabled", "disabled"],
|
|
361
|
+
};
|
|
362
|
+
} else {
|
|
363
|
+
// Package - show as read-only with action indicator
|
|
364
|
+
return {
|
|
365
|
+
id: item.id,
|
|
366
|
+
label: formatUnifiedItemLabel(item, "enabled", theme, false),
|
|
367
|
+
currentValue: "enabled",
|
|
368
|
+
values: ["enabled"], // Packages don't toggle, they use actions
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
});
|
|
283
372
|
|
|
284
373
|
const settingsList = new SettingsList(
|
|
285
|
-
|
|
286
|
-
Math.min(
|
|
374
|
+
settingsItems,
|
|
375
|
+
Math.min(items.length + 2, 16),
|
|
287
376
|
getSettingsListTheme(),
|
|
288
377
|
(id: string, newValue: string) => {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
if (!entry || !item) return;
|
|
378
|
+
const item = byId.get(id);
|
|
379
|
+
if (!item || item.type !== "local") return;
|
|
292
380
|
|
|
293
381
|
const state = newValue as State;
|
|
294
382
|
staged.set(id, state);
|
|
295
|
-
|
|
296
|
-
|
|
383
|
+
|
|
384
|
+
const settingsItem = settingsItems.find((x) => x.id === id);
|
|
385
|
+
if (settingsItem) {
|
|
386
|
+
const changed = state !== item.originalState;
|
|
387
|
+
settingsItem.label = formatUnifiedItemLabel(item, state, theme, changed);
|
|
388
|
+
}
|
|
297
389
|
tui.requestRender();
|
|
298
390
|
},
|
|
299
|
-
() => done("cancel"),
|
|
300
|
-
{ enableSearch:
|
|
391
|
+
() => done({ type: "cancel" }),
|
|
392
|
+
{ enableSearch: items.length > 8 }
|
|
301
393
|
);
|
|
302
394
|
|
|
303
395
|
container.addChild(settingsList);
|
|
@@ -305,15 +397,20 @@ async function showInteractiveOnce(
|
|
|
305
397
|
|
|
306
398
|
// Footer with keyboard shortcuts
|
|
307
399
|
const hasChanges = Array.from(staged.entries()).some(([id, state]) => {
|
|
308
|
-
const
|
|
309
|
-
return
|
|
400
|
+
const item = byId.get(id);
|
|
401
|
+
return item?.type === "local" && item.originalState !== state;
|
|
310
402
|
});
|
|
311
403
|
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
404
|
+
const footerParts: string[] = [];
|
|
405
|
+
footerParts.push("↑↓ Navigate");
|
|
406
|
+
if (hasLocals) footerParts.push("Space/Enter Toggle");
|
|
407
|
+
if (hasLocals) footerParts.push(hasChanges ? "S Save*" : "S Save");
|
|
408
|
+
if (hasPackages) footerParts.push("A Actions");
|
|
409
|
+
footerParts.push("R Browse");
|
|
410
|
+
footerParts.push("? Help");
|
|
411
|
+
footerParts.push("Esc Cancel");
|
|
315
412
|
|
|
316
|
-
container.addChild(new Text(theme.fg("dim",
|
|
413
|
+
container.addChild(new Text(theme.fg("dim", footerParts.join(" | ")), 2, 0));
|
|
317
414
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
318
415
|
|
|
319
416
|
return {
|
|
@@ -325,23 +422,29 @@ async function showInteractiveOnce(
|
|
|
325
422
|
},
|
|
326
423
|
handleInput(data: string) {
|
|
327
424
|
if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
|
|
328
|
-
done("apply");
|
|
425
|
+
done({ type: "apply" });
|
|
329
426
|
return;
|
|
330
427
|
}
|
|
331
|
-
if (data === "
|
|
332
|
-
|
|
428
|
+
if (data === "a" || data === "A") {
|
|
429
|
+
// Get currently selected item and show actions
|
|
430
|
+
// Access internal selectedIndex from SettingsList
|
|
431
|
+
const selIdx = (settingsList as unknown as { selectedIndex: number }).selectedIndex ?? 0;
|
|
432
|
+
const selectedId = settingsItems[selIdx]?.id ?? settingsItems[0]?.id;
|
|
433
|
+
if (selectedId) {
|
|
434
|
+
done({ type: "action", itemId: selectedId });
|
|
435
|
+
}
|
|
333
436
|
return;
|
|
334
437
|
}
|
|
335
438
|
if (data === "r" || data === "R") {
|
|
336
|
-
done("remote");
|
|
439
|
+
done({ type: "remote" });
|
|
337
440
|
return;
|
|
338
441
|
}
|
|
339
442
|
if (data === "?" || data === "h" || data === "H") {
|
|
340
|
-
done("help");
|
|
443
|
+
done({ type: "help" });
|
|
341
444
|
return;
|
|
342
445
|
}
|
|
343
446
|
if (data === "m" || data === "M") {
|
|
344
|
-
done("menu");
|
|
447
|
+
done({ type: "menu" });
|
|
345
448
|
return;
|
|
346
449
|
}
|
|
347
450
|
settingsList.handleInput?.(data);
|
|
@@ -350,25 +453,42 @@ async function showInteractiveOnce(
|
|
|
350
453
|
};
|
|
351
454
|
});
|
|
352
455
|
|
|
353
|
-
|
|
354
|
-
|
|
456
|
+
// Handle results
|
|
457
|
+
if (result.type === "cancel") {
|
|
458
|
+
if (staged.size > 0) {
|
|
355
459
|
ctx.ui.notify("No changes applied.", "info");
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
await showInstalledPackages(ctx, pi);
|
|
359
|
-
return false; // Return to main menu
|
|
360
|
-
case "remote":
|
|
361
|
-
await showRemote("", ctx, pi);
|
|
362
|
-
return false; // Return to main menu
|
|
363
|
-
case "help":
|
|
364
|
-
showHelp(ctx);
|
|
365
|
-
return false; // Return to main menu
|
|
366
|
-
case "menu":
|
|
367
|
-
return false; // Return to main menu (already there)
|
|
460
|
+
}
|
|
461
|
+
return true;
|
|
368
462
|
}
|
|
369
463
|
|
|
370
|
-
|
|
371
|
-
|
|
464
|
+
if (result.type === "remote") {
|
|
465
|
+
await showRemote("", ctx, pi);
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (result.type === "help") {
|
|
470
|
+
showHelp(ctx);
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (result.type === "menu") {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (result.type === "action") {
|
|
479
|
+
const item = byId.get(result.itemId);
|
|
480
|
+
if (item?.type === "package") {
|
|
481
|
+
const exitManager = await handlePackageAction(item, ctx, pi);
|
|
482
|
+
return exitManager; // true = exit manager, false = return to unified view
|
|
483
|
+
}
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Apply changes for local extensions
|
|
488
|
+
const localItems = items.filter((i) => i.type === "local" && i.activePath) as Required<
|
|
489
|
+
Pick<UnifiedItem, "id" | "activePath" | "disabledPath" | "originalState">
|
|
490
|
+
>[];
|
|
491
|
+
const apply = await applyStagedChangesUnified(localItems, staged);
|
|
372
492
|
|
|
373
493
|
if (apply.errors.length > 0) {
|
|
374
494
|
ctx.ui.notify(
|
|
@@ -377,82 +497,164 @@ async function showInteractiveOnce(
|
|
|
377
497
|
);
|
|
378
498
|
} else if (apply.changed === 0) {
|
|
379
499
|
ctx.ui.notify("No changes to apply.", "info");
|
|
380
|
-
return false; // Return to main menu
|
|
381
500
|
} else {
|
|
382
501
|
ctx.ui.notify(`Applied ${apply.changed} extension change(s).`, "info");
|
|
383
502
|
}
|
|
384
503
|
|
|
385
|
-
// Prompt for reload
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
504
|
+
// Prompt for reload if changes were made
|
|
505
|
+
if (apply.changed > 0) {
|
|
506
|
+
const shouldReload = await ctx.ui.confirm(
|
|
507
|
+
"Reload Required",
|
|
508
|
+
"Extensions changed. Reload pi now?"
|
|
509
|
+
);
|
|
390
510
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
511
|
+
if (shouldReload) {
|
|
512
|
+
ctx.ui.setEditorText("/reload");
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
394
515
|
}
|
|
395
516
|
|
|
396
|
-
return false;
|
|
517
|
+
return false;
|
|
397
518
|
}
|
|
398
519
|
|
|
399
|
-
function
|
|
400
|
-
|
|
520
|
+
function formatUnifiedItemLabel(
|
|
521
|
+
item: UnifiedItem,
|
|
401
522
|
state: State,
|
|
402
523
|
theme: Theme,
|
|
403
524
|
changed = false
|
|
404
525
|
): string {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
526
|
+
if (item.type === "local") {
|
|
527
|
+
const statusIcon = state === "enabled" ? theme.fg("success", "●") : theme.fg("error", "○");
|
|
528
|
+
const scopeIcon = item.scope === "global" ? theme.fg("muted", "G") : theme.fg("accent", "P");
|
|
529
|
+
const changeMarker = changed ? theme.fg("warning", " *") : "";
|
|
530
|
+
const name = theme.bold(item.displayName);
|
|
531
|
+
const summary = theme.fg("dim", item.summary);
|
|
532
|
+
return `${statusIcon} [${scopeIcon}] ${name} - ${summary}${changeMarker}`;
|
|
533
|
+
} else {
|
|
534
|
+
// Package
|
|
535
|
+
const pkgIcon = theme.fg("accent", "📦");
|
|
536
|
+
const scopeIcon = item.scope === "global" ? theme.fg("muted", "G") : theme.fg("accent", "P");
|
|
537
|
+
const name = theme.bold(item.displayName);
|
|
538
|
+
const version = item.version ? theme.fg("dim", `@${item.version}`) : "";
|
|
539
|
+
const source = theme.fg("dim", item.summary.split(" (")[0] ?? "");
|
|
540
|
+
return `${pkgIcon} [${scopeIcon}] ${name}${version} - ${source}`;
|
|
541
|
+
}
|
|
411
542
|
}
|
|
412
543
|
|
|
413
|
-
async function
|
|
544
|
+
async function applyStagedChangesUnified(
|
|
545
|
+
items: Required<Pick<UnifiedItem, "id" | "activePath" | "disabledPath" | "originalState">>[],
|
|
546
|
+
staged: Map<string, State>
|
|
547
|
+
) {
|
|
414
548
|
let changed = 0;
|
|
415
549
|
const errors: string[] = [];
|
|
416
550
|
|
|
417
|
-
for (const
|
|
418
|
-
const target = staged.get(
|
|
419
|
-
if (target ===
|
|
551
|
+
for (const item of items) {
|
|
552
|
+
const target = staged.get(item.id) ?? item.originalState;
|
|
553
|
+
if (target === item.originalState) continue;
|
|
420
554
|
|
|
421
|
-
const result = await
|
|
555
|
+
const result = await setStateUnified(item, target);
|
|
422
556
|
if (result.ok) {
|
|
423
|
-
entry.state = target;
|
|
424
557
|
changed++;
|
|
425
558
|
} else {
|
|
426
|
-
errors.push(`${
|
|
559
|
+
errors.push(`${item.id}: ${result.error}`);
|
|
427
560
|
}
|
|
428
561
|
}
|
|
429
562
|
|
|
430
563
|
return { changed, errors };
|
|
431
564
|
}
|
|
432
565
|
|
|
566
|
+
async function setStateUnified(
|
|
567
|
+
item: Pick<UnifiedItem, "activePath" | "disabledPath">,
|
|
568
|
+
target: State
|
|
569
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
570
|
+
try {
|
|
571
|
+
if (!item.activePath || !item.disabledPath) {
|
|
572
|
+
return { ok: false, error: "Missing paths" };
|
|
573
|
+
}
|
|
574
|
+
if (target === "enabled") {
|
|
575
|
+
await rename(item.disabledPath, item.activePath);
|
|
576
|
+
} else {
|
|
577
|
+
await rename(item.activePath, item.disabledPath);
|
|
578
|
+
}
|
|
579
|
+
return { ok: true };
|
|
580
|
+
} catch (error) {
|
|
581
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function handlePackageAction(
|
|
586
|
+
item: UnifiedItem,
|
|
587
|
+
ctx: ExtensionCommandContext,
|
|
588
|
+
pi: ExtensionAPI
|
|
589
|
+
): Promise<boolean> {
|
|
590
|
+
if (!ctx.hasUI || !item.source) return true;
|
|
591
|
+
|
|
592
|
+
const choice = await ctx.ui.select(`${item.displayName} Actions`, [
|
|
593
|
+
`Update package`,
|
|
594
|
+
`Remove package`,
|
|
595
|
+
`View details`,
|
|
596
|
+
`Back to manager`,
|
|
597
|
+
]);
|
|
598
|
+
|
|
599
|
+
if (!choice || choice.includes("Back")) {
|
|
600
|
+
return false; // Stay in manager
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (choice.includes("Update")) {
|
|
604
|
+
await updatePackage(item.source, ctx, pi);
|
|
605
|
+
} else if (choice.includes("Remove")) {
|
|
606
|
+
await removePackage(item.source, ctx, pi);
|
|
607
|
+
} else if (choice.includes("details")) {
|
|
608
|
+
ctx.ui.notify(
|
|
609
|
+
`Name: ${item.displayName}\nVersion: ${item.version || "unknown"}\nSource: ${item.source}\nScope: ${item.scope}`,
|
|
610
|
+
"info"
|
|
611
|
+
);
|
|
612
|
+
// Show actions again
|
|
613
|
+
return handlePackageAction(item, ctx, pi);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return false; // Stay in manager
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Legacy function kept for backward compatibility - use unified view instead
|
|
620
|
+
async function showInstalledPackagesLegacy(ctx: ExtensionCommandContext, pi: ExtensionAPI) {
|
|
621
|
+
ctx.ui.notify(
|
|
622
|
+
"📦 Use /extensions for the unified view.\nInstalled packages are now shown alongside local extensions.",
|
|
623
|
+
"info"
|
|
624
|
+
);
|
|
625
|
+
// Small delay then open the main manager
|
|
626
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
627
|
+
await showInteractive(ctx, pi);
|
|
628
|
+
}
|
|
629
|
+
|
|
433
630
|
function showHelp(ctx: ExtensionCommandContext): void {
|
|
434
631
|
const lines = [
|
|
435
632
|
"Extensions Manager Help",
|
|
436
633
|
"",
|
|
437
|
-
"
|
|
438
|
-
"
|
|
439
|
-
"
|
|
440
|
-
"
|
|
634
|
+
"Unified View:",
|
|
635
|
+
" Local extensions and npm/git packages are displayed together",
|
|
636
|
+
" Local extensions show ● enabled / ○ disabled with G/P scope",
|
|
637
|
+
" Packages show 📦 with name@version and G/P scope",
|
|
441
638
|
"",
|
|
442
639
|
"Navigation:",
|
|
443
640
|
" ↑↓ Navigate list",
|
|
444
|
-
" Space/Enter Toggle enabled/disabled",
|
|
445
|
-
" S Save changes",
|
|
446
|
-
"
|
|
641
|
+
" Space/Enter Toggle local extension enabled/disabled",
|
|
642
|
+
" S Save changes to local extensions",
|
|
643
|
+
" A Actions on selected package (update/remove)",
|
|
447
644
|
" R Browse remote packages",
|
|
448
|
-
" M Main menu (exit to command line)",
|
|
449
645
|
" ?/H Show this help",
|
|
450
646
|
" Esc Cancel",
|
|
451
647
|
"",
|
|
648
|
+
"Extension Sources:",
|
|
649
|
+
" - ~/.pi/agent/extensions/ (global - G)",
|
|
650
|
+
" - .pi/extensions/ (project-local - P)",
|
|
651
|
+
" - npm packages installed via pi install",
|
|
652
|
+
" - git packages installed via pi install",
|
|
653
|
+
"",
|
|
452
654
|
"Commands:",
|
|
453
655
|
" /extensions Open manager",
|
|
454
656
|
" /extensions list List local extensions",
|
|
455
|
-
" /extensions installed List installed packages",
|
|
657
|
+
" /extensions installed List installed packages (legacy)",
|
|
456
658
|
" /extensions remote Browse community packages",
|
|
457
659
|
" /extensions search <q> Search for packages",
|
|
458
660
|
" /extensions install <s> Install package (npm:, git:, or path)",
|
|
@@ -465,7 +667,6 @@ function showHelp(ctx: ExtensionCommandContext): void {
|
|
|
465
667
|
} else {
|
|
466
668
|
console.log(output);
|
|
467
669
|
}
|
|
468
|
-
// Note: Removed auto-return to main menu (was causing memory leak potential)
|
|
469
670
|
}
|
|
470
671
|
|
|
471
672
|
// ============== Remote Package Management ==============
|
|
@@ -478,7 +679,8 @@ async function showRemote(args: string, ctx: ExtensionCommandContext, pi: Extens
|
|
|
478
679
|
switch (sub) {
|
|
479
680
|
case "list":
|
|
480
681
|
case "installed":
|
|
481
|
-
|
|
682
|
+
// Legacy: redirect to unified view
|
|
683
|
+
await showInstalledPackagesLegacy(ctx, pi);
|
|
482
684
|
return;
|
|
483
685
|
case "install":
|
|
484
686
|
if (query) {
|
|
@@ -501,7 +703,7 @@ async function showRemote(args: string, ctx: ExtensionCommandContext, pi: Extens
|
|
|
501
703
|
}
|
|
502
704
|
|
|
503
705
|
async function showRemoteMenu(ctx: ExtensionCommandContext, pi: ExtensionAPI) {
|
|
504
|
-
type MenuAction = "browse" | "search" | "install" | "
|
|
706
|
+
type MenuAction = "browse" | "search" | "install" | "cancel" | "main" | "help";
|
|
505
707
|
|
|
506
708
|
const result = await ctx.ui.custom<MenuAction>((tui, theme, _kb, done) => {
|
|
507
709
|
const container = new Container();
|
|
@@ -509,6 +711,9 @@ async function showRemoteMenu(ctx: ExtensionCommandContext, pi: ExtensionAPI) {
|
|
|
509
711
|
// Header
|
|
510
712
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
511
713
|
container.addChild(new Text(theme.fg("accent", theme.bold("Community Packages")), 2, 1));
|
|
714
|
+
container.addChild(
|
|
715
|
+
new Text(theme.fg("muted", "Use /extensions for unified view of installed items"), 2, 0)
|
|
716
|
+
);
|
|
512
717
|
container.addChild(new Spacer(1));
|
|
513
718
|
|
|
514
719
|
const menuItems: SelectItem[] = [
|
|
@@ -519,7 +724,6 @@ async function showRemoteMenu(ctx: ExtensionCommandContext, pi: ExtensionAPI) {
|
|
|
519
724
|
},
|
|
520
725
|
{ value: "search", label: "🔎 Search packages", description: "Search npm with custom query" },
|
|
521
726
|
{ value: "install", label: "📥 Install by source", description: "npm:, git:, or local path" },
|
|
522
|
-
{ value: "list", label: "📋 List installed", description: "View your installed packages" },
|
|
523
727
|
];
|
|
524
728
|
|
|
525
729
|
const selectList = new SelectList(menuItems, menuItems.length + 2, {
|
|
@@ -576,9 +780,6 @@ async function showRemoteMenu(ctx: ExtensionCommandContext, pi: ExtensionAPI) {
|
|
|
576
780
|
case "install":
|
|
577
781
|
await promptInstall(ctx, pi);
|
|
578
782
|
break;
|
|
579
|
-
case "list":
|
|
580
|
-
await showInstalledPackages(ctx, pi);
|
|
581
|
-
break;
|
|
582
783
|
case "main":
|
|
583
784
|
return;
|
|
584
785
|
case "help":
|
|
@@ -1344,7 +1545,8 @@ async function removePackage(source: string, ctx: ExtensionCommandContext, pi: E
|
|
|
1344
1545
|
|
|
1345
1546
|
// ============== Installed Packages ==============
|
|
1346
1547
|
|
|
1347
|
-
|
|
1548
|
+
// Legacy list view for non-interactive mode and backward compatibility
|
|
1549
|
+
async function showInstalledPackagesList(ctx: ExtensionCommandContext, pi: ExtensionAPI) {
|
|
1348
1550
|
const packages = await getInstalledPackages(ctx, pi);
|
|
1349
1551
|
|
|
1350
1552
|
if (packages.length === 0) {
|
|
@@ -1420,7 +1622,8 @@ async function showPackageActions(
|
|
|
1420
1622
|
);
|
|
1421
1623
|
await showPackageActions(source, pkg, ctx, pi);
|
|
1422
1624
|
} else if (choice.includes("Back")) {
|
|
1423
|
-
|
|
1625
|
+
// Return to unified view instead of old list
|
|
1626
|
+
await showInstalledPackagesLegacy(ctx, pi);
|
|
1424
1627
|
}
|
|
1425
1628
|
}
|
|
1426
1629
|
|
|
@@ -1803,22 +2006,6 @@ async function parseDirectoryIndex(
|
|
|
1803
2006
|
return undefined;
|
|
1804
2007
|
}
|
|
1805
2008
|
|
|
1806
|
-
async function setState(
|
|
1807
|
-
entry: ExtensionEntry,
|
|
1808
|
-
target: State
|
|
1809
|
-
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
1810
|
-
try {
|
|
1811
|
-
if (target === "enabled") {
|
|
1812
|
-
await rename(entry.disabledPath, entry.activePath);
|
|
1813
|
-
} else {
|
|
1814
|
-
await rename(entry.activePath, entry.disabledPath);
|
|
1815
|
-
}
|
|
1816
|
-
return { ok: true };
|
|
1817
|
-
} catch (error) {
|
|
1818
|
-
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
2009
|
async function readSummary(filePath: string): Promise<string> {
|
|
1823
2010
|
try {
|
|
1824
2011
|
const text = await readFile(filePath, "utf8");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extmgr",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Enhanced UX for managing local Pi extensions and community packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"extensions": [
|
|
30
30
|
"./index.ts"
|
|
31
31
|
],
|
|
32
|
+
"video": "https://github.com/ayagmar/pi-extmgr/releases/download/v0.0.2/Screencast_20260207_013142.mp4",
|
|
32
33
|
"image": "https://i.imgur.com/TjAp7Hv.png"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|