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
package/src/ui/unified.ts
CHANGED
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
* Unified extension manager UI
|
|
3
3
|
* Displays local extensions and installed packages in one view
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import {
|
|
6
|
+
DynamicBorder,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionCommandContext,
|
|
9
|
+
getSettingsListTheme,
|
|
10
|
+
type Theme,
|
|
11
|
+
} from "@mariozechner/pi-coding-agent";
|
|
8
12
|
import {
|
|
9
13
|
Container,
|
|
14
|
+
Key,
|
|
15
|
+
matchesKey,
|
|
16
|
+
type SettingItem,
|
|
10
17
|
SettingsList,
|
|
11
|
-
Text,
|
|
12
18
|
Spacer,
|
|
13
|
-
|
|
14
|
-
matchesKey,
|
|
15
|
-
Key,
|
|
19
|
+
Text,
|
|
16
20
|
} from "@mariozechner/pi-tui";
|
|
17
|
-
import
|
|
21
|
+
import { UI } from "../constants.js";
|
|
18
22
|
import {
|
|
19
23
|
discoverExtensions,
|
|
20
24
|
removeLocalExtension,
|
|
@@ -22,32 +26,40 @@ import {
|
|
|
22
26
|
} from "../extensions/discovery.js";
|
|
23
27
|
import { getInstalledPackages } from "../packages/discovery.js";
|
|
24
28
|
import {
|
|
25
|
-
updatePackageWithOutcome,
|
|
26
29
|
removePackageWithOutcome,
|
|
27
|
-
updatePackagesWithOutcome,
|
|
28
30
|
showInstalledPackagesList,
|
|
31
|
+
updatePackagesWithOutcome,
|
|
32
|
+
updatePackageWithOutcome,
|
|
29
33
|
} from "../packages/management.js";
|
|
30
|
-
import { showRemote } from "./remote.js";
|
|
31
|
-
import { showHelp } from "./help.js";
|
|
32
|
-
import { formatEntry as formatExtEntry, dynamicTruncate, formatBytes } from "../utils/format.js";
|
|
33
34
|
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} from "
|
|
40
|
-
import { buildFooterState, buildFooterShortcuts, getPendingToggleChangeCount } from "./footer.js";
|
|
41
|
-
import { logExtensionDelete, logExtensionToggle } from "../utils/history.js";
|
|
35
|
+
type InstalledPackage,
|
|
36
|
+
type LocalUnifiedItem,
|
|
37
|
+
type State,
|
|
38
|
+
type UnifiedAction,
|
|
39
|
+
type UnifiedItem,
|
|
40
|
+
} from "../types/index.js";
|
|
42
41
|
import { getKnownUpdates, promptAutoUpdateWizard } from "../utils/auto-update.js";
|
|
43
|
-
import { updateExtmgrStatus } from "../utils/status.js";
|
|
44
42
|
import { parseChoiceByLabel } from "../utils/command.js";
|
|
43
|
+
import { dynamicTruncate, formatBytes, formatEntry as formatExtEntry } from "../utils/format.js";
|
|
44
|
+
import { logExtensionDelete, logExtensionToggle } from "../utils/history.js";
|
|
45
|
+
import { hasCustomUI, runCustomUI } from "../utils/mode.js";
|
|
45
46
|
import { notify } from "../utils/notify.js";
|
|
46
47
|
import { getPackageSourceKind, normalizePackageIdentity } from "../utils/package-source.js";
|
|
47
|
-
import { hasCustomUI, runCustomUI } from "../utils/mode.js";
|
|
48
48
|
import { getSettingsListSelectedIndex } from "../utils/settings-list.js";
|
|
49
|
-
import {
|
|
49
|
+
import { updateExtmgrStatus } from "../utils/status.js";
|
|
50
|
+
import { confirmReload } from "../utils/ui-helpers.js";
|
|
51
|
+
import { runTaskWithLoader } from "./async-task.js";
|
|
52
|
+
import { buildFooterShortcuts, buildFooterState, getPendingToggleChangeCount } from "./footer.js";
|
|
53
|
+
import { showHelp } from "./help.js";
|
|
50
54
|
import { configurePackageExtensions } from "./package-config.js";
|
|
55
|
+
import { showRemote } from "./remote.js";
|
|
56
|
+
import {
|
|
57
|
+
formatSize,
|
|
58
|
+
getChangeMarker,
|
|
59
|
+
getPackageIcon,
|
|
60
|
+
getScopeIcon,
|
|
61
|
+
getStatusIcon,
|
|
62
|
+
} from "./theme.js";
|
|
51
63
|
|
|
52
64
|
async function showInteractiveFallback(
|
|
53
65
|
ctx: ExtensionCommandContext,
|
|
@@ -82,11 +94,46 @@ async function showInteractiveOnce(
|
|
|
82
94
|
ctx: ExtensionCommandContext,
|
|
83
95
|
pi: ExtensionAPI
|
|
84
96
|
): Promise<boolean> {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
const initialData = await runTaskWithLoader(
|
|
98
|
+
ctx,
|
|
99
|
+
{
|
|
100
|
+
title: "Extensions Manager",
|
|
101
|
+
message: "Loading extensions and packages...",
|
|
102
|
+
},
|
|
103
|
+
async ({ signal, setMessage }) => {
|
|
104
|
+
const localEntriesPromise = discoverExtensions(ctx.cwd);
|
|
105
|
+
const installedPackagesPromise = getInstalledPackages(
|
|
106
|
+
ctx,
|
|
107
|
+
pi,
|
|
108
|
+
(current, total) => {
|
|
109
|
+
if (total <= 0) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
setMessage(`Loading package metadata... ${current}/${total}`);
|
|
113
|
+
},
|
|
114
|
+
signal
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const [localEntries, installedPackages] = await Promise.all([
|
|
118
|
+
localEntriesPromise,
|
|
119
|
+
installedPackagesPromise,
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
return { localEntries, installedPackages };
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (!initialData) {
|
|
127
|
+
notify(
|
|
128
|
+
ctx,
|
|
129
|
+
"The unified extensions manager requires the full interactive TUI. Showing read-only local and installed package lists instead.",
|
|
130
|
+
"warning"
|
|
131
|
+
);
|
|
132
|
+
await showInteractiveFallback(ctx, pi);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const { localEntries, installedPackages } = initialData;
|
|
90
137
|
|
|
91
138
|
// Build unified items list.
|
|
92
139
|
const knownUpdates = getKnownUpdates(ctx);
|
|
@@ -153,8 +200,8 @@ async function showInteractiveOnce(
|
|
|
153
200
|
if (!item) continue;
|
|
154
201
|
|
|
155
202
|
if (item.type === "local") {
|
|
156
|
-
const currentState = staged.get(item.id) ?? item.state
|
|
157
|
-
const changed =
|
|
203
|
+
const currentState = staged.get(item.id) ?? item.state;
|
|
204
|
+
const changed = currentState !== item.originalState;
|
|
158
205
|
settingsItem.label = formatUnifiedItemLabel(item, currentState, theme, changed);
|
|
159
206
|
} else {
|
|
160
207
|
settingsItem.label = formatUnifiedItemLabel(item, "enabled", theme, false);
|
|
@@ -172,7 +219,11 @@ async function showInteractiveOnce(
|
|
|
172
219
|
if (!item || item.type !== "local") return;
|
|
173
220
|
|
|
174
221
|
const state = newValue as State;
|
|
175
|
-
|
|
222
|
+
if (state === item.originalState) {
|
|
223
|
+
staged.delete(id);
|
|
224
|
+
} else {
|
|
225
|
+
staged.set(id, state);
|
|
226
|
+
}
|
|
176
227
|
|
|
177
228
|
const settingsItem = settingsItems.find((x) => x.id === id);
|
|
178
229
|
if (settingsItem) {
|
|
@@ -343,16 +394,7 @@ export function buildUnifiedItems(
|
|
|
343
394
|
isDuplicate = true;
|
|
344
395
|
break;
|
|
345
396
|
}
|
|
346
|
-
if (
|
|
347
|
-
pkgResolvedNormalized &&
|
|
348
|
-
(localPath.startsWith(`${pkgResolvedNormalized}/`) ||
|
|
349
|
-
pkgResolvedNormalized.startsWith(localPath))
|
|
350
|
-
) {
|
|
351
|
-
isDuplicate = true;
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
const localDir = localPath.split("/").slice(0, -1).join("/");
|
|
355
|
-
if (pkgResolvedNormalized && pkgResolvedNormalized === localDir) {
|
|
397
|
+
if (pkgResolvedNormalized && localPath.startsWith(`${pkgResolvedNormalized}/`)) {
|
|
356
398
|
isDuplicate = true;
|
|
357
399
|
break;
|
|
358
400
|
}
|
|
@@ -363,7 +405,6 @@ export function buildUnifiedItems(
|
|
|
363
405
|
type: "package",
|
|
364
406
|
id: `pkg:${pkg.source}`,
|
|
365
407
|
displayName: pkg.name,
|
|
366
|
-
summary: pkg.description || `${pkg.source} (${pkg.scope})`,
|
|
367
408
|
scope: pkg.scope,
|
|
368
409
|
source: pkg.source,
|
|
369
410
|
version: pkg.version,
|
|
@@ -395,8 +436,8 @@ function buildSettingsItems(
|
|
|
395
436
|
): SettingItem[] {
|
|
396
437
|
return items.map((item) => {
|
|
397
438
|
if (item.type === "local") {
|
|
398
|
-
const currentState = staged.get(item.id) ?? item.state
|
|
399
|
-
const changed =
|
|
439
|
+
const currentState = staged.get(item.id) ?? item.state;
|
|
440
|
+
const changed = currentState !== item.originalState;
|
|
400
441
|
return {
|
|
401
442
|
id: item.id,
|
|
402
443
|
label: formatUnifiedItemLabel(item, currentState, theme, changed),
|
|
@@ -421,7 +462,7 @@ function formatUnifiedItemLabel(
|
|
|
421
462
|
changed = false
|
|
422
463
|
): string {
|
|
423
464
|
if (item.type === "local") {
|
|
424
|
-
const statusIcon = getStatusIcon(theme, state
|
|
465
|
+
const statusIcon = getStatusIcon(theme, state);
|
|
425
466
|
const scopeIcon = getScopeIcon(theme, item.scope);
|
|
426
467
|
const changeMarker = getChangeMarker(theme, changed);
|
|
427
468
|
const name = theme.bold(item.displayName);
|
|
@@ -429,7 +470,7 @@ function formatUnifiedItemLabel(
|
|
|
429
470
|
return `${statusIcon} [${scopeIcon}] ${name} - ${summary}${changeMarker}`;
|
|
430
471
|
}
|
|
431
472
|
|
|
432
|
-
const sourceKind = getPackageSourceKind(item.source
|
|
473
|
+
const sourceKind = getPackageSourceKind(item.source);
|
|
433
474
|
const pkgIcon = getPackageIcon(
|
|
434
475
|
theme,
|
|
435
476
|
sourceKind === "npm" || sourceKind === "git" || sourceKind === "local" ? sourceKind : "local"
|
|
@@ -463,8 +504,8 @@ function formatUnifiedItemLabel(
|
|
|
463
504
|
return `${pkgIcon} [${scopeIcon}] ${name}${version}${updateBadge} - ${summary}`;
|
|
464
505
|
}
|
|
465
506
|
|
|
466
|
-
function getToggleItemsForApply(items: UnifiedItem[]):
|
|
467
|
-
return items.filter((item) => item.type === "local");
|
|
507
|
+
function getToggleItemsForApply(items: UnifiedItem[]): LocalUnifiedItem[] {
|
|
508
|
+
return items.filter((item): item is LocalUnifiedItem => item.type === "local");
|
|
468
509
|
}
|
|
469
510
|
|
|
470
511
|
async function applyToggleChangesFromManager(
|
|
@@ -473,7 +514,7 @@ async function applyToggleChangesFromManager(
|
|
|
473
514
|
ctx: ExtensionCommandContext,
|
|
474
515
|
pi: ExtensionAPI,
|
|
475
516
|
options?: { promptReload?: boolean }
|
|
476
|
-
): Promise<{ changed: number; reloaded: boolean }> {
|
|
517
|
+
): Promise<{ changed: number; reloaded: boolean; hasErrors: boolean }> {
|
|
477
518
|
const toggleItems = getToggleItemsForApply(items);
|
|
478
519
|
const apply = await applyStagedChanges(toggleItems, staged, pi);
|
|
479
520
|
|
|
@@ -492,24 +533,14 @@ async function applyToggleChangesFromManager(
|
|
|
492
533
|
const shouldPromptReload = options?.promptReload ?? true;
|
|
493
534
|
|
|
494
535
|
if (shouldPromptReload) {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
"Local extensions changed. Reload pi now?"
|
|
498
|
-
);
|
|
499
|
-
|
|
500
|
-
if (shouldReload) {
|
|
501
|
-
await (ctx as ExtensionCommandContext & { reload: () => Promise<void> }).reload();
|
|
502
|
-
return { changed: apply.changed, reloaded: true };
|
|
503
|
-
}
|
|
504
|
-
} else {
|
|
505
|
-
ctx.ui.notify(
|
|
506
|
-
"Changes saved. Reload pi later to fully apply extension state updates.",
|
|
507
|
-
"info"
|
|
508
|
-
);
|
|
536
|
+
const reloaded = await confirmReload(ctx, "Local extensions changed.");
|
|
537
|
+
return { changed: apply.changed, reloaded, hasErrors: apply.errors.length > 0 };
|
|
509
538
|
}
|
|
539
|
+
|
|
540
|
+
ctx.ui.notify("Changes saved. Reload pi later to fully apply extension state updates.", "info");
|
|
510
541
|
}
|
|
511
542
|
|
|
512
|
-
return { changed: apply.changed, reloaded: false };
|
|
543
|
+
return { changed: apply.changed, reloaded: false, hasErrors: apply.errors.length > 0 };
|
|
513
544
|
}
|
|
514
545
|
|
|
515
546
|
async function resolvePendingChangesBeforeLeave(
|
|
@@ -519,7 +550,7 @@ async function resolvePendingChangesBeforeLeave(
|
|
|
519
550
|
ctx: ExtensionCommandContext,
|
|
520
551
|
pi: ExtensionAPI,
|
|
521
552
|
destinationLabel: string
|
|
522
|
-
): Promise<"continue" | "stay"
|
|
553
|
+
): Promise<"continue" | "stay"> {
|
|
523
554
|
const pendingCount = getPendingToggleChangeCount(staged, byId);
|
|
524
555
|
if (pendingCount === 0) return "continue";
|
|
525
556
|
|
|
@@ -537,10 +568,10 @@ async function resolvePendingChangesBeforeLeave(
|
|
|
537
568
|
return "continue";
|
|
538
569
|
}
|
|
539
570
|
|
|
540
|
-
const
|
|
571
|
+
const apply = await applyToggleChangesFromManager(items, staged, ctx, pi, {
|
|
541
572
|
promptReload: false,
|
|
542
573
|
});
|
|
543
|
-
return
|
|
574
|
+
return apply.changed === 0 && apply.hasErrors ? "stay" : "continue";
|
|
544
575
|
}
|
|
545
576
|
|
|
546
577
|
const PALETTE_OPTIONS = {
|
|
@@ -611,7 +642,6 @@ async function navigateWithPendingGuard(
|
|
|
611
642
|
QUICK_DESTINATION_LABELS[destination]
|
|
612
643
|
);
|
|
613
644
|
if (pending === "stay") return "stay";
|
|
614
|
-
if (pending === "exit") return "exit";
|
|
615
645
|
|
|
616
646
|
switch (destination) {
|
|
617
647
|
case "install":
|
|
@@ -666,6 +696,7 @@ async function handleUnifiedAction(
|
|
|
666
696
|
if (choice === "Save and exit") {
|
|
667
697
|
const apply = await applyToggleChangesFromManager(items, staged, ctx, pi);
|
|
668
698
|
if (apply.reloaded) return true;
|
|
699
|
+
if (apply.changed === 0 && apply.hasErrors) return false;
|
|
669
700
|
}
|
|
670
701
|
}
|
|
671
702
|
|
|
@@ -675,7 +706,6 @@ async function handleUnifiedAction(
|
|
|
675
706
|
if (result.type === "remote") {
|
|
676
707
|
const pending = await resolvePendingChangesBeforeLeave(items, staged, byId, ctx, pi, "Remote");
|
|
677
708
|
if (pending === "stay") return false;
|
|
678
|
-
if (pending === "exit") return true;
|
|
679
709
|
|
|
680
710
|
await showRemote("", ctx, pi);
|
|
681
711
|
return false;
|
|
@@ -684,7 +714,6 @@ async function handleUnifiedAction(
|
|
|
684
714
|
if (result.type === "help") {
|
|
685
715
|
const pending = await resolvePendingChangesBeforeLeave(items, staged, byId, ctx, pi, "Help");
|
|
686
716
|
if (pending === "stay") return false;
|
|
687
|
-
if (pending === "exit") return true;
|
|
688
717
|
|
|
689
718
|
showHelp(ctx);
|
|
690
719
|
return false;
|
|
@@ -741,7 +770,6 @@ async function handleUnifiedAction(
|
|
|
741
770
|
pendingDestination
|
|
742
771
|
);
|
|
743
772
|
if (pending === "stay") return false;
|
|
744
|
-
if (pending === "exit") return true;
|
|
745
773
|
|
|
746
774
|
if (item.type === "local") {
|
|
747
775
|
if (result.action !== "remove") return false;
|
|
@@ -753,7 +781,7 @@ async function handleUnifiedAction(
|
|
|
753
781
|
if (!confirmed) return false;
|
|
754
782
|
|
|
755
783
|
const removal = await removeLocalExtension(
|
|
756
|
-
{ activePath: item.activePath
|
|
784
|
+
{ activePath: item.activePath, disabledPath: item.disabledPath },
|
|
757
785
|
ctx.cwd
|
|
758
786
|
);
|
|
759
787
|
if (!removal.ok) {
|
|
@@ -768,20 +796,11 @@ async function handleUnifiedAction(
|
|
|
768
796
|
"info"
|
|
769
797
|
);
|
|
770
798
|
|
|
771
|
-
|
|
772
|
-
"Reload Recommended",
|
|
773
|
-
"Extension removed. Reload pi now?"
|
|
774
|
-
);
|
|
775
|
-
if (shouldReload) {
|
|
776
|
-
await (ctx as ExtensionCommandContext & { reload: () => Promise<void> }).reload();
|
|
777
|
-
return true;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
return false;
|
|
799
|
+
return await confirmReload(ctx, "Extension removed.");
|
|
781
800
|
}
|
|
782
801
|
|
|
783
802
|
const pkg: InstalledPackage = {
|
|
784
|
-
source: item.source
|
|
803
|
+
source: item.source,
|
|
785
804
|
name: item.displayName,
|
|
786
805
|
...(item.version ? { version: item.version } : {}),
|
|
787
806
|
scope: item.scope,
|
|
@@ -827,7 +846,7 @@ async function handleUnifiedAction(
|
|
|
827
846
|
}
|
|
828
847
|
|
|
829
848
|
async function applyStagedChanges(
|
|
830
|
-
items:
|
|
849
|
+
items: LocalUnifiedItem[],
|
|
831
850
|
staged: Map<string, State>,
|
|
832
851
|
pi: ExtensionAPI
|
|
833
852
|
) {
|
|
@@ -835,10 +854,6 @@ async function applyStagedChanges(
|
|
|
835
854
|
const errors: string[] = [];
|
|
836
855
|
|
|
837
856
|
for (const item of items) {
|
|
838
|
-
if (item.type !== "local" || !item.originalState || !item.activePath || !item.disabledPath) {
|
|
839
|
-
continue;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
857
|
const target = staged.get(item.id) ?? item.originalState;
|
|
843
858
|
if (target === item.originalState) continue;
|
|
844
859
|
|
package/src/utils/auto-update.ts
CHANGED
|
@@ -1,37 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-update logic and background checker
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
5
|
-
ExtensionAPI,
|
|
6
|
-
ExtensionCommandContext,
|
|
7
|
-
ExtensionContext,
|
|
4
|
+
import {
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionCommandContext,
|
|
7
|
+
type ExtensionContext,
|
|
8
8
|
} from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
9
|
+
import { getPackageCatalog } from "../packages/catalog.js";
|
|
10
|
+
import { logAutoUpdateConfig } from "./history.js";
|
|
11
11
|
import { notify } from "./notify.js";
|
|
12
|
+
import { normalizePackageIdentity } from "./package-source.js";
|
|
12
13
|
import {
|
|
14
|
+
type AutoUpdateConfig,
|
|
15
|
+
calculateNextCheck,
|
|
13
16
|
getAutoUpdateConfig,
|
|
14
|
-
saveAutoUpdateConfig,
|
|
15
17
|
getScheduleInterval,
|
|
16
|
-
calculateNextCheck,
|
|
17
18
|
parseDuration,
|
|
18
|
-
|
|
19
|
+
saveAutoUpdateConfig,
|
|
19
20
|
} from "./settings.js";
|
|
20
|
-
import { parseNpmSource } from "./format.js";
|
|
21
|
-
import { execNpm } from "./npm-exec.js";
|
|
22
|
-
import { normalizePackageIdentity } from "./package-source.js";
|
|
23
|
-
import { logAutoUpdateConfig } from "./history.js";
|
|
24
|
-
import { TIMEOUTS } from "../constants.js";
|
|
25
21
|
|
|
26
|
-
import { startTimer, stopTimer
|
|
22
|
+
import { isTimerRunning, startTimer, stopTimer } from "./timer.js";
|
|
27
23
|
|
|
28
24
|
// Context provider for safe session handling
|
|
29
25
|
export type ContextProvider = () => (ExtensionCommandContext | ExtensionContext) | undefined;
|
|
30
26
|
|
|
31
|
-
function getUpdateIdentity(pkg: InstalledPackage): string {
|
|
32
|
-
return normalizePackageIdentity(pkg.source);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
27
|
/**
|
|
36
28
|
* Start auto-update background checker
|
|
37
29
|
* Uses a context provider to avoid stale context issues when sessions switch
|
|
@@ -67,7 +59,10 @@ export function startAutoUpdateTimer(
|
|
|
67
59
|
stopAutoUpdateTimer();
|
|
68
60
|
return;
|
|
69
61
|
}
|
|
70
|
-
|
|
62
|
+
|
|
63
|
+
void checkForUpdates(pi, checkCtx, onUpdateAvailable).catch((error) => {
|
|
64
|
+
console.warn("[extmgr] Auto-update check failed:", error);
|
|
65
|
+
});
|
|
71
66
|
},
|
|
72
67
|
{ initialDelayMs }
|
|
73
68
|
);
|
|
@@ -96,19 +91,9 @@ export async function checkForUpdates(
|
|
|
96
91
|
ctx: ExtensionCommandContext | ExtensionContext,
|
|
97
92
|
onUpdateAvailable?: (packages: string[]) => void
|
|
98
93
|
): Promise<string[]> {
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const updatesAvailable: string[] = [];
|
|
103
|
-
const updatedPackageNames: string[] = [];
|
|
104
|
-
|
|
105
|
-
for (const pkg of npmPackages) {
|
|
106
|
-
const hasUpdate = await checkPackageUpdate(pkg, ctx, pi);
|
|
107
|
-
if (hasUpdate) {
|
|
108
|
-
updatesAvailable.push(getUpdateIdentity(pkg));
|
|
109
|
-
updatedPackageNames.push(pkg.name);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
94
|
+
const updates = await getPackageCatalog(ctx.cwd).checkForAvailableUpdates();
|
|
95
|
+
const updatesAvailable = updates.map((update) => normalizePackageIdentity(update.source));
|
|
96
|
+
const updatedPackageNames = updates.map((update) => update.displayName);
|
|
112
97
|
|
|
113
98
|
const checkedAt = Date.now();
|
|
114
99
|
const config = getAutoUpdateConfig(ctx);
|
|
@@ -126,37 +111,6 @@ export async function checkForUpdates(
|
|
|
126
111
|
return updatedPackageNames;
|
|
127
112
|
}
|
|
128
113
|
|
|
129
|
-
/**
|
|
130
|
-
* Check if a specific package has updates available
|
|
131
|
-
*/
|
|
132
|
-
async function checkPackageUpdate(
|
|
133
|
-
pkg: InstalledPackage,
|
|
134
|
-
ctx: ExtensionCommandContext | ExtensionContext,
|
|
135
|
-
pi: ExtensionAPI
|
|
136
|
-
): Promise<boolean> {
|
|
137
|
-
const parsed = parseNpmSource(pkg.source);
|
|
138
|
-
const pkgName = parsed?.name;
|
|
139
|
-
if (!pkgName) return false;
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
const res = await execNpm(pi, ["view", pkgName, "version", "--json"], ctx, {
|
|
143
|
-
timeout: TIMEOUTS.npmView,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (res.code !== 0) return false;
|
|
147
|
-
|
|
148
|
-
const latestVersion = JSON.parse(res.stdout) as string;
|
|
149
|
-
const currentVersion = pkg.version;
|
|
150
|
-
|
|
151
|
-
if (!currentVersion) return false;
|
|
152
|
-
|
|
153
|
-
// Simple version comparison (assumes semver)
|
|
154
|
-
return latestVersion !== currentVersion;
|
|
155
|
-
} catch {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
114
|
/**
|
|
161
115
|
* Get status text for display
|
|
162
116
|
*/
|
package/src/utils/cache.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistent cache for package metadata to reduce npm API calls
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { join } from "node:path";
|
|
4
|
+
import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
5
|
import { homedir } from "node:os";
|
|
7
|
-
import
|
|
6
|
+
import { join } from "node:path";
|
|
8
7
|
import { CACHE_LIMITS } from "../constants.js";
|
|
8
|
+
import { type InstalledPackage, type NpmPackage } from "../types/index.js";
|
|
9
9
|
import { parseNpmSource } from "./format.js";
|
|
10
10
|
|
|
11
11
|
const CACHE_DIR = process.env.PI_EXTMGR_CACHE_DIR
|
package/src/utils/command.ts
CHANGED
package/src/utils/format.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Formatting utilities
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type ExtensionEntry, type InstalledPackage } from "../types/index.js";
|
|
5
5
|
|
|
6
6
|
export function truncate(text: string, maxLength: number): string {
|
|
7
7
|
if (text.length <= maxLength) return text;
|
|
8
|
-
return text.slice(0, maxLength
|
|
8
|
+
if (maxLength <= 3) return text.slice(0, maxLength);
|
|
9
|
+
return `${text.slice(0, maxLength - 3)}...`;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -54,7 +55,7 @@ export function formatBytes(bytes: number): string {
|
|
|
54
55
|
const k = 1024;
|
|
55
56
|
const sizes = ["B", "KB", "MB", "GB"];
|
|
56
57
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
57
|
-
return `${parseFloat((bytes /
|
|
58
|
+
return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
const GIT_PATTERNS = {
|
package/src/utils/history.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Extension change history tracking using pi.appendEntry()
|
|
3
3
|
* This persists extension management actions to the session
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
import { type Dirent } from "node:fs";
|
|
6
7
|
import { readdir, readFile } from "node:fs/promises";
|
|
7
8
|
import { homedir } from "node:os";
|
|
8
9
|
import { join } from "node:path";
|
|
10
|
+
import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
9
11
|
|
|
10
12
|
export type ChangeAction =
|
|
11
13
|
| "extension_toggle"
|
|
@@ -278,7 +280,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
278
280
|
async function walkSessionFiles(dir: string): Promise<string[]> {
|
|
279
281
|
const result: string[] = [];
|
|
280
282
|
|
|
281
|
-
let entries;
|
|
283
|
+
let entries: Dirent<string>[];
|
|
282
284
|
try {
|
|
283
285
|
entries = await readdir(dir, { withFileTypes: true, encoding: "utf8" });
|
|
284
286
|
} catch {
|
package/src/utils/mode.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* UI capability helpers
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type ExtensionCommandContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
import { notify } from "./notify.js";
|
|
6
6
|
|
|
7
7
|
type AnyContext = ExtensionCommandContext | ExtensionContext;
|
package/src/utils/network.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
export async function fetchWithTimeout(
|
|
1
|
+
export async function fetchWithTimeout(
|
|
2
|
+
url: string,
|
|
3
|
+
timeoutMs: number,
|
|
4
|
+
signal?: AbortSignal
|
|
5
|
+
): Promise<Response> {
|
|
2
6
|
const controller = new AbortController();
|
|
3
7
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
8
|
+
const combinedSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
4
9
|
|
|
5
10
|
try {
|
|
6
|
-
return await fetch(url, { signal:
|
|
11
|
+
return await fetch(url, { signal: combinedSignal });
|
|
7
12
|
} catch (error) {
|
|
8
13
|
if (error instanceof Error && error.name === "AbortError") {
|
|
14
|
+
if (signal?.aborted) {
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
9
17
|
throw new Error(`Request timed out after ${Math.ceil(timeoutMs / 1000)}s`);
|
|
10
18
|
}
|
|
11
19
|
throw error;
|
package/src/utils/notify.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized notification handling for UI and non-UI modes
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type ExtensionCommandContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
export type NotifyLevel = "info" | "warning" | "error";
|
|
7
7
|
|
package/src/utils/npm-exec.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { execPath, platform } from "node:process";
|
|
3
|
-
import type
|
|
3
|
+
import { type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
interface NpmCommandResolutionOptions {
|
|
6
6
|
platform?: NodeJS.Platform;
|
|
@@ -9,6 +9,7 @@ interface NpmCommandResolutionOptions {
|
|
|
9
9
|
|
|
10
10
|
interface NpmExecOptions {
|
|
11
11
|
timeout: number;
|
|
12
|
+
signal?: AbortSignal;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
function getNpmCliPath(nodeExecPath: string, runtimePlatform: NodeJS.Platform): string {
|
|
@@ -43,5 +44,6 @@ export async function execNpm(
|
|
|
43
44
|
return pi.exec(resolved.command, resolved.args, {
|
|
44
45
|
timeout: options.timeout,
|
|
45
46
|
cwd: ctx.cwd,
|
|
47
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
46
48
|
});
|
|
47
49
|
}
|