pi-extmgr 0.1.26 → 0.1.27

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.
@@ -4,17 +4,22 @@
4
4
  import { mkdir, rm, writeFile, cp } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
6
  import { homedir } from "node:os";
7
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
7
+ import type {
8
+ ExtensionAPI,
9
+ ExtensionCommandContext,
10
+ ProgressEvent,
11
+ } from "@mariozechner/pi-coding-agent";
8
12
  import { normalizePackageSource } from "../utils/format.js";
9
13
  import { fileExists } from "../utils/fs.js";
10
- import { clearSearchCache, isSourceInstalled } from "./discovery.js";
14
+ import { clearSearchCache } from "./discovery.js";
15
+ import { getPackageCatalog } from "./catalog.js";
11
16
  import { discoverPackageExtensionEntrypoints, readPackageManifest } from "./extensions.js";
12
- import { waitForCondition } from "../utils/retry.js";
13
17
  import { logPackageInstall } from "../utils/history.js";
14
18
  import { clearUpdatesAvailable } from "../utils/settings.js";
15
19
  import { notify, error as notifyError, success } from "../utils/notify.js";
16
20
  import { confirmAction, confirmReload, showProgress } from "../utils/ui-helpers.js";
17
21
  import { tryOperation } from "../utils/mode.js";
22
+ import { runTaskWithLoader } from "../ui/async-task.js";
18
23
  import { updateExtmgrStatus } from "../utils/status.js";
19
24
  import { execNpm } from "../utils/npm-exec.js";
20
25
  import { normalizePackageIdentity } from "../utils/package-source.js";
@@ -27,6 +32,10 @@ export interface InstallOptions {
27
32
  scope?: InstallScope;
28
33
  }
29
34
 
35
+ function getProgressMessage(event: ProgressEvent, fallback: string): string {
36
+ return event.message?.trim() || fallback;
37
+ }
38
+
30
39
  async function resolveInstallScope(
31
40
  ctx: ExtensionCommandContext,
32
41
  explicitScope?: InstallScope
@@ -195,11 +204,24 @@ export async function installPackage(
195
204
 
196
205
  showProgress(ctx, "Installing", normalized);
197
206
 
198
- const args = ["install", ...(scope === "project" ? ["-l"] : []), normalized];
199
- const res = await pi.exec("pi", args, { timeout: TIMEOUTS.packageInstall, cwd: ctx.cwd });
200
-
201
- if (res.code !== 0) {
202
- const errorMsg = `Install failed:\n${res.stderr || res.stdout || `exit ${res.code}`}`;
207
+ try {
208
+ await runTaskWithLoader(
209
+ ctx,
210
+ {
211
+ title: "Install Package",
212
+ message: `Installing ${normalized}...`,
213
+ cancellable: false,
214
+ },
215
+ async ({ setMessage }) => {
216
+ await getPackageCatalog(ctx.cwd).install(normalized, scope, (event) => {
217
+ setMessage(getProgressMessage(event, `Installing ${normalized}...`));
218
+ });
219
+ return undefined;
220
+ }
221
+ );
222
+ } catch (error) {
223
+ const message = error instanceof Error ? error.message : String(error);
224
+ const errorMsg = `Install failed:\n${message}`;
203
225
  logPackageInstall(pi, normalized, normalized, undefined, scope, false, errorMsg);
204
226
  notifyError(ctx, errorMsg);
205
227
  void updateExtmgrStatus(ctx, pi);
@@ -211,24 +233,6 @@ export async function installPackage(
211
233
  success(ctx, `Installed ${normalized} (${scope})`);
212
234
  clearUpdatesAvailable(pi, ctx, [normalizePackageIdentity(normalized)]);
213
235
 
214
- // Wait for the extension to be discoverable before reloading.
215
- // This prevents a race condition where ctx.reload() runs before
216
- // settings.json or extension files are fully flushed to disk.
217
- notify(ctx, "Waiting for extension to be ready...", "info");
218
- const isReady = await waitForCondition(() => isSourceInstalled(normalized, ctx, pi, { scope }), {
219
- maxAttempts: 10,
220
- delayMs: 100,
221
- backoff: "exponential",
222
- });
223
-
224
- if (!isReady) {
225
- notify(
226
- ctx,
227
- "Extension may not be immediately available. Reload pi manually if needed.",
228
- "warning"
229
- );
230
- }
231
-
232
236
  const reloaded = await confirmReload(ctx, "Package installed.");
233
237
  if (!reloaded) {
234
238
  void updateExtmgrStatus(ctx, pi);
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * Package management (update, remove)
3
3
  */
4
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
4
+ import type {
5
+ ExtensionAPI,
6
+ ExtensionCommandContext,
7
+ ProgressEvent,
8
+ } from "@mariozechner/pi-coding-agent";
5
9
  import type { InstalledPackage } from "../types/index.js";
6
10
  import {
7
11
  getInstalledPackages,
12
+ getInstalledPackagesAllScopes,
8
13
  clearSearchCache,
9
- parseInstalledPackagesOutputAllScopes,
10
- isSourceInstalled,
11
14
  } from "./discovery.js";
12
- import { waitForCondition } from "../utils/retry.js";
15
+ import { getPackageCatalog } from "./catalog.js";
13
16
  import { formatInstalledPackageLabel } from "../utils/format.js";
14
17
  import { normalizePackageIdentity } from "../utils/package-source.js";
15
18
  import { logPackageUpdate, logPackageRemove } from "../utils/history.js";
@@ -22,8 +25,9 @@ import {
22
25
  formatListOutput,
23
26
  } from "../utils/ui-helpers.js";
24
27
  import { requireUI } from "../utils/mode.js";
28
+ import { runTaskWithLoader } from "../ui/async-task.js";
25
29
  import { updateExtmgrStatus } from "../utils/status.js";
26
- import { TIMEOUTS, UI } from "../constants.js";
30
+ import { UI } from "../constants.js";
27
31
 
28
32
  export interface PackageMutationOutcome {
29
33
  reloaded: boolean;
@@ -41,9 +45,8 @@ function packageMutationOutcome(
41
45
  return { ...NO_PACKAGE_MUTATION_OUTCOME, ...overrides };
42
46
  }
43
47
 
44
- function isUpToDateOutput(stdout: string): boolean {
45
- const pinnedAsStatus = /^\s*pinned\b(?!\s+dependency\b)(?:\s*$|\s*[:(-])/im.test(stdout);
46
- return /already\s+up\s+to\s+date/i.test(stdout) || pinnedAsStatus;
48
+ function getProgressMessage(event: ProgressEvent, fallback: string): string {
49
+ return event.message?.trim() || fallback;
47
50
  }
48
51
 
49
52
  async function updatePackageInternal(
@@ -53,29 +56,45 @@ async function updatePackageInternal(
53
56
  ): Promise<PackageMutationOutcome> {
54
57
  showProgress(ctx, "Updating", source);
55
58
 
56
- const res = await pi.exec("pi", ["update", source], {
57
- timeout: TIMEOUTS.packageUpdate,
58
- cwd: ctx.cwd,
59
- });
59
+ const updateIdentity = normalizePackageIdentity(source);
60
+
61
+ try {
62
+ const updates = await getPackageCatalog(ctx.cwd).checkForAvailableUpdates();
63
+ const hasUpdate = updates.some(
64
+ (update) => normalizePackageIdentity(update.source) === updateIdentity
65
+ );
60
66
 
61
- if (res.code !== 0) {
62
- const errorMsg = `Update failed: ${res.stderr || res.stdout || `exit ${res.code}`}`;
67
+ if (!hasUpdate) {
68
+ notify(ctx, `${source} is already up to date (or pinned).`, "info");
69
+ logPackageUpdate(pi, source, source, undefined, true);
70
+ clearUpdatesAvailable(pi, ctx, [updateIdentity]);
71
+ void updateExtmgrStatus(ctx, pi);
72
+ return NO_PACKAGE_MUTATION_OUTCOME;
73
+ }
74
+
75
+ await runTaskWithLoader(
76
+ ctx,
77
+ {
78
+ title: "Update Package",
79
+ message: `Updating ${source}...`,
80
+ cancellable: false,
81
+ },
82
+ async ({ setMessage }) => {
83
+ await getPackageCatalog(ctx.cwd).update(source, (event) => {
84
+ setMessage(getProgressMessage(event, `Updating ${source}...`));
85
+ });
86
+ return undefined;
87
+ }
88
+ );
89
+ } catch (error) {
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ const errorMsg = `Update failed: ${message}`;
63
92
  logPackageUpdate(pi, source, source, undefined, false, errorMsg);
64
93
  notifyError(ctx, errorMsg);
65
94
  void updateExtmgrStatus(ctx, pi);
66
95
  return NO_PACKAGE_MUTATION_OUTCOME;
67
96
  }
68
97
 
69
- const updateIdentity = normalizePackageIdentity(source);
70
- const stdout = res.stdout || "";
71
- if (isUpToDateOutput(stdout)) {
72
- notify(ctx, `${source} is already up to date (or pinned).`, "info");
73
- logPackageUpdate(pi, source, source, undefined, true);
74
- clearUpdatesAvailable(pi, ctx, [updateIdentity]);
75
- void updateExtmgrStatus(ctx, pi);
76
- return NO_PACKAGE_MUTATION_OUTCOME;
77
- }
78
-
79
98
  logPackageUpdate(pi, source, source, undefined, true);
80
99
  success(ctx, `Updated ${source}`);
81
100
  clearUpdatesAvailable(pi, ctx, [updateIdentity]);
@@ -93,25 +112,39 @@ async function updatePackagesInternal(
93
112
  ): Promise<PackageMutationOutcome> {
94
113
  showProgress(ctx, "Updating", "all packages");
95
114
 
96
- const res = await pi.exec("pi", ["update"], { timeout: TIMEOUTS.packageUpdateAll, cwd: ctx.cwd });
115
+ try {
116
+ const updates = await getPackageCatalog(ctx.cwd).checkForAvailableUpdates();
117
+ if (updates.length === 0) {
118
+ notify(ctx, "All packages are already up to date.", "info");
119
+ logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
120
+ clearUpdatesAvailable(pi, ctx);
121
+ void updateExtmgrStatus(ctx, pi);
122
+ return NO_PACKAGE_MUTATION_OUTCOME;
123
+ }
97
124
 
98
- if (res.code !== 0) {
99
- const errorMsg = `Update failed: ${res.stderr || res.stdout || `exit ${res.code}`}`;
125
+ await runTaskWithLoader(
126
+ ctx,
127
+ {
128
+ title: "Update Packages",
129
+ message: "Updating all packages...",
130
+ cancellable: false,
131
+ },
132
+ async ({ setMessage }) => {
133
+ await getPackageCatalog(ctx.cwd).update(undefined, (event) => {
134
+ setMessage(getProgressMessage(event, "Updating all packages..."));
135
+ });
136
+ return undefined;
137
+ }
138
+ );
139
+ } catch (error) {
140
+ const message = error instanceof Error ? error.message : String(error);
141
+ const errorMsg = `Update failed: ${message}`;
100
142
  logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, false, errorMsg);
101
143
  notifyError(ctx, errorMsg);
102
144
  void updateExtmgrStatus(ctx, pi);
103
145
  return NO_PACKAGE_MUTATION_OUTCOME;
104
146
  }
105
147
 
106
- const stdout = res.stdout || "";
107
- if (isUpToDateOutput(stdout) || stdout.trim() === "") {
108
- notify(ctx, "All packages are already up to date.", "info");
109
- logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
110
- clearUpdatesAvailable(pi, ctx);
111
- void updateExtmgrStatus(ctx, pi);
112
- return NO_PACKAGE_MUTATION_OUTCOME;
113
- }
114
-
115
148
  logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
116
149
  success(ctx, "Packages updated");
117
150
  clearUpdatesAvailable(pi, ctx);
@@ -157,13 +190,10 @@ function packageIdentity(source: string): string {
157
190
  return normalizePackageIdentity(source);
158
191
  }
159
192
 
160
- async function getInstalledPackagesAllScopes(
161
- ctx: ExtensionCommandContext,
162
- pi: ExtensionAPI
193
+ async function getInstalledPackagesAllScopesForRemoval(
194
+ ctx: ExtensionCommandContext
163
195
  ): Promise<InstalledPackage[]> {
164
- const res = await pi.exec("pi", ["list"], { timeout: TIMEOUTS.listPackages, cwd: ctx.cwd });
165
- if (res.code !== 0) return [];
166
- return parseInstalledPackagesOutputAllScopes(res.stdout || "");
196
+ return getInstalledPackagesAllScopes(ctx);
167
197
  }
168
198
 
169
199
  type RemovalScopeChoice = "both" | "global" | "project" | "cancel";
@@ -197,14 +227,9 @@ async function selectRemovalScope(ctx: ExtensionCommandContext): Promise<Removal
197
227
 
198
228
  function buildRemovalTargets(
199
229
  matching: InstalledPackage[],
200
- source: string,
201
230
  hasUI: boolean,
202
231
  scopeChoice: RemovalScopeChoice
203
232
  ): RemovalTarget[] {
204
- if (matching.length === 0) {
205
- return [{ scope: "global", source, name: source }];
206
- }
207
-
208
233
  const byScope = new Map(matching.map((pkg) => [pkg.scope, pkg] as const));
209
234
  const addTarget = (scope: "global" | "project") => {
210
235
  const pkg = byScope.get(scope);
@@ -253,18 +278,30 @@ async function executeRemovalTargets(
253
278
  for (const target of targets) {
254
279
  showProgress(ctx, "Removing", `${target.source} (${target.scope})`);
255
280
 
256
- const args = ["remove", ...(target.scope === "project" ? ["-l"] : []), target.source];
257
- const res = await pi.exec("pi", args, { timeout: TIMEOUTS.packageRemove, cwd: ctx.cwd });
258
-
259
- if (res.code !== 0) {
260
- const errorMsg = `Remove failed (${target.scope}): ${res.stderr || res.stdout || `exit ${res.code}`}`;
281
+ try {
282
+ await runTaskWithLoader(
283
+ ctx,
284
+ {
285
+ title: "Remove Package",
286
+ message: `Removing ${target.source}...`,
287
+ cancellable: false,
288
+ },
289
+ async ({ setMessage }) => {
290
+ await getPackageCatalog(ctx.cwd).remove(target.source, target.scope, (event) => {
291
+ setMessage(getProgressMessage(event, `Removing ${target.source}...`));
292
+ });
293
+ return undefined;
294
+ }
295
+ );
296
+
297
+ logPackageRemove(pi, target.source, target.name, true);
298
+ results.push({ target, success: true });
299
+ } catch (error) {
300
+ const message = error instanceof Error ? error.message : String(error);
301
+ const errorMsg = `Remove failed (${target.scope}): ${message}`;
261
302
  logPackageRemove(pi, target.source, target.name, false, errorMsg);
262
303
  results.push({ target, success: false, error: errorMsg });
263
- continue;
264
304
  }
265
-
266
- logPackageRemove(pi, target.source, target.name, true);
267
- results.push({ target, success: true });
268
305
  }
269
306
 
270
307
  return results;
@@ -300,7 +337,7 @@ async function removePackageInternal(
300
337
  ctx: ExtensionCommandContext,
301
338
  pi: ExtensionAPI
302
339
  ): Promise<PackageMutationOutcome> {
303
- const installed = await getInstalledPackagesAllScopes(ctx, pi);
340
+ const installed = await getInstalledPackagesAllScopesForRemoval(ctx);
304
341
  const identity = packageIdentity(source);
305
342
  const matching = installed.filter((p) => packageIdentity(p.source) === identity);
306
343
 
@@ -314,7 +351,12 @@ async function removePackageInternal(
314
351
  return NO_PACKAGE_MUTATION_OUTCOME;
315
352
  }
316
353
 
317
- const targets = buildRemovalTargets(matching, source, ctx.hasUI, scopeChoice);
354
+ if (matching.length === 0) {
355
+ notify(ctx, `${source} is not installed.`, "info");
356
+ return NO_PACKAGE_MUTATION_OUTCOME;
357
+ }
358
+
359
+ const targets = buildRemovalTargets(matching, ctx.hasUI, scopeChoice);
318
360
  if (targets.length === 0) {
319
361
  notify(ctx, "Nothing to remove.", "info");
320
362
  return NO_PACKAGE_MUTATION_OUTCOME;
@@ -343,7 +385,7 @@ async function removePackageInternal(
343
385
  .filter((result) => result.success)
344
386
  .map((result) => result.target);
345
387
 
346
- const remaining = (await getInstalledPackagesAllScopes(ctx, pi)).filter(
388
+ const remaining = (await getInstalledPackagesAllScopesForRemoval(ctx)).filter(
347
389
  (p) => packageIdentity(p.source) === identity
348
390
  );
349
391
  notifyRemovalSummary(source, remaining, failures, ctx);
@@ -354,28 +396,6 @@ async function removePackageInternal(
354
396
 
355
397
  const successfulRemovalCount = successfulTargets.length;
356
398
 
357
- // Wait for successfully removed targets to disappear from their target scopes before reloading.
358
- if (successfulTargets.length > 0) {
359
- notify(ctx, "Waiting for removal to complete...", "info");
360
- const isRemoved = await waitForCondition(
361
- async () => {
362
- const installedChecks = await Promise.all(
363
- successfulTargets.map((target) =>
364
- isSourceInstalled(target.source, ctx, pi, {
365
- scope: target.scope,
366
- })
367
- )
368
- );
369
- return installedChecks.every((installedInScope) => !installedInScope);
370
- },
371
- { maxAttempts: 10, delayMs: 100, backoff: "exponential" }
372
- );
373
-
374
- if (!isRemoved) {
375
- notify(ctx, "Extension may still be active. Restart pi manually if needed.", "warning");
376
- }
377
- }
378
-
379
399
  if (successfulRemovalCount === 0) {
380
400
  void updateExtmgrStatus(ctx, pi);
381
401
  return NO_PACKAGE_MUTATION_OUTCOME;
@@ -431,9 +451,9 @@ export async function promptRemove(ctx: ExtensionCommandContext, pi: ExtensionAP
431
451
 
432
452
  export async function showInstalledPackagesList(
433
453
  ctx: ExtensionCommandContext,
434
- pi: ExtensionAPI
454
+ _pi: ExtensionAPI
435
455
  ): Promise<void> {
436
- const packages = await getInstalledPackages(ctx, pi);
456
+ const packages = await getInstalledPackagesAllScopes(ctx);
437
457
 
438
458
  if (packages.length === 0) {
439
459
  notify(ctx, "No packages installed.", "info");
@@ -0,0 +1,158 @@
1
+ import type {
2
+ ExtensionCommandContext,
3
+ ExtensionContext,
4
+ Theme,
5
+ } from "@mariozechner/pi-coding-agent";
6
+ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
7
+ import { CancellableLoader, Container, Loader, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
8
+ import { hasCustomUI } from "../utils/mode.js";
9
+
10
+ type AnyContext = ExtensionCommandContext | ExtensionContext;
11
+
12
+ const TASK_ABORTED = Symbol("task-aborted");
13
+ const TASK_FAILED = Symbol("task-failed");
14
+
15
+ export interface TaskControls {
16
+ signal: AbortSignal;
17
+ setMessage: (message: string) => void;
18
+ }
19
+
20
+ interface LoaderConfig {
21
+ title: string;
22
+ message: string;
23
+ cancellable?: boolean;
24
+ }
25
+
26
+ function createLoaderComponent(
27
+ tui: TUI,
28
+ theme: Theme,
29
+ title: string,
30
+ message: string,
31
+ cancellable: boolean,
32
+ onCancel: () => void
33
+ ): {
34
+ container: Container;
35
+ loader: Loader | CancellableLoader;
36
+ signal: AbortSignal;
37
+ } {
38
+ const container = new Container();
39
+ const borderColor = (text: string) => theme.fg("accent", text);
40
+ const loader = cancellable
41
+ ? new CancellableLoader(
42
+ tui,
43
+ (text) => theme.fg("accent", text),
44
+ (text) => theme.fg("muted", text),
45
+ message
46
+ )
47
+ : new Loader(
48
+ tui,
49
+ (text) => theme.fg("accent", text),
50
+ (text) => theme.fg("muted", text),
51
+ message
52
+ );
53
+
54
+ container.addChild(new DynamicBorder(borderColor));
55
+ container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
56
+ container.addChild(loader);
57
+
58
+ if (cancellable) {
59
+ (loader as CancellableLoader).onAbort = onCancel;
60
+ container.addChild(new Spacer(1));
61
+ container.addChild(new Text(theme.fg("dim", "Esc cancel"), 1, 0));
62
+ }
63
+
64
+ container.addChild(new Spacer(1));
65
+ container.addChild(new DynamicBorder(borderColor));
66
+
67
+ const signal = cancellable ? (loader as CancellableLoader).signal : new AbortController().signal;
68
+
69
+ return { container, loader, signal };
70
+ }
71
+
72
+ export async function runTaskWithLoader<T>(
73
+ ctx: AnyContext,
74
+ config: LoaderConfig,
75
+ task: (controls: TaskControls) => Promise<T>
76
+ ): Promise<T | undefined> {
77
+ if (!hasCustomUI(ctx)) {
78
+ return task({
79
+ signal: new AbortController().signal,
80
+ setMessage: () => undefined,
81
+ });
82
+ }
83
+
84
+ let taskError: unknown;
85
+
86
+ const result = await ctx.ui.custom<T | typeof TASK_ABORTED | typeof TASK_FAILED>(
87
+ (tui, theme, _keybindings, done) => {
88
+ let finished = false;
89
+ const finish = (value: T | typeof TASK_ABORTED | typeof TASK_FAILED): void => {
90
+ if (finished) {
91
+ return;
92
+ }
93
+ finished = true;
94
+ done(value);
95
+ };
96
+
97
+ const { container, loader, signal } = createLoaderComponent(
98
+ tui,
99
+ theme,
100
+ config.title,
101
+ config.message,
102
+ config.cancellable ?? true,
103
+ () => finish(TASK_ABORTED)
104
+ );
105
+
106
+ void task({
107
+ signal,
108
+ setMessage: (message) => {
109
+ loader.setMessage(message);
110
+ tui.requestRender();
111
+ },
112
+ })
113
+ .then((value) => finish(value))
114
+ .catch((error) => {
115
+ if (signal.aborted) {
116
+ finish(TASK_ABORTED);
117
+ return;
118
+ }
119
+
120
+ taskError = error;
121
+ finish(TASK_FAILED);
122
+ });
123
+
124
+ return {
125
+ render(width: number) {
126
+ return container.render(width);
127
+ },
128
+ invalidate() {
129
+ container.invalidate();
130
+ },
131
+ handleInput(data: string) {
132
+ if (loader instanceof CancellableLoader) {
133
+ loader.handleInput(data);
134
+ tui.requestRender();
135
+ }
136
+ },
137
+ dispose() {
138
+ if (loader instanceof CancellableLoader) {
139
+ loader.dispose();
140
+ return;
141
+ }
142
+
143
+ loader.stop();
144
+ },
145
+ };
146
+ }
147
+ );
148
+
149
+ if (result === TASK_ABORTED) {
150
+ return undefined;
151
+ }
152
+
153
+ if (result === TASK_FAILED) {
154
+ throw taskError;
155
+ }
156
+
157
+ return result;
158
+ }
@@ -21,6 +21,7 @@ import {
21
21
  import { notify } from "../utils/notify.js";
22
22
  import { logExtensionToggle } from "../utils/history.js";
23
23
  import { requireCustomUI, runCustomUI } from "../utils/mode.js";
24
+ import { runTaskWithLoader } from "./async-task.js";
24
25
  import { getPackageSourceKind } from "../utils/package-source.js";
25
26
  import { getSettingsListSelectedIndex } from "../utils/settings-list.js";
26
27
  import { fileExists } from "../utils/fs.js";
@@ -329,8 +330,32 @@ export async function configurePackageExtensions(
329
330
  return { changed: 0, reloaded: false };
330
331
  }
331
332
 
332
- const discovered = await discoverPackageExtensions([pkg], ctx.cwd);
333
- const rows = await buildPackageConfigRows(discovered);
333
+ let initialData: { rows: PackageConfigRow[] } | undefined;
334
+ try {
335
+ initialData = await runTaskWithLoader(
336
+ ctx,
337
+ {
338
+ title: `Configure ${pkg.name}`,
339
+ message: "Discovering package extensions...",
340
+ cancellable: false,
341
+ },
342
+ async () => {
343
+ const discovered = await discoverPackageExtensions([pkg], ctx.cwd);
344
+ const rows = await buildPackageConfigRows(discovered);
345
+ return { rows };
346
+ }
347
+ );
348
+ } catch (error) {
349
+ notify(ctx, error instanceof Error ? error.message : String(error), "error");
350
+ return { changed: 0, reloaded: false };
351
+ }
352
+
353
+ if (!initialData) {
354
+ notify(ctx, "Package extension configuration requires the full interactive TUI.", "warning");
355
+ return { changed: 0, reloaded: false };
356
+ }
357
+
358
+ const { rows } = initialData;
334
359
 
335
360
  if (rows.length === 0) {
336
361
  notify(ctx, "No configurable extensions discovered for this package.", "info");