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/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 { getPackageSourceKind } from "../utils/package-source.js";
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
- // Type guard for SettingsList with selectedIndex
49
- interface SelectableList {
50
- selectedIndex?: number;
51
- handleInput?(data: string): void;
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 ctx.ui.custom<UnifiedAction>((tui, theme, _keybindings, done) => {
112
- const container = new Container();
113
-
114
- // Header
115
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
116
- container.addChild(new Text(theme.fg("accent", theme.bold("Extensions Manager")), 2, 0));
117
- container.addChild(
118
- new Text(
119
- theme.fg(
120
- "muted",
121
- `${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter toggle local • Enter/A actions • c configure pkg extensions • u update pkg • x remove selected`
122
- ),
123
- 2,
124
- 0
125
- )
126
- );
127
- container.addChild(
128
- new Text(
129
- theme.fg("dim", "Quick: i Install | f Search | U Update all | t Auto-update | p Palette"),
130
- 2,
131
- 0
132
- )
133
- );
134
- container.addChild(new Spacer(1));
135
-
136
- // Build settings items
137
- const settingsItems = buildSettingsItems(items, staged, theme);
138
-
139
- const settingsList = new SettingsList(
140
- settingsItems,
141
- Math.min(items.length + 2, UI.maxListHeight),
142
- getSettingsListTheme(),
143
- (id: string, newValue: string) => {
144
- const item = byId.get(id);
145
- if (!item || item.type !== "local") return;
146
-
147
- const state = newValue as State;
148
- staged.set(id, state);
149
-
150
- const settingsItem = settingsItems.find((x) => x.id === id);
151
- if (settingsItem) {
152
- const changed = state !== item.originalState;
153
- settingsItem.label = formatUnifiedItemLabel(item, state, theme, changed);
154
- }
155
- tui.requestRender();
156
- },
157
- () => done({ type: "cancel" }),
158
- { enableSearch: items.length > UI.searchThreshold }
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
- if (selectedId && selectedItem?.type === "local") {
238
- if (data === "x" || data === "X") {
239
- done({ type: "action", itemId: selectedId, action: "remove" });
240
- return;
241
- }
242
- }
243
-
244
- if (data === "r" || data === "R") {
245
- done({ type: "remote" });
246
- return;
247
- }
248
- if (data === "?" || data === "h" || data === "H") {
249
- done({ type: "help" });
250
- return;
251
- }
252
- if (data === "m" || data === "M" || data === "p" || data === "P") {
253
- done({ type: "menu" });
254
- return;
255
- }
256
- settingsList.handleInput?.(data);
257
- tui.requestRender();
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.name),
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(output, "info");
898
+ ctx.ui.notify(titledOutput, "info");
855
899
  } else {
900
+ console.log("Local extensions:");
856
901
  console.log(output);
857
902
  }
858
903
  }
@@ -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
- // Run an initial check immediately.
52
- const initialCtx = getCtx();
53
- if (initialCtx) {
54
- void checkForUpdates(pi, initialCtx, onUpdateAvailable);
55
- }
56
-
57
- // Set up recurring checks
58
- startTimer(interval, () => {
59
- const checkCtx = getCtx();
60
- if (!checkCtx) {
61
- stopAutoUpdateTimer();
62
- return;
63
- }
64
- void checkForUpdates(pi, checkCtx, onUpdateAvailable);
65
- });
66
-
67
- // Persist that timer is running
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.name);
108
+ updatesAvailable.push(getUpdateIdentity(pkg));
109
+ updatedPackageNames.push(pkg.name);
106
110
  }
107
111
  }
108
112
 
109
- // Update last check time
113
+ const checkedAt = Date.now();
110
114
  const config = getAutoUpdateConfig(ctx);
111
115
  saveAutoUpdateConfig(pi, {
112
116
  ...config,
113
- lastCheck: Date.now(),
117
+ lastCheck: checkedAt,
114
118
  nextCheck: calculateNextCheck(config.intervalMs),
115
119
  updatesAvailable,
116
120
  });
117
121
 
118
- if (updatesAvailable.length > 0 && onUpdateAvailable) {
119
- onUpdateAvailable(updatesAvailable);
122
+ if (updatedPackageNames.length > 0 && onUpdateAvailable) {
123
+ onUpdateAvailable(updatedPackageNames);
120
124
  }
121
125
 
122
- return updatesAvailable;
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 names currently known to have updates available
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
  }
@@ -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?: { query: string; results: string[]; timestamp: number } | undefined;
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();