pi-extmgr 0.1.27 → 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,34 +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 { runTaskWithLoader } from "./async-task.js";
33
- import { formatEntry as formatExtEntry, dynamicTruncate, formatBytes } from "../utils/format.js";
34
34
  import {
35
- getStatusIcon,
36
- getPackageIcon,
37
- getScopeIcon,
38
- getChangeMarker,
39
- formatSize,
40
- } from "./theme.js";
41
- import { buildFooterState, buildFooterShortcuts, getPendingToggleChangeCount } from "./footer.js";
42
- 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";
43
41
  import { getKnownUpdates, promptAutoUpdateWizard } from "../utils/auto-update.js";
44
- import { updateExtmgrStatus } from "../utils/status.js";
45
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";
46
46
  import { notify } from "../utils/notify.js";
47
- import { confirmReload } from "../utils/ui-helpers.js";
48
47
  import { getPackageSourceKind, normalizePackageIdentity } from "../utils/package-source.js";
49
- import { hasCustomUI, runCustomUI } from "../utils/mode.js";
50
48
  import { getSettingsListSelectedIndex } from "../utils/settings-list.js";
51
- 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";
52
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";
53
63
 
54
64
  async function showInteractiveFallback(
55
65
  ctx: ExtensionCommandContext,
@@ -190,8 +200,8 @@ async function showInteractiveOnce(
190
200
  if (!item) continue;
191
201
 
192
202
  if (item.type === "local") {
193
- const currentState = staged.get(item.id) ?? item.state!;
194
- const changed = staged.has(item.id) && currentState !== item.originalState;
203
+ const currentState = staged.get(item.id) ?? item.state;
204
+ const changed = currentState !== item.originalState;
195
205
  settingsItem.label = formatUnifiedItemLabel(item, currentState, theme, changed);
196
206
  } else {
197
207
  settingsItem.label = formatUnifiedItemLabel(item, "enabled", theme, false);
@@ -209,7 +219,11 @@ async function showInteractiveOnce(
209
219
  if (!item || item.type !== "local") return;
210
220
 
211
221
  const state = newValue as State;
212
- staged.set(id, state);
222
+ if (state === item.originalState) {
223
+ staged.delete(id);
224
+ } else {
225
+ staged.set(id, state);
226
+ }
213
227
 
214
228
  const settingsItem = settingsItems.find((x) => x.id === id);
215
229
  if (settingsItem) {
@@ -380,16 +394,7 @@ export function buildUnifiedItems(
380
394
  isDuplicate = true;
381
395
  break;
382
396
  }
383
- if (
384
- pkgResolvedNormalized &&
385
- (localPath.startsWith(`${pkgResolvedNormalized}/`) ||
386
- pkgResolvedNormalized.startsWith(localPath))
387
- ) {
388
- isDuplicate = true;
389
- break;
390
- }
391
- const localDir = localPath.split("/").slice(0, -1).join("/");
392
- if (pkgResolvedNormalized && pkgResolvedNormalized === localDir) {
397
+ if (pkgResolvedNormalized && localPath.startsWith(`${pkgResolvedNormalized}/`)) {
393
398
  isDuplicate = true;
394
399
  break;
395
400
  }
@@ -400,7 +405,6 @@ export function buildUnifiedItems(
400
405
  type: "package",
401
406
  id: `pkg:${pkg.source}`,
402
407
  displayName: pkg.name,
403
- summary: pkg.description || `${pkg.source} (${pkg.scope})`,
404
408
  scope: pkg.scope,
405
409
  source: pkg.source,
406
410
  version: pkg.version,
@@ -432,8 +436,8 @@ function buildSettingsItems(
432
436
  ): SettingItem[] {
433
437
  return items.map((item) => {
434
438
  if (item.type === "local") {
435
- const currentState = staged.get(item.id) ?? item.state!;
436
- 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;
437
441
  return {
438
442
  id: item.id,
439
443
  label: formatUnifiedItemLabel(item, currentState, theme, changed),
@@ -458,7 +462,7 @@ function formatUnifiedItemLabel(
458
462
  changed = false
459
463
  ): string {
460
464
  if (item.type === "local") {
461
- const statusIcon = getStatusIcon(theme, state === "enabled" ? "enabled" : "disabled");
465
+ const statusIcon = getStatusIcon(theme, state);
462
466
  const scopeIcon = getScopeIcon(theme, item.scope);
463
467
  const changeMarker = getChangeMarker(theme, changed);
464
468
  const name = theme.bold(item.displayName);
@@ -466,7 +470,7 @@ function formatUnifiedItemLabel(
466
470
  return `${statusIcon} [${scopeIcon}] ${name} - ${summary}${changeMarker}`;
467
471
  }
468
472
 
469
- const sourceKind = getPackageSourceKind(item.source ?? "");
473
+ const sourceKind = getPackageSourceKind(item.source);
470
474
  const pkgIcon = getPackageIcon(
471
475
  theme,
472
476
  sourceKind === "npm" || sourceKind === "git" || sourceKind === "local" ? sourceKind : "local"
@@ -500,8 +504,8 @@ function formatUnifiedItemLabel(
500
504
  return `${pkgIcon} [${scopeIcon}] ${name}${version}${updateBadge} - ${summary}`;
501
505
  }
502
506
 
503
- function getToggleItemsForApply(items: UnifiedItem[]): UnifiedItem[] {
504
- return items.filter((item) => item.type === "local");
507
+ function getToggleItemsForApply(items: UnifiedItem[]): LocalUnifiedItem[] {
508
+ return items.filter((item): item is LocalUnifiedItem => item.type === "local");
505
509
  }
506
510
 
507
511
  async function applyToggleChangesFromManager(
@@ -510,7 +514,7 @@ async function applyToggleChangesFromManager(
510
514
  ctx: ExtensionCommandContext,
511
515
  pi: ExtensionAPI,
512
516
  options?: { promptReload?: boolean }
513
- ): Promise<{ changed: number; reloaded: boolean }> {
517
+ ): Promise<{ changed: number; reloaded: boolean; hasErrors: boolean }> {
514
518
  const toggleItems = getToggleItemsForApply(items);
515
519
  const apply = await applyStagedChanges(toggleItems, staged, pi);
516
520
 
@@ -529,24 +533,14 @@ async function applyToggleChangesFromManager(
529
533
  const shouldPromptReload = options?.promptReload ?? true;
530
534
 
531
535
  if (shouldPromptReload) {
532
- const shouldReload = await ctx.ui.confirm(
533
- "Reload Required",
534
- "Local extensions changed. Reload pi now?"
535
- );
536
-
537
- if (shouldReload) {
538
- await ctx.reload();
539
- return { changed: apply.changed, reloaded: true };
540
- }
541
- } else {
542
- ctx.ui.notify(
543
- "Changes saved. Reload pi later to fully apply extension state updates.",
544
- "info"
545
- );
536
+ const reloaded = await confirmReload(ctx, "Local extensions changed.");
537
+ return { changed: apply.changed, reloaded, hasErrors: apply.errors.length > 0 };
546
538
  }
539
+
540
+ ctx.ui.notify("Changes saved. Reload pi later to fully apply extension state updates.", "info");
547
541
  }
548
542
 
549
- return { changed: apply.changed, reloaded: false };
543
+ return { changed: apply.changed, reloaded: false, hasErrors: apply.errors.length > 0 };
550
544
  }
551
545
 
552
546
  async function resolvePendingChangesBeforeLeave(
@@ -556,7 +550,7 @@ async function resolvePendingChangesBeforeLeave(
556
550
  ctx: ExtensionCommandContext,
557
551
  pi: ExtensionAPI,
558
552
  destinationLabel: string
559
- ): Promise<"continue" | "stay" | "exit"> {
553
+ ): Promise<"continue" | "stay"> {
560
554
  const pendingCount = getPendingToggleChangeCount(staged, byId);
561
555
  if (pendingCount === 0) return "continue";
562
556
 
@@ -574,10 +568,10 @@ async function resolvePendingChangesBeforeLeave(
574
568
  return "continue";
575
569
  }
576
570
 
577
- const result = await applyToggleChangesFromManager(items, staged, ctx, pi, {
571
+ const apply = await applyToggleChangesFromManager(items, staged, ctx, pi, {
578
572
  promptReload: false,
579
573
  });
580
- return result.reloaded ? "exit" : "continue";
574
+ return apply.changed === 0 && apply.hasErrors ? "stay" : "continue";
581
575
  }
582
576
 
583
577
  const PALETTE_OPTIONS = {
@@ -648,7 +642,6 @@ async function navigateWithPendingGuard(
648
642
  QUICK_DESTINATION_LABELS[destination]
649
643
  );
650
644
  if (pending === "stay") return "stay";
651
- if (pending === "exit") return "exit";
652
645
 
653
646
  switch (destination) {
654
647
  case "install":
@@ -703,6 +696,7 @@ async function handleUnifiedAction(
703
696
  if (choice === "Save and exit") {
704
697
  const apply = await applyToggleChangesFromManager(items, staged, ctx, pi);
705
698
  if (apply.reloaded) return true;
699
+ if (apply.changed === 0 && apply.hasErrors) return false;
706
700
  }
707
701
  }
708
702
 
@@ -712,7 +706,6 @@ async function handleUnifiedAction(
712
706
  if (result.type === "remote") {
713
707
  const pending = await resolvePendingChangesBeforeLeave(items, staged, byId, ctx, pi, "Remote");
714
708
  if (pending === "stay") return false;
715
- if (pending === "exit") return true;
716
709
 
717
710
  await showRemote("", ctx, pi);
718
711
  return false;
@@ -721,7 +714,6 @@ async function handleUnifiedAction(
721
714
  if (result.type === "help") {
722
715
  const pending = await resolvePendingChangesBeforeLeave(items, staged, byId, ctx, pi, "Help");
723
716
  if (pending === "stay") return false;
724
- if (pending === "exit") return true;
725
717
 
726
718
  showHelp(ctx);
727
719
  return false;
@@ -778,7 +770,6 @@ async function handleUnifiedAction(
778
770
  pendingDestination
779
771
  );
780
772
  if (pending === "stay") return false;
781
- if (pending === "exit") return true;
782
773
 
783
774
  if (item.type === "local") {
784
775
  if (result.action !== "remove") return false;
@@ -790,7 +781,7 @@ async function handleUnifiedAction(
790
781
  if (!confirmed) return false;
791
782
 
792
783
  const removal = await removeLocalExtension(
793
- { activePath: item.activePath!, disabledPath: item.disabledPath! },
784
+ { activePath: item.activePath, disabledPath: item.disabledPath },
794
785
  ctx.cwd
795
786
  );
796
787
  if (!removal.ok) {
@@ -805,16 +796,11 @@ async function handleUnifiedAction(
805
796
  "info"
806
797
  );
807
798
 
808
- const reloaded = await confirmReload(ctx, "Extension removed.");
809
- if (reloaded) {
810
- return true;
811
- }
812
-
813
- return false;
799
+ return await confirmReload(ctx, "Extension removed.");
814
800
  }
815
801
 
816
802
  const pkg: InstalledPackage = {
817
- source: item.source!,
803
+ source: item.source,
818
804
  name: item.displayName,
819
805
  ...(item.version ? { version: item.version } : {}),
820
806
  scope: item.scope,
@@ -860,7 +846,7 @@ async function handleUnifiedAction(
860
846
  }
861
847
 
862
848
  async function applyStagedChanges(
863
- items: UnifiedItem[],
849
+ items: LocalUnifiedItem[],
864
850
  staged: Map<string, State>,
865
851
  pi: ExtensionAPI
866
852
  ) {
@@ -868,10 +854,6 @@ async function applyStagedChanges(
868
854
  const errors: string[] = [];
869
855
 
870
856
  for (const item of items) {
871
- if (item.type !== "local" || !item.originalState || !item.activePath || !item.disabledPath) {
872
- continue;
873
- }
874
-
875
857
  const target = staged.get(item.id) ?? item.originalState;
876
858
  if (target === item.originalState) continue;
877
859
 
@@ -1,25 +1,25 @@
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
9
  import { getPackageCatalog } from "../packages/catalog.js";
10
+ import { logAutoUpdateConfig } from "./history.js";
10
11
  import { notify } from "./notify.js";
12
+ import { normalizePackageIdentity } from "./package-source.js";
11
13
  import {
14
+ type AutoUpdateConfig,
15
+ calculateNextCheck,
12
16
  getAutoUpdateConfig,
13
- saveAutoUpdateConfig,
14
17
  getScheduleInterval,
15
- calculateNextCheck,
16
18
  parseDuration,
17
- type AutoUpdateConfig,
19
+ saveAutoUpdateConfig,
18
20
  } from "./settings.js";
19
- import { normalizePackageIdentity } from "./package-source.js";
20
- import { logAutoUpdateConfig } from "./history.js";
21
21
 
22
- import { startTimer, stopTimer, isTimerRunning } from "./timer.js";
22
+ import { isTimerRunning, startTimer, stopTimer } from "./timer.js";
23
23
 
24
24
  // Context provider for safe session handling
25
25
  export type ContextProvider = () => (ExtensionCommandContext | ExtensionContext) | undefined;
@@ -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,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;
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Package source parsing helpers shared across discovery/management flows.
3
3
  */
4
+ import { homedir } from "node:os";
5
+ import { join, resolve as resolvePath } from "node:path";
6
+ import { fileURLToPath } from "node:url";
4
7
  import { parseNpmSource } from "./format.js";
5
8
 
6
9
  export type PackageSourceKind = "npm" | "git" | "local" | "unknown";
@@ -61,9 +64,35 @@ export function stripGitSourcePrefix(source: string): string {
61
64
  return withoutGitPlus.startsWith("git:") ? withoutGitPlus.slice(4) : withoutGitPlus;
62
65
  }
63
66
 
67
+ function resolveLocalSourceForIdentity(source: string, cwd?: string): string {
68
+ if (source.startsWith("file://")) {
69
+ try {
70
+ return fileURLToPath(source);
71
+ } catch {
72
+ return source;
73
+ }
74
+ }
75
+
76
+ if (source.startsWith("~/")) {
77
+ return join(homedir(), source.slice(2));
78
+ }
79
+
80
+ if (
81
+ cwd &&
82
+ (source.startsWith("./") ||
83
+ source.startsWith("../") ||
84
+ source.startsWith(".\\") ||
85
+ source.startsWith("..\\"))
86
+ ) {
87
+ return resolvePath(cwd, source);
88
+ }
89
+
90
+ return source;
91
+ }
92
+
64
93
  export function normalizePackageIdentity(
65
94
  source: string,
66
- options?: { resolvedPath?: string }
95
+ options?: { resolvedPath?: string; cwd?: string }
67
96
  ): string {
68
97
  const normalized = sanitizeSource(source);
69
98
  const kind = getPackageSourceKind(normalized);
@@ -80,7 +109,9 @@ export function normalizePackageIdentity(
80
109
  }
81
110
 
82
111
  if (kind === "local") {
83
- return `local:${normalizeLocalSourceIdentity(options?.resolvedPath ?? normalized)}`;
112
+ const localSource =
113
+ options?.resolvedPath ?? resolveLocalSourceForIdentity(normalized, options?.cwd);
114
+ return `local:${normalizeLocalSourceIdentity(localSource)}`;
84
115
  }
85
116
 
86
117
  return `raw:${normalized.replace(/\\/g, "/").toLowerCase()}`;
@@ -23,7 +23,7 @@ export async function retryWithBackoff<T>(
23
23
  if (attempt < maxAttempts) {
24
24
  const delay =
25
25
  backoff === "exponential"
26
- ? delayMs * Math.pow(2, attempt - 1)
26
+ ? delayMs * 2 ** (attempt - 1)
27
27
  : backoff === "linear"
28
28
  ? delayMs * attempt
29
29
  : delayMs;
@@ -2,14 +2,15 @@
2
2
  * Auto-update settings storage
3
3
  * Persists to disk so config survives across pi sessions.
4
4
  */
5
- import type {
6
- ExtensionAPI,
7
- ExtensionCommandContext,
8
- ExtensionContext,
9
- } from "@mariozechner/pi-coding-agent";
10
- import { readFile, writeFile, mkdir, rename, rm } from "node:fs/promises";
5
+
6
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
11
7
  import { homedir } from "node:os";
12
8
  import { join } from "node:path";
9
+ import {
10
+ type ExtensionAPI,
11
+ type ExtensionCommandContext,
12
+ type ExtensionContext,
13
+ } from "@mariozechner/pi-coding-agent";
13
14
  import { fileExists } from "./fs.js";
14
15
  import { normalizePackageIdentity } from "./package-source.js";
15
16
 
@@ -328,8 +329,16 @@ export function parseDuration(input: string): { ms: number; display: string } |
328
329
  /^(\d+)\s*(h|hr|hrs|hour|hours|d|day|days|w|wk|wks|week|weeks|m|mo|mos|month|months)$/
329
330
  );
330
331
  if (durationMatch) {
331
- const value = parseInt(durationMatch[1]!, 10);
332
- const unit = durationMatch[2]![0]; // First character of unit
332
+ const [, rawValue, rawUnit] = durationMatch;
333
+ if (!rawValue || !rawUnit) {
334
+ return undefined;
335
+ }
336
+
337
+ const value = Number.parseInt(rawValue, 10);
338
+ const unit = rawUnit[0];
339
+ if (!unit) {
340
+ return undefined;
341
+ }
333
342
 
334
343
  let ms: number;
335
344
  let display: string;
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Status bar helpers for extmgr
3
3
  */
4
- import type {
5
- ExtensionAPI,
6
- ExtensionCommandContext,
7
- ExtensionContext,
4
+ import {
5
+ type ExtensionAPI,
6
+ type ExtensionCommandContext,
7
+ type ExtensionContext,
8
+ getAgentDir,
8
9
  } from "@mariozechner/pi-coding-agent";
9
10
  import { getPackageCatalog, type PackageCatalog } from "../packages/catalog.js";
10
11
  import { getAutoUpdateStatus } from "./auto-update.js";
@@ -15,14 +16,15 @@ type CatalogInstalledPackages = Awaited<ReturnType<PackageCatalog["listInstalled
15
16
 
16
17
  function filterStaleUpdates(
17
18
  knownUpdates: string[],
18
- installedPackages: CatalogInstalledPackages
19
+ installedPackages: CatalogInstalledPackages,
20
+ cwd: string
19
21
  ): string[] {
20
22
  const installedIdentities = new Set(
21
23
  installedPackages.map((pkg) =>
22
- normalizePackageIdentity(
23
- pkg.source,
24
- pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : undefined
25
- )
24
+ normalizePackageIdentity(pkg.source, {
25
+ ...(pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : {}),
26
+ cwd: pkg.scope === "project" ? cwd : getAgentDir(),
27
+ })
26
28
  )
27
29
  );
28
30
  return knownUpdates.filter((identity) => installedIdentities.has(identity));
@@ -52,7 +54,7 @@ export async function updateExtmgrStatus(
52
54
 
53
55
  // Validate updates against actually installed packages (handles external pi update)
54
56
  const knownUpdates = autoUpdateConfig.updatesAvailable ?? [];
55
- const validUpdates = filterStaleUpdates(knownUpdates, packages);
57
+ const validUpdates = filterStaleUpdates(knownUpdates, packages, ctx.cwd);
56
58
 
57
59
  // If stale updates were filtered, persist the correction
58
60
  if (validUpdates.length !== knownUpdates.length) {
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Common UI helper patterns
3
3
  */
4
- import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
5
- import { notify } from "./notify.js";
4
+ import { type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
6
5
  import { UI } from "../constants.js";
6
+ import { notify } from "./notify.js";
7
7
 
8
8
  /**
9
9
  * Confirm and trigger reload