pi-extmgr 0.1.26 → 0.1.28
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 +7 -5
- package/package.json +13 -16
- package/src/commands/auto-update.ts +4 -4
- package/src/commands/cache.ts +1 -1
- package/src/commands/history.ts +3 -3
- package/src/commands/install.ts +2 -2
- package/src/commands/registry.ts +7 -7
- package/src/commands/types.ts +1 -1
- package/src/extensions/discovery.ts +4 -3
- package/src/index.ts +15 -15
- package/src/packages/catalog.ts +163 -0
- package/src/packages/discovery.ts +77 -262
- package/src/packages/extensions.ts +10 -5
- package/src/packages/install.ts +42 -37
- package/src/packages/management.ts +145 -99
- package/src/types/index.ts +16 -9
- package/src/ui/async-task.ts +194 -0
- package/src/ui/footer.ts +4 -8
- package/src/ui/help.ts +2 -2
- package/src/ui/package-config.ts +62 -49
- package/src/ui/remote.ts +83 -28
- package/src/ui/theme.ts +2 -2
- package/src/ui/unified.ts +104 -89
- package/src/utils/auto-update.ts +18 -64
- package/src/utils/cache.ts +3 -3
- package/src/utils/command.ts +1 -1
- package/src/utils/format.ts +4 -3
- package/src/utils/history.ts +4 -2
- package/src/utils/mode.ts +1 -1
- package/src/utils/network.ts +10 -2
- package/src/utils/notify.ts +1 -1
- package/src/utils/npm-exec.ts +3 -1
- package/src/utils/package-source.ts +84 -2
- package/src/utils/retry.ts +1 -1
- package/src/utils/settings.ts +17 -8
- package/src/utils/status.ts +16 -12
- package/src/utils/ui-helpers.ts +3 -3
|
@@ -3,23 +3,20 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import
|
|
7
|
-
ExtensionAPI,
|
|
8
|
-
ExtensionCommandContext,
|
|
9
|
-
ExtensionContext,
|
|
6
|
+
import {
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionCommandContext,
|
|
9
|
+
type ExtensionContext,
|
|
10
|
+
getAgentDir,
|
|
10
11
|
} from "@mariozechner/pi-coding-agent";
|
|
11
|
-
import type { InstalledPackage, NpmPackage, SearchCache } from "../types/index.js";
|
|
12
12
|
import { CACHE_TTL, TIMEOUTS } from "../constants.js";
|
|
13
|
-
import {
|
|
13
|
+
import { type InstalledPackage, type NpmPackage, type SearchCache } from "../types/index.js";
|
|
14
14
|
import { parseNpmSource } from "../utils/format.js";
|
|
15
|
-
import {
|
|
16
|
-
getPackageSourceKind,
|
|
17
|
-
normalizePackageIdentity,
|
|
18
|
-
splitGitRepoAndRef,
|
|
19
|
-
stripGitSourcePrefix,
|
|
20
|
-
} from "../utils/package-source.js";
|
|
21
|
-
import { execNpm } from "../utils/npm-exec.js";
|
|
15
|
+
import { readSummary } from "../utils/fs.js";
|
|
22
16
|
import { fetchWithTimeout } from "../utils/network.js";
|
|
17
|
+
import { execNpm } from "../utils/npm-exec.js";
|
|
18
|
+
import { normalizePackageIdentity } from "../utils/package-source.js";
|
|
19
|
+
import { getPackageCatalog } from "./catalog.js";
|
|
23
20
|
|
|
24
21
|
const NPM_SEARCH_API = "https://registry.npmjs.org/-/v1/search";
|
|
25
22
|
const NPM_SEARCH_PAGE_SIZE = 250;
|
|
@@ -41,6 +38,18 @@ interface NpmSearchResponse {
|
|
|
41
38
|
|
|
42
39
|
let searchCache: SearchCache | null = null;
|
|
43
40
|
|
|
41
|
+
function createAbortError(): Error {
|
|
42
|
+
const error = new Error("Operation cancelled");
|
|
43
|
+
error.name = "AbortError";
|
|
44
|
+
return error;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
48
|
+
if (signal?.aborted) {
|
|
49
|
+
throw createAbortError();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
export function getSearchCache(): SearchCache | null {
|
|
45
54
|
return searchCache;
|
|
46
55
|
}
|
|
@@ -61,13 +70,13 @@ export function isCacheValid(query: string): boolean {
|
|
|
61
70
|
|
|
62
71
|
// Import persistent cache
|
|
63
72
|
import {
|
|
64
|
-
getCachedSearch,
|
|
65
|
-
setCachedSearch,
|
|
66
73
|
getCachedPackage,
|
|
67
|
-
setCachedPackage,
|
|
68
|
-
getPackageDescriptions,
|
|
69
74
|
getCachedPackageSize,
|
|
75
|
+
getCachedSearch,
|
|
76
|
+
getPackageDescriptions,
|
|
77
|
+
setCachedPackage,
|
|
70
78
|
setCachedPackageSize,
|
|
79
|
+
setCachedSearch,
|
|
71
80
|
} from "../utils/cache.js";
|
|
72
81
|
|
|
73
82
|
function toNpmPackage(entry: NpmSearchResultObject): NpmPackage | undefined {
|
|
@@ -88,7 +97,8 @@ function toNpmPackage(entry: NpmSearchResultObject): NpmPackage | undefined {
|
|
|
88
97
|
|
|
89
98
|
async function fetchNpmSearchPage(
|
|
90
99
|
query: string,
|
|
91
|
-
from: number
|
|
100
|
+
from: number,
|
|
101
|
+
signal?: AbortSignal
|
|
92
102
|
): Promise<{
|
|
93
103
|
total: number;
|
|
94
104
|
resultCount: number;
|
|
@@ -101,7 +111,8 @@ async function fetchNpmSearchPage(
|
|
|
101
111
|
});
|
|
102
112
|
const response = await fetchWithTimeout(
|
|
103
113
|
`${NPM_SEARCH_API}?${params.toString()}`,
|
|
104
|
-
TIMEOUTS.npmSearch
|
|
114
|
+
TIMEOUTS.npmSearch,
|
|
115
|
+
signal
|
|
105
116
|
);
|
|
106
117
|
|
|
107
118
|
if (!response.ok) {
|
|
@@ -120,13 +131,16 @@ async function fetchNpmSearchPage(
|
|
|
120
131
|
};
|
|
121
132
|
}
|
|
122
133
|
|
|
123
|
-
export async function fetchNpmRegistrySearchResults(
|
|
134
|
+
export async function fetchNpmRegistrySearchResults(
|
|
135
|
+
query: string,
|
|
136
|
+
signal?: AbortSignal
|
|
137
|
+
): Promise<NpmPackage[]> {
|
|
124
138
|
const packagesByName = new Map<string, NpmPackage>();
|
|
125
139
|
let from = 0;
|
|
126
140
|
let total = Infinity;
|
|
127
141
|
|
|
128
142
|
while (from < total) {
|
|
129
|
-
const page = await fetchNpmSearchPage(query, from);
|
|
143
|
+
const page = await fetchNpmSearchPage(query, from, signal);
|
|
130
144
|
total = page.total;
|
|
131
145
|
|
|
132
146
|
if (page.resultCount === 0) {
|
|
@@ -148,11 +162,10 @@ export async function fetchNpmRegistrySearchResults(query: string): Promise<NpmP
|
|
|
148
162
|
export async function searchNpmPackages(
|
|
149
163
|
query: string,
|
|
150
164
|
ctx: ExtensionCommandContext,
|
|
151
|
-
|
|
165
|
+
options?: { signal?: AbortSignal }
|
|
152
166
|
): Promise<NpmPackage[]> {
|
|
153
|
-
// Check persistent cache first
|
|
154
167
|
const cached = await getCachedSearch(query);
|
|
155
|
-
if (cached
|
|
168
|
+
if (cached) {
|
|
156
169
|
if (ctx.hasUI) {
|
|
157
170
|
ctx.ui.notify(`Using ${cached.length} cached results`, "info");
|
|
158
171
|
}
|
|
@@ -163,7 +176,7 @@ export async function searchNpmPackages(
|
|
|
163
176
|
ctx.ui.notify(`Searching npm for "${query}"...`, "info");
|
|
164
177
|
}
|
|
165
178
|
|
|
166
|
-
const packages = await fetchNpmRegistrySearchResults(query);
|
|
179
|
+
const packages = await fetchNpmRegistrySearchResults(query, options?.signal);
|
|
167
180
|
|
|
168
181
|
// Cache the results
|
|
169
182
|
await setCachedSearch(query, packages);
|
|
@@ -174,256 +187,50 @@ export async function searchNpmPackages(
|
|
|
174
187
|
export async function getInstalledPackages(
|
|
175
188
|
ctx: ExtensionCommandContext | ExtensionContext,
|
|
176
189
|
pi: ExtensionAPI,
|
|
177
|
-
onProgress?: (current: number, total: number) => void
|
|
190
|
+
onProgress?: (current: number, total: number) => void,
|
|
191
|
+
signal?: AbortSignal
|
|
178
192
|
): Promise<InstalledPackage[]> {
|
|
179
|
-
|
|
180
|
-
if (res.code !== 0) return [];
|
|
193
|
+
throwIfAborted(signal);
|
|
181
194
|
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
195
|
+
const packages = await getPackageCatalog(ctx.cwd).listInstalledPackages();
|
|
196
|
+
if (packages.length === 0) {
|
|
184
197
|
return [];
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// Fetch metadata (descriptions and sizes) for packages in parallel
|
|
190
|
-
await addPackageMetadata(packages, ctx, pi, onProgress);
|
|
191
|
-
|
|
200
|
+
await addPackageMetadata(packages, ctx, pi, onProgress, signal);
|
|
201
|
+
throwIfAborted(signal);
|
|
192
202
|
return packages;
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
.trim()
|
|
198
|
-
.replace(/\s+\((filtered|pinned)\)$/i, "")
|
|
199
|
-
.trim();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function getInstalledPackageIdentity(pkg: InstalledPackage): string {
|
|
203
|
-
return normalizePackageIdentity(
|
|
204
|
-
pkg.source,
|
|
205
|
-
pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : undefined
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function isScopeHeader(lowerTrimmed: string, scope: "global" | "project"): boolean {
|
|
210
|
-
if (scope === "global") {
|
|
211
|
-
return (
|
|
212
|
-
lowerTrimmed === "global" ||
|
|
213
|
-
lowerTrimmed === "user" ||
|
|
214
|
-
lowerTrimmed.startsWith("global packages") ||
|
|
215
|
-
lowerTrimmed.startsWith("global:") ||
|
|
216
|
-
lowerTrimmed.startsWith("user packages") ||
|
|
217
|
-
lowerTrimmed.startsWith("user:")
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return (
|
|
222
|
-
lowerTrimmed === "project" ||
|
|
223
|
-
lowerTrimmed === "local" ||
|
|
224
|
-
lowerTrimmed.startsWith("project packages") ||
|
|
225
|
-
lowerTrimmed.startsWith("project:") ||
|
|
226
|
-
lowerTrimmed.startsWith("local packages") ||
|
|
227
|
-
lowerTrimmed.startsWith("local:")
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function looksLikePackageSource(source: string): boolean {
|
|
232
|
-
return getPackageSourceKind(source) !== "unknown";
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function parseResolvedPathLine(line: string): string | undefined {
|
|
236
|
-
const resolvedMatch = line.match(/^resolved\s*:\s*(.+)$/i);
|
|
237
|
-
if (resolvedMatch?.[1]) {
|
|
238
|
-
return resolvedMatch[1].trim();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
line.startsWith("/") ||
|
|
243
|
-
line.startsWith("./") ||
|
|
244
|
-
line.startsWith("../") ||
|
|
245
|
-
line.startsWith(".\\") ||
|
|
246
|
-
line.startsWith("..\\") ||
|
|
247
|
-
line.startsWith("~/") ||
|
|
248
|
-
line.startsWith("file://") ||
|
|
249
|
-
/^[a-zA-Z]:[\\/]/.test(line) ||
|
|
250
|
-
line.startsWith("\\\\")
|
|
251
|
-
) {
|
|
252
|
-
return line;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return undefined;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function parseInstalledPackagesOutputInternal(text: string): InstalledPackage[] {
|
|
259
|
-
const packages: InstalledPackage[] = [];
|
|
260
|
-
|
|
261
|
-
const lines = text.split("\n");
|
|
262
|
-
let currentScope: "global" | "project" = "global";
|
|
263
|
-
let currentPackage: InstalledPackage | undefined;
|
|
264
|
-
|
|
265
|
-
for (const rawLine of lines) {
|
|
266
|
-
if (!rawLine.trim()) continue;
|
|
267
|
-
|
|
268
|
-
const isIndented = /^(?:\t+|\s{4,})/.test(rawLine);
|
|
269
|
-
const trimmed = rawLine.trim();
|
|
270
|
-
|
|
271
|
-
if (isIndented && currentPackage) {
|
|
272
|
-
const resolved = parseResolvedPathLine(trimmed);
|
|
273
|
-
if (resolved) {
|
|
274
|
-
currentPackage.resolvedPath = resolved;
|
|
275
|
-
}
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const lowerTrimmed = trimmed.toLowerCase();
|
|
280
|
-
if (isScopeHeader(lowerTrimmed, "global")) {
|
|
281
|
-
currentScope = "global";
|
|
282
|
-
currentPackage = undefined;
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
if (isScopeHeader(lowerTrimmed, "project")) {
|
|
286
|
-
currentScope = "project";
|
|
287
|
-
currentPackage = undefined;
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const candidate = trimmed.replace(/^[-•]?\s*/, "").trim();
|
|
292
|
-
if (!looksLikePackageSource(candidate)) continue;
|
|
293
|
-
|
|
294
|
-
const source = sanitizeListSourceSuffix(candidate);
|
|
295
|
-
const { name, version } = parsePackageNameAndVersion(source);
|
|
205
|
+
function getInstalledPackageIdentity(pkg: InstalledPackage, options?: { cwd?: string }): string {
|
|
206
|
+
const baseCwd = pkg.scope === "project" ? options?.cwd : getAgentDir();
|
|
296
207
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
packages.push(pkg);
|
|
302
|
-
currentPackage = pkg;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return packages;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function shouldReplaceInstalledPackage(
|
|
309
|
-
current: InstalledPackage | undefined,
|
|
310
|
-
candidate: InstalledPackage
|
|
311
|
-
): boolean {
|
|
312
|
-
if (!current) {
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (current.scope !== candidate.scope) {
|
|
317
|
-
return candidate.scope === "project";
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export function parseInstalledPackagesOutput(text: string): InstalledPackage[] {
|
|
324
|
-
const parsed = parseInstalledPackagesOutputInternal(text);
|
|
325
|
-
const deduped = new Map<string, InstalledPackage>();
|
|
326
|
-
|
|
327
|
-
for (const pkg of parsed) {
|
|
328
|
-
const identity = getInstalledPackageIdentity(pkg);
|
|
329
|
-
const current = deduped.get(identity);
|
|
330
|
-
if (shouldReplaceInstalledPackage(current, pkg)) {
|
|
331
|
-
deduped.set(identity, pkg);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return Array.from(deduped.values());
|
|
208
|
+
return normalizePackageIdentity(pkg.source, {
|
|
209
|
+
...(pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : {}),
|
|
210
|
+
...(baseCwd ? { cwd: baseCwd } : {}),
|
|
211
|
+
});
|
|
336
212
|
}
|
|
337
213
|
|
|
338
|
-
/**
|
|
339
|
-
* Check whether a specific package source is installed.
|
|
340
|
-
* Matches on normalized package source and optional scope.
|
|
341
|
-
*/
|
|
342
214
|
export async function isSourceInstalled(
|
|
343
215
|
source: string,
|
|
344
216
|
ctx: ExtensionCommandContext | ExtensionContext,
|
|
345
|
-
pi: ExtensionAPI,
|
|
346
217
|
options?: { scope?: "global" | "project" }
|
|
347
218
|
): Promise<boolean> {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (res.code !== 0) return false;
|
|
219
|
+
const installed = await getPackageCatalog(ctx.cwd).listInstalledPackages({ dedupe: false });
|
|
220
|
+
const expected = normalizePackageIdentity(source, { cwd: ctx.cwd });
|
|
351
221
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return installed.some((pkg) => {
|
|
356
|
-
if (getInstalledPackageIdentity(pkg) !== expected) {
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
return options?.scope ? pkg.scope === options.scope : true;
|
|
360
|
-
});
|
|
361
|
-
} catch {
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* parseInstalledPackagesOutputAllScopes returns the raw parsed entries from
|
|
368
|
-
* parseInstalledPackagesOutputInternal without deduplication or scope merging.
|
|
369
|
-
* Prefer parseInstalledPackagesOutput for user-facing lists, since it applies
|
|
370
|
-
* deduplication and normalized scope selection.
|
|
371
|
-
*/
|
|
372
|
-
export function parseInstalledPackagesOutputAllScopes(text: string): InstalledPackage[] {
|
|
373
|
-
return parseInstalledPackagesOutputInternal(text);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function extractGitPackageName(repoSpec: string): string {
|
|
377
|
-
// git@github.com:user/repo(.git)
|
|
378
|
-
if (repoSpec.startsWith("git@")) {
|
|
379
|
-
const afterColon = repoSpec.split(":").slice(1).join(":");
|
|
380
|
-
if (afterColon) {
|
|
381
|
-
const last = afterColon.split("/").pop() || afterColon;
|
|
382
|
-
return last.replace(/\.git$/i, "") || repoSpec;
|
|
222
|
+
return installed.some((pkg) => {
|
|
223
|
+
if (getInstalledPackageIdentity(pkg, { cwd: ctx.cwd }) !== expected) {
|
|
224
|
+
return false;
|
|
383
225
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
// https://..., ssh://..., git://...
|
|
387
|
-
try {
|
|
388
|
-
const url = new URL(repoSpec);
|
|
389
|
-
const last = url.pathname.split("/").filter(Boolean).pop();
|
|
390
|
-
if (last) {
|
|
391
|
-
return last.replace(/\.git$/i, "") || repoSpec;
|
|
392
|
-
}
|
|
393
|
-
} catch {
|
|
394
|
-
// Fallback below
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const last = repoSpec.split(/[/:]/).filter(Boolean).pop();
|
|
398
|
-
return (last ? last.replace(/\.git$/i, "") : repoSpec) || repoSpec;
|
|
226
|
+
return options?.scope ? pkg.scope === options.scope : true;
|
|
227
|
+
});
|
|
399
228
|
}
|
|
400
229
|
|
|
401
|
-
function
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const parsedNpm = parseNpmSource(fullSource);
|
|
406
|
-
if (parsedNpm) {
|
|
407
|
-
return parsedNpm;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const sourceKind = getPackageSourceKind(fullSource);
|
|
411
|
-
if (sourceKind === "git") {
|
|
412
|
-
const gitSpec = stripGitSourcePrefix(fullSource);
|
|
413
|
-
const { repo } = splitGitRepoAndRef(gitSpec);
|
|
414
|
-
return { name: extractGitPackageName(repo) };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (fullSource.includes("node_modules/")) {
|
|
418
|
-
const nmMatch = fullSource.match(/node_modules\/(.+)$/);
|
|
419
|
-
if (nmMatch?.[1]) {
|
|
420
|
-
return { name: nmMatch[1] };
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const pathParts = fullSource.split(/[\\/]/);
|
|
425
|
-
const fileName = pathParts[pathParts.length - 1];
|
|
426
|
-
return { name: fileName || fullSource };
|
|
230
|
+
export async function getInstalledPackagesAllScopes(
|
|
231
|
+
ctx: ExtensionCommandContext | ExtensionContext
|
|
232
|
+
): Promise<InstalledPackage[]> {
|
|
233
|
+
return getPackageCatalog(ctx.cwd).listInstalledPackages({ dedupe: false });
|
|
427
234
|
}
|
|
428
235
|
|
|
429
236
|
async function hydratePackageFromResolvedPath(pkg: InstalledPackage): Promise<void> {
|
|
@@ -471,7 +278,8 @@ async function hydratePackageFromResolvedPath(pkg: InstalledPackage): Promise<vo
|
|
|
471
278
|
async function fetchPackageSize(
|
|
472
279
|
pkgName: string,
|
|
473
280
|
ctx: ExtensionCommandContext | ExtensionContext,
|
|
474
|
-
pi: ExtensionAPI
|
|
281
|
+
pi: ExtensionAPI,
|
|
282
|
+
signal?: AbortSignal
|
|
475
283
|
): Promise<number | undefined> {
|
|
476
284
|
// Check cache first
|
|
477
285
|
const cachedSize = await getCachedPackageSize(pkgName);
|
|
@@ -481,6 +289,7 @@ async function fetchPackageSize(
|
|
|
481
289
|
// Try to get unpacked size from npm view
|
|
482
290
|
const res = await execNpm(pi, ["view", pkgName, "dist.unpackedSize", "--json"], ctx, {
|
|
483
291
|
timeout: TIMEOUTS.npmView,
|
|
292
|
+
...(signal ? { signal } : {}),
|
|
484
293
|
});
|
|
485
294
|
if (res.code === 0) {
|
|
486
295
|
try {
|
|
@@ -503,25 +312,29 @@ async function addPackageMetadata(
|
|
|
503
312
|
packages: InstalledPackage[],
|
|
504
313
|
ctx: ExtensionCommandContext | ExtensionContext,
|
|
505
314
|
pi: ExtensionAPI,
|
|
506
|
-
onProgress?: (current: number, total: number) => void
|
|
315
|
+
onProgress?: (current: number, total: number) => void,
|
|
316
|
+
signal?: AbortSignal
|
|
507
317
|
): Promise<void> {
|
|
508
|
-
|
|
318
|
+
throwIfAborted(signal);
|
|
319
|
+
|
|
509
320
|
const cachedDescriptions = await getPackageDescriptions(packages);
|
|
510
321
|
for (const [source, description] of cachedDescriptions) {
|
|
511
322
|
const pkg = packages.find((p) => p.source === source);
|
|
512
323
|
if (pkg) pkg.description = description;
|
|
513
324
|
}
|
|
514
325
|
|
|
515
|
-
// Process remaining packages in batches
|
|
516
326
|
const batchSize = 5;
|
|
517
327
|
for (let i = 0; i < packages.length; i += batchSize) {
|
|
328
|
+
throwIfAborted(signal);
|
|
329
|
+
|
|
518
330
|
const batch = packages.slice(i, i + batchSize);
|
|
519
331
|
|
|
520
|
-
// Report progress
|
|
521
332
|
onProgress?.(i, packages.length);
|
|
522
333
|
|
|
523
334
|
await Promise.all(
|
|
524
335
|
batch.map(async (pkg) => {
|
|
336
|
+
throwIfAborted(signal);
|
|
337
|
+
|
|
525
338
|
await hydratePackageFromResolvedPath(pkg);
|
|
526
339
|
|
|
527
340
|
const needsDescription = !pkg.description;
|
|
@@ -546,6 +359,7 @@ async function addPackageMetadata(
|
|
|
546
359
|
} else {
|
|
547
360
|
const res = await execNpm(pi, ["view", pkgName, "description", "--json"], ctx, {
|
|
548
361
|
timeout: TIMEOUTS.npmView,
|
|
362
|
+
...(signal ? { signal } : {}),
|
|
549
363
|
});
|
|
550
364
|
if (res.code === 0) {
|
|
551
365
|
try {
|
|
@@ -565,7 +379,7 @@ async function addPackageMetadata(
|
|
|
565
379
|
}
|
|
566
380
|
|
|
567
381
|
if (needsSize) {
|
|
568
|
-
pkg.size = await fetchPackageSize(pkgName, ctx, pi);
|
|
382
|
+
pkg.size = await fetchPackageSize(pkgName, ctx, pi, signal);
|
|
569
383
|
}
|
|
570
384
|
}
|
|
571
385
|
} else if (pkg.source.startsWith("git:")) {
|
|
@@ -578,8 +392,9 @@ async function addPackageMetadata(
|
|
|
578
392
|
}
|
|
579
393
|
})
|
|
580
394
|
);
|
|
395
|
+
|
|
396
|
+
throwIfAborted(signal);
|
|
581
397
|
}
|
|
582
398
|
|
|
583
|
-
// Final progress update
|
|
584
399
|
onProgress?.(packages.length, packages.length);
|
|
585
400
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { type Dirent } from "node:fs";
|
|
3
|
+
import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
3
5
|
import { dirname, join, matchesGlob, relative, resolve } from "node:path";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { execFile } from "node:child_process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
type InstalledPackage,
|
|
11
|
+
type PackageExtensionEntry,
|
|
12
|
+
type Scope,
|
|
13
|
+
type State,
|
|
14
|
+
} from "../types/index.js";
|
|
10
15
|
import { parseNpmSource } from "../utils/format.js";
|
|
11
16
|
import { fileExists, readSummary } from "../utils/fs.js";
|
|
12
17
|
import { resolveNpmCommand } from "../utils/npm-exec.js";
|
package/src/packages/install.ts
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package installation logic
|
|
3
3
|
*/
|
|
4
|
-
import { mkdir, rm, writeFile
|
|
5
|
-
import { join } from "node:path";
|
|
4
|
+
import { cp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
6
5
|
import { homedir } from "node:os";
|
|
7
|
-
import
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
type ExtensionAPI,
|
|
9
|
+
type ExtensionCommandContext,
|
|
10
|
+
type ProgressEvent,
|
|
11
|
+
} from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { TIMEOUTS } from "../constants.js";
|
|
13
|
+
import { runTaskWithLoader } from "../ui/async-task.js";
|
|
8
14
|
import { normalizePackageSource } from "../utils/format.js";
|
|
9
15
|
import { fileExists } from "../utils/fs.js";
|
|
10
|
-
import { clearSearchCache, isSourceInstalled } from "./discovery.js";
|
|
11
|
-
import { discoverPackageExtensionEntrypoints, readPackageManifest } from "./extensions.js";
|
|
12
|
-
import { waitForCondition } from "../utils/retry.js";
|
|
13
16
|
import { logPackageInstall } from "../utils/history.js";
|
|
14
|
-
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
15
|
-
import { notify, error as notifyError, success } from "../utils/notify.js";
|
|
16
|
-
import { confirmAction, confirmReload, showProgress } from "../utils/ui-helpers.js";
|
|
17
17
|
import { tryOperation } from "../utils/mode.js";
|
|
18
|
-
import {
|
|
18
|
+
import { fetchWithTimeout } from "../utils/network.js";
|
|
19
|
+
import { notify, error as notifyError, success } from "../utils/notify.js";
|
|
19
20
|
import { execNpm } from "../utils/npm-exec.js";
|
|
20
21
|
import { normalizePackageIdentity } from "../utils/package-source.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
22
|
+
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
23
|
+
import { updateExtmgrStatus } from "../utils/status.js";
|
|
24
|
+
import { confirmAction, confirmReload, showProgress } from "../utils/ui-helpers.js";
|
|
25
|
+
import { getPackageCatalog } from "./catalog.js";
|
|
26
|
+
import { clearSearchCache } from "./discovery.js";
|
|
27
|
+
import { discoverPackageExtensionEntrypoints, readPackageManifest } from "./extensions.js";
|
|
23
28
|
|
|
24
29
|
export type InstallScope = "global" | "project";
|
|
25
30
|
|
|
@@ -27,6 +32,10 @@ export interface InstallOptions {
|
|
|
27
32
|
scope?: InstallScope;
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
function getProgressMessage(event: ProgressEvent, fallback: string): string {
|
|
36
|
+
return event.message?.trim() || fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
async function resolveInstallScope(
|
|
31
40
|
ctx: ExtensionCommandContext,
|
|
32
41
|
explicitScope?: InstallScope
|
|
@@ -162,7 +171,7 @@ export async function installPackage(
|
|
|
162
171
|
|
|
163
172
|
// Check if it's a GitHub URL to a .ts file - handle as direct download
|
|
164
173
|
const githubTsMatch = source.match(
|
|
165
|
-
/^https:\/\/github\.com\/([
|
|
174
|
+
/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+\.ts)$/
|
|
166
175
|
);
|
|
167
176
|
const githubInfo = safeExtractGithubMatch(githubTsMatch);
|
|
168
177
|
if (githubInfo) {
|
|
@@ -195,11 +204,25 @@ export async function installPackage(
|
|
|
195
204
|
|
|
196
205
|
showProgress(ctx, "Installing", normalized);
|
|
197
206
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
207
|
+
try {
|
|
208
|
+
await runTaskWithLoader(
|
|
209
|
+
ctx,
|
|
210
|
+
{
|
|
211
|
+
title: "Install Package",
|
|
212
|
+
message: `Installing ${normalized}...`,
|
|
213
|
+
cancellable: false,
|
|
214
|
+
fallbackWithoutLoader: true,
|
|
215
|
+
},
|
|
216
|
+
async ({ setMessage }) => {
|
|
217
|
+
await getPackageCatalog(ctx.cwd).install(normalized, scope, (event) => {
|
|
218
|
+
setMessage(getProgressMessage(event, `Installing ${normalized}...`));
|
|
219
|
+
});
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
225
|
+
const errorMsg = `Install failed:\n${message}`;
|
|
203
226
|
logPackageInstall(pi, normalized, normalized, undefined, scope, false, errorMsg);
|
|
204
227
|
notifyError(ctx, errorMsg);
|
|
205
228
|
void updateExtmgrStatus(ctx, pi);
|
|
@@ -209,25 +232,7 @@ export async function installPackage(
|
|
|
209
232
|
clearSearchCache();
|
|
210
233
|
logPackageInstall(pi, normalized, normalized, undefined, scope, true);
|
|
211
234
|
success(ctx, `Installed ${normalized} (${scope})`);
|
|
212
|
-
clearUpdatesAvailable(pi, ctx, [normalizePackageIdentity(normalized)]);
|
|
213
|
-
|
|
214
|
-
// Wait for the extension to be discoverable before reloading.
|
|
215
|
-
// This prevents a race condition where ctx.reload() runs before
|
|
216
|
-
// settings.json or extension files are fully flushed to disk.
|
|
217
|
-
notify(ctx, "Waiting for extension to be ready...", "info");
|
|
218
|
-
const isReady = await waitForCondition(() => isSourceInstalled(normalized, ctx, pi, { scope }), {
|
|
219
|
-
maxAttempts: 10,
|
|
220
|
-
delayMs: 100,
|
|
221
|
-
backoff: "exponential",
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
if (!isReady) {
|
|
225
|
-
notify(
|
|
226
|
-
ctx,
|
|
227
|
-
"Extension may not be immediately available. Reload pi manually if needed.",
|
|
228
|
-
"warning"
|
|
229
|
-
);
|
|
230
|
-
}
|
|
235
|
+
clearUpdatesAvailable(pi, ctx, [normalizePackageIdentity(normalized, { cwd: ctx.cwd })]);
|
|
231
236
|
|
|
232
237
|
const reloaded = await confirmReload(ctx, "Package installed.");
|
|
233
238
|
if (!reloaded) {
|