pi-extmgr 0.1.22 → 0.1.24
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/index.ts +6 -1
- package/src/packages/discovery.ts +53 -28
- package/src/packages/extensions.ts +171 -63
- package/src/packages/install.ts +118 -24
- package/src/packages/management.ts +58 -37
- package/src/ui/package-config.ts +157 -126
- package/src/ui/remote.ts +79 -54
- package/src/ui/unified.ts +222 -173
- package/src/utils/auto-update.ts +36 -31
- package/src/utils/command.ts +77 -1
- package/src/utils/format.ts +23 -3
- package/src/utils/history.ts +41 -2
- package/src/utils/mode.ts +56 -5
- package/src/utils/npm-exec.ts +47 -0
- package/src/utils/package-source.ts +43 -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,162 +110,202 @@ 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
|
}
|
|
264
302
|
|
|
265
303
|
function normalizePathForDuplicateCheck(value: string): string {
|
|
266
|
-
|
|
304
|
+
const normalized = value.replace(/\\/g, "/");
|
|
305
|
+
const looksWindowsPath =
|
|
306
|
+
/^[a-zA-Z]:\//.test(normalized) || normalized.startsWith("//") || value.includes("\\");
|
|
307
|
+
|
|
308
|
+
return looksWindowsPath ? normalized.toLowerCase() : normalized;
|
|
267
309
|
}
|
|
268
310
|
|
|
269
311
|
export function buildUnifiedItems(
|
|
@@ -328,7 +370,7 @@ export function buildUnifiedItems(
|
|
|
328
370
|
version: pkg.version,
|
|
329
371
|
description: pkg.description,
|
|
330
372
|
size: pkg.size,
|
|
331
|
-
updateAvailable: knownUpdates.has(pkg.
|
|
373
|
+
updateAvailable: knownUpdates.has(normalizePackageIdentity(pkg.source)),
|
|
332
374
|
});
|
|
333
375
|
}
|
|
334
376
|
|
|
@@ -716,10 +758,12 @@ async function handleUnifiedAction(
|
|
|
716
758
|
ctx.cwd
|
|
717
759
|
);
|
|
718
760
|
if (!removal.ok) {
|
|
761
|
+
logExtensionDelete(pi, item.id, false, removal.error);
|
|
719
762
|
ctx.ui.notify(`Failed to remove extension: ${removal.error}`, "error");
|
|
720
763
|
return false;
|
|
721
764
|
}
|
|
722
765
|
|
|
766
|
+
logExtensionDelete(pi, item.id, true);
|
|
723
767
|
ctx.ui.notify(
|
|
724
768
|
`Removed ${item.displayName}${removal.removedDirectory ? " (directory)" : ""}.`,
|
|
725
769
|
"info"
|
|
@@ -821,12 +865,15 @@ export async function showInstalledPackagesLegacy(
|
|
|
821
865
|
ctx: ExtensionCommandContext,
|
|
822
866
|
pi: ExtensionAPI
|
|
823
867
|
): Promise<void> {
|
|
868
|
+
if (!hasCustomUI(ctx)) {
|
|
869
|
+
await showInstalledPackagesList(ctx, pi);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
824
873
|
ctx.ui.notify(
|
|
825
874
|
"📦 Use /extensions for the unified view.\nInstalled packages are now shown alongside local extensions.",
|
|
826
875
|
"info"
|
|
827
876
|
);
|
|
828
|
-
// Small delay then open the main manager
|
|
829
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
830
877
|
await showInteractive(ctx, pi);
|
|
831
878
|
}
|
|
832
879
|
|
|
@@ -845,10 +892,12 @@ export async function showListOnly(ctx: ExtensionCommandContext): Promise<void>
|
|
|
845
892
|
|
|
846
893
|
const lines = entries.map(formatExtEntry);
|
|
847
894
|
const output = lines.join("\n");
|
|
895
|
+
const titledOutput = `Local extensions:\n${output}`;
|
|
848
896
|
|
|
849
897
|
if (ctx.hasUI) {
|
|
850
|
-
ctx.ui.notify(
|
|
898
|
+
ctx.ui.notify(titledOutput, "info");
|
|
851
899
|
} else {
|
|
900
|
+
console.log("Local extensions:");
|
|
852
901
|
console.log(output);
|
|
853
902
|
}
|
|
854
903
|
}
|
package/src/utils/auto-update.ts
CHANGED
|
@@ -18,6 +18,9 @@ import {
|
|
|
18
18
|
type AutoUpdateConfig,
|
|
19
19
|
} from "./settings.js";
|
|
20
20
|
import { parseNpmSource } from "./format.js";
|
|
21
|
+
import { execNpm } from "./npm-exec.js";
|
|
22
|
+
import { normalizePackageIdentity } from "./package-source.js";
|
|
23
|
+
import { logAutoUpdateConfig } from "./history.js";
|
|
21
24
|
import { TIMEOUTS } from "../constants.js";
|
|
22
25
|
|
|
23
26
|
import { startTimer, stopTimer, isTimerRunning } from "./timer.js";
|
|
@@ -25,6 +28,10 @@ import { startTimer, stopTimer, isTimerRunning } from "./timer.js";
|
|
|
25
28
|
// Context provider for safe session handling
|
|
26
29
|
export type ContextProvider = () => (ExtensionCommandContext | ExtensionContext) | undefined;
|
|
27
30
|
|
|
31
|
+
function getUpdateIdentity(pkg: InstalledPackage): string {
|
|
32
|
+
return normalizePackageIdentity(pkg.source);
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
/**
|
|
29
36
|
* Start auto-update background checker
|
|
30
37
|
* Uses a context provider to avoid stale context issues when sessions switch
|
|
@@ -47,27 +54,23 @@ export function startAutoUpdateTimer(
|
|
|
47
54
|
const interval = getScheduleInterval(config);
|
|
48
55
|
if (!interval) return;
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
saveAutoUpdateConfig(pi, {
|
|
68
|
-
...config,
|
|
69
|
-
nextCheck: calculateNextCheck(config.intervalMs),
|
|
70
|
-
});
|
|
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
|
+
);
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
/**
|
|
@@ -97,28 +100,30 @@ export async function checkForUpdates(
|
|
|
97
100
|
const npmPackages = packages.filter((p) => p.source.startsWith("npm:"));
|
|
98
101
|
|
|
99
102
|
const updatesAvailable: string[] = [];
|
|
103
|
+
const updatedPackageNames: string[] = [];
|
|
100
104
|
|
|
101
105
|
for (const pkg of npmPackages) {
|
|
102
106
|
const hasUpdate = await checkPackageUpdate(pkg, ctx, pi);
|
|
103
107
|
if (hasUpdate) {
|
|
104
|
-
updatesAvailable.push(pkg
|
|
108
|
+
updatesAvailable.push(getUpdateIdentity(pkg));
|
|
109
|
+
updatedPackageNames.push(pkg.name);
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
|
|
113
|
+
const checkedAt = Date.now();
|
|
109
114
|
const config = getAutoUpdateConfig(ctx);
|
|
110
115
|
saveAutoUpdateConfig(pi, {
|
|
111
116
|
...config,
|
|
112
|
-
lastCheck:
|
|
117
|
+
lastCheck: checkedAt,
|
|
113
118
|
nextCheck: calculateNextCheck(config.intervalMs),
|
|
114
119
|
updatesAvailable,
|
|
115
120
|
});
|
|
116
121
|
|
|
117
|
-
if (
|
|
118
|
-
onUpdateAvailable(
|
|
122
|
+
if (updatedPackageNames.length > 0 && onUpdateAvailable) {
|
|
123
|
+
onUpdateAvailable(updatedPackageNames);
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
return
|
|
126
|
+
return updatedPackageNames;
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
/**
|
|
@@ -134,9 +139,8 @@ async function checkPackageUpdate(
|
|
|
134
139
|
if (!pkgName) return false;
|
|
135
140
|
|
|
136
141
|
try {
|
|
137
|
-
const res = await pi
|
|
142
|
+
const res = await execNpm(pi, ["view", pkgName, "version", "--json"], ctx, {
|
|
138
143
|
timeout: TIMEOUTS.npmView,
|
|
139
|
-
cwd: ctx.cwd,
|
|
140
144
|
});
|
|
141
145
|
|
|
142
146
|
if (res.code !== 0) return false;
|
|
@@ -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
|
}
|