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/packages/install.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-cod
|
|
|
8
8
|
import { normalizePackageSource } from "../utils/format.js";
|
|
9
9
|
import { fileExists } from "../utils/fs.js";
|
|
10
10
|
import { clearSearchCache, isSourceInstalled } from "./discovery.js";
|
|
11
|
-
import {
|
|
11
|
+
import { discoverPackageExtensionEntrypoints, readPackageManifest } from "./extensions.js";
|
|
12
12
|
import { waitForCondition } from "../utils/retry.js";
|
|
13
13
|
import { logPackageInstall } from "../utils/history.js";
|
|
14
14
|
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
@@ -16,6 +16,8 @@ import { notify, error as notifyError, success } from "../utils/notify.js";
|
|
|
16
16
|
import { confirmAction, confirmReload, showProgress } from "../utils/ui-helpers.js";
|
|
17
17
|
import { tryOperation } from "../utils/mode.js";
|
|
18
18
|
import { updateExtmgrStatus } from "../utils/status.js";
|
|
19
|
+
import { execNpm } from "../utils/npm-exec.js";
|
|
20
|
+
import { normalizePackageIdentity } from "../utils/package-source.js";
|
|
19
21
|
import { TIMEOUTS } from "../constants.js";
|
|
20
22
|
|
|
21
23
|
export type InstallScope = "global" | "project";
|
|
@@ -71,20 +73,93 @@ function safeExtractGithubMatch(match: RegExpMatchArray | null): GithubUrlInfo |
|
|
|
71
73
|
return { owner, repo, branch, filePath };
|
|
72
74
|
}
|
|
73
75
|
|
|
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
|
+
async function ensureTarAvailable(
|
|
93
|
+
pi: ExtensionAPI,
|
|
94
|
+
ctx: ExtensionCommandContext
|
|
95
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
96
|
+
const result = await pi.exec("tar", ["--version"], {
|
|
97
|
+
timeout: 5_000,
|
|
98
|
+
cwd: ctx.cwd,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (result.code === 0) {
|
|
102
|
+
return { ok: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
error:
|
|
108
|
+
"Standalone local installs require the `tar` command on PATH. Install tar or use managed package install instead.",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
74
112
|
async function hasStandaloneEntrypoint(packageRoot: string): Promise<boolean> {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
113
|
+
const entrypoints = await discoverPackageExtensionEntrypoints(packageRoot, {
|
|
114
|
+
allowConventionDirectory: false,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
for (const path of entrypoints) {
|
|
118
|
+
if (await fileExists(join(packageRoot, path))) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function getStandaloneDependencyError(packageRoot: string): Promise<string | undefined> {
|
|
127
|
+
const manifest = await readPackageManifest(packageRoot);
|
|
128
|
+
const dependencies = manifest?.dependencies;
|
|
129
|
+
if (!dependencies || typeof dependencies !== "object") {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const missingDependencies: string[] = [];
|
|
134
|
+
for (const dependencyName of Object.keys(dependencies)) {
|
|
135
|
+
const dependencyPath = join(packageRoot, "node_modules", dependencyName);
|
|
136
|
+
if (!(await fileExists(dependencyPath))) {
|
|
137
|
+
missingDependencies.push(dependencyName);
|
|
81
138
|
}
|
|
82
|
-
return false;
|
|
83
139
|
}
|
|
84
140
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
141
|
+
if (missingDependencies.length === 0) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const packageName = manifest?.name ?? "This package";
|
|
146
|
+
return `${packageName} declares runtime dependencies that are not bundled for standalone install: ${missingDependencies.join(", ")}. Use managed install instead, or bundle dependencies in the package tarball.`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function cleanupStandaloneTempArtifacts(tempDir: string, extractDir?: string): Promise<void> {
|
|
150
|
+
const paths = [extractDir, tempDir].filter((path): path is string => Boolean(path));
|
|
151
|
+
|
|
152
|
+
await Promise.allSettled(
|
|
153
|
+
paths.map(async (path) => {
|
|
154
|
+
try {
|
|
155
|
+
await rm(path, { recursive: true, force: true });
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.warn(
|
|
158
|
+
`[extmgr] Failed to remove temporary standalone install artifact at ${path}:`,
|
|
159
|
+
error
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
})
|
|
88
163
|
);
|
|
89
164
|
}
|
|
90
165
|
|
|
@@ -149,7 +224,7 @@ export async function installPackage(
|
|
|
149
224
|
clearSearchCache();
|
|
150
225
|
logPackageInstall(pi, normalized, normalized, undefined, scope, true);
|
|
151
226
|
success(ctx, `Installed ${normalized} (${scope})`);
|
|
152
|
-
clearUpdatesAvailable(pi, ctx);
|
|
227
|
+
clearUpdatesAvailable(pi, ctx, [normalizePackageIdentity(normalized)]);
|
|
153
228
|
|
|
154
229
|
// Wait for the extension to be discoverable before reloading.
|
|
155
230
|
// This prevents a race condition where ctx.reload() runs before
|
|
@@ -207,7 +282,7 @@ export async function installFromUrl(
|
|
|
207
282
|
await mkdir(extensionDir, { recursive: true });
|
|
208
283
|
notify(ctx, `Downloading ${fileName}...`, "info");
|
|
209
284
|
|
|
210
|
-
const response = await
|
|
285
|
+
const response = await fetchWithTimeout(url, TIMEOUTS.packageInstall);
|
|
211
286
|
if (!response.ok) {
|
|
212
287
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
213
288
|
}
|
|
@@ -230,7 +305,6 @@ export async function installFromUrl(
|
|
|
230
305
|
const { fileName: name, destPath } = result;
|
|
231
306
|
logPackageInstall(pi, url, name, undefined, scope, true);
|
|
232
307
|
success(ctx, `Installed ${name} to:\n${destPath}`);
|
|
233
|
-
clearUpdatesAvailable(pi, ctx);
|
|
234
308
|
|
|
235
309
|
const reloaded = await confirmReload(ctx, "Extension installed.");
|
|
236
310
|
if (!reloaded) {
|
|
@@ -291,9 +365,8 @@ export async function installPackageLocally(
|
|
|
291
365
|
await mkdir(extensionDir, { recursive: true });
|
|
292
366
|
showProgress(ctx, "Fetching", packageName);
|
|
293
367
|
|
|
294
|
-
const viewRes = await pi
|
|
368
|
+
const viewRes = await execNpm(pi, ["view", packageName, "--json"], ctx, {
|
|
295
369
|
timeout: TIMEOUTS.fetchPackageInfo,
|
|
296
|
-
cwd: ctx.cwd,
|
|
297
370
|
});
|
|
298
371
|
|
|
299
372
|
if (viewRes.code !== 0) {
|
|
@@ -325,17 +398,33 @@ export async function installPackageLocally(
|
|
|
325
398
|
}
|
|
326
399
|
const { version, tarballUrl } = result;
|
|
327
400
|
|
|
401
|
+
const tarAvailability = await ensureTarAvailable(pi, ctx);
|
|
402
|
+
if (!tarAvailability.ok) {
|
|
403
|
+
notifyError(ctx, tarAvailability.error);
|
|
404
|
+
logPackageInstall(
|
|
405
|
+
pi,
|
|
406
|
+
`npm:${packageName}`,
|
|
407
|
+
packageName,
|
|
408
|
+
version,
|
|
409
|
+
scope,
|
|
410
|
+
false,
|
|
411
|
+
tarAvailability.error
|
|
412
|
+
);
|
|
413
|
+
void updateExtmgrStatus(ctx, pi);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
328
417
|
// Download and extract
|
|
418
|
+
const tempDir = join(extensionDir, ".temp");
|
|
329
419
|
const extractResult = await tryOperation(
|
|
330
420
|
ctx,
|
|
331
421
|
async () => {
|
|
332
|
-
const tempDir = join(extensionDir, ".temp");
|
|
333
422
|
await mkdir(tempDir, { recursive: true });
|
|
334
423
|
const tarballPath = join(tempDir, `${packageName.replace(/[@/]/g, "-")}-${version}.tgz`);
|
|
335
424
|
|
|
336
425
|
showProgress(ctx, "Downloading", `${packageName}@${version}`);
|
|
337
426
|
|
|
338
|
-
const response = await
|
|
427
|
+
const response = await fetchWithTimeout(tarballUrl, TIMEOUTS.packageInstall);
|
|
339
428
|
if (!response.ok) {
|
|
340
429
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
341
430
|
}
|
|
@@ -343,12 +432,13 @@ export async function installPackageLocally(
|
|
|
343
432
|
const buffer = await response.arrayBuffer();
|
|
344
433
|
await writeFile(tarballPath, new Uint8Array(buffer));
|
|
345
434
|
|
|
346
|
-
return { tarballPath
|
|
435
|
+
return { tarballPath };
|
|
347
436
|
},
|
|
348
437
|
"Download failed"
|
|
349
438
|
);
|
|
350
439
|
|
|
351
440
|
if (!extractResult) {
|
|
441
|
+
await cleanupStandaloneTempArtifacts(tempDir);
|
|
352
442
|
logPackageInstall(
|
|
353
443
|
pi,
|
|
354
444
|
`npm:${packageName}`,
|
|
@@ -361,7 +451,7 @@ export async function installPackageLocally(
|
|
|
361
451
|
void updateExtmgrStatus(ctx, pi);
|
|
362
452
|
return;
|
|
363
453
|
}
|
|
364
|
-
const { tarballPath
|
|
454
|
+
const { tarballPath } = extractResult;
|
|
365
455
|
|
|
366
456
|
// Extract
|
|
367
457
|
const extractDir = join(
|
|
@@ -390,17 +480,22 @@ export async function installPackageLocally(
|
|
|
390
480
|
const hasEntrypoint = await hasStandaloneEntrypoint(extractDir);
|
|
391
481
|
if (!hasEntrypoint) {
|
|
392
482
|
throw new Error(
|
|
393
|
-
`Package ${packageName} does not contain a runnable extension entrypoint (
|
|
483
|
+
`Package ${packageName} does not contain a runnable standalone extension entrypoint (manifest-declared entrypoint, index.ts, or index.js)`
|
|
394
484
|
);
|
|
395
485
|
}
|
|
396
486
|
|
|
487
|
+
const dependencyError = await getStandaloneDependencyError(extractDir);
|
|
488
|
+
if (dependencyError) {
|
|
489
|
+
throw new Error(dependencyError);
|
|
490
|
+
}
|
|
491
|
+
|
|
397
492
|
return true;
|
|
398
493
|
},
|
|
399
494
|
"Extraction failed"
|
|
400
495
|
);
|
|
401
496
|
|
|
402
497
|
if (!extractSuccess) {
|
|
403
|
-
await
|
|
498
|
+
await cleanupStandaloneTempArtifacts(tempDir, extractDir);
|
|
404
499
|
logPackageInstall(
|
|
405
500
|
pi,
|
|
406
501
|
`npm:${packageName}`,
|
|
@@ -429,7 +524,7 @@ export async function installPackageLocally(
|
|
|
429
524
|
"Failed to copy extension"
|
|
430
525
|
);
|
|
431
526
|
|
|
432
|
-
await
|
|
527
|
+
await cleanupStandaloneTempArtifacts(tempDir, extractDir);
|
|
433
528
|
|
|
434
529
|
if (!destResult) {
|
|
435
530
|
logPackageInstall(
|
|
@@ -448,7 +543,6 @@ export async function installPackageLocally(
|
|
|
448
543
|
clearSearchCache();
|
|
449
544
|
logPackageInstall(pi, `npm:${packageName}`, packageName, version, scope, true);
|
|
450
545
|
success(ctx, `Installed ${packageName}@${version} locally to:\n${destResult}`);
|
|
451
|
-
clearUpdatesAvailable(pi, ctx);
|
|
452
546
|
|
|
453
547
|
const reloaded = await confirmReload(ctx, "Extension installed.");
|
|
454
548
|
if (!reloaded) {
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
isSourceInstalled,
|
|
11
11
|
} from "./discovery.js";
|
|
12
12
|
import { waitForCondition } from "../utils/retry.js";
|
|
13
|
-
import { formatInstalledPackageLabel
|
|
14
|
-
import {
|
|
13
|
+
import { formatInstalledPackageLabel } from "../utils/format.js";
|
|
14
|
+
import { normalizePackageIdentity } from "../utils/package-source.js";
|
|
15
15
|
import { logPackageUpdate, logPackageRemove } from "../utils/history.js";
|
|
16
16
|
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
17
17
|
import { notify, error as notifyError, success } from "../utils/notify.js";
|
|
@@ -33,12 +33,19 @@ const NO_PACKAGE_MUTATION_OUTCOME: PackageMutationOutcome = {
|
|
|
33
33
|
reloaded: false,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
const BULK_UPDATE_LABEL = "all packages";
|
|
37
|
+
|
|
36
38
|
function packageMutationOutcome(
|
|
37
39
|
overrides: Partial<PackageMutationOutcome>
|
|
38
40
|
): PackageMutationOutcome {
|
|
39
41
|
return { ...NO_PACKAGE_MUTATION_OUTCOME, ...overrides };
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
function isUpToDateOutput(stdout: string): boolean {
|
|
45
|
+
const pinnedAsStatus = /^\s*pinned\b(?!\s+dependency\b)(?:\s*$|\s*[:(-])/im.test(stdout);
|
|
46
|
+
return /already\s+up\s+to\s+date/i.test(stdout) || pinnedAsStatus;
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
async function updatePackageInternal(
|
|
43
50
|
source: string,
|
|
44
51
|
ctx: ExtensionCommandContext,
|
|
@@ -59,17 +66,19 @@ async function updatePackageInternal(
|
|
|
59
66
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
const updateIdentity = normalizePackageIdentity(source);
|
|
62
70
|
const stdout = res.stdout || "";
|
|
63
|
-
if (
|
|
71
|
+
if (isUpToDateOutput(stdout)) {
|
|
64
72
|
notify(ctx, `${source} is already up to date (or pinned).`, "info");
|
|
65
73
|
logPackageUpdate(pi, source, source, undefined, true);
|
|
74
|
+
clearUpdatesAvailable(pi, ctx, [updateIdentity]);
|
|
66
75
|
void updateExtmgrStatus(ctx, pi);
|
|
67
76
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
logPackageUpdate(pi, source, source, undefined, true);
|
|
71
80
|
success(ctx, `Updated ${source}`);
|
|
72
|
-
clearUpdatesAvailable(pi, ctx);
|
|
81
|
+
clearUpdatesAvailable(pi, ctx, [updateIdentity]);
|
|
73
82
|
|
|
74
83
|
const reloaded = await confirmReload(ctx, "Package updated.");
|
|
75
84
|
if (!reloaded) {
|
|
@@ -87,18 +96,23 @@ async function updatePackagesInternal(
|
|
|
87
96
|
const res = await pi.exec("pi", ["update"], { timeout: TIMEOUTS.packageUpdateAll, cwd: ctx.cwd });
|
|
88
97
|
|
|
89
98
|
if (res.code !== 0) {
|
|
90
|
-
|
|
99
|
+
const errorMsg = `Update failed: ${res.stderr || res.stdout || `exit ${res.code}`}`;
|
|
100
|
+
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, false, errorMsg);
|
|
101
|
+
notifyError(ctx, errorMsg);
|
|
91
102
|
void updateExtmgrStatus(ctx, pi);
|
|
92
103
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
const stdout = res.stdout || "";
|
|
96
|
-
if (stdout
|
|
107
|
+
if (isUpToDateOutput(stdout) || stdout.trim() === "") {
|
|
97
108
|
notify(ctx, "All packages are already up to date.", "info");
|
|
109
|
+
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
|
|
110
|
+
clearUpdatesAvailable(pi, ctx);
|
|
98
111
|
void updateExtmgrStatus(ctx, pi);
|
|
99
112
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
|
|
102
116
|
success(ctx, "Packages updated");
|
|
103
117
|
clearUpdatesAvailable(pi, ctx);
|
|
104
118
|
|
|
@@ -139,23 +153,8 @@ export async function updatePackagesWithOutcome(
|
|
|
139
153
|
return updatePackagesInternal(ctx, pi);
|
|
140
154
|
}
|
|
141
155
|
|
|
142
|
-
function packageIdentity(source: string
|
|
143
|
-
|
|
144
|
-
if (npm?.name) {
|
|
145
|
-
return `npm:${npm.name}`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (getPackageSourceKind(source) === "git") {
|
|
149
|
-
const gitSpec = source.startsWith("git:") ? source.slice(4) : source;
|
|
150
|
-
const { repo } = splitGitRepoAndRef(gitSpec);
|
|
151
|
-
return `git:${repo}`;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (fallbackName) {
|
|
155
|
-
return `name:${fallbackName}`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return `src:${source}`;
|
|
156
|
+
function packageIdentity(source: string): string {
|
|
157
|
+
return normalizePackageIdentity(source);
|
|
159
158
|
}
|
|
160
159
|
|
|
161
160
|
async function getInstalledPackagesAllScopes(
|
|
@@ -238,12 +237,18 @@ function formatRemovalTargets(targets: RemovalTarget[]): string {
|
|
|
238
237
|
return targets.map((t) => `${t.scope}: ${t.source}`).join("\n");
|
|
239
238
|
}
|
|
240
239
|
|
|
240
|
+
interface RemovalExecutionResult {
|
|
241
|
+
target: RemovalTarget;
|
|
242
|
+
success: boolean;
|
|
243
|
+
error?: string;
|
|
244
|
+
}
|
|
245
|
+
|
|
241
246
|
async function executeRemovalTargets(
|
|
242
247
|
targets: RemovalTarget[],
|
|
243
248
|
ctx: ExtensionCommandContext,
|
|
244
249
|
pi: ExtensionAPI
|
|
245
|
-
): Promise<
|
|
246
|
-
const
|
|
250
|
+
): Promise<RemovalExecutionResult[]> {
|
|
251
|
+
const results: RemovalExecutionResult[] = [];
|
|
247
252
|
|
|
248
253
|
for (const target of targets) {
|
|
249
254
|
showProgress(ctx, "Removing", `${target.source} (${target.scope})`);
|
|
@@ -254,14 +259,15 @@ async function executeRemovalTargets(
|
|
|
254
259
|
if (res.code !== 0) {
|
|
255
260
|
const errorMsg = `Remove failed (${target.scope}): ${res.stderr || res.stdout || `exit ${res.code}`}`;
|
|
256
261
|
logPackageRemove(pi, target.source, target.name, false, errorMsg);
|
|
257
|
-
|
|
262
|
+
results.push({ target, success: false, error: errorMsg });
|
|
258
263
|
continue;
|
|
259
264
|
}
|
|
260
265
|
|
|
261
266
|
logPackageRemove(pi, target.source, target.name, true);
|
|
267
|
+
results.push({ target, success: true });
|
|
262
268
|
}
|
|
263
269
|
|
|
264
|
-
return
|
|
270
|
+
return results;
|
|
265
271
|
}
|
|
266
272
|
|
|
267
273
|
function notifyRemovalSummary(
|
|
@@ -295,9 +301,8 @@ async function removePackageInternal(
|
|
|
295
301
|
pi: ExtensionAPI
|
|
296
302
|
): Promise<PackageMutationOutcome> {
|
|
297
303
|
const installed = await getInstalledPackagesAllScopes(ctx, pi);
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
const matching = installed.filter((p) => packageIdentity(p.source, p.name) === identity);
|
|
304
|
+
const identity = packageIdentity(source);
|
|
305
|
+
const matching = installed.filter((p) => packageIdentity(p.source) === identity);
|
|
301
306
|
|
|
302
307
|
const hasBothScopes =
|
|
303
308
|
matching.some((pkg) => pkg.scope === "global") &&
|
|
@@ -326,25 +331,36 @@ async function removePackageInternal(
|
|
|
326
331
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
327
332
|
}
|
|
328
333
|
|
|
329
|
-
const
|
|
334
|
+
const results = await executeRemovalTargets(targets, ctx, pi);
|
|
330
335
|
clearSearchCache();
|
|
331
336
|
|
|
337
|
+
const failures = results
|
|
338
|
+
.filter((result): result is RemovalExecutionResult & { success: false; error: string } =>
|
|
339
|
+
Boolean(!result.success && result.error)
|
|
340
|
+
)
|
|
341
|
+
.map((result) => result.error);
|
|
342
|
+
const successfulTargets = results
|
|
343
|
+
.filter((result) => result.success)
|
|
344
|
+
.map((result) => result.target);
|
|
345
|
+
|
|
332
346
|
const remaining = (await getInstalledPackagesAllScopes(ctx, pi)).filter(
|
|
333
|
-
(p) => packageIdentity(p.source
|
|
347
|
+
(p) => packageIdentity(p.source) === identity
|
|
334
348
|
);
|
|
335
349
|
notifyRemovalSummary(source, remaining, failures, ctx);
|
|
336
350
|
|
|
337
|
-
if (failures.length === 0) {
|
|
338
|
-
clearUpdatesAvailable(pi, ctx);
|
|
351
|
+
if (failures.length === 0 && remaining.length === 0) {
|
|
352
|
+
clearUpdatesAvailable(pi, ctx, [identity]);
|
|
339
353
|
}
|
|
340
354
|
|
|
341
|
-
|
|
342
|
-
|
|
355
|
+
const successfulRemovalCount = successfulTargets.length;
|
|
356
|
+
|
|
357
|
+
// Wait for successfully removed targets to disappear from their target scopes before reloading.
|
|
358
|
+
if (successfulTargets.length > 0) {
|
|
343
359
|
notify(ctx, "Waiting for removal to complete...", "info");
|
|
344
360
|
const isRemoved = await waitForCondition(
|
|
345
361
|
async () => {
|
|
346
362
|
const installedChecks = await Promise.all(
|
|
347
|
-
|
|
363
|
+
successfulTargets.map((target) =>
|
|
348
364
|
isSourceInstalled(target.source, ctx, pi, {
|
|
349
365
|
scope: target.scope,
|
|
350
366
|
})
|
|
@@ -360,6 +376,11 @@ async function removePackageInternal(
|
|
|
360
376
|
}
|
|
361
377
|
}
|
|
362
378
|
|
|
379
|
+
if (successfulRemovalCount === 0) {
|
|
380
|
+
void updateExtmgrStatus(ctx, pi);
|
|
381
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
382
|
+
}
|
|
383
|
+
|
|
363
384
|
const reloaded = await confirmReload(ctx, "Removal complete.");
|
|
364
385
|
if (!reloaded) {
|
|
365
386
|
void updateExtmgrStatus(ctx, pi);
|