pi-extmgr 0.1.24 → 0.1.26
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/package.json +1 -1
- package/src/constants.ts +0 -2
- package/src/packages/discovery.ts +99 -22
- package/src/packages/install.ts +1 -16
- package/src/ui/package-config.ts +1 -2
- package/src/ui/unified.ts +1 -2
- package/src/utils/cache.ts +15 -2
- package/src/utils/network.ts +15 -0
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -68,8 +68,6 @@ export type CacheLimitKey = keyof typeof CACHE_LIMITS;
|
|
|
68
68
|
export const UI = {
|
|
69
69
|
/** Maximum height for scrollable lists in terminal rows */
|
|
70
70
|
maxListHeight: 16,
|
|
71
|
-
/** Minimum number of items before enabling search functionality */
|
|
72
|
-
searchThreshold: 8,
|
|
73
71
|
/** Default confirmation dialog timeout: 30 seconds */
|
|
74
72
|
confirmTimeout: 30_000,
|
|
75
73
|
/** Extended confirmation timeout for destructive operations: 1 minute */
|
|
@@ -19,6 +19,25 @@ import {
|
|
|
19
19
|
stripGitSourcePrefix,
|
|
20
20
|
} from "../utils/package-source.js";
|
|
21
21
|
import { execNpm } from "../utils/npm-exec.js";
|
|
22
|
+
import { fetchWithTimeout } from "../utils/network.js";
|
|
23
|
+
|
|
24
|
+
const NPM_SEARCH_API = "https://registry.npmjs.org/-/v1/search";
|
|
25
|
+
const NPM_SEARCH_PAGE_SIZE = 250;
|
|
26
|
+
|
|
27
|
+
interface NpmSearchResultObject {
|
|
28
|
+
package?: {
|
|
29
|
+
name?: string;
|
|
30
|
+
version?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
keywords?: string[];
|
|
33
|
+
date?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface NpmSearchResponse {
|
|
38
|
+
total?: number;
|
|
39
|
+
objects?: NpmSearchResultObject[];
|
|
40
|
+
}
|
|
22
41
|
|
|
23
42
|
let searchCache: SearchCache | null = null;
|
|
24
43
|
|
|
@@ -51,15 +70,86 @@ import {
|
|
|
51
70
|
setCachedPackageSize,
|
|
52
71
|
} from "../utils/cache.js";
|
|
53
72
|
|
|
73
|
+
function toNpmPackage(entry: NpmSearchResultObject): NpmPackage | undefined {
|
|
74
|
+
const pkg = entry.package;
|
|
75
|
+
if (!pkg) return undefined;
|
|
76
|
+
|
|
77
|
+
const name = pkg.name?.trim();
|
|
78
|
+
if (!name) return undefined;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
version: pkg.version,
|
|
83
|
+
description: pkg.description,
|
|
84
|
+
keywords: Array.isArray(pkg.keywords) ? pkg.keywords : undefined,
|
|
85
|
+
date: pkg.date,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function fetchNpmSearchPage(
|
|
90
|
+
query: string,
|
|
91
|
+
from: number
|
|
92
|
+
): Promise<{
|
|
93
|
+
total: number;
|
|
94
|
+
resultCount: number;
|
|
95
|
+
packages: NpmPackage[];
|
|
96
|
+
}> {
|
|
97
|
+
const params = new URLSearchParams({
|
|
98
|
+
text: query,
|
|
99
|
+
size: String(NPM_SEARCH_PAGE_SIZE),
|
|
100
|
+
from: String(from),
|
|
101
|
+
});
|
|
102
|
+
const response = await fetchWithTimeout(
|
|
103
|
+
`${NPM_SEARCH_API}?${params.toString()}`,
|
|
104
|
+
TIMEOUTS.npmSearch
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(`npm registry search failed: HTTP ${response.status}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const data = (await response.json()) as NpmSearchResponse;
|
|
112
|
+
const objects = data.objects ?? [];
|
|
113
|
+
const packages = objects.map(toNpmPackage).filter((pkg): pkg is NpmPackage => !!pkg);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
total:
|
|
117
|
+
typeof data.total === "number" && Number.isFinite(data.total) ? data.total : packages.length,
|
|
118
|
+
resultCount: objects.length,
|
|
119
|
+
packages,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function fetchNpmRegistrySearchResults(query: string): Promise<NpmPackage[]> {
|
|
124
|
+
const packagesByName = new Map<string, NpmPackage>();
|
|
125
|
+
let from = 0;
|
|
126
|
+
let total = Infinity;
|
|
127
|
+
|
|
128
|
+
while (from < total) {
|
|
129
|
+
const page = await fetchNpmSearchPage(query, from);
|
|
130
|
+
total = page.total;
|
|
131
|
+
|
|
132
|
+
if (page.resultCount === 0) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const pkg of page.packages) {
|
|
137
|
+
if (!packagesByName.has(pkg.name)) {
|
|
138
|
+
packagesByName.set(pkg.name, pkg);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
from += page.resultCount;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return [...packagesByName.values()];
|
|
146
|
+
}
|
|
147
|
+
|
|
54
148
|
export async function searchNpmPackages(
|
|
55
149
|
query: string,
|
|
56
150
|
ctx: ExtensionCommandContext,
|
|
57
|
-
|
|
151
|
+
_pi: ExtensionAPI
|
|
58
152
|
): Promise<NpmPackage[]> {
|
|
59
|
-
// Pull more results so browse mode has meaningful pagination.
|
|
60
|
-
// npm search can still cap server-side, but this improves coverage.
|
|
61
|
-
const searchLimit = 250;
|
|
62
|
-
|
|
63
153
|
// Check persistent cache first
|
|
64
154
|
const cached = await getCachedSearch(query);
|
|
65
155
|
if (cached && cached.length > 0) {
|
|
@@ -73,25 +163,12 @@ export async function searchNpmPackages(
|
|
|
73
163
|
ctx.ui.notify(`Searching npm for "${query}"...`, "info");
|
|
74
164
|
}
|
|
75
165
|
|
|
76
|
-
const
|
|
77
|
-
timeout: TIMEOUTS.npmSearch,
|
|
78
|
-
});
|
|
166
|
+
const packages = await fetchNpmRegistrySearchResults(query);
|
|
79
167
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const parsed = JSON.parse(res.stdout || "[]") as NpmPackage[];
|
|
86
|
-
const filtered = parsed.filter((p) => !!p?.name);
|
|
168
|
+
// Cache the results
|
|
169
|
+
await setCachedSearch(query, packages);
|
|
87
170
|
|
|
88
|
-
|
|
89
|
-
await setCachedSearch(query, filtered);
|
|
90
|
-
|
|
91
|
-
return filtered;
|
|
92
|
-
} catch {
|
|
93
|
-
throw new Error("Failed to parse npm search output");
|
|
94
|
-
}
|
|
171
|
+
return packages;
|
|
95
172
|
}
|
|
96
173
|
|
|
97
174
|
export async function getInstalledPackages(
|
package/src/packages/install.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { tryOperation } from "../utils/mode.js";
|
|
|
18
18
|
import { updateExtmgrStatus } from "../utils/status.js";
|
|
19
19
|
import { execNpm } from "../utils/npm-exec.js";
|
|
20
20
|
import { normalizePackageIdentity } from "../utils/package-source.js";
|
|
21
|
+
import { fetchWithTimeout } from "../utils/network.js";
|
|
21
22
|
import { TIMEOUTS } from "../constants.js";
|
|
22
23
|
|
|
23
24
|
export type InstallScope = "global" | "project";
|
|
@@ -73,22 +74,6 @@ function safeExtractGithubMatch(match: RegExpMatchArray | null): GithubUrlInfo |
|
|
|
73
74
|
return { owner, repo, branch, filePath };
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
|
|
77
|
-
const controller = new AbortController();
|
|
78
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
return await fetch(url, { signal: controller.signal });
|
|
82
|
-
} catch (error) {
|
|
83
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
84
|
-
throw new Error(`Download timed out after ${Math.ceil(timeoutMs / 1000)}s`);
|
|
85
|
-
}
|
|
86
|
-
throw error;
|
|
87
|
-
} finally {
|
|
88
|
-
clearTimeout(timer);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
77
|
async function ensureTarAvailable(
|
|
93
78
|
pi: ExtensionAPI,
|
|
94
79
|
ctx: ExtensionCommandContext
|
package/src/ui/package-config.ts
CHANGED
package/src/ui/unified.ts
CHANGED
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();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
|
|
2
|
+
const controller = new AbortController();
|
|
3
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
return await fetch(url, { signal: controller.signal });
|
|
7
|
+
} catch (error) {
|
|
8
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
9
|
+
throw new Error(`Request timed out after ${Math.ceil(timeoutMs / 1000)}s`);
|
|
10
|
+
}
|
|
11
|
+
throw error;
|
|
12
|
+
} finally {
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
}
|
|
15
|
+
}
|