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/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/packages/discovery.ts +144 -51
- package/src/packages/extensions.ts +171 -63
- package/src/packages/install.ts +101 -22
- package/src/packages/management.ts +12 -37
- package/src/ui/package-config.ts +157 -126
- package/src/ui/remote.ts +77 -52
- package/src/ui/unified.ts +217 -172
- package/src/utils/auto-update.ts +34 -29
- package/src/utils/cache.ts +15 -2
- package/src/utils/history.ts +41 -2
- package/src/utils/mode.ts +56 -5
- package/src/utils/network.ts +15 -0
- package/src/utils/package-source.ts +31 -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
|
@@ -9,6 +9,7 @@ import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
|
9
9
|
import type { InstalledPackage, PackageExtensionEntry, Scope, State } from "../types/index.js";
|
|
10
10
|
import { parseNpmSource } from "../utils/format.js";
|
|
11
11
|
import { fileExists, readSummary } from "../utils/fs.js";
|
|
12
|
+
import { resolveNpmCommand } from "../utils/npm-exec.js";
|
|
12
13
|
|
|
13
14
|
interface PackageSettingsObject {
|
|
14
15
|
source: string;
|
|
@@ -19,6 +20,14 @@ interface SettingsFile {
|
|
|
19
20
|
packages?: (string | PackageSettingsObject)[];
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
export interface PackageManifest {
|
|
24
|
+
name?: string;
|
|
25
|
+
dependencies?: Record<string, string>;
|
|
26
|
+
pi?: {
|
|
27
|
+
extensions?: unknown;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
const execFileAsync = promisify(execFile);
|
|
23
32
|
let globalNpmRootCache: string | null | undefined;
|
|
24
33
|
|
|
@@ -50,7 +59,8 @@ async function getGlobalNpmRoot(): Promise<string | undefined> {
|
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
try {
|
|
53
|
-
const
|
|
62
|
+
const npmCommand = resolveNpmCommand(["root", "-g"]);
|
|
63
|
+
const { stdout } = await execFileAsync(npmCommand.command, npmCommand.args, {
|
|
54
64
|
timeout: 2_000,
|
|
55
65
|
windowsHide: true,
|
|
56
66
|
});
|
|
@@ -206,6 +216,125 @@ async function writeSettingsFile(path: string, settings: SettingsFile): Promise<
|
|
|
206
216
|
}
|
|
207
217
|
}
|
|
208
218
|
|
|
219
|
+
function findPackageSettingsIndex(
|
|
220
|
+
packages: SettingsFile["packages"] extends infer T ? NonNullable<T> : never,
|
|
221
|
+
normalizedSource: string
|
|
222
|
+
): number {
|
|
223
|
+
return packages.findIndex((pkg) => {
|
|
224
|
+
if (typeof pkg === "string") {
|
|
225
|
+
return normalizeSource(pkg) === normalizedSource;
|
|
226
|
+
}
|
|
227
|
+
return normalizeSource(pkg.source) === normalizedSource;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function toPackageSettingsObject(
|
|
232
|
+
existing: string | PackageSettingsObject | undefined,
|
|
233
|
+
packageSource: string
|
|
234
|
+
): PackageSettingsObject {
|
|
235
|
+
if (typeof existing === "string") {
|
|
236
|
+
return { source: existing, extensions: [] };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (existing && typeof existing.source === "string") {
|
|
240
|
+
return {
|
|
241
|
+
source: existing.source,
|
|
242
|
+
extensions: Array.isArray(existing.extensions) ? [...existing.extensions] : [],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { source: packageSource, extensions: [] };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function updateExtensionMarkers(
|
|
250
|
+
existingTokens: string[] | undefined,
|
|
251
|
+
changes: ReadonlyMap<string, State>
|
|
252
|
+
): string[] {
|
|
253
|
+
const nextTokens: string[] = [];
|
|
254
|
+
|
|
255
|
+
for (const token of existingTokens ?? []) {
|
|
256
|
+
if (typeof token !== "string") {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (token[0] !== "+" && token[0] !== "-") {
|
|
261
|
+
nextTokens.push(token);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const tokenPath = normalizeRelativePath(token.slice(1));
|
|
266
|
+
if (!changes.has(tokenPath)) {
|
|
267
|
+
nextTokens.push(token);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const [extensionPath, target] of Array.from(changes.entries()).sort((a, b) =>
|
|
272
|
+
a[0].localeCompare(b[0])
|
|
273
|
+
)) {
|
|
274
|
+
nextTokens.push(`${target === "enabled" ? "+" : "-"}${extensionPath}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return nextTokens;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function validatePackageExtensionSettings(
|
|
281
|
+
scope: Scope,
|
|
282
|
+
cwd: string
|
|
283
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
284
|
+
try {
|
|
285
|
+
await readSettingsFile(getSettingsPath(scope, cwd), { strict: true });
|
|
286
|
+
return { ok: true };
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return {
|
|
289
|
+
ok: false,
|
|
290
|
+
error: error instanceof Error ? error.message : String(error),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function applyPackageExtensionStateChanges(
|
|
296
|
+
packageSource: string,
|
|
297
|
+
scope: Scope,
|
|
298
|
+
changes: readonly { extensionPath: string; target: State }[],
|
|
299
|
+
cwd: string
|
|
300
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
301
|
+
try {
|
|
302
|
+
if (changes.length === 0) {
|
|
303
|
+
return { ok: true };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const settingsPath = getSettingsPath(scope, cwd);
|
|
307
|
+
const settings = await readSettingsFile(settingsPath, { strict: true });
|
|
308
|
+
const normalizedSource = normalizeSource(packageSource);
|
|
309
|
+
const packages = [...(settings.packages ?? [])];
|
|
310
|
+
const index = findPackageSettingsIndex(packages, normalizedSource);
|
|
311
|
+
const packageEntry = toPackageSettingsObject(packages[index], packageSource);
|
|
312
|
+
|
|
313
|
+
const normalizedChanges = new Map<string, State>();
|
|
314
|
+
for (const change of changes) {
|
|
315
|
+
normalizedChanges.set(normalizeRelativePath(change.extensionPath), change.target);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
packageEntry.extensions = updateExtensionMarkers(packageEntry.extensions, normalizedChanges);
|
|
319
|
+
|
|
320
|
+
if (index === -1) {
|
|
321
|
+
packages.push(packageEntry);
|
|
322
|
+
} else {
|
|
323
|
+
packages[index] = packageEntry;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
settings.packages = packages;
|
|
327
|
+
await writeSettingsFile(settingsPath, settings);
|
|
328
|
+
|
|
329
|
+
return { ok: true };
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
error: error instanceof Error ? error.message : String(error),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
209
338
|
function safeMatchesGlob(targetPath: string, pattern: string): boolean {
|
|
210
339
|
try {
|
|
211
340
|
return matchesGlob(targetPath, pattern);
|
|
@@ -405,20 +534,29 @@ async function resolveManifestExtensionEntries(
|
|
|
405
534
|
return Array.from(selected).sort((a, b) => a.localeCompare(b));
|
|
406
535
|
}
|
|
407
536
|
|
|
408
|
-
export async function
|
|
537
|
+
export async function readPackageManifest(
|
|
409
538
|
packageRoot: string
|
|
410
|
-
): Promise<
|
|
539
|
+
): Promise<PackageManifest | undefined> {
|
|
411
540
|
const packageJsonPath = join(packageRoot, "package.json");
|
|
412
541
|
|
|
413
|
-
let parsed: { pi?: { extensions?: unknown } };
|
|
414
542
|
try {
|
|
415
543
|
const raw = await readFile(packageJsonPath, "utf8");
|
|
416
|
-
parsed = JSON.parse(raw) as
|
|
544
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
545
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
return parsed as PackageManifest;
|
|
417
549
|
} catch {
|
|
418
550
|
return undefined;
|
|
419
551
|
}
|
|
552
|
+
}
|
|
420
553
|
|
|
421
|
-
|
|
554
|
+
export async function resolveManifestExtensionEntrypoints(
|
|
555
|
+
packageRoot: string,
|
|
556
|
+
manifest?: PackageManifest
|
|
557
|
+
): Promise<string[] | undefined> {
|
|
558
|
+
const parsed = manifest ?? (await readPackageManifest(packageRoot));
|
|
559
|
+
const extensions = parsed?.pi?.extensions;
|
|
422
560
|
if (!Array.isArray(extensions)) {
|
|
423
561
|
return undefined;
|
|
424
562
|
}
|
|
@@ -427,12 +565,35 @@ export async function resolveManifestExtensionEntrypoints(
|
|
|
427
565
|
return resolveManifestExtensionEntries(packageRoot, entries);
|
|
428
566
|
}
|
|
429
567
|
|
|
430
|
-
async function
|
|
431
|
-
const
|
|
568
|
+
async function resolveConventionExtensionEntrypoints(packageRoot: string): Promise<string[]> {
|
|
569
|
+
const extensionsDir = join(packageRoot, "extensions");
|
|
570
|
+
return collectExtensionFilesFromDir(packageRoot, extensionsDir);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export async function discoverPackageExtensionEntrypoints(
|
|
574
|
+
packageRoot: string,
|
|
575
|
+
options?: {
|
|
576
|
+
allowConventionDirectory?: boolean;
|
|
577
|
+
allowRootIndexFallback?: boolean;
|
|
578
|
+
}
|
|
579
|
+
): Promise<string[]> {
|
|
580
|
+
const manifest = await readPackageManifest(packageRoot);
|
|
581
|
+
const manifestEntrypoints = await resolveManifestExtensionEntrypoints(packageRoot, manifest);
|
|
432
582
|
if (manifestEntrypoints !== undefined) {
|
|
433
583
|
return manifestEntrypoints;
|
|
434
584
|
}
|
|
435
585
|
|
|
586
|
+
if (options?.allowConventionDirectory !== false) {
|
|
587
|
+
const conventionEntrypoints = await resolveConventionExtensionEntrypoints(packageRoot);
|
|
588
|
+
if (conventionEntrypoints.length > 0) {
|
|
589
|
+
return conventionEntrypoints.sort((a, b) => a.localeCompare(b));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (options?.allowRootIndexFallback === false) {
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
|
|
436
597
|
const indexTs = join(packageRoot, "index.ts");
|
|
437
598
|
if (await fileExists(indexTs)) {
|
|
438
599
|
return ["index.ts"];
|
|
@@ -456,7 +617,7 @@ export async function discoverPackageExtensions(
|
|
|
456
617
|
const packageRoot = await toPackageRoot(pkg, cwd);
|
|
457
618
|
if (!packageRoot) continue;
|
|
458
619
|
|
|
459
|
-
const extensionPaths = await
|
|
620
|
+
const extensionPaths = await discoverPackageExtensionEntrypoints(packageRoot);
|
|
460
621
|
for (const extensionPath of extensionPaths) {
|
|
461
622
|
const normalizedPath = normalizeRelativePath(extensionPath);
|
|
462
623
|
const absolutePath = resolve(packageRoot, extensionPath);
|
|
@@ -490,58 +651,5 @@ export async function setPackageExtensionState(
|
|
|
490
651
|
target: State,
|
|
491
652
|
cwd: string
|
|
492
653
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
493
|
-
|
|
494
|
-
const settingsPath = getSettingsPath(scope, cwd);
|
|
495
|
-
const settings = await readSettingsFile(settingsPath, { strict: true });
|
|
496
|
-
|
|
497
|
-
const normalizedSource = normalizeSource(packageSource);
|
|
498
|
-
const normalizedPath = normalizeRelativePath(extensionPath);
|
|
499
|
-
const marker = `${target === "enabled" ? "+" : "-"}${normalizedPath}`;
|
|
500
|
-
|
|
501
|
-
const packages = [...(settings.packages ?? [])];
|
|
502
|
-
let index = packages.findIndex((pkg) => {
|
|
503
|
-
if (typeof pkg === "string") {
|
|
504
|
-
return normalizeSource(pkg) === normalizedSource;
|
|
505
|
-
}
|
|
506
|
-
return normalizeSource(pkg.source) === normalizedSource;
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
let packageEntry: PackageSettingsObject;
|
|
510
|
-
if (index === -1) {
|
|
511
|
-
packageEntry = { source: packageSource, extensions: [marker] };
|
|
512
|
-
packages.push(packageEntry);
|
|
513
|
-
index = packages.length - 1;
|
|
514
|
-
} else {
|
|
515
|
-
const existing = packages[index];
|
|
516
|
-
if (typeof existing === "string") {
|
|
517
|
-
packageEntry = { source: existing, extensions: [] };
|
|
518
|
-
} else if (existing && typeof existing.source === "string") {
|
|
519
|
-
packageEntry = {
|
|
520
|
-
source: existing.source,
|
|
521
|
-
extensions: Array.isArray(existing.extensions) ? [...existing.extensions] : [],
|
|
522
|
-
};
|
|
523
|
-
} else {
|
|
524
|
-
packageEntry = { source: packageSource, extensions: [] };
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
packageEntry.extensions = (packageEntry.extensions ?? []).filter((token) => {
|
|
528
|
-
if (typeof token !== "string") return false;
|
|
529
|
-
if (token[0] !== "+" && token[0] !== "-") return true;
|
|
530
|
-
return normalizeRelativePath(token.slice(1)) !== normalizedPath;
|
|
531
|
-
});
|
|
532
|
-
packageEntry.extensions.push(marker);
|
|
533
|
-
packages[index] = packageEntry;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
settings.packages = packages;
|
|
537
|
-
|
|
538
|
-
await writeSettingsFile(settingsPath, settings);
|
|
539
|
-
|
|
540
|
-
return { ok: true };
|
|
541
|
-
} catch (error) {
|
|
542
|
-
return {
|
|
543
|
-
ok: false,
|
|
544
|
-
error: error instanceof Error ? error.message : String(error),
|
|
545
|
-
};
|
|
546
|
-
}
|
|
654
|
+
return applyPackageExtensionStateChanges(packageSource, scope, [{ extensionPath, target }], cwd);
|
|
547
655
|
}
|
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";
|
|
@@ -17,6 +17,8 @@ import { confirmAction, confirmReload, showProgress } from "../utils/ui-helpers.
|
|
|
17
17
|
import { tryOperation } from "../utils/mode.js";
|
|
18
18
|
import { updateExtmgrStatus } from "../utils/status.js";
|
|
19
19
|
import { execNpm } from "../utils/npm-exec.js";
|
|
20
|
+
import { normalizePackageIdentity } from "../utils/package-source.js";
|
|
21
|
+
import { fetchWithTimeout } from "../utils/network.js";
|
|
20
22
|
import { TIMEOUTS } from "../constants.js";
|
|
21
23
|
|
|
22
24
|
export type InstallScope = "global" | "project";
|
|
@@ -72,20 +74,77 @@ function safeExtractGithubMatch(match: RegExpMatchArray | null): GithubUrlInfo |
|
|
|
72
74
|
return { owner, repo, branch, filePath };
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
async function ensureTarAvailable(
|
|
78
|
+
pi: ExtensionAPI,
|
|
79
|
+
ctx: ExtensionCommandContext
|
|
80
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
81
|
+
const result = await pi.exec("tar", ["--version"], {
|
|
82
|
+
timeout: 5_000,
|
|
83
|
+
cwd: ctx.cwd,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (result.code === 0) {
|
|
87
|
+
return { ok: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
error:
|
|
93
|
+
"Standalone local installs require the `tar` command on PATH. Install tar or use managed package install instead.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
75
97
|
async function hasStandaloneEntrypoint(packageRoot: string): Promise<boolean> {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
const entrypoints = await discoverPackageExtensionEntrypoints(packageRoot, {
|
|
99
|
+
allowConventionDirectory: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
for (const path of entrypoints) {
|
|
103
|
+
if (await fileExists(join(packageRoot, path))) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function getStandaloneDependencyError(packageRoot: string): Promise<string | undefined> {
|
|
112
|
+
const manifest = await readPackageManifest(packageRoot);
|
|
113
|
+
const dependencies = manifest?.dependencies;
|
|
114
|
+
if (!dependencies || typeof dependencies !== "object") {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const missingDependencies: string[] = [];
|
|
119
|
+
for (const dependencyName of Object.keys(dependencies)) {
|
|
120
|
+
const dependencyPath = join(packageRoot, "node_modules", dependencyName);
|
|
121
|
+
if (!(await fileExists(dependencyPath))) {
|
|
122
|
+
missingDependencies.push(dependencyName);
|
|
82
123
|
}
|
|
83
|
-
return false;
|
|
84
124
|
}
|
|
85
125
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
126
|
+
if (missingDependencies.length === 0) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const packageName = manifest?.name ?? "This package";
|
|
131
|
+
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.`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function cleanupStandaloneTempArtifacts(tempDir: string, extractDir?: string): Promise<void> {
|
|
135
|
+
const paths = [extractDir, tempDir].filter((path): path is string => Boolean(path));
|
|
136
|
+
|
|
137
|
+
await Promise.allSettled(
|
|
138
|
+
paths.map(async (path) => {
|
|
139
|
+
try {
|
|
140
|
+
await rm(path, { recursive: true, force: true });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn(
|
|
143
|
+
`[extmgr] Failed to remove temporary standalone install artifact at ${path}:`,
|
|
144
|
+
error
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
})
|
|
89
148
|
);
|
|
90
149
|
}
|
|
91
150
|
|
|
@@ -150,7 +209,7 @@ export async function installPackage(
|
|
|
150
209
|
clearSearchCache();
|
|
151
210
|
logPackageInstall(pi, normalized, normalized, undefined, scope, true);
|
|
152
211
|
success(ctx, `Installed ${normalized} (${scope})`);
|
|
153
|
-
clearUpdatesAvailable(pi, ctx);
|
|
212
|
+
clearUpdatesAvailable(pi, ctx, [normalizePackageIdentity(normalized)]);
|
|
154
213
|
|
|
155
214
|
// Wait for the extension to be discoverable before reloading.
|
|
156
215
|
// This prevents a race condition where ctx.reload() runs before
|
|
@@ -208,7 +267,7 @@ export async function installFromUrl(
|
|
|
208
267
|
await mkdir(extensionDir, { recursive: true });
|
|
209
268
|
notify(ctx, `Downloading ${fileName}...`, "info");
|
|
210
269
|
|
|
211
|
-
const response = await
|
|
270
|
+
const response = await fetchWithTimeout(url, TIMEOUTS.packageInstall);
|
|
212
271
|
if (!response.ok) {
|
|
213
272
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
214
273
|
}
|
|
@@ -231,7 +290,6 @@ export async function installFromUrl(
|
|
|
231
290
|
const { fileName: name, destPath } = result;
|
|
232
291
|
logPackageInstall(pi, url, name, undefined, scope, true);
|
|
233
292
|
success(ctx, `Installed ${name} to:\n${destPath}`);
|
|
234
|
-
clearUpdatesAvailable(pi, ctx);
|
|
235
293
|
|
|
236
294
|
const reloaded = await confirmReload(ctx, "Extension installed.");
|
|
237
295
|
if (!reloaded) {
|
|
@@ -325,17 +383,33 @@ export async function installPackageLocally(
|
|
|
325
383
|
}
|
|
326
384
|
const { version, tarballUrl } = result;
|
|
327
385
|
|
|
386
|
+
const tarAvailability = await ensureTarAvailable(pi, ctx);
|
|
387
|
+
if (!tarAvailability.ok) {
|
|
388
|
+
notifyError(ctx, tarAvailability.error);
|
|
389
|
+
logPackageInstall(
|
|
390
|
+
pi,
|
|
391
|
+
`npm:${packageName}`,
|
|
392
|
+
packageName,
|
|
393
|
+
version,
|
|
394
|
+
scope,
|
|
395
|
+
false,
|
|
396
|
+
tarAvailability.error
|
|
397
|
+
);
|
|
398
|
+
void updateExtmgrStatus(ctx, pi);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
328
402
|
// Download and extract
|
|
403
|
+
const tempDir = join(extensionDir, ".temp");
|
|
329
404
|
const extractResult = await tryOperation(
|
|
330
405
|
ctx,
|
|
331
406
|
async () => {
|
|
332
|
-
const tempDir = join(extensionDir, ".temp");
|
|
333
407
|
await mkdir(tempDir, { recursive: true });
|
|
334
408
|
const tarballPath = join(tempDir, `${packageName.replace(/[@/]/g, "-")}-${version}.tgz`);
|
|
335
409
|
|
|
336
410
|
showProgress(ctx, "Downloading", `${packageName}@${version}`);
|
|
337
411
|
|
|
338
|
-
const response = await
|
|
412
|
+
const response = await fetchWithTimeout(tarballUrl, TIMEOUTS.packageInstall);
|
|
339
413
|
if (!response.ok) {
|
|
340
414
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
341
415
|
}
|
|
@@ -343,12 +417,13 @@ export async function installPackageLocally(
|
|
|
343
417
|
const buffer = await response.arrayBuffer();
|
|
344
418
|
await writeFile(tarballPath, new Uint8Array(buffer));
|
|
345
419
|
|
|
346
|
-
return { tarballPath
|
|
420
|
+
return { tarballPath };
|
|
347
421
|
},
|
|
348
422
|
"Download failed"
|
|
349
423
|
);
|
|
350
424
|
|
|
351
425
|
if (!extractResult) {
|
|
426
|
+
await cleanupStandaloneTempArtifacts(tempDir);
|
|
352
427
|
logPackageInstall(
|
|
353
428
|
pi,
|
|
354
429
|
`npm:${packageName}`,
|
|
@@ -361,7 +436,7 @@ export async function installPackageLocally(
|
|
|
361
436
|
void updateExtmgrStatus(ctx, pi);
|
|
362
437
|
return;
|
|
363
438
|
}
|
|
364
|
-
const { tarballPath
|
|
439
|
+
const { tarballPath } = extractResult;
|
|
365
440
|
|
|
366
441
|
// Extract
|
|
367
442
|
const extractDir = join(
|
|
@@ -390,17 +465,22 @@ export async function installPackageLocally(
|
|
|
390
465
|
const hasEntrypoint = await hasStandaloneEntrypoint(extractDir);
|
|
391
466
|
if (!hasEntrypoint) {
|
|
392
467
|
throw new Error(
|
|
393
|
-
`Package ${packageName} does not contain a runnable extension entrypoint (
|
|
468
|
+
`Package ${packageName} does not contain a runnable standalone extension entrypoint (manifest-declared entrypoint, index.ts, or index.js)`
|
|
394
469
|
);
|
|
395
470
|
}
|
|
396
471
|
|
|
472
|
+
const dependencyError = await getStandaloneDependencyError(extractDir);
|
|
473
|
+
if (dependencyError) {
|
|
474
|
+
throw new Error(dependencyError);
|
|
475
|
+
}
|
|
476
|
+
|
|
397
477
|
return true;
|
|
398
478
|
},
|
|
399
479
|
"Extraction failed"
|
|
400
480
|
);
|
|
401
481
|
|
|
402
482
|
if (!extractSuccess) {
|
|
403
|
-
await
|
|
483
|
+
await cleanupStandaloneTempArtifacts(tempDir, extractDir);
|
|
404
484
|
logPackageInstall(
|
|
405
485
|
pi,
|
|
406
486
|
`npm:${packageName}`,
|
|
@@ -429,7 +509,7 @@ export async function installPackageLocally(
|
|
|
429
509
|
"Failed to copy extension"
|
|
430
510
|
);
|
|
431
511
|
|
|
432
|
-
await
|
|
512
|
+
await cleanupStandaloneTempArtifacts(tempDir, extractDir);
|
|
433
513
|
|
|
434
514
|
if (!destResult) {
|
|
435
515
|
logPackageInstall(
|
|
@@ -448,7 +528,6 @@ export async function installPackageLocally(
|
|
|
448
528
|
clearSearchCache();
|
|
449
529
|
logPackageInstall(pi, `npm:${packageName}`, packageName, version, scope, true);
|
|
450
530
|
success(ctx, `Installed ${packageName}@${version} locally to:\n${destResult}`);
|
|
451
|
-
clearUpdatesAvailable(pi, ctx);
|
|
452
531
|
|
|
453
532
|
const reloaded = await confirmReload(ctx, "Extension installed.");
|
|
454
533
|
if (!reloaded) {
|
|
@@ -10,12 +10,8 @@ import {
|
|
|
10
10
|
isSourceInstalled,
|
|
11
11
|
} from "./discovery.js";
|
|
12
12
|
import { waitForCondition } from "../utils/retry.js";
|
|
13
|
-
import { formatInstalledPackageLabel
|
|
14
|
-
import {
|
|
15
|
-
getPackageSourceKind,
|
|
16
|
-
normalizeLocalSourceIdentity,
|
|
17
|
-
splitGitRepoAndRef,
|
|
18
|
-
} from "../utils/package-source.js";
|
|
13
|
+
import { formatInstalledPackageLabel } from "../utils/format.js";
|
|
14
|
+
import { normalizePackageIdentity } from "../utils/package-source.js";
|
|
19
15
|
import { logPackageUpdate, logPackageRemove } from "../utils/history.js";
|
|
20
16
|
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
21
17
|
import { notify, error as notifyError, success } from "../utils/notify.js";
|
|
@@ -70,18 +66,19 @@ async function updatePackageInternal(
|
|
|
70
66
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
71
67
|
}
|
|
72
68
|
|
|
69
|
+
const updateIdentity = normalizePackageIdentity(source);
|
|
73
70
|
const stdout = res.stdout || "";
|
|
74
71
|
if (isUpToDateOutput(stdout)) {
|
|
75
72
|
notify(ctx, `${source} is already up to date (or pinned).`, "info");
|
|
76
73
|
logPackageUpdate(pi, source, source, undefined, true);
|
|
77
|
-
clearUpdatesAvailable(pi, ctx);
|
|
74
|
+
clearUpdatesAvailable(pi, ctx, [updateIdentity]);
|
|
78
75
|
void updateExtmgrStatus(ctx, pi);
|
|
79
76
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
80
77
|
}
|
|
81
78
|
|
|
82
79
|
logPackageUpdate(pi, source, source, undefined, true);
|
|
83
80
|
success(ctx, `Updated ${source}`);
|
|
84
|
-
clearUpdatesAvailable(pi, ctx);
|
|
81
|
+
clearUpdatesAvailable(pi, ctx, [updateIdentity]);
|
|
85
82
|
|
|
86
83
|
const reloaded = await confirmReload(ctx, "Package updated.");
|
|
87
84
|
if (!reloaded) {
|
|
@@ -156,29 +153,8 @@ export async function updatePackagesWithOutcome(
|
|
|
156
153
|
return updatePackagesInternal(ctx, pi);
|
|
157
154
|
}
|
|
158
155
|
|
|
159
|
-
function packageIdentity(source: string
|
|
160
|
-
|
|
161
|
-
if (npm?.name) {
|
|
162
|
-
return `npm:${npm.name}`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const sourceKind = getPackageSourceKind(source);
|
|
166
|
-
|
|
167
|
-
if (sourceKind === "git") {
|
|
168
|
-
const gitSpec = source.startsWith("git:") ? source.slice(4) : source;
|
|
169
|
-
const { repo } = splitGitRepoAndRef(gitSpec);
|
|
170
|
-
return `git:${repo}`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (sourceKind === "local") {
|
|
174
|
-
return `src:${normalizeLocalSourceIdentity(source)}`;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (fallbackName) {
|
|
178
|
-
return `name:${fallbackName}`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return `src:${source}`;
|
|
156
|
+
function packageIdentity(source: string): string {
|
|
157
|
+
return normalizePackageIdentity(source);
|
|
182
158
|
}
|
|
183
159
|
|
|
184
160
|
async function getInstalledPackagesAllScopes(
|
|
@@ -325,9 +301,8 @@ async function removePackageInternal(
|
|
|
325
301
|
pi: ExtensionAPI
|
|
326
302
|
): Promise<PackageMutationOutcome> {
|
|
327
303
|
const installed = await getInstalledPackagesAllScopes(ctx, pi);
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
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);
|
|
331
306
|
|
|
332
307
|
const hasBothScopes =
|
|
333
308
|
matching.some((pkg) => pkg.scope === "global") &&
|
|
@@ -369,12 +344,12 @@ async function removePackageInternal(
|
|
|
369
344
|
.map((result) => result.target);
|
|
370
345
|
|
|
371
346
|
const remaining = (await getInstalledPackagesAllScopes(ctx, pi)).filter(
|
|
372
|
-
(p) => packageIdentity(p.source
|
|
347
|
+
(p) => packageIdentity(p.source) === identity
|
|
373
348
|
);
|
|
374
349
|
notifyRemovalSummary(source, remaining, failures, ctx);
|
|
375
350
|
|
|
376
|
-
if (failures.length === 0) {
|
|
377
|
-
clearUpdatesAvailable(pi, ctx);
|
|
351
|
+
if (failures.length === 0 && remaining.length === 0) {
|
|
352
|
+
clearUpdatesAvailable(pi, ctx, [identity]);
|
|
378
353
|
}
|
|
379
354
|
|
|
380
355
|
const successfulRemovalCount = successfulTargets.length;
|