pi-extmgr 0.1.6 → 0.1.7
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 +12 -1
- package/package.json +1 -1
- package/src/index.ts +39 -6
- package/src/types/index.ts +3 -1
- package/src/ui/help.ts +9 -1
- package/src/ui/remote.ts +6 -4
- package/src/ui/unified.ts +129 -14
- package/src/utils/auto-update.ts +74 -0
- package/src/utils/settings.ts +42 -4
- package/src/utils/ui-helpers.ts +6 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ Then reload Pi with `/reload`.
|
|
|
22
22
|
- **Package actions**: Update or remove installed packages with `A`
|
|
23
23
|
- **Browse community**: Search and install from npm (`R` to browse)
|
|
24
24
|
- **History tracking**: See what you've changed with `/extensions history`
|
|
25
|
+
- **Smart reload prompt**: After applying extension state changes, extmgr can trigger reload directly when Pi exposes a reload API (falls back to `/reload` command text otherwise)
|
|
25
26
|
|
|
26
27
|
## Usage
|
|
27
28
|
|
|
@@ -38,7 +39,13 @@ Open the manager:
|
|
|
38
39
|
| `↑↓` | Navigate |
|
|
39
40
|
| `Space/Enter` | Toggle local extension on/off |
|
|
40
41
|
| `S` | Save changes |
|
|
41
|
-
| `A`
|
|
42
|
+
| `Enter` / `A` | Actions on selected package (update/remove/view) |
|
|
43
|
+
| `u` | Update selected package directly |
|
|
44
|
+
| `X` | Remove selected package directly |
|
|
45
|
+
| `i` | Quick install by source |
|
|
46
|
+
| `f` | Quick search |
|
|
47
|
+
| `U` | Update all packages |
|
|
48
|
+
| `t` | Auto-update wizard |
|
|
42
49
|
| `R` | Browse remote packages |
|
|
43
50
|
| `?` / `H` | Help |
|
|
44
51
|
| `Esc` | Exit |
|
|
@@ -50,6 +57,8 @@ Open the manager:
|
|
|
50
57
|
/extensions search <query> # Search npm
|
|
51
58
|
/extensions install <source> # Install package
|
|
52
59
|
/extensions remove [source] # Remove package
|
|
60
|
+
/extensions update [source] # Update one package (or all when omitted)
|
|
61
|
+
/extensions auto-update [every] # No arg opens wizard; accepts 1d, 1w, never, etc.
|
|
53
62
|
/extensions history # View change history
|
|
54
63
|
/extensions stats # View statistics
|
|
55
64
|
/extensions clear-cache # Clear metadata cache
|
|
@@ -72,6 +81,8 @@ Open the manager:
|
|
|
72
81
|
- **Two install modes**:
|
|
73
82
|
- **Managed** (npm): Auto-updates with `pi update`, stored in pi's package cache
|
|
74
83
|
- **Local** (standalone): Copies to `~/.pi/agent/extensions/{package}/`, supports multi-file extensions
|
|
84
|
+
- **Auto-update schedule is persistent**: `/extensions auto-update 1d` stays active across future Pi sessions.
|
|
85
|
+
- **Reload is API-aware**: When extmgr asks to reload, it will call Pi's reload API when available, otherwise it pre-fills `/reload` for you.
|
|
75
86
|
- **Remove requires restart**: After removing a package, you need to fully restart Pi (not just `/reload`) for it to be completely unloaded.
|
|
76
87
|
|
|
77
88
|
## Keyboard shortcut
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -14,19 +14,26 @@ import { showInteractive, showListOnly, showInstalledPackagesLegacy } from "./ui
|
|
|
14
14
|
import { showRemote } from "./ui/remote.js";
|
|
15
15
|
import { showHelp } from "./ui/help.js";
|
|
16
16
|
import { installPackage } from "./packages/install.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
removePackage,
|
|
19
|
+
promptRemove,
|
|
20
|
+
showInstalledPackagesList,
|
|
21
|
+
updatePackage,
|
|
22
|
+
updatePackages,
|
|
23
|
+
} from "./packages/management.js";
|
|
18
24
|
import { getInstalledPackages } from "./packages/discovery.js";
|
|
19
25
|
import { getRecentChanges, formatChangeEntry, getChangeStats } from "./utils/history.js";
|
|
20
26
|
import { clearCache } from "./utils/cache.js";
|
|
21
27
|
import { notify } from "./utils/notify.js";
|
|
22
28
|
import { formatListOutput } from "./utils/ui-helpers.js";
|
|
23
|
-
import { parseDuration } from "./utils/settings.js";
|
|
29
|
+
import { parseDuration, getAutoUpdateConfig } from "./utils/settings.js";
|
|
24
30
|
import {
|
|
25
31
|
startAutoUpdateTimer,
|
|
26
32
|
stopAutoUpdateTimer,
|
|
27
33
|
enableAutoUpdate,
|
|
28
34
|
disableAutoUpdate,
|
|
29
35
|
getAutoUpdateStatus,
|
|
36
|
+
promptAutoUpdateWizard,
|
|
30
37
|
} from "./utils/auto-update.js";
|
|
31
38
|
|
|
32
39
|
export default function extensionsManager(pi: ExtensionAPI) {
|
|
@@ -43,6 +50,7 @@ export default function extensionsManager(pi: ExtensionAPI) {
|
|
|
43
50
|
{ value: "install", description: "Install a package" },
|
|
44
51
|
{ value: "remove", description: "Remove an installed package" },
|
|
45
52
|
{ value: "uninstall", description: "Remove an installed package (alias)" },
|
|
53
|
+
{ value: "update", description: "Update one package or all packages" },
|
|
46
54
|
{ value: "history", description: "View extension change history" },
|
|
47
55
|
{ value: "stats", description: "View extension manager statistics" },
|
|
48
56
|
{ value: "clear-cache", description: "Clear metadata cache" },
|
|
@@ -84,6 +92,8 @@ export default function extensionsManager(pi: ExtensionAPI) {
|
|
|
84
92
|
rest.length > 0 ? removePackage(rest.join(" "), ctx, pi) : promptRemove(ctx, pi),
|
|
85
93
|
uninstall: () =>
|
|
86
94
|
rest.length > 0 ? removePackage(rest.join(" "), ctx, pi) : promptRemove(ctx, pi),
|
|
95
|
+
update: () =>
|
|
96
|
+
rest.length > 0 ? updatePackage(rest.join(" "), ctx, pi) : updatePackages(ctx, pi),
|
|
87
97
|
"auto-update": () => handleAutoUpdateCommand(rest.join(" "), ctx),
|
|
88
98
|
history: () => showHistory(ctx, pi),
|
|
89
99
|
stats: () => showStats(ctx, pi),
|
|
@@ -101,7 +111,7 @@ export default function extensionsManager(pi: ExtensionAPI) {
|
|
|
101
111
|
} else {
|
|
102
112
|
notify(
|
|
103
113
|
ctx,
|
|
104
|
-
`Unknown command: ${subcommand ?? "(empty)"}. Try: local, remote, installed, search, install, remove`,
|
|
114
|
+
`Unknown command: ${subcommand ?? "(empty)"}. Try: local, remote, installed, search, install, remove, update`,
|
|
105
115
|
"warning"
|
|
106
116
|
);
|
|
107
117
|
}
|
|
@@ -125,6 +135,11 @@ export default function extensionsManager(pi: ExtensionAPI) {
|
|
|
125
135
|
statusParts.push(autoUpdateStatus);
|
|
126
136
|
}
|
|
127
137
|
|
|
138
|
+
const knownUpdates = getAutoUpdateConfig(ctx).updatesAvailable ?? [];
|
|
139
|
+
if (knownUpdates.length > 0) {
|
|
140
|
+
statusParts.push(`${knownUpdates.length} update${knownUpdates.length === 1 ? "" : "s"}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
if (statusParts.length > 0) {
|
|
129
144
|
ctx.ui.setStatus("extmgr", ctx.ui.theme.fg("dim", statusParts.join(" • ")));
|
|
130
145
|
} else {
|
|
@@ -160,11 +175,26 @@ export default function extensionsManager(pi: ExtensionAPI) {
|
|
|
160
175
|
});
|
|
161
176
|
|
|
162
177
|
// Handle auto-update command
|
|
163
|
-
function handleAutoUpdateCommand(
|
|
178
|
+
async function handleAutoUpdateCommand(
|
|
164
179
|
args: string,
|
|
165
180
|
ctx: ExtensionCommandContext | ExtensionContext
|
|
166
|
-
): void {
|
|
167
|
-
const
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
const trimmed = args.trim();
|
|
183
|
+
|
|
184
|
+
// Interactive wizard when no arguments are provided
|
|
185
|
+
if (!trimmed && ctx.hasUI) {
|
|
186
|
+
await promptAutoUpdateWizard(pi, ctx, (packages) => {
|
|
187
|
+
notify(
|
|
188
|
+
ctx,
|
|
189
|
+
`Updates available for ${packages.length} package(s): ${packages.join(", ")}`,
|
|
190
|
+
"info"
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
void updateStatusBar(ctx);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const duration = parseDuration(trimmed);
|
|
168
198
|
|
|
169
199
|
if (!duration) {
|
|
170
200
|
// Show current status
|
|
@@ -226,6 +256,7 @@ async function handleNonInteractive(
|
|
|
226
256
|
console.log(" /extensions installed - List installed packages");
|
|
227
257
|
console.log(" /extensions install <source> - Install a package");
|
|
228
258
|
console.log(" /extensions remove <source> - Remove a package");
|
|
259
|
+
console.log(" /extensions update [source] - Update one package or all packages");
|
|
229
260
|
};
|
|
230
261
|
|
|
231
262
|
const nonInteractiveHandlers: Record<string, () => Promise<void> | void> = {
|
|
@@ -246,6 +277,8 @@ async function handleNonInteractive(
|
|
|
246
277
|
rest.length > 0
|
|
247
278
|
? removePackage(rest.join(" "), ctx, pi)
|
|
248
279
|
: console.log("Usage: /extensions remove <npm:package|git:url|path>"),
|
|
280
|
+
update: () =>
|
|
281
|
+
rest.length > 0 ? updatePackage(rest.join(" "), ctx, pi) : updatePackages(ctx, pi),
|
|
249
282
|
"auto-update": () => {
|
|
250
283
|
console.log("Auto-update requires interactive mode.");
|
|
251
284
|
console.log("Usage: /extensions auto-update <duration>");
|
package/src/types/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ export interface UnifiedItem {
|
|
|
49
49
|
version?: string | undefined;
|
|
50
50
|
description?: string | undefined;
|
|
51
51
|
size?: number | undefined; // Package size in bytes
|
|
52
|
+
updateAvailable?: boolean | undefined;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export interface SearchCache {
|
|
@@ -64,7 +65,8 @@ export type UnifiedAction =
|
|
|
64
65
|
| { type: "remote" }
|
|
65
66
|
| { type: "help" }
|
|
66
67
|
| { type: "menu" }
|
|
67
|
-
| { type: "
|
|
68
|
+
| { type: "quick"; action: "install" | "search" | "update-all" | "auto-update" }
|
|
69
|
+
| { type: "action"; itemId: string; action?: "menu" | "update" | "remove" | "details" };
|
|
68
70
|
|
|
69
71
|
export type BrowseAction =
|
|
70
72
|
| { type: "package"; name: string }
|
package/src/ui/help.ts
CHANGED
|
@@ -16,7 +16,13 @@ export function showHelp(ctx: ExtensionCommandContext): void {
|
|
|
16
16
|
" ↑↓ Navigate list",
|
|
17
17
|
" Space/Enter Toggle local extension enabled/disabled",
|
|
18
18
|
" S Save changes to local extensions",
|
|
19
|
-
" A
|
|
19
|
+
" Enter/A Open actions for selected package",
|
|
20
|
+
" u Update selected package",
|
|
21
|
+
" X Remove selected package",
|
|
22
|
+
" i Quick install by source",
|
|
23
|
+
" f Quick search",
|
|
24
|
+
" U Update all packages",
|
|
25
|
+
" t Auto-update wizard",
|
|
20
26
|
" R Browse remote packages",
|
|
21
27
|
" ?/H Show this help",
|
|
22
28
|
" Esc Cancel",
|
|
@@ -35,6 +41,8 @@ export function showHelp(ctx: ExtensionCommandContext): void {
|
|
|
35
41
|
" /extensions search <q> Search for packages",
|
|
36
42
|
" /extensions install <s> Install package (npm:, git:, or path)",
|
|
37
43
|
" /extensions remove <s> Remove installed package",
|
|
44
|
+
" /extensions update [s] Update package (or all packages)",
|
|
45
|
+
" /extensions auto-update Show or change update schedule",
|
|
38
46
|
];
|
|
39
47
|
|
|
40
48
|
const output = lines.join("\n");
|
package/src/ui/remote.ts
CHANGED
|
@@ -174,7 +174,7 @@ export async function browseRemotePackages(
|
|
|
174
174
|
await showRemoteMenu(ctx, pi);
|
|
175
175
|
return;
|
|
176
176
|
case "package":
|
|
177
|
-
await showPackageDetails(result.name, ctx, pi);
|
|
177
|
+
await showPackageDetails(result.name, ctx, pi, query, offset);
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
}
|
|
@@ -182,7 +182,9 @@ export async function browseRemotePackages(
|
|
|
182
182
|
async function showPackageDetails(
|
|
183
183
|
packageName: string,
|
|
184
184
|
ctx: ExtensionCommandContext,
|
|
185
|
-
pi: ExtensionAPI
|
|
185
|
+
pi: ExtensionAPI,
|
|
186
|
+
previousQuery: string,
|
|
187
|
+
previousOffset: number
|
|
186
188
|
): Promise<void> {
|
|
187
189
|
if (!ctx.hasUI) {
|
|
188
190
|
console.log(`Package: ${packageName}`);
|
|
@@ -230,9 +232,9 @@ async function showPackageDetails(
|
|
|
230
232
|
ctx.ui.notify(`Package: ${packageName}\n${infoRes.stdout.slice(0, 500)}`, "info");
|
|
231
233
|
}
|
|
232
234
|
}
|
|
233
|
-
await showPackageDetails(packageName, ctx, pi);
|
|
235
|
+
await showPackageDetails(packageName, ctx, pi, previousQuery, previousOffset);
|
|
234
236
|
} else if (choice.includes("Back")) {
|
|
235
|
-
await browseRemotePackages(ctx,
|
|
237
|
+
await browseRemotePackages(ctx, previousQuery, pi, previousOffset);
|
|
236
238
|
}
|
|
237
239
|
}
|
|
238
240
|
|
package/src/ui/unified.ts
CHANGED
|
@@ -17,11 +17,16 @@ import {
|
|
|
17
17
|
import type { UnifiedItem, State, UnifiedAction, InstalledPackage } from "../types/index.js";
|
|
18
18
|
import { discoverExtensions, setExtensionState } from "../extensions/discovery.js";
|
|
19
19
|
import { getInstalledPackages } from "../packages/discovery.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
showPackageActions,
|
|
22
|
+
updatePackage,
|
|
23
|
+
removePackage,
|
|
24
|
+
updatePackages,
|
|
25
|
+
} from "../packages/management.js";
|
|
21
26
|
import { showRemote } from "./remote.js";
|
|
22
27
|
import { showHelp } from "./help.js";
|
|
23
28
|
import { discoverExtensions as discoverExt } from "../extensions/discovery.js";
|
|
24
|
-
import { formatEntry as formatExtEntry, dynamicTruncate } from "../utils/format.js";
|
|
29
|
+
import { formatEntry as formatExtEntry, dynamicTruncate, formatBytes } from "../utils/format.js";
|
|
25
30
|
import {
|
|
26
31
|
getStatusIcon,
|
|
27
32
|
getPackageIcon,
|
|
@@ -30,6 +35,7 @@ import {
|
|
|
30
35
|
formatSize,
|
|
31
36
|
} from "./theme.js";
|
|
32
37
|
import { logExtensionToggle } from "../utils/history.js";
|
|
38
|
+
import { getKnownUpdates, promptAutoUpdateWizard } from "../utils/auto-update.js";
|
|
33
39
|
|
|
34
40
|
export async function showInteractive(
|
|
35
41
|
ctx: ExtensionCommandContext,
|
|
@@ -53,7 +59,8 @@ async function showInteractiveOnce(
|
|
|
53
59
|
]);
|
|
54
60
|
|
|
55
61
|
// Build unified items list
|
|
56
|
-
const
|
|
62
|
+
const knownUpdates = getKnownUpdates(ctx);
|
|
63
|
+
const items = buildUnifiedItems(localEntries, installedPackages, knownUpdates);
|
|
57
64
|
|
|
58
65
|
// If nothing found, show quick actions
|
|
59
66
|
if (items.length === 0) {
|
|
@@ -85,12 +92,15 @@ async function showInteractiveOnce(
|
|
|
85
92
|
new Text(
|
|
86
93
|
theme.fg(
|
|
87
94
|
"muted",
|
|
88
|
-
`${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter
|
|
95
|
+
`${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter toggle locals • Enter/A actions • u update pkg • x remove pkg`
|
|
89
96
|
),
|
|
90
97
|
2,
|
|
91
98
|
0
|
|
92
99
|
)
|
|
93
100
|
);
|
|
101
|
+
container.addChild(
|
|
102
|
+
new Text(theme.fg("dim", "Quick: i Install | f Search | U Update all | t Auto-update"), 2, 0)
|
|
103
|
+
);
|
|
94
104
|
container.addChild(new Spacer(1));
|
|
95
105
|
|
|
96
106
|
// Build settings items
|
|
@@ -134,19 +144,66 @@ async function showInteractiveOnce(
|
|
|
134
144
|
container.invalidate();
|
|
135
145
|
},
|
|
136
146
|
handleInput(data: string) {
|
|
147
|
+
const getSelectedId = (): string | undefined => {
|
|
148
|
+
const selIdx = (settingsList as unknown as { selectedIndex: number }).selectedIndex ?? 0;
|
|
149
|
+
return settingsItems[selIdx]?.id ?? settingsItems[0]?.id;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const selectedId = getSelectedId();
|
|
153
|
+
const selectedItem = selectedId ? byId.get(selectedId) : undefined;
|
|
154
|
+
|
|
137
155
|
if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
|
|
138
156
|
done({ type: "apply" });
|
|
139
157
|
return;
|
|
140
158
|
}
|
|
159
|
+
|
|
160
|
+
// Enter on a package opens its action menu (fewer clicks)
|
|
161
|
+
if ((data === "\r" || data === "\n") && selectedId && selectedItem?.type === "package") {
|
|
162
|
+
done({ type: "action", itemId: selectedId, action: "menu" });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
141
166
|
if (data === "a" || data === "A") {
|
|
142
|
-
// Get currently selected item and show actions
|
|
143
|
-
const selIdx = (settingsList as unknown as { selectedIndex: number }).selectedIndex ?? 0;
|
|
144
|
-
const selectedId = settingsItems[selIdx]?.id ?? settingsItems[0]?.id;
|
|
145
167
|
if (selectedId) {
|
|
146
|
-
done({ type: "action", itemId: selectedId });
|
|
168
|
+
done({ type: "action", itemId: selectedId, action: "menu" });
|
|
147
169
|
}
|
|
148
170
|
return;
|
|
149
171
|
}
|
|
172
|
+
|
|
173
|
+
// Quick actions (global)
|
|
174
|
+
if (data === "i") {
|
|
175
|
+
done({ type: "quick", action: "install" });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (data === "f") {
|
|
179
|
+
done({ type: "quick", action: "search" });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (data === "U") {
|
|
183
|
+
done({ type: "quick", action: "update-all" });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (data === "t" || data === "T") {
|
|
187
|
+
done({ type: "quick", action: "auto-update" });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Fast package actions
|
|
192
|
+
if (selectedId && selectedItem?.type === "package") {
|
|
193
|
+
if (data === "u") {
|
|
194
|
+
done({ type: "action", itemId: selectedId, action: "update" });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (data === "x" || data === "X") {
|
|
198
|
+
done({ type: "action", itemId: selectedId, action: "remove" });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (data === "v" || data === "V") {
|
|
202
|
+
done({ type: "action", itemId: selectedId, action: "details" });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
150
207
|
if (data === "r" || data === "R") {
|
|
151
208
|
done({ type: "remote" });
|
|
152
209
|
return;
|
|
@@ -170,7 +227,8 @@ async function showInteractiveOnce(
|
|
|
170
227
|
|
|
171
228
|
function buildUnifiedItems(
|
|
172
229
|
localEntries: Awaited<ReturnType<typeof discoverExtensions>>,
|
|
173
|
-
installedPackages: InstalledPackage[]
|
|
230
|
+
installedPackages: InstalledPackage[],
|
|
231
|
+
knownUpdates: Set<string>
|
|
174
232
|
): UnifiedItem[] {
|
|
175
233
|
const items: UnifiedItem[] = [];
|
|
176
234
|
|
|
@@ -207,6 +265,7 @@ function buildUnifiedItems(
|
|
|
207
265
|
version: pkg.version,
|
|
208
266
|
description: pkg.description,
|
|
209
267
|
size: pkg.size,
|
|
268
|
+
updateAvailable: knownUpdates.has(pkg.name),
|
|
210
269
|
});
|
|
211
270
|
}
|
|
212
271
|
|
|
@@ -260,7 +319,13 @@ function buildFooter(
|
|
|
260
319
|
footerParts.push("↑↓ Navigate");
|
|
261
320
|
if (hasLocals) footerParts.push("Space/Enter Toggle");
|
|
262
321
|
if (hasLocals) footerParts.push(hasChanges ? "S Save*" : "S Save");
|
|
263
|
-
if (hasPackages) footerParts.push("A Actions");
|
|
322
|
+
if (hasPackages) footerParts.push("Enter/A Actions");
|
|
323
|
+
if (hasPackages) footerParts.push("u Update");
|
|
324
|
+
if (hasPackages) footerParts.push("X Remove");
|
|
325
|
+
footerParts.push("i Install");
|
|
326
|
+
footerParts.push("f Search");
|
|
327
|
+
footerParts.push("U Update all");
|
|
328
|
+
footerParts.push("t Auto-update");
|
|
264
329
|
footerParts.push("R Browse");
|
|
265
330
|
footerParts.push("? Help");
|
|
266
331
|
footerParts.push("Esc Cancel");
|
|
@@ -286,6 +351,7 @@ function formatUnifiedItemLabel(
|
|
|
286
351
|
const scopeIcon = getScopeIcon(theme, item.scope as "global" | "project");
|
|
287
352
|
const name = theme.bold(item.displayName);
|
|
288
353
|
const version = item.version ? theme.fg("dim", `@${item.version}`) : "";
|
|
354
|
+
const updateBadge = item.updateAvailable ? ` ${theme.fg("warning", "[update]")}` : "";
|
|
289
355
|
|
|
290
356
|
// Build info parts
|
|
291
357
|
const infoParts: string[] = [];
|
|
@@ -308,7 +374,7 @@ function formatUnifiedItemLabel(
|
|
|
308
374
|
}
|
|
309
375
|
|
|
310
376
|
const summary = theme.fg("dim", infoParts.join(" • "));
|
|
311
|
-
return `${pkgIcon} [${scopeIcon}] ${name}${version} - ${summary}`;
|
|
377
|
+
return `${pkgIcon} [${scopeIcon}] ${name}${version}${updateBadge} - ${summary}`;
|
|
312
378
|
}
|
|
313
379
|
}
|
|
314
380
|
|
|
@@ -342,6 +408,28 @@ async function handleUnifiedAction(
|
|
|
342
408
|
return false;
|
|
343
409
|
}
|
|
344
410
|
|
|
411
|
+
if (result.type === "quick") {
|
|
412
|
+
switch (result.action) {
|
|
413
|
+
case "install":
|
|
414
|
+
await showRemote("install", ctx, pi);
|
|
415
|
+
return false;
|
|
416
|
+
case "search":
|
|
417
|
+
await showRemote("search", ctx, pi);
|
|
418
|
+
return false;
|
|
419
|
+
case "update-all":
|
|
420
|
+
await updatePackages(ctx, pi);
|
|
421
|
+
return false;
|
|
422
|
+
case "auto-update":
|
|
423
|
+
await promptAutoUpdateWizard(pi, ctx, (packages) => {
|
|
424
|
+
ctx.ui.notify(
|
|
425
|
+
`Updates available for ${packages.length} package(s): ${packages.join(", ")}`,
|
|
426
|
+
"info"
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
345
433
|
if (result.type === "action") {
|
|
346
434
|
const item = byId.get(result.itemId);
|
|
347
435
|
if (item?.type === "package") {
|
|
@@ -350,9 +438,31 @@ async function handleUnifiedAction(
|
|
|
350
438
|
name: item.displayName,
|
|
351
439
|
...(item.version ? { version: item.version } : {}),
|
|
352
440
|
scope: item.scope as "global" | "project",
|
|
441
|
+
...(item.description ? { description: item.description } : {}),
|
|
442
|
+
...(item.size !== undefined ? { size: item.size } : {}),
|
|
353
443
|
};
|
|
354
|
-
|
|
355
|
-
|
|
444
|
+
|
|
445
|
+
switch (result.action) {
|
|
446
|
+
case "update":
|
|
447
|
+
await updatePackage(pkg.source, ctx, pi);
|
|
448
|
+
return false;
|
|
449
|
+
case "remove":
|
|
450
|
+
await removePackage(pkg.source, ctx, pi);
|
|
451
|
+
return false;
|
|
452
|
+
case "details": {
|
|
453
|
+
const sizeStr = pkg.size !== undefined ? `\nSize: ${formatBytes(pkg.size)}` : "";
|
|
454
|
+
ctx.ui.notify(
|
|
455
|
+
`Name: ${pkg.name}\nVersion: ${pkg.version || "unknown"}\nSource: ${pkg.source}\nScope: ${pkg.scope}${sizeStr}${pkg.description ? `\nDescription: ${pkg.description}` : ""}`,
|
|
456
|
+
"info"
|
|
457
|
+
);
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
case "menu":
|
|
461
|
+
default: {
|
|
462
|
+
const exitManager = await showPackageActions(pkg, ctx, pi);
|
|
463
|
+
return exitManager;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
356
466
|
}
|
|
357
467
|
return false;
|
|
358
468
|
}
|
|
@@ -390,7 +500,12 @@ async function handleUnifiedAction(
|
|
|
390
500
|
);
|
|
391
501
|
|
|
392
502
|
if (shouldReload) {
|
|
393
|
-
ctx.
|
|
503
|
+
const reload = (ctx as ExtensionCommandContext & { reload?: () => Promise<void> }).reload;
|
|
504
|
+
if (typeof reload === "function") {
|
|
505
|
+
await reload.call(ctx);
|
|
506
|
+
} else {
|
|
507
|
+
ctx.ui.setEditorText("/reload");
|
|
508
|
+
}
|
|
394
509
|
return true;
|
|
395
510
|
}
|
|
396
511
|
}
|
package/src/utils/auto-update.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
saveAutoUpdateConfig,
|
|
15
15
|
getScheduleInterval,
|
|
16
16
|
calculateNextCheck,
|
|
17
|
+
parseDuration,
|
|
17
18
|
type AutoUpdateConfig,
|
|
18
19
|
} from "./settings.js";
|
|
19
20
|
|
|
@@ -98,6 +99,7 @@ export async function checkForUpdates(
|
|
|
98
99
|
...config,
|
|
99
100
|
lastCheck: Date.now(),
|
|
100
101
|
nextCheck: calculateNextCheck(config.intervalMs),
|
|
102
|
+
updatesAvailable,
|
|
101
103
|
});
|
|
102
104
|
|
|
103
105
|
if (updatesAvailable.length > 0 && onUpdateAvailable) {
|
|
@@ -152,6 +154,76 @@ export function getAutoUpdateStatus(ctx: ExtensionCommandContext | ExtensionCont
|
|
|
152
154
|
return `${indicator} ${config.displayText}`;
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Return package names currently known to have updates available
|
|
159
|
+
* (from the latest background check).
|
|
160
|
+
*/
|
|
161
|
+
export function getKnownUpdates(ctx: ExtensionCommandContext | ExtensionContext): Set<string> {
|
|
162
|
+
const config = getAutoUpdateConfig(ctx);
|
|
163
|
+
return new Set(config.updatesAvailable ?? []);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Interactive wizard to configure auto-update frequency.
|
|
168
|
+
*/
|
|
169
|
+
export async function promptAutoUpdateWizard(
|
|
170
|
+
pi: ExtensionAPI,
|
|
171
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
172
|
+
onUpdateAvailable?: (packages: string[]) => void
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
if (!ctx.hasUI) {
|
|
175
|
+
notify(ctx, "Auto-update wizard requires interactive mode.", "warning");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const current = getAutoUpdateConfig(ctx);
|
|
180
|
+
const choice = await ctx.ui.select(`Auto-update (${current.displayText})`, [
|
|
181
|
+
"Off",
|
|
182
|
+
"Every hour",
|
|
183
|
+
"Daily",
|
|
184
|
+
"Weekly",
|
|
185
|
+
"Custom...",
|
|
186
|
+
"Cancel",
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
if (!choice || choice === "Cancel") return;
|
|
190
|
+
|
|
191
|
+
if (choice === "Off") {
|
|
192
|
+
disableAutoUpdate(pi, ctx);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (choice === "Every hour") {
|
|
197
|
+
enableAutoUpdate(pi, ctx, 60 * 60 * 1000, "1 hour", onUpdateAvailable);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (choice === "Daily") {
|
|
202
|
+
enableAutoUpdate(pi, ctx, 24 * 60 * 60 * 1000, "daily", onUpdateAvailable);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (choice === "Weekly") {
|
|
207
|
+
enableAutoUpdate(pi, ctx, 7 * 24 * 60 * 60 * 1000, "weekly", onUpdateAvailable);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const input = await ctx.ui.input("Auto-update interval", current.displayText || "1d");
|
|
212
|
+
if (!input?.trim()) return;
|
|
213
|
+
|
|
214
|
+
const parsed = parseDuration(input.trim());
|
|
215
|
+
if (!parsed) {
|
|
216
|
+
notify(ctx, "Invalid duration. Examples: 1h, 1d, 1w, 1m, never", "warning");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (parsed.ms === 0) {
|
|
221
|
+
disableAutoUpdate(pi, ctx);
|
|
222
|
+
} else {
|
|
223
|
+
enableAutoUpdate(pi, ctx, parsed.ms, parsed.display, onUpdateAvailable);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
155
227
|
/**
|
|
156
228
|
* Enable auto-update with specified interval
|
|
157
229
|
*/
|
|
@@ -168,6 +240,7 @@ export function enableAutoUpdate(
|
|
|
168
240
|
displayText,
|
|
169
241
|
lastCheck: Date.now(),
|
|
170
242
|
nextCheck: calculateNextCheck(intervalMs),
|
|
243
|
+
updatesAvailable: [],
|
|
171
244
|
};
|
|
172
245
|
|
|
173
246
|
saveAutoUpdateConfig(pi, config);
|
|
@@ -189,6 +262,7 @@ export function disableAutoUpdate(
|
|
|
189
262
|
intervalMs: 0,
|
|
190
263
|
enabled: false,
|
|
191
264
|
displayText: "off",
|
|
265
|
+
updatesAvailable: [],
|
|
192
266
|
});
|
|
193
267
|
|
|
194
268
|
notify(ctx, "Auto-update disabled", "info");
|
package/src/utils/settings.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-update settings storage
|
|
3
|
-
*
|
|
3
|
+
* Persists to disk so config survives across pi sessions.
|
|
4
4
|
*/
|
|
5
5
|
import type {
|
|
6
6
|
ExtensionAPI,
|
|
7
7
|
ExtensionCommandContext,
|
|
8
8
|
ExtensionContext,
|
|
9
9
|
} from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
10
13
|
|
|
11
14
|
export interface AutoUpdateConfig {
|
|
12
15
|
intervalMs: number;
|
|
@@ -14,6 +17,7 @@ export interface AutoUpdateConfig {
|
|
|
14
17
|
nextCheck?: number;
|
|
15
18
|
enabled: boolean;
|
|
16
19
|
displayText: string; // Human-readable description
|
|
20
|
+
updatesAvailable?: string[];
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
const DEFAULT_CONFIG: AutoUpdateConfig = {
|
|
@@ -23,16 +27,43 @@ const DEFAULT_CONFIG: AutoUpdateConfig = {
|
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
const SETTINGS_KEY = "extmgr-auto-update";
|
|
30
|
+
const SETTINGS_DIR = process.env.PI_EXTMGR_CACHE_DIR
|
|
31
|
+
? process.env.PI_EXTMGR_CACHE_DIR
|
|
32
|
+
: join(homedir(), ".pi", "agent", ".extmgr-cache");
|
|
33
|
+
const SETTINGS_FILE = join(SETTINGS_DIR, "auto-update.json");
|
|
34
|
+
|
|
35
|
+
function readConfigFromDisk(): AutoUpdateConfig | undefined {
|
|
36
|
+
try {
|
|
37
|
+
const raw = readFileSync(SETTINGS_FILE, "utf8");
|
|
38
|
+
const parsed = JSON.parse(raw) as Partial<AutoUpdateConfig>;
|
|
39
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeConfigToDisk(config: AutoUpdateConfig): void {
|
|
46
|
+
try {
|
|
47
|
+
mkdirSync(SETTINGS_DIR, { recursive: true });
|
|
48
|
+
writeFileSync(SETTINGS_FILE, JSON.stringify(config, null, 2), "utf8");
|
|
49
|
+
} catch {
|
|
50
|
+
// Best effort; session state still works even if disk write fails
|
|
51
|
+
}
|
|
52
|
+
}
|
|
26
53
|
|
|
27
54
|
/**
|
|
28
|
-
* Get auto-update config
|
|
55
|
+
* Get auto-update config.
|
|
56
|
+
* Priority:
|
|
57
|
+
* 1) latest value in current session entries
|
|
58
|
+
* 2) persisted value on disk
|
|
59
|
+
* 3) defaults
|
|
29
60
|
*/
|
|
30
61
|
export function getAutoUpdateConfig(
|
|
31
62
|
ctx: ExtensionCommandContext | ExtensionContext
|
|
32
63
|
): AutoUpdateConfig {
|
|
33
64
|
const entries = ctx.sessionManager.getEntries();
|
|
34
65
|
|
|
35
|
-
// Find most recent config entry
|
|
66
|
+
// Find most recent config entry in current session
|
|
36
67
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
37
68
|
const entry = entries[i];
|
|
38
69
|
if (entry?.type === "custom" && entry.customType === SETTINGS_KEY && entry.data) {
|
|
@@ -40,19 +71,26 @@ export function getAutoUpdateConfig(
|
|
|
40
71
|
}
|
|
41
72
|
}
|
|
42
73
|
|
|
74
|
+
const persisted = readConfigFromDisk();
|
|
75
|
+
if (persisted) {
|
|
76
|
+
return persisted;
|
|
77
|
+
}
|
|
78
|
+
|
|
43
79
|
return { ...DEFAULT_CONFIG };
|
|
44
80
|
}
|
|
45
81
|
|
|
46
82
|
/**
|
|
47
|
-
* Save auto-update config to session
|
|
83
|
+
* Save auto-update config to session + disk.
|
|
48
84
|
*/
|
|
49
85
|
export function saveAutoUpdateConfig(pi: ExtensionAPI, config: Partial<AutoUpdateConfig>): void {
|
|
50
86
|
const fullConfig: AutoUpdateConfig = {
|
|
51
87
|
...DEFAULT_CONFIG,
|
|
88
|
+
...(readConfigFromDisk() ?? {}),
|
|
52
89
|
...config,
|
|
53
90
|
};
|
|
54
91
|
|
|
55
92
|
pi.appendEntry(SETTINGS_KEY, fullConfig);
|
|
93
|
+
writeConfigToDisk(fullConfig);
|
|
56
94
|
}
|
|
57
95
|
|
|
58
96
|
/**
|
package/src/utils/ui-helpers.ts
CHANGED
|
@@ -20,7 +20,12 @@ export async function confirmReload(
|
|
|
20
20
|
const confirmed = await ctx.ui.confirm("Reload Required", `${reason}\nReload pi now?`);
|
|
21
21
|
|
|
22
22
|
if (confirmed) {
|
|
23
|
-
ctx.
|
|
23
|
+
const reload = (ctx as ExtensionCommandContext & { reload?: () => Promise<void> }).reload;
|
|
24
|
+
if (typeof reload === "function") {
|
|
25
|
+
await reload.call(ctx);
|
|
26
|
+
} else {
|
|
27
|
+
ctx.ui.setEditorText("/reload");
|
|
28
|
+
}
|
|
24
29
|
return true;
|
|
25
30
|
}
|
|
26
31
|
|