pi-extmgr 0.1.11 → 0.1.13
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/packages/discovery.ts +47 -9
- package/src/packages/management.ts +95 -27
- package/src/ui/unified.ts +58 -23
- package/src/utils/package-source.ts +38 -0
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package discovery and listing
|
|
3
3
|
*/
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
4
6
|
import type {
|
|
5
7
|
ExtensionAPI,
|
|
6
8
|
ExtensionCommandContext,
|
|
@@ -10,7 +12,7 @@ import type { InstalledPackage, NpmPackage, SearchCache } from "../types/index.j
|
|
|
10
12
|
import { CACHE_TTL, TIMEOUTS } from "../constants.js";
|
|
11
13
|
import { readSummary } from "../utils/fs.js";
|
|
12
14
|
import { parseNpmSource } from "../utils/format.js";
|
|
13
|
-
import { splitGitRepoAndRef } from "../utils/package-source.js";
|
|
15
|
+
import { getPackageSourceKind, splitGitRepoAndRef } from "../utils/package-source.js";
|
|
14
16
|
|
|
15
17
|
let searchCache: SearchCache | null = null;
|
|
16
18
|
|
|
@@ -280,8 +282,9 @@ function parsePackageNameAndVersion(fullSource: string): {
|
|
|
280
282
|
return parsedNpm;
|
|
281
283
|
}
|
|
282
284
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
+
const sourceKind = getPackageSourceKind(fullSource);
|
|
286
|
+
if (sourceKind === "git") {
|
|
287
|
+
const gitSpec = fullSource.startsWith("git:") ? fullSource.slice(4) : fullSource;
|
|
285
288
|
const { repo } = splitGitRepoAndRef(gitSpec);
|
|
286
289
|
return { name: extractGitPackageName(repo) };
|
|
287
290
|
}
|
|
@@ -298,6 +301,45 @@ function parsePackageNameAndVersion(fullSource: string): {
|
|
|
298
301
|
return { name: fileName || fullSource };
|
|
299
302
|
}
|
|
300
303
|
|
|
304
|
+
async function hydratePackageFromResolvedPath(pkg: InstalledPackage): Promise<void> {
|
|
305
|
+
if (!pkg.resolvedPath) return;
|
|
306
|
+
|
|
307
|
+
const manifestPath = /(?:^|[\\/])package\.json$/i.test(pkg.resolvedPath)
|
|
308
|
+
? pkg.resolvedPath
|
|
309
|
+
: join(pkg.resolvedPath, "package.json");
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const raw = await readFile(manifestPath, "utf8");
|
|
313
|
+
const manifest = JSON.parse(raw) as {
|
|
314
|
+
name?: unknown;
|
|
315
|
+
version?: unknown;
|
|
316
|
+
description?: unknown;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
if (!pkg.version && typeof manifest.version === "string" && manifest.version.trim()) {
|
|
320
|
+
pkg.version = manifest.version.trim();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (
|
|
324
|
+
!pkg.description &&
|
|
325
|
+
typeof manifest.description === "string" &&
|
|
326
|
+
manifest.description.trim()
|
|
327
|
+
) {
|
|
328
|
+
pkg.description = manifest.description.trim();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (
|
|
332
|
+
(!pkg.name || pkg.name === pkg.source) &&
|
|
333
|
+
typeof manifest.name === "string" &&
|
|
334
|
+
manifest.name.trim()
|
|
335
|
+
) {
|
|
336
|
+
pkg.name = manifest.name.trim();
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
// ignore
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
301
343
|
/**
|
|
302
344
|
* Fetch package size from npm view
|
|
303
345
|
*/
|
|
@@ -356,7 +398,8 @@ async function addPackageMetadata(
|
|
|
356
398
|
|
|
357
399
|
await Promise.all(
|
|
358
400
|
batch.map(async (pkg) => {
|
|
359
|
-
|
|
401
|
+
await hydratePackageFromResolvedPath(pkg);
|
|
402
|
+
|
|
360
403
|
const needsDescription = !pkg.description;
|
|
361
404
|
const needsSize = pkg.size === undefined && pkg.source.startsWith("npm:");
|
|
362
405
|
|
|
@@ -364,7 +407,6 @@ async function addPackageMetadata(
|
|
|
364
407
|
|
|
365
408
|
try {
|
|
366
409
|
if (pkg.source.endsWith(".ts") || pkg.source.endsWith(".js")) {
|
|
367
|
-
// For local files, read description from file
|
|
368
410
|
if (needsDescription) {
|
|
369
411
|
pkg.description = await readSummary(pkg.source);
|
|
370
412
|
}
|
|
@@ -373,13 +415,11 @@ async function addPackageMetadata(
|
|
|
373
415
|
const pkgName = parsed?.name;
|
|
374
416
|
|
|
375
417
|
if (pkgName) {
|
|
376
|
-
// Get description
|
|
377
418
|
if (needsDescription) {
|
|
378
419
|
const cached = await getCachedPackage(pkgName);
|
|
379
420
|
if (cached?.description) {
|
|
380
421
|
pkg.description = cached.description;
|
|
381
422
|
} else {
|
|
382
|
-
// Fetch from npm and cache it
|
|
383
423
|
const res = await pi.exec("npm", ["view", pkgName, "description", "--json"], {
|
|
384
424
|
timeout: TIMEOUTS.npmView,
|
|
385
425
|
cwd: ctx.cwd,
|
|
@@ -389,7 +429,6 @@ async function addPackageMetadata(
|
|
|
389
429
|
const desc = JSON.parse(res.stdout) as string;
|
|
390
430
|
if (typeof desc === "string" && desc) {
|
|
391
431
|
pkg.description = desc;
|
|
392
|
-
// Cache the description
|
|
393
432
|
await setCachedPackage(pkgName, {
|
|
394
433
|
name: pkgName,
|
|
395
434
|
description: desc,
|
|
@@ -402,7 +441,6 @@ async function addPackageMetadata(
|
|
|
402
441
|
}
|
|
403
442
|
}
|
|
404
443
|
|
|
405
|
-
// Get size
|
|
406
444
|
if (needsSize) {
|
|
407
445
|
pkg.size = await fetchPackageSize(pkgName, ctx, pi);
|
|
408
446
|
}
|
|
@@ -23,11 +23,27 @@ import { requireUI } from "../utils/mode.js";
|
|
|
23
23
|
import { updateExtmgrStatus } from "../utils/status.js";
|
|
24
24
|
import { TIMEOUTS, UI } from "../constants.js";
|
|
25
25
|
|
|
26
|
-
export
|
|
26
|
+
export interface PackageMutationOutcome {
|
|
27
|
+
reloaded: boolean;
|
|
28
|
+
restartRequested: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const NO_PACKAGE_MUTATION_OUTCOME: PackageMutationOutcome = {
|
|
32
|
+
reloaded: false,
|
|
33
|
+
restartRequested: false,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function packageMutationOutcome(
|
|
37
|
+
overrides: Partial<PackageMutationOutcome>
|
|
38
|
+
): PackageMutationOutcome {
|
|
39
|
+
return { ...NO_PACKAGE_MUTATION_OUTCOME, ...overrides };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function updatePackageInternal(
|
|
27
43
|
source: string,
|
|
28
44
|
ctx: ExtensionCommandContext,
|
|
29
45
|
pi: ExtensionAPI
|
|
30
|
-
): Promise<
|
|
46
|
+
): Promise<PackageMutationOutcome> {
|
|
31
47
|
showProgress(ctx, "Updating", source);
|
|
32
48
|
|
|
33
49
|
const res = await pi.exec("pi", ["update", source], {
|
|
@@ -40,28 +56,29 @@ export async function updatePackage(
|
|
|
40
56
|
logPackageUpdate(pi, source, source, undefined, undefined, false, errorMsg);
|
|
41
57
|
notifyError(ctx, errorMsg);
|
|
42
58
|
void updateExtmgrStatus(ctx, pi);
|
|
43
|
-
return;
|
|
59
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
const stdout = res.stdout || "";
|
|
47
63
|
if (stdout.includes("already up to date") || stdout.includes("pinned")) {
|
|
48
64
|
notify(ctx, `${source} is already up to date (or pinned).`, "info");
|
|
49
65
|
logPackageUpdate(pi, source, source, undefined, undefined, true);
|
|
50
|
-
} else {
|
|
51
|
-
logPackageUpdate(pi, source, source, undefined, undefined, true);
|
|
52
|
-
success(ctx, `Updated ${source}`);
|
|
53
66
|
void updateExtmgrStatus(ctx, pi);
|
|
54
|
-
|
|
55
|
-
return;
|
|
67
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
56
68
|
}
|
|
57
69
|
|
|
70
|
+
logPackageUpdate(pi, source, source, undefined, undefined, true);
|
|
71
|
+
success(ctx, `Updated ${source}`);
|
|
58
72
|
void updateExtmgrStatus(ctx, pi);
|
|
73
|
+
|
|
74
|
+
const reloaded = await confirmReload(ctx, "Package updated.");
|
|
75
|
+
return packageMutationOutcome({ reloaded });
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
|
|
78
|
+
async function updatePackagesInternal(
|
|
62
79
|
ctx: ExtensionCommandContext,
|
|
63
80
|
pi: ExtensionAPI
|
|
64
|
-
): Promise<
|
|
81
|
+
): Promise<PackageMutationOutcome> {
|
|
65
82
|
showProgress(ctx, "Updating", "all packages");
|
|
66
83
|
|
|
67
84
|
const res = await pi.exec("pi", ["update"], { timeout: 300000, cwd: ctx.cwd });
|
|
@@ -69,20 +86,51 @@ export async function updatePackages(
|
|
|
69
86
|
if (res.code !== 0) {
|
|
70
87
|
notifyError(ctx, `Update failed: ${res.stderr || res.stdout || `exit ${res.code}`}`);
|
|
71
88
|
void updateExtmgrStatus(ctx, pi);
|
|
72
|
-
return;
|
|
89
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
73
90
|
}
|
|
74
91
|
|
|
75
92
|
const stdout = res.stdout || "";
|
|
76
93
|
if (stdout.includes("already up to date") || stdout.trim() === "") {
|
|
77
94
|
notify(ctx, "All packages are already up to date.", "info");
|
|
78
|
-
} else {
|
|
79
|
-
success(ctx, "Packages updated");
|
|
80
95
|
void updateExtmgrStatus(ctx, pi);
|
|
81
|
-
|
|
82
|
-
return;
|
|
96
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
83
97
|
}
|
|
84
98
|
|
|
99
|
+
success(ctx, "Packages updated");
|
|
85
100
|
void updateExtmgrStatus(ctx, pi);
|
|
101
|
+
|
|
102
|
+
const reloaded = await confirmReload(ctx, "Packages updated.");
|
|
103
|
+
return packageMutationOutcome({ reloaded });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function updatePackage(
|
|
107
|
+
source: string,
|
|
108
|
+
ctx: ExtensionCommandContext,
|
|
109
|
+
pi: ExtensionAPI
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
await updatePackageInternal(source, ctx, pi);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function updatePackageWithOutcome(
|
|
115
|
+
source: string,
|
|
116
|
+
ctx: ExtensionCommandContext,
|
|
117
|
+
pi: ExtensionAPI
|
|
118
|
+
): Promise<PackageMutationOutcome> {
|
|
119
|
+
return updatePackageInternal(source, ctx, pi);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function updatePackages(
|
|
123
|
+
ctx: ExtensionCommandContext,
|
|
124
|
+
pi: ExtensionAPI
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
await updatePackagesInternal(ctx, pi);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function updatePackagesWithOutcome(
|
|
130
|
+
ctx: ExtensionCommandContext,
|
|
131
|
+
pi: ExtensionAPI
|
|
132
|
+
): Promise<PackageMutationOutcome> {
|
|
133
|
+
return updatePackagesInternal(ctx, pi);
|
|
86
134
|
}
|
|
87
135
|
|
|
88
136
|
function packageIdentity(source: string, fallbackName?: string): string {
|
|
@@ -234,11 +282,11 @@ function notifyRemovalSummary(
|
|
|
234
282
|
}
|
|
235
283
|
}
|
|
236
284
|
|
|
237
|
-
|
|
285
|
+
async function removePackageInternal(
|
|
238
286
|
source: string,
|
|
239
287
|
ctx: ExtensionCommandContext,
|
|
240
288
|
pi: ExtensionAPI
|
|
241
|
-
): Promise<
|
|
289
|
+
): Promise<PackageMutationOutcome> {
|
|
242
290
|
const installed = await getInstalledPackagesAllScopes(ctx, pi);
|
|
243
291
|
const direct = installed.find((p) => p.source === source);
|
|
244
292
|
const identity = packageIdentity(source, direct?.name);
|
|
@@ -251,13 +299,13 @@ export async function removePackage(
|
|
|
251
299
|
|
|
252
300
|
if (scopeChoice === "cancel") {
|
|
253
301
|
notify(ctx, "Removal cancelled.", "info");
|
|
254
|
-
return;
|
|
302
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
255
303
|
}
|
|
256
304
|
|
|
257
305
|
const targets = buildRemovalTargets(matching, source, ctx.hasUI, scopeChoice);
|
|
258
306
|
if (targets.length === 0) {
|
|
259
307
|
notify(ctx, "Nothing to remove.", "info");
|
|
260
|
-
return;
|
|
308
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
261
309
|
}
|
|
262
310
|
|
|
263
311
|
const confirmed = await confirmAction(
|
|
@@ -268,7 +316,7 @@ export async function removePackage(
|
|
|
268
316
|
);
|
|
269
317
|
if (!confirmed) {
|
|
270
318
|
notify(ctx, "Removal cancelled.", "info");
|
|
271
|
-
return;
|
|
319
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
272
320
|
}
|
|
273
321
|
|
|
274
322
|
const failures = await executeRemovalTargets(targets, ctx, pi);
|
|
@@ -281,10 +329,28 @@ export async function removePackage(
|
|
|
281
329
|
|
|
282
330
|
void updateExtmgrStatus(ctx, pi);
|
|
283
331
|
|
|
284
|
-
await confirmRestart(
|
|
332
|
+
const restartRequested = await confirmRestart(
|
|
285
333
|
ctx,
|
|
286
334
|
`Removal complete.\n\n⚠️ Extensions/prompts/skills/themes from removed packages are fully unloaded after restarting pi.`
|
|
287
335
|
);
|
|
336
|
+
|
|
337
|
+
return packageMutationOutcome({ restartRequested });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function removePackage(
|
|
341
|
+
source: string,
|
|
342
|
+
ctx: ExtensionCommandContext,
|
|
343
|
+
pi: ExtensionAPI
|
|
344
|
+
): Promise<void> {
|
|
345
|
+
await removePackageInternal(source, ctx, pi);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function removePackageWithOutcome(
|
|
349
|
+
source: string,
|
|
350
|
+
ctx: ExtensionCommandContext,
|
|
351
|
+
pi: ExtensionAPI
|
|
352
|
+
): Promise<PackageMutationOutcome> {
|
|
353
|
+
return removePackageInternal(source, ctx, pi);
|
|
288
354
|
}
|
|
289
355
|
|
|
290
356
|
export async function promptRemove(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
@@ -344,12 +410,14 @@ export async function showPackageActions(
|
|
|
344
410
|
: "back";
|
|
345
411
|
|
|
346
412
|
switch (action) {
|
|
347
|
-
case "remove":
|
|
348
|
-
await
|
|
349
|
-
return
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
413
|
+
case "remove": {
|
|
414
|
+
const outcome = await removePackageWithOutcome(pkg.source, ctx, pi);
|
|
415
|
+
return outcome.reloaded || outcome.restartRequested;
|
|
416
|
+
}
|
|
417
|
+
case "update": {
|
|
418
|
+
const outcome = await updatePackageWithOutcome(pkg.source, ctx, pi);
|
|
419
|
+
return outcome.reloaded || outcome.restartRequested;
|
|
420
|
+
}
|
|
353
421
|
case "details": {
|
|
354
422
|
const sizeStr = pkg.size !== undefined ? `\nSize: ${formatBytes(pkg.size)}` : "";
|
|
355
423
|
notify(
|
package/src/ui/unified.ts
CHANGED
|
@@ -30,9 +30,9 @@ import { getInstalledPackages } from "../packages/discovery.js";
|
|
|
30
30
|
import { discoverPackageExtensions, setPackageExtensionState } from "../packages/extensions.js";
|
|
31
31
|
import {
|
|
32
32
|
showPackageActions,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
updatePackageWithOutcome,
|
|
34
|
+
removePackageWithOutcome,
|
|
35
|
+
updatePackagesWithOutcome,
|
|
36
36
|
} from "../packages/management.js";
|
|
37
37
|
import { showRemote } from "./remote.js";
|
|
38
38
|
import { showHelp } from "./help.js";
|
|
@@ -48,6 +48,7 @@ import { logExtensionToggle } from "../utils/history.js";
|
|
|
48
48
|
import { getKnownUpdates, promptAutoUpdateWizard } from "../utils/auto-update.js";
|
|
49
49
|
import { updateExtmgrStatus } from "../utils/status.js";
|
|
50
50
|
import { parseChoiceByLabel } from "../utils/command.js";
|
|
51
|
+
import { getPackageSourceKind } from "../utils/package-source.js";
|
|
51
52
|
import { UI } from "../constants.js";
|
|
52
53
|
|
|
53
54
|
// Type guard for SettingsList with selectedIndex
|
|
@@ -268,7 +269,7 @@ async function showInteractiveOnce(
|
|
|
268
269
|
return await handleUnifiedAction(result, items, staged, byId, ctx, pi);
|
|
269
270
|
}
|
|
270
271
|
|
|
271
|
-
function buildUnifiedItems(
|
|
272
|
+
export function buildUnifiedItems(
|
|
272
273
|
localEntries: Awaited<ReturnType<typeof discoverExtensions>>,
|
|
273
274
|
installedPackages: InstalledPackage[],
|
|
274
275
|
packageExtensions: PackageExtensionEntry[],
|
|
@@ -277,7 +278,22 @@ function buildUnifiedItems(
|
|
|
277
278
|
const items: UnifiedItem[] = [];
|
|
278
279
|
const localPaths = new Set<string>();
|
|
279
280
|
|
|
280
|
-
const
|
|
281
|
+
const packageExtensionGroups = new Map<string, PackageExtensionEntry[]>();
|
|
282
|
+
for (const entry of packageExtensions) {
|
|
283
|
+
const key = `${entry.packageScope}:${entry.packageSource.toLowerCase()}`;
|
|
284
|
+
const group = packageExtensionGroups.get(key) ?? [];
|
|
285
|
+
group.push(entry);
|
|
286
|
+
packageExtensionGroups.set(key, group);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const visiblePackageExtensions = packageExtensions.filter((entry) => {
|
|
290
|
+
const key = `${entry.packageScope}:${entry.packageSource.toLowerCase()}`;
|
|
291
|
+
const group = packageExtensionGroups.get(key) ?? [];
|
|
292
|
+
|
|
293
|
+
// Avoid duplicate-looking rows for packages that expose a single enabled extension entrypoint.
|
|
294
|
+
// Keep extension rows when there are multiple entrypoints, or when an entry is disabled so it can be re-enabled.
|
|
295
|
+
return group.length > 1 || entry.state === "disabled";
|
|
296
|
+
});
|
|
281
297
|
|
|
282
298
|
// Add local extensions
|
|
283
299
|
for (const entry of localEntries) {
|
|
@@ -295,8 +311,7 @@ function buildUnifiedItems(
|
|
|
295
311
|
});
|
|
296
312
|
}
|
|
297
313
|
|
|
298
|
-
for (const entry of
|
|
299
|
-
packageSourcesWithExtensions.add(entry.packageSource.toLowerCase());
|
|
314
|
+
for (const entry of visiblePackageExtensions) {
|
|
300
315
|
items.push({
|
|
301
316
|
type: "package-extension",
|
|
302
317
|
id: entry.id,
|
|
@@ -314,8 +329,6 @@ function buildUnifiedItems(
|
|
|
314
329
|
const pkgSourceLower = pkg.source.toLowerCase();
|
|
315
330
|
const pkgResolvedLower = pkg.resolvedPath?.toLowerCase() ?? "";
|
|
316
331
|
|
|
317
|
-
if (packageSourcesWithExtensions.has(pkgSourceLower)) continue;
|
|
318
|
-
|
|
319
332
|
let isDuplicate = false;
|
|
320
333
|
for (const localPath of localPaths) {
|
|
321
334
|
if (pkgSourceLower === localPath || pkgResolvedLower === localPath) {
|
|
@@ -355,7 +368,7 @@ function buildUnifiedItems(
|
|
|
355
368
|
items.sort((a, b) => {
|
|
356
369
|
const rank = (type: UnifiedItem["type"]): number => {
|
|
357
370
|
if (type === "local") return 0;
|
|
358
|
-
if (type === "package
|
|
371
|
+
if (type === "package") return 1;
|
|
359
372
|
return 2;
|
|
360
373
|
};
|
|
361
374
|
|
|
@@ -427,7 +440,7 @@ function formatUnifiedItemLabel(
|
|
|
427
440
|
theme: Theme,
|
|
428
441
|
changed = false
|
|
429
442
|
): string {
|
|
430
|
-
if (item.type === "local"
|
|
443
|
+
if (item.type === "local") {
|
|
431
444
|
const statusIcon = getStatusIcon(theme, state === "enabled" ? "enabled" : "disabled");
|
|
432
445
|
const scopeIcon = getScopeIcon(theme, item.scope as "global" | "project");
|
|
433
446
|
const changeMarker = getChangeMarker(theme, changed);
|
|
@@ -436,7 +449,26 @@ function formatUnifiedItemLabel(
|
|
|
436
449
|
return `${statusIcon} [${scopeIcon}] ${name} - ${summary}${changeMarker}`;
|
|
437
450
|
}
|
|
438
451
|
|
|
439
|
-
|
|
452
|
+
if (item.type === "package-extension") {
|
|
453
|
+
const statusIcon = getStatusIcon(theme, state === "enabled" ? "enabled" : "disabled");
|
|
454
|
+
const scopeIcon = getScopeIcon(theme, item.scope as "global" | "project");
|
|
455
|
+
const sourceKind = getPackageSourceKind(item.packageSource ?? "");
|
|
456
|
+
const pkgIcon = getPackageIcon(
|
|
457
|
+
theme,
|
|
458
|
+
sourceKind === "npm" || sourceKind === "git" || sourceKind === "local" ? sourceKind : "local"
|
|
459
|
+
);
|
|
460
|
+
const sourceLabel = sourceKind === "unknown" ? "package" : `${sourceKind} package`;
|
|
461
|
+
const changeMarker = getChangeMarker(theme, changed);
|
|
462
|
+
const name = theme.bold(item.displayName);
|
|
463
|
+
const summary = theme.fg("dim", `${item.summary} • ${sourceLabel}`);
|
|
464
|
+
return `${statusIcon} ${pkgIcon} [${scopeIcon}] ${name} - ${summary}${changeMarker}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const sourceKind = getPackageSourceKind(item.source ?? "");
|
|
468
|
+
const pkgIcon = getPackageIcon(
|
|
469
|
+
theme,
|
|
470
|
+
sourceKind === "npm" || sourceKind === "git" || sourceKind === "local" ? sourceKind : "local"
|
|
471
|
+
);
|
|
440
472
|
const scopeIcon = getScopeIcon(theme, item.scope as "global" | "project");
|
|
441
473
|
const name = theme.bold(item.displayName);
|
|
442
474
|
const version = item.version ? theme.fg("dim", `@${item.version}`) : "";
|
|
@@ -449,9 +481,9 @@ function formatUnifiedItemLabel(
|
|
|
449
481
|
// Reserved space: icon (2) + scope (3) + name (~25) + version (~10) + separator (3) = ~43 chars
|
|
450
482
|
if (item.description) {
|
|
451
483
|
infoParts.push(dynamicTruncate(item.description, 43));
|
|
452
|
-
} else if (
|
|
484
|
+
} else if (sourceKind === "npm") {
|
|
453
485
|
infoParts.push("npm");
|
|
454
|
-
} else if (
|
|
486
|
+
} else if (sourceKind === "git") {
|
|
455
487
|
infoParts.push("git");
|
|
456
488
|
} else {
|
|
457
489
|
infoParts.push("local");
|
|
@@ -616,9 +648,10 @@ async function navigateWithPendingGuard(
|
|
|
616
648
|
case "browse":
|
|
617
649
|
await showRemote("", ctx, pi);
|
|
618
650
|
return "done";
|
|
619
|
-
case "update-all":
|
|
620
|
-
await
|
|
621
|
-
return "done";
|
|
651
|
+
case "update-all": {
|
|
652
|
+
const outcome = await updatePackagesWithOutcome(ctx, pi);
|
|
653
|
+
return outcome.reloaded || outcome.restartRequested ? "exit" : "done";
|
|
654
|
+
}
|
|
622
655
|
case "auto-update":
|
|
623
656
|
await promptAutoUpdateWizard(pi, ctx, (packages) => {
|
|
624
657
|
ctx.ui.notify(
|
|
@@ -784,12 +817,14 @@ async function handleUnifiedAction(
|
|
|
784
817
|
};
|
|
785
818
|
|
|
786
819
|
switch (result.action) {
|
|
787
|
-
case "update":
|
|
788
|
-
await
|
|
789
|
-
return
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
820
|
+
case "update": {
|
|
821
|
+
const outcome = await updatePackageWithOutcome(pkg.source, ctx, pi);
|
|
822
|
+
return outcome.reloaded || outcome.restartRequested;
|
|
823
|
+
}
|
|
824
|
+
case "remove": {
|
|
825
|
+
const outcome = await removePackageWithOutcome(pkg.source, ctx, pi);
|
|
826
|
+
return outcome.reloaded || outcome.restartRequested;
|
|
827
|
+
}
|
|
793
828
|
case "details": {
|
|
794
829
|
const sizeStr = pkg.size !== undefined ? `\nSize: ${formatBytes(pkg.size)}` : "";
|
|
795
830
|
ctx.ui.notify(
|
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
* Package source parsing helpers shared across discovery/management flows.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
export type PackageSourceKind = "npm" | "git" | "local" | "unknown";
|
|
6
|
+
|
|
7
|
+
function sanitizeSource(source: string): string {
|
|
8
|
+
return source
|
|
9
|
+
.trim()
|
|
10
|
+
.replace(/\s+\((filtered|pinned)\)$/i, "")
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getPackageSourceKind(source: string): PackageSourceKind {
|
|
15
|
+
const normalized = sanitizeSource(source);
|
|
16
|
+
|
|
17
|
+
if (normalized.startsWith("npm:")) return "npm";
|
|
18
|
+
|
|
19
|
+
if (
|
|
20
|
+
normalized.startsWith("git:") ||
|
|
21
|
+
normalized.startsWith("http://") ||
|
|
22
|
+
normalized.startsWith("https://") ||
|
|
23
|
+
normalized.startsWith("ssh://") ||
|
|
24
|
+
/^git@[^\s:]+:.+/.test(normalized)
|
|
25
|
+
) {
|
|
26
|
+
return "git";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
normalized.startsWith("/") ||
|
|
31
|
+
normalized.startsWith("./") ||
|
|
32
|
+
normalized.startsWith("../") ||
|
|
33
|
+
normalized.startsWith("~/") ||
|
|
34
|
+
/^[a-zA-Z]:[\\/]/.test(normalized) ||
|
|
35
|
+
normalized.startsWith("\\\\")
|
|
36
|
+
) {
|
|
37
|
+
return "local";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return "unknown";
|
|
41
|
+
}
|
|
42
|
+
|
|
5
43
|
export function splitGitRepoAndRef(gitSpec: string): { repo: string; ref?: string | undefined } {
|
|
6
44
|
const lastAt = gitSpec.lastIndexOf("@");
|
|
7
45
|
if (lastAt <= 0) {
|