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.
- package/README.md +3 -2
- package/package.json +3 -3
- package/src/packages/catalog.ts +162 -0
- package/src/packages/discovery.ts +59 -247
- package/src/packages/install.ts +30 -26
- package/src/packages/management.ts +103 -83
- package/src/ui/async-task.ts +158 -0
- package/src/ui/package-config.ts +27 -2
- package/src/ui/remote.ts +69 -17
- package/src/ui/unified.ts +45 -12
- package/src/utils/auto-update.ts +8 -54
- package/src/utils/network.ts +10 -2
- package/src/utils/npm-exec.ts +2 -0
- package/src/utils/package-source.ts +51 -0
- package/src/utils/status.ts +5 -3
- package/src/utils/ui-helpers.ts +1 -1
package/src/packages/install.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
45
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
|
161
|
-
ctx: ExtensionCommandContext
|
|
162
|
-
pi: ExtensionAPI
|
|
193
|
+
async function getInstalledPackagesAllScopesForRemoval(
|
|
194
|
+
ctx: ExtensionCommandContext
|
|
163
195
|
): Promise<InstalledPackage[]> {
|
|
164
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
454
|
+
_pi: ExtensionAPI
|
|
435
455
|
): Promise<void> {
|
|
436
|
-
const packages = await
|
|
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
|
+
}
|
package/src/ui/package-config.ts
CHANGED
|
@@ -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
|
-
|
|
333
|
-
|
|
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");
|