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/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 type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
6
- import type { Theme } from "@mariozechner/pi-coding-agent";
7
- import { getSettingsListTheme, DynamicBorder } from "@mariozechner/pi-coding-agent";
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
- type SettingItem,
14
- matchesKey,
15
- Key,
19
+ Text,
16
20
  } from "@mariozechner/pi-tui";
17
- import type { UnifiedItem, State, UnifiedAction, InstalledPackage } from "../types/index.js";
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
- getStatusIcon,
35
- getPackageIcon,
36
- getScopeIcon,
37
- getChangeMarker,
38
- formatSize,
39
- } from "./theme.js";
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 { UI } from "../constants.js";
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
- // Load local extensions and installed packages.
86
- const [localEntries, installedPackages] = await Promise.all([
87
- discoverExtensions(ctx.cwd),
88
- getInstalledPackages(ctx, pi),
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 = staged.has(item.id) && currentState !== item.originalState;
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
- staged.set(id, state);
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 = staged.has(item.id) && staged.get(item.id) !== item.originalState;
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 === "enabled" ? "enabled" : "disabled");
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[]): 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 shouldReload = await ctx.ui.confirm(
496
- "Reload Required",
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" | "exit"> {
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 result = await applyToggleChangesFromManager(items, staged, ctx, pi, {
571
+ const apply = await applyToggleChangesFromManager(items, staged, ctx, pi, {
541
572
  promptReload: false,
542
573
  });
543
- return result.reloaded ? "exit" : "continue";
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!, disabledPath: item.disabledPath! },
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
- const shouldReload = await ctx.ui.confirm(
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: UnifiedItem[],
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
 
@@ -1,37 +1,29 @@
1
1
  /**
2
2
  * Auto-update logic and background checker
3
3
  */
4
- import type {
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 type { InstalledPackage } from "../types/index.js";
10
- import { getInstalledPackages } from "../packages/discovery.js";
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
- type AutoUpdateConfig,
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, isTimerRunning } from "./timer.js";
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
- void checkForUpdates(pi, checkCtx, onUpdateAvailable);
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 packages = await getInstalledPackages(ctx, pi);
100
- const npmPackages = packages.filter((p) => p.source.startsWith("npm:"));
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
  */
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Persistent cache for package metadata to reduce npm API calls
3
3
  */
4
- import { readFile, writeFile, mkdir, access, rename, rm } from "node:fs/promises";
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 type { NpmPackage, InstalledPackage } from "../types/index.js";
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
@@ -18,7 +18,7 @@ export function tokenizeArgs(input: string): string[] {
18
18
  };
19
19
 
20
20
  for (let i = 0; i < input.length; i++) {
21
- const char = input[i]!;
21
+ const char = input.charAt(i);
22
22
  const next = input[i + 1];
23
23
 
24
24
  if (inSingleQuote) {
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Formatting utilities
3
3
  */
4
- import type { ExtensionEntry, InstalledPackage } from "../types/index.js";
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 - 3) + "...";
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 / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
58
+ return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
58
59
  }
59
60
 
60
61
  const GIT_PATTERNS = {
@@ -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
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
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 { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
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;
@@ -1,11 +1,19 @@
1
- export async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
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: controller.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;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Centralized notification handling for UI and non-UI modes
3
3
  */
4
- import type { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
4
+ import { type ExtensionCommandContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
5
5
 
6
6
  export type NotifyLevel = "info" | "warning" | "error";
7
7
 
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { execPath, platform } from "node:process";
3
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
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
  }