pi-extmgr 0.1.23 → 0.1.25
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 +25 -12
- package/package.json +1 -1
- package/src/commands/auto-update.ts +1 -1
- package/src/commands/cache.ts +5 -1
- package/src/commands/history.ts +4 -2
- package/src/commands/registry.ts +1 -1
- package/src/packages/discovery.ts +144 -51
- package/src/packages/extensions.ts +171 -63
- package/src/packages/install.ts +101 -22
- package/src/packages/management.ts +12 -37
- package/src/ui/package-config.ts +157 -126
- package/src/ui/remote.ts +77 -52
- package/src/ui/unified.ts +217 -172
- package/src/utils/auto-update.ts +34 -29
- package/src/utils/cache.ts +15 -2
- package/src/utils/history.ts +41 -2
- package/src/utils/mode.ts +56 -5
- package/src/utils/network.ts +15 -0
- package/src/utils/package-source.ts +31 -0
- package/src/utils/settings-list.ts +12 -0
- package/src/utils/settings.ts +35 -7
- package/src/utils/status.ts +10 -2
- package/src/utils/timer.ts +32 -8
- package/src/utils/ui-helpers.ts +2 -1
package/src/ui/unified.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
updatePackageWithOutcome,
|
|
26
26
|
removePackageWithOutcome,
|
|
27
27
|
updatePackagesWithOutcome,
|
|
28
|
+
showInstalledPackagesList,
|
|
28
29
|
} from "../packages/management.js";
|
|
29
30
|
import { showRemote } from "./remote.js";
|
|
30
31
|
import { showHelp } from "./help.js";
|
|
@@ -37,38 +38,39 @@ import {
|
|
|
37
38
|
formatSize,
|
|
38
39
|
} from "./theme.js";
|
|
39
40
|
import { buildFooterState, buildFooterShortcuts, getPendingToggleChangeCount } from "./footer.js";
|
|
40
|
-
import { logExtensionToggle } from "../utils/history.js";
|
|
41
|
+
import { logExtensionDelete, logExtensionToggle } from "../utils/history.js";
|
|
41
42
|
import { getKnownUpdates, promptAutoUpdateWizard } from "../utils/auto-update.js";
|
|
42
43
|
import { updateExtmgrStatus } from "../utils/status.js";
|
|
43
44
|
import { parseChoiceByLabel } from "../utils/command.js";
|
|
44
|
-
import {
|
|
45
|
+
import { notify } from "../utils/notify.js";
|
|
46
|
+
import { getPackageSourceKind, normalizePackageIdentity } from "../utils/package-source.js";
|
|
47
|
+
import { hasCustomUI, runCustomUI } from "../utils/mode.js";
|
|
48
|
+
import { getSettingsListSelectedIndex } from "../utils/settings-list.js";
|
|
45
49
|
import { UI } from "../constants.js";
|
|
46
50
|
import { configurePackageExtensions } from "./package-config.js";
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Safely gets the selected index from a SettingsList component
|
|
56
|
-
* Returns undefined if the component doesn't have the expected interface
|
|
57
|
-
*/
|
|
58
|
-
function getSelectedIndex(settingsList: unknown): number | undefined {
|
|
59
|
-
if (settingsList && typeof settingsList === "object") {
|
|
60
|
-
const selectable = settingsList as SelectableList;
|
|
61
|
-
if (typeof selectable.selectedIndex === "number") {
|
|
62
|
-
return selectable.selectedIndex;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return undefined;
|
|
52
|
+
async function showInteractiveFallback(
|
|
53
|
+
ctx: ExtensionCommandContext,
|
|
54
|
+
pi: ExtensionAPI
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
await showListOnly(ctx);
|
|
57
|
+
await showInstalledPackagesList(ctx, pi);
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
export async function showInteractive(
|
|
69
61
|
ctx: ExtensionCommandContext,
|
|
70
62
|
pi: ExtensionAPI
|
|
71
63
|
): Promise<void> {
|
|
64
|
+
if (!hasCustomUI(ctx)) {
|
|
65
|
+
notify(
|
|
66
|
+
ctx,
|
|
67
|
+
"The unified extensions manager requires the full interactive TUI. Showing read-only local and installed package lists instead.",
|
|
68
|
+
"warning"
|
|
69
|
+
);
|
|
70
|
+
await showInteractiveFallback(ctx, pi);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
72
74
|
// Main loop - keeps showing the menu until user explicitly exits
|
|
73
75
|
while (true) {
|
|
74
76
|
const shouldExit = await showInteractiveOnce(ctx, pi);
|
|
@@ -108,156 +110,192 @@ async function showInteractiveOnce(
|
|
|
108
110
|
const staged = new Map<string, State>();
|
|
109
111
|
const byId = new Map(items.map((item) => [item.id, item]));
|
|
110
112
|
|
|
111
|
-
const result = await
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
container.addChild(settingsList);
|
|
162
|
-
container.addChild(new Spacer(1));
|
|
163
|
-
|
|
164
|
-
// Footer with keyboard shortcuts
|
|
165
|
-
const footerState = buildFooterState(items);
|
|
166
|
-
container.addChild(new Text(theme.fg("dim", buildFooterShortcuts(footerState)), 2, 0));
|
|
167
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
render(width: number) {
|
|
171
|
-
return container.render(width);
|
|
172
|
-
},
|
|
173
|
-
invalidate() {
|
|
174
|
-
container.invalidate();
|
|
175
|
-
},
|
|
176
|
-
handleInput(data: string) {
|
|
177
|
-
const selIdx = getSelectedIndex(settingsList) ?? 0;
|
|
178
|
-
const selectedId = settingsItems[selIdx]?.id ?? settingsItems[0]?.id;
|
|
179
|
-
const selectedItem = selectedId ? byId.get(selectedId) : undefined;
|
|
180
|
-
|
|
181
|
-
if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
|
|
182
|
-
done({ type: "apply" });
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Enter on a package opens its action menu (fewer clicks)
|
|
187
|
-
if ((data === "\r" || data === "\n") && selectedId && selectedItem?.type === "package") {
|
|
188
|
-
done({ type: "action", itemId: selectedId, action: "menu" });
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (data === "a" || data === "A") {
|
|
193
|
-
if (selectedId) {
|
|
194
|
-
done({ type: "action", itemId: selectedId, action: "menu" });
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Quick actions (global)
|
|
200
|
-
if (data === "i") {
|
|
201
|
-
done({ type: "quick", action: "install" });
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (data === "f") {
|
|
205
|
-
done({ type: "quick", action: "search" });
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
if (data === "U") {
|
|
209
|
-
done({ type: "quick", action: "update-all" });
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (data === "t" || data === "T") {
|
|
213
|
-
done({ type: "quick", action: "auto-update" });
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Fast actions on selected row
|
|
218
|
-
if (selectedId && selectedItem?.type === "package") {
|
|
219
|
-
if (data === "u") {
|
|
220
|
-
done({ type: "action", itemId: selectedId, action: "update" });
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (data === "x" || data === "X") {
|
|
224
|
-
done({ type: "action", itemId: selectedId, action: "remove" });
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
if (data === "v" || data === "V") {
|
|
228
|
-
done({ type: "action", itemId: selectedId, action: "details" });
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
if (data === "c" || data === "C") {
|
|
232
|
-
done({ type: "action", itemId: selectedId, action: "configure" });
|
|
233
|
-
return;
|
|
113
|
+
const result = await runCustomUI(
|
|
114
|
+
ctx,
|
|
115
|
+
"The unified extensions manager",
|
|
116
|
+
() =>
|
|
117
|
+
ctx.ui.custom<UnifiedAction>((tui, theme, _keybindings, done) => {
|
|
118
|
+
const container = new Container();
|
|
119
|
+
|
|
120
|
+
const titleText = new Text("", 2, 0);
|
|
121
|
+
const subtitleText = new Text("", 2, 0);
|
|
122
|
+
const quickText = new Text("", 2, 0);
|
|
123
|
+
const footerState = buildFooterState(items);
|
|
124
|
+
const footerText = new Text("", 2, 0);
|
|
125
|
+
|
|
126
|
+
// Header
|
|
127
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
128
|
+
container.addChild(titleText);
|
|
129
|
+
container.addChild(subtitleText);
|
|
130
|
+
container.addChild(quickText);
|
|
131
|
+
container.addChild(new Spacer(1));
|
|
132
|
+
|
|
133
|
+
// Build settings items
|
|
134
|
+
const settingsItems = buildSettingsItems(items, staged, theme);
|
|
135
|
+
const syncThemedContent = (): void => {
|
|
136
|
+
titleText.setText(theme.fg("accent", theme.bold("Extensions Manager")));
|
|
137
|
+
subtitleText.setText(
|
|
138
|
+
theme.fg(
|
|
139
|
+
"muted",
|
|
140
|
+
`${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter toggle local • Enter/A actions • c configure pkg extensions • u update pkg • x remove selected`
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
quickText.setText(
|
|
144
|
+
theme.fg(
|
|
145
|
+
"dim",
|
|
146
|
+
"Quick: i Install | f Search | U Update all | t Auto-update | p Palette"
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
footerText.setText(theme.fg("dim", buildFooterShortcuts(footerState)));
|
|
150
|
+
|
|
151
|
+
for (const settingsItem of settingsItems) {
|
|
152
|
+
const item = byId.get(settingsItem.id);
|
|
153
|
+
if (!item) continue;
|
|
154
|
+
|
|
155
|
+
if (item.type === "local") {
|
|
156
|
+
const currentState = staged.get(item.id) ?? item.state!;
|
|
157
|
+
const changed = staged.has(item.id) && currentState !== item.originalState;
|
|
158
|
+
settingsItem.label = formatUnifiedItemLabel(item, currentState, theme, changed);
|
|
159
|
+
} else {
|
|
160
|
+
settingsItem.label = formatUnifiedItemLabel(item, "enabled", theme, false);
|
|
161
|
+
}
|
|
234
162
|
}
|
|
235
|
-
}
|
|
163
|
+
};
|
|
164
|
+
syncThemedContent();
|
|
165
|
+
|
|
166
|
+
const settingsList = new SettingsList(
|
|
167
|
+
settingsItems,
|
|
168
|
+
Math.min(items.length + 2, UI.maxListHeight),
|
|
169
|
+
getSettingsListTheme(),
|
|
170
|
+
(id: string, newValue: string) => {
|
|
171
|
+
const item = byId.get(id);
|
|
172
|
+
if (!item || item.type !== "local") return;
|
|
173
|
+
|
|
174
|
+
const state = newValue as State;
|
|
175
|
+
staged.set(id, state);
|
|
176
|
+
|
|
177
|
+
const settingsItem = settingsItems.find((x) => x.id === id);
|
|
178
|
+
if (settingsItem) {
|
|
179
|
+
const changed = state !== item.originalState;
|
|
180
|
+
settingsItem.label = formatUnifiedItemLabel(item, state, theme, changed);
|
|
181
|
+
}
|
|
182
|
+
tui.requestRender();
|
|
183
|
+
},
|
|
184
|
+
() => done({ type: "cancel" }),
|
|
185
|
+
{ enableSearch: items.length > UI.searchThreshold }
|
|
186
|
+
);
|
|
236
187
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
188
|
+
container.addChild(settingsList);
|
|
189
|
+
container.addChild(new Spacer(1));
|
|
190
|
+
|
|
191
|
+
// Footer with keyboard shortcuts
|
|
192
|
+
container.addChild(footerText);
|
|
193
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
render(width: number) {
|
|
197
|
+
return container.render(width);
|
|
198
|
+
},
|
|
199
|
+
invalidate() {
|
|
200
|
+
container.invalidate();
|
|
201
|
+
syncThemedContent();
|
|
202
|
+
},
|
|
203
|
+
handleInput(data: string) {
|
|
204
|
+
const selIdx = getSettingsListSelectedIndex(settingsList) ?? 0;
|
|
205
|
+
const selectedId = settingsItems[selIdx]?.id ?? settingsItems[0]?.id;
|
|
206
|
+
const selectedItem = selectedId ? byId.get(selectedId) : undefined;
|
|
207
|
+
|
|
208
|
+
if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
|
|
209
|
+
done({ type: "apply" });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Enter on a package opens its action menu (fewer clicks)
|
|
214
|
+
if (
|
|
215
|
+
(data === "\r" || data === "\n") &&
|
|
216
|
+
selectedId &&
|
|
217
|
+
selectedItem?.type === "package"
|
|
218
|
+
) {
|
|
219
|
+
done({ type: "action", itemId: selectedId, action: "menu" });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (data === "a" || data === "A") {
|
|
224
|
+
if (selectedId) {
|
|
225
|
+
done({ type: "action", itemId: selectedId, action: "menu" });
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Quick actions (global)
|
|
231
|
+
if (data === "i") {
|
|
232
|
+
done({ type: "quick", action: "install" });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (data === "f") {
|
|
236
|
+
done({ type: "quick", action: "search" });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (data === "U") {
|
|
240
|
+
done({ type: "quick", action: "update-all" });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (data === "t" || data === "T") {
|
|
244
|
+
done({ type: "quick", action: "auto-update" });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Fast actions on selected row
|
|
249
|
+
if (selectedId && selectedItem?.type === "package") {
|
|
250
|
+
if (data === "u") {
|
|
251
|
+
done({ type: "action", itemId: selectedId, action: "update" });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (data === "x" || data === "X") {
|
|
255
|
+
done({ type: "action", itemId: selectedId, action: "remove" });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (data === "v" || data === "V") {
|
|
259
|
+
done({ type: "action", itemId: selectedId, action: "details" });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (data === "c" || data === "C") {
|
|
263
|
+
done({ type: "action", itemId: selectedId, action: "configure" });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (selectedId && selectedItem?.type === "local") {
|
|
269
|
+
if (data === "x" || data === "X") {
|
|
270
|
+
done({ type: "action", itemId: selectedId, action: "remove" });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (data === "r" || data === "R") {
|
|
276
|
+
done({ type: "remote" });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (data === "?" || data === "h" || data === "H") {
|
|
280
|
+
done({ type: "help" });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (data === "m" || data === "M" || data === "p" || data === "P") {
|
|
284
|
+
done({ type: "menu" });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
settingsList.handleInput?.(data);
|
|
288
|
+
tui.requestRender();
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}),
|
|
292
|
+
"Showing read-only local and installed package lists instead."
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (!result) {
|
|
296
|
+
await showInteractiveFallback(ctx, pi);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
261
299
|
|
|
262
300
|
return await handleUnifiedAction(result, items, staged, byId, ctx, pi);
|
|
263
301
|
}
|
|
@@ -332,7 +370,7 @@ export function buildUnifiedItems(
|
|
|
332
370
|
version: pkg.version,
|
|
333
371
|
description: pkg.description,
|
|
334
372
|
size: pkg.size,
|
|
335
|
-
updateAvailable: knownUpdates.has(pkg.
|
|
373
|
+
updateAvailable: knownUpdates.has(normalizePackageIdentity(pkg.source)),
|
|
336
374
|
});
|
|
337
375
|
}
|
|
338
376
|
|
|
@@ -720,10 +758,12 @@ async function handleUnifiedAction(
|
|
|
720
758
|
ctx.cwd
|
|
721
759
|
);
|
|
722
760
|
if (!removal.ok) {
|
|
761
|
+
logExtensionDelete(pi, item.id, false, removal.error);
|
|
723
762
|
ctx.ui.notify(`Failed to remove extension: ${removal.error}`, "error");
|
|
724
763
|
return false;
|
|
725
764
|
}
|
|
726
765
|
|
|
766
|
+
logExtensionDelete(pi, item.id, true);
|
|
727
767
|
ctx.ui.notify(
|
|
728
768
|
`Removed ${item.displayName}${removal.removedDirectory ? " (directory)" : ""}.`,
|
|
729
769
|
"info"
|
|
@@ -825,12 +865,15 @@ export async function showInstalledPackagesLegacy(
|
|
|
825
865
|
ctx: ExtensionCommandContext,
|
|
826
866
|
pi: ExtensionAPI
|
|
827
867
|
): Promise<void> {
|
|
868
|
+
if (!hasCustomUI(ctx)) {
|
|
869
|
+
await showInstalledPackagesList(ctx, pi);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
828
873
|
ctx.ui.notify(
|
|
829
874
|
"📦 Use /extensions for the unified view.\nInstalled packages are now shown alongside local extensions.",
|
|
830
875
|
"info"
|
|
831
876
|
);
|
|
832
|
-
// Small delay then open the main manager
|
|
833
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
834
877
|
await showInteractive(ctx, pi);
|
|
835
878
|
}
|
|
836
879
|
|
|
@@ -849,10 +892,12 @@ export async function showListOnly(ctx: ExtensionCommandContext): Promise<void>
|
|
|
849
892
|
|
|
850
893
|
const lines = entries.map(formatExtEntry);
|
|
851
894
|
const output = lines.join("\n");
|
|
895
|
+
const titledOutput = `Local extensions:\n${output}`;
|
|
852
896
|
|
|
853
897
|
if (ctx.hasUI) {
|
|
854
|
-
ctx.ui.notify(
|
|
898
|
+
ctx.ui.notify(titledOutput, "info");
|
|
855
899
|
} else {
|
|
900
|
+
console.log("Local extensions:");
|
|
856
901
|
console.log(output);
|
|
857
902
|
}
|
|
858
903
|
}
|
package/src/utils/auto-update.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
} from "./settings.js";
|
|
20
20
|
import { parseNpmSource } from "./format.js";
|
|
21
21
|
import { execNpm } from "./npm-exec.js";
|
|
22
|
+
import { normalizePackageIdentity } from "./package-source.js";
|
|
23
|
+
import { logAutoUpdateConfig } from "./history.js";
|
|
22
24
|
import { TIMEOUTS } from "../constants.js";
|
|
23
25
|
|
|
24
26
|
import { startTimer, stopTimer, isTimerRunning } from "./timer.js";
|
|
@@ -26,6 +28,10 @@ import { startTimer, stopTimer, isTimerRunning } from "./timer.js";
|
|
|
26
28
|
// Context provider for safe session handling
|
|
27
29
|
export type ContextProvider = () => (ExtensionCommandContext | ExtensionContext) | undefined;
|
|
28
30
|
|
|
31
|
+
function getUpdateIdentity(pkg: InstalledPackage): string {
|
|
32
|
+
return normalizePackageIdentity(pkg.source);
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
/**
|
|
30
36
|
* Start auto-update background checker
|
|
31
37
|
* Uses a context provider to avoid stale context issues when sessions switch
|
|
@@ -48,27 +54,23 @@ export function startAutoUpdateTimer(
|
|
|
48
54
|
const interval = getScheduleInterval(config);
|
|
49
55
|
if (!interval) return;
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
saveAutoUpdateConfig(pi, {
|
|
69
|
-
...config,
|
|
70
|
-
nextCheck: calculateNextCheck(config.intervalMs),
|
|
71
|
-
});
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const nextCheck = config.nextCheck;
|
|
59
|
+
const initialDelayMs =
|
|
60
|
+
typeof nextCheck === "number" && nextCheck > now ? Math.max(0, nextCheck - now) : 0;
|
|
61
|
+
|
|
62
|
+
startTimer(
|
|
63
|
+
interval,
|
|
64
|
+
() => {
|
|
65
|
+
const checkCtx = getCtx();
|
|
66
|
+
if (!checkCtx) {
|
|
67
|
+
stopAutoUpdateTimer();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
void checkForUpdates(pi, checkCtx, onUpdateAvailable);
|
|
71
|
+
},
|
|
72
|
+
{ initialDelayMs }
|
|
73
|
+
);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/**
|
|
@@ -98,28 +100,30 @@ export async function checkForUpdates(
|
|
|
98
100
|
const npmPackages = packages.filter((p) => p.source.startsWith("npm:"));
|
|
99
101
|
|
|
100
102
|
const updatesAvailable: string[] = [];
|
|
103
|
+
const updatedPackageNames: string[] = [];
|
|
101
104
|
|
|
102
105
|
for (const pkg of npmPackages) {
|
|
103
106
|
const hasUpdate = await checkPackageUpdate(pkg, ctx, pi);
|
|
104
107
|
if (hasUpdate) {
|
|
105
|
-
updatesAvailable.push(pkg
|
|
108
|
+
updatesAvailable.push(getUpdateIdentity(pkg));
|
|
109
|
+
updatedPackageNames.push(pkg.name);
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
const checkedAt = Date.now();
|
|
110
114
|
const config = getAutoUpdateConfig(ctx);
|
|
111
115
|
saveAutoUpdateConfig(pi, {
|
|
112
116
|
...config,
|
|
113
|
-
lastCheck:
|
|
117
|
+
lastCheck: checkedAt,
|
|
114
118
|
nextCheck: calculateNextCheck(config.intervalMs),
|
|
115
119
|
updatesAvailable,
|
|
116
120
|
});
|
|
117
121
|
|
|
118
|
-
if (
|
|
119
|
-
onUpdateAvailable(
|
|
122
|
+
if (updatedPackageNames.length > 0 && onUpdateAvailable) {
|
|
123
|
+
onUpdateAvailable(updatedPackageNames);
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
return
|
|
126
|
+
return updatedPackageNames;
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
/**
|
|
@@ -168,7 +172,7 @@ export function getAutoUpdateStatus(ctx: ExtensionCommandContext | ExtensionCont
|
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
/**
|
|
171
|
-
* Return package
|
|
175
|
+
* Return normalized package identities currently known to have updates available
|
|
172
176
|
* (from the latest background check).
|
|
173
177
|
*/
|
|
174
178
|
export function getKnownUpdates(ctx: ExtensionCommandContext | ExtensionContext): Set<string> {
|
|
@@ -251,12 +255,12 @@ export function enableAutoUpdate(
|
|
|
251
255
|
intervalMs,
|
|
252
256
|
enabled: true,
|
|
253
257
|
displayText,
|
|
254
|
-
lastCheck: Date.now(),
|
|
255
258
|
nextCheck: calculateNextCheck(intervalMs),
|
|
256
259
|
updatesAvailable: [],
|
|
257
260
|
};
|
|
258
261
|
|
|
259
262
|
saveAutoUpdateConfig(pi, config);
|
|
263
|
+
logAutoUpdateConfig(pi, `set to ${displayText}`, true);
|
|
260
264
|
|
|
261
265
|
const getCtx: ContextProvider = () => ctx;
|
|
262
266
|
|
|
@@ -280,6 +284,7 @@ export function disableAutoUpdate(
|
|
|
280
284
|
displayText: "off",
|
|
281
285
|
updatesAvailable: [],
|
|
282
286
|
});
|
|
287
|
+
logAutoUpdateConfig(pi, "disabled", true);
|
|
283
288
|
|
|
284
289
|
notify(ctx, "Auto-update disabled", "info");
|
|
285
290
|
}
|
package/src/utils/cache.ts
CHANGED
|
@@ -12,6 +12,7 @@ const CACHE_DIR = process.env.PI_EXTMGR_CACHE_DIR
|
|
|
12
12
|
? process.env.PI_EXTMGR_CACHE_DIR
|
|
13
13
|
: join(homedir(), ".pi", "agent", ".extmgr-cache");
|
|
14
14
|
const CACHE_FILE = join(CACHE_DIR, "metadata.json");
|
|
15
|
+
const CURRENT_SEARCH_CACHE_STRATEGY = "npm-registry-v1-paginated";
|
|
15
16
|
|
|
16
17
|
interface CachedPackageData {
|
|
17
18
|
name: string;
|
|
@@ -29,6 +30,7 @@ interface CacheData {
|
|
|
29
30
|
query: string;
|
|
30
31
|
results: string[];
|
|
31
32
|
timestamp: number;
|
|
33
|
+
strategy: string;
|
|
32
34
|
}
|
|
33
35
|
| undefined;
|
|
34
36
|
}
|
|
@@ -92,12 +94,15 @@ function normalizeCacheFromDisk(input: unknown): CacheData {
|
|
|
92
94
|
const query = input.lastSearch.query;
|
|
93
95
|
const timestamp = input.lastSearch.timestamp;
|
|
94
96
|
const results = input.lastSearch.results;
|
|
97
|
+
const strategy = input.lastSearch.strategy;
|
|
95
98
|
|
|
96
99
|
if (
|
|
97
100
|
typeof query === "string" &&
|
|
98
101
|
typeof timestamp === "number" &&
|
|
99
102
|
Number.isFinite(timestamp) &&
|
|
100
|
-
Array.isArray(results)
|
|
103
|
+
Array.isArray(results) &&
|
|
104
|
+
typeof strategy === "string" &&
|
|
105
|
+
strategy.trim()
|
|
101
106
|
) {
|
|
102
107
|
const normalizedResults = results.filter(
|
|
103
108
|
(value): value is string => typeof value === "string"
|
|
@@ -106,6 +111,7 @@ function normalizeCacheFromDisk(input: unknown): CacheData {
|
|
|
106
111
|
query,
|
|
107
112
|
timestamp,
|
|
108
113
|
results: normalizedResults,
|
|
114
|
+
strategy: strategy.trim(),
|
|
109
115
|
};
|
|
110
116
|
}
|
|
111
117
|
}
|
|
@@ -194,7 +200,9 @@ async function saveCache(): Promise<void> {
|
|
|
194
200
|
const data: {
|
|
195
201
|
version: number;
|
|
196
202
|
packages: Record<string, CachedPackageData>;
|
|
197
|
-
lastSearch?:
|
|
203
|
+
lastSearch?:
|
|
204
|
+
| { query: string; results: string[]; timestamp: number; strategy: string }
|
|
205
|
+
| undefined;
|
|
198
206
|
} = {
|
|
199
207
|
version: memoryCache.version,
|
|
200
208
|
packages: Object.fromEntries(memoryCache.packages),
|
|
@@ -276,6 +284,10 @@ export async function getCachedSearch(query: string): Promise<NpmPackage[] | nul
|
|
|
276
284
|
return null;
|
|
277
285
|
}
|
|
278
286
|
|
|
287
|
+
if (cache.lastSearch.strategy !== CURRENT_SEARCH_CACHE_STRATEGY) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
279
291
|
// Reconstruct packages from cached names
|
|
280
292
|
const packages: NpmPackage[] = [];
|
|
281
293
|
for (const name of cache.lastSearch.results) {
|
|
@@ -313,6 +325,7 @@ export async function setCachedSearch(query: string, packages: NpmPackage[]): Pr
|
|
|
313
325
|
query,
|
|
314
326
|
results: packages.map((p) => p.name),
|
|
315
327
|
timestamp: Date.now(),
|
|
328
|
+
strategy: CURRENT_SEARCH_CACHE_STRATEGY,
|
|
316
329
|
};
|
|
317
330
|
|
|
318
331
|
await enqueueCacheSave();
|