pi-extmgr 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/package.json +13 -16
- package/src/commands/auto-update.ts +4 -4
- package/src/commands/cache.ts +1 -1
- package/src/commands/history.ts +3 -3
- package/src/commands/install.ts +2 -2
- package/src/commands/registry.ts +7 -7
- package/src/commands/types.ts +1 -1
- package/src/extensions/discovery.ts +4 -3
- package/src/index.ts +15 -15
- package/src/packages/catalog.ts +163 -0
- package/src/packages/discovery.ts +77 -262
- package/src/packages/extensions.ts +10 -5
- package/src/packages/install.ts +42 -37
- package/src/packages/management.ts +145 -99
- package/src/types/index.ts +16 -9
- package/src/ui/async-task.ts +194 -0
- package/src/ui/footer.ts +4 -8
- package/src/ui/help.ts +2 -2
- package/src/ui/package-config.ts +62 -49
- package/src/ui/remote.ts +83 -28
- package/src/ui/theme.ts +2 -2
- package/src/ui/unified.ts +104 -89
- package/src/utils/auto-update.ts +18 -64
- package/src/utils/cache.ts +3 -3
- package/src/utils/command.ts +1 -1
- package/src/utils/format.ts +4 -3
- package/src/utils/history.ts +4 -2
- package/src/utils/mode.ts +1 -1
- package/src/utils/network.ts +10 -2
- package/src/utils/notify.ts +1 -1
- package/src/utils/npm-exec.ts +3 -1
- package/src/utils/package-source.ts +84 -2
- package/src/utils/retry.ts +1 -1
- package/src/utils/settings.ts +17 -8
- package/src/utils/status.ts +16 -12
- package/src/utils/ui-helpers.ts +3 -3
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package management (update, remove)
|
|
3
3
|
*/
|
|
4
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import type { InstalledPackage } from "../types/index.js";
|
|
6
4
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "
|
|
12
|
-
import {
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionCommandContext,
|
|
7
|
+
getAgentDir,
|
|
8
|
+
type ProgressEvent,
|
|
9
|
+
} from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { UI } from "../constants.js";
|
|
11
|
+
import { type InstalledPackage } from "../types/index.js";
|
|
12
|
+
import { runTaskWithLoader } from "../ui/async-task.js";
|
|
13
13
|
import { formatInstalledPackageLabel } from "../utils/format.js";
|
|
14
|
+
import { logPackageRemove, logPackageUpdate } from "../utils/history.js";
|
|
15
|
+
import { requireUI } from "../utils/mode.js";
|
|
16
|
+
import { notify, error as notifyError, success } from "../utils/notify.js";
|
|
14
17
|
import { normalizePackageIdentity } from "../utils/package-source.js";
|
|
15
|
-
import { logPackageUpdate, logPackageRemove } from "../utils/history.js";
|
|
16
18
|
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
17
|
-
import {
|
|
19
|
+
import { updateExtmgrStatus } from "../utils/status.js";
|
|
18
20
|
import {
|
|
19
21
|
confirmAction,
|
|
20
22
|
confirmReload,
|
|
21
|
-
showProgress,
|
|
22
23
|
formatListOutput,
|
|
24
|
+
showProgress,
|
|
23
25
|
} from "../utils/ui-helpers.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
26
|
+
import { getPackageCatalog } from "./catalog.js";
|
|
27
|
+
import {
|
|
28
|
+
clearSearchCache,
|
|
29
|
+
getInstalledPackages,
|
|
30
|
+
getInstalledPackagesAllScopes,
|
|
31
|
+
} from "./discovery.js";
|
|
27
32
|
|
|
28
33
|
export interface PackageMutationOutcome {
|
|
29
34
|
reloaded: boolean;
|
|
@@ -41,9 +46,8 @@ function packageMutationOutcome(
|
|
|
41
46
|
return { ...NO_PACKAGE_MUTATION_OUTCOME, ...overrides };
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
return /already\s+up\s+to\s+date/i.test(stdout) || pinnedAsStatus;
|
|
49
|
+
function getProgressMessage(event: ProgressEvent, fallback: string): string {
|
|
50
|
+
return event.message?.trim() || fallback;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
async function updatePackageInternal(
|
|
@@ -53,29 +57,46 @@ async function updatePackageInternal(
|
|
|
53
57
|
): Promise<PackageMutationOutcome> {
|
|
54
58
|
showProgress(ctx, "Updating", source);
|
|
55
59
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
const updateIdentity = normalizePackageIdentity(source, { cwd: ctx.cwd });
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const updates = await getPackageCatalog(ctx.cwd).checkForAvailableUpdates();
|
|
64
|
+
const hasUpdate = updates.some(
|
|
65
|
+
(update) => normalizePackageIdentity(update.source) === updateIdentity
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (!hasUpdate) {
|
|
69
|
+
notify(ctx, `${source} is already up to date (or pinned).`, "info");
|
|
70
|
+
logPackageUpdate(pi, source, source, undefined, true);
|
|
71
|
+
clearUpdatesAvailable(pi, ctx, [updateIdentity]);
|
|
72
|
+
void updateExtmgrStatus(ctx, pi);
|
|
73
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
74
|
+
}
|
|
60
75
|
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
await runTaskWithLoader(
|
|
77
|
+
ctx,
|
|
78
|
+
{
|
|
79
|
+
title: "Update Package",
|
|
80
|
+
message: `Updating ${source}...`,
|
|
81
|
+
cancellable: false,
|
|
82
|
+
fallbackWithoutLoader: true,
|
|
83
|
+
},
|
|
84
|
+
async ({ setMessage }) => {
|
|
85
|
+
await getPackageCatalog(ctx.cwd).update(source, (event) => {
|
|
86
|
+
setMessage(getProgressMessage(event, `Updating ${source}...`));
|
|
87
|
+
});
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
const errorMsg = `Update failed: ${message}`;
|
|
63
94
|
logPackageUpdate(pi, source, source, undefined, false, errorMsg);
|
|
64
95
|
notifyError(ctx, errorMsg);
|
|
65
96
|
void updateExtmgrStatus(ctx, pi);
|
|
66
97
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
67
98
|
}
|
|
68
99
|
|
|
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
100
|
logPackageUpdate(pi, source, source, undefined, true);
|
|
80
101
|
success(ctx, `Updated ${source}`);
|
|
81
102
|
clearUpdatesAvailable(pi, ctx, [updateIdentity]);
|
|
@@ -93,25 +114,40 @@ async function updatePackagesInternal(
|
|
|
93
114
|
): Promise<PackageMutationOutcome> {
|
|
94
115
|
showProgress(ctx, "Updating", "all packages");
|
|
95
116
|
|
|
96
|
-
|
|
117
|
+
try {
|
|
118
|
+
const updates = await getPackageCatalog(ctx.cwd).checkForAvailableUpdates();
|
|
119
|
+
if (updates.length === 0) {
|
|
120
|
+
notify(ctx, "All packages are already up to date.", "info");
|
|
121
|
+
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
|
|
122
|
+
clearUpdatesAvailable(pi, ctx);
|
|
123
|
+
void updateExtmgrStatus(ctx, pi);
|
|
124
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
125
|
+
}
|
|
97
126
|
|
|
98
|
-
|
|
99
|
-
|
|
127
|
+
await runTaskWithLoader(
|
|
128
|
+
ctx,
|
|
129
|
+
{
|
|
130
|
+
title: "Update Packages",
|
|
131
|
+
message: "Updating all packages...",
|
|
132
|
+
cancellable: false,
|
|
133
|
+
fallbackWithoutLoader: true,
|
|
134
|
+
},
|
|
135
|
+
async ({ setMessage }) => {
|
|
136
|
+
await getPackageCatalog(ctx.cwd).update(undefined, (event) => {
|
|
137
|
+
setMessage(getProgressMessage(event, "Updating all packages..."));
|
|
138
|
+
});
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
144
|
+
const errorMsg = `Update failed: ${message}`;
|
|
100
145
|
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, false, errorMsg);
|
|
101
146
|
notifyError(ctx, errorMsg);
|
|
102
147
|
void updateExtmgrStatus(ctx, pi);
|
|
103
148
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
104
149
|
}
|
|
105
150
|
|
|
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
151
|
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
|
|
116
152
|
success(ctx, "Packages updated");
|
|
117
153
|
clearUpdatesAvailable(pi, ctx);
|
|
@@ -153,17 +189,37 @@ export async function updatePackagesWithOutcome(
|
|
|
153
189
|
return updatePackagesInternal(ctx, pi);
|
|
154
190
|
}
|
|
155
191
|
|
|
156
|
-
function packageIdentity(
|
|
157
|
-
|
|
192
|
+
function packageIdentity(
|
|
193
|
+
source: string,
|
|
194
|
+
options?: { resolvedPath?: string; cwd?: string }
|
|
195
|
+
): string {
|
|
196
|
+
return normalizePackageIdentity(source, options);
|
|
158
197
|
}
|
|
159
198
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
199
|
+
function packageSourceIdentities(source: string, ctx: ExtensionCommandContext): Set<string> {
|
|
200
|
+
return new Set([
|
|
201
|
+
packageIdentity(source, { cwd: ctx.cwd }),
|
|
202
|
+
packageIdentity(source, { cwd: getAgentDir() }),
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function installedPackageMatchesSource(
|
|
207
|
+
pkg: InstalledPackage,
|
|
208
|
+
identities: Set<string>,
|
|
209
|
+
ctx: ExtensionCommandContext
|
|
210
|
+
): boolean {
|
|
211
|
+
return identities.has(
|
|
212
|
+
packageIdentity(pkg.source, {
|
|
213
|
+
...(pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : {}),
|
|
214
|
+
cwd: pkg.scope === "project" ? ctx.cwd : getAgentDir(),
|
|
215
|
+
})
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function getInstalledPackagesAllScopesForRemoval(
|
|
220
|
+
ctx: ExtensionCommandContext
|
|
163
221
|
): Promise<InstalledPackage[]> {
|
|
164
|
-
|
|
165
|
-
if (res.code !== 0) return [];
|
|
166
|
-
return parseInstalledPackagesOutputAllScopes(res.stdout || "");
|
|
222
|
+
return getInstalledPackagesAllScopes(ctx);
|
|
167
223
|
}
|
|
168
224
|
|
|
169
225
|
type RemovalScopeChoice = "both" | "global" | "project" | "cancel";
|
|
@@ -197,14 +253,9 @@ async function selectRemovalScope(ctx: ExtensionCommandContext): Promise<Removal
|
|
|
197
253
|
|
|
198
254
|
function buildRemovalTargets(
|
|
199
255
|
matching: InstalledPackage[],
|
|
200
|
-
source: string,
|
|
201
256
|
hasUI: boolean,
|
|
202
257
|
scopeChoice: RemovalScopeChoice
|
|
203
258
|
): RemovalTarget[] {
|
|
204
|
-
if (matching.length === 0) {
|
|
205
|
-
return [{ scope: "global", source, name: source }];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
259
|
const byScope = new Map(matching.map((pkg) => [pkg.scope, pkg] as const));
|
|
209
260
|
const addTarget = (scope: "global" | "project") => {
|
|
210
261
|
const pkg = byScope.get(scope);
|
|
@@ -219,7 +270,6 @@ function buildRemovalTargets(
|
|
|
219
270
|
return addTarget("global");
|
|
220
271
|
case "project":
|
|
221
272
|
return addTarget("project");
|
|
222
|
-
case "cancel":
|
|
223
273
|
default:
|
|
224
274
|
return [];
|
|
225
275
|
}
|
|
@@ -253,18 +303,31 @@ async function executeRemovalTargets(
|
|
|
253
303
|
for (const target of targets) {
|
|
254
304
|
showProgress(ctx, "Removing", `${target.source} (${target.scope})`);
|
|
255
305
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
306
|
+
try {
|
|
307
|
+
await runTaskWithLoader(
|
|
308
|
+
ctx,
|
|
309
|
+
{
|
|
310
|
+
title: "Remove Package",
|
|
311
|
+
message: `Removing ${target.source}...`,
|
|
312
|
+
cancellable: false,
|
|
313
|
+
fallbackWithoutLoader: true,
|
|
314
|
+
},
|
|
315
|
+
async ({ setMessage }) => {
|
|
316
|
+
await getPackageCatalog(ctx.cwd).remove(target.source, target.scope, (event) => {
|
|
317
|
+
setMessage(getProgressMessage(event, `Removing ${target.source}...`));
|
|
318
|
+
});
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
logPackageRemove(pi, target.source, target.name, true);
|
|
324
|
+
results.push({ target, success: true });
|
|
325
|
+
} catch (error) {
|
|
326
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
327
|
+
const errorMsg = `Remove failed (${target.scope}): ${message}`;
|
|
261
328
|
logPackageRemove(pi, target.source, target.name, false, errorMsg);
|
|
262
329
|
results.push({ target, success: false, error: errorMsg });
|
|
263
|
-
continue;
|
|
264
330
|
}
|
|
265
|
-
|
|
266
|
-
logPackageRemove(pi, target.source, target.name, true);
|
|
267
|
-
results.push({ target, success: true });
|
|
268
331
|
}
|
|
269
332
|
|
|
270
333
|
return results;
|
|
@@ -300,9 +363,9 @@ async function removePackageInternal(
|
|
|
300
363
|
ctx: ExtensionCommandContext,
|
|
301
364
|
pi: ExtensionAPI
|
|
302
365
|
): Promise<PackageMutationOutcome> {
|
|
303
|
-
const installed = await
|
|
304
|
-
const
|
|
305
|
-
const matching = installed.filter((
|
|
366
|
+
const installed = await getInstalledPackagesAllScopesForRemoval(ctx);
|
|
367
|
+
const identities = packageSourceIdentities(source, ctx);
|
|
368
|
+
const matching = installed.filter((pkg) => installedPackageMatchesSource(pkg, identities, ctx));
|
|
306
369
|
|
|
307
370
|
const hasBothScopes =
|
|
308
371
|
matching.some((pkg) => pkg.scope === "global") &&
|
|
@@ -314,7 +377,12 @@ async function removePackageInternal(
|
|
|
314
377
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
315
378
|
}
|
|
316
379
|
|
|
317
|
-
|
|
380
|
+
if (matching.length === 0) {
|
|
381
|
+
notify(ctx, `${source} is not installed.`, "info");
|
|
382
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const targets = buildRemovalTargets(matching, ctx.hasUI, scopeChoice);
|
|
318
386
|
if (targets.length === 0) {
|
|
319
387
|
notify(ctx, "Nothing to remove.", "info");
|
|
320
388
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
@@ -343,39 +411,17 @@ async function removePackageInternal(
|
|
|
343
411
|
.filter((result) => result.success)
|
|
344
412
|
.map((result) => result.target);
|
|
345
413
|
|
|
346
|
-
const remaining = (await
|
|
347
|
-
(
|
|
414
|
+
const remaining = (await getInstalledPackagesAllScopesForRemoval(ctx)).filter((pkg) =>
|
|
415
|
+
installedPackageMatchesSource(pkg, identities, ctx)
|
|
348
416
|
);
|
|
349
417
|
notifyRemovalSummary(source, remaining, failures, ctx);
|
|
350
418
|
|
|
351
419
|
if (failures.length === 0 && remaining.length === 0) {
|
|
352
|
-
clearUpdatesAvailable(pi, ctx,
|
|
420
|
+
clearUpdatesAvailable(pi, ctx, identities);
|
|
353
421
|
}
|
|
354
422
|
|
|
355
423
|
const successfulRemovalCount = successfulTargets.length;
|
|
356
424
|
|
|
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
425
|
if (successfulRemovalCount === 0) {
|
|
380
426
|
void updateExtmgrStatus(ctx, pi);
|
|
381
427
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
@@ -431,9 +477,9 @@ export async function promptRemove(ctx: ExtensionCommandContext, pi: ExtensionAP
|
|
|
431
477
|
|
|
432
478
|
export async function showInstalledPackagesList(
|
|
433
479
|
ctx: ExtensionCommandContext,
|
|
434
|
-
|
|
480
|
+
_pi: ExtensionAPI
|
|
435
481
|
): Promise<void> {
|
|
436
|
-
const packages = await
|
|
482
|
+
const packages = await getInstalledPackagesAllScopes(ctx);
|
|
437
483
|
|
|
438
484
|
if (packages.length === 0) {
|
|
439
485
|
notify(ctx, "No packages installed.", "info");
|
package/src/types/index.ts
CHANGED
|
@@ -46,25 +46,32 @@ export interface PackageExtensionEntry {
|
|
|
46
46
|
state: State;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export interface
|
|
50
|
-
type: "local"
|
|
49
|
+
export interface LocalUnifiedItem {
|
|
50
|
+
type: "local";
|
|
51
51
|
id: string;
|
|
52
52
|
displayName: string;
|
|
53
53
|
summary: string;
|
|
54
54
|
scope: Scope;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
state: State;
|
|
56
|
+
activePath: string;
|
|
57
|
+
disabledPath: string;
|
|
58
|
+
originalState: State;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PackageUnifiedItem {
|
|
62
|
+
type: "package";
|
|
63
|
+
id: string;
|
|
64
|
+
displayName: string;
|
|
65
|
+
scope: Scope;
|
|
66
|
+
source: string;
|
|
62
67
|
version?: string | undefined;
|
|
63
68
|
description?: string | undefined;
|
|
64
69
|
size?: number | undefined; // Package size in bytes
|
|
65
70
|
updateAvailable?: boolean | undefined;
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
export type UnifiedItem = LocalUnifiedItem | PackageUnifiedItem;
|
|
74
|
+
|
|
68
75
|
export interface SearchCache {
|
|
69
76
|
query: string;
|
|
70
77
|
results: NpmPackage[];
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamicBorder,
|
|
3
|
+
type ExtensionCommandContext,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
type Theme,
|
|
6
|
+
} 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
|
+
type TaskSuccess<T> = { type: "ok"; value: T };
|
|
16
|
+
|
|
17
|
+
export interface TaskControls {
|
|
18
|
+
signal: AbortSignal;
|
|
19
|
+
setMessage: (message: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface LoaderConfig {
|
|
23
|
+
title: string;
|
|
24
|
+
message: string;
|
|
25
|
+
cancellable?: boolean;
|
|
26
|
+
fallbackWithoutLoader?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createLoaderComponent(
|
|
30
|
+
tui: TUI,
|
|
31
|
+
theme: Theme,
|
|
32
|
+
title: string,
|
|
33
|
+
message: string,
|
|
34
|
+
cancellable: boolean,
|
|
35
|
+
onCancel: () => void
|
|
36
|
+
): {
|
|
37
|
+
container: Container;
|
|
38
|
+
loader: Loader | CancellableLoader;
|
|
39
|
+
signal: AbortSignal;
|
|
40
|
+
} {
|
|
41
|
+
const container = new Container();
|
|
42
|
+
const borderColor = (text: string) => theme.fg("accent", text);
|
|
43
|
+
const loader = cancellable
|
|
44
|
+
? new CancellableLoader(
|
|
45
|
+
tui,
|
|
46
|
+
(text) => theme.fg("accent", text),
|
|
47
|
+
(text) => theme.fg("muted", text),
|
|
48
|
+
message
|
|
49
|
+
)
|
|
50
|
+
: new Loader(
|
|
51
|
+
tui,
|
|
52
|
+
(text) => theme.fg("accent", text),
|
|
53
|
+
(text) => theme.fg("muted", text),
|
|
54
|
+
message
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
container.addChild(new DynamicBorder(borderColor));
|
|
58
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
|
|
59
|
+
container.addChild(loader);
|
|
60
|
+
|
|
61
|
+
if (cancellable) {
|
|
62
|
+
(loader as CancellableLoader).onAbort = onCancel;
|
|
63
|
+
container.addChild(new Spacer(1));
|
|
64
|
+
container.addChild(new Text(theme.fg("dim", "Esc cancel"), 1, 0));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
container.addChild(new Spacer(1));
|
|
68
|
+
container.addChild(new DynamicBorder(borderColor));
|
|
69
|
+
|
|
70
|
+
const signal = cancellable ? (loader as CancellableLoader).signal : new AbortController().signal;
|
|
71
|
+
|
|
72
|
+
return { container, loader, signal };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function runTaskWithoutLoader<T>(task: (controls: TaskControls) => Promise<T>): Promise<T> {
|
|
76
|
+
return Promise.resolve().then(() =>
|
|
77
|
+
task({
|
|
78
|
+
signal: new AbortController().signal,
|
|
79
|
+
setMessage: () => undefined,
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function runTaskWithLoader<T>(
|
|
85
|
+
ctx: AnyContext,
|
|
86
|
+
config: LoaderConfig,
|
|
87
|
+
task: (controls: TaskControls) => Promise<T>
|
|
88
|
+
): Promise<T | undefined> {
|
|
89
|
+
if (!hasCustomUI(ctx)) {
|
|
90
|
+
return runTaskWithoutLoader(task);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let taskError: unknown;
|
|
94
|
+
let startedTask: Promise<T> | undefined;
|
|
95
|
+
let cleanupStartedTaskUI: (() => void) | undefined;
|
|
96
|
+
|
|
97
|
+
const result = await ctx.ui.custom<
|
|
98
|
+
TaskSuccess<T> | typeof TASK_ABORTED | typeof TASK_FAILED | undefined
|
|
99
|
+
>((tui, theme, _keybindings, done) => {
|
|
100
|
+
let finished = false;
|
|
101
|
+
const finish = (
|
|
102
|
+
value: TaskSuccess<T> | typeof TASK_ABORTED | typeof TASK_FAILED | undefined
|
|
103
|
+
): void => {
|
|
104
|
+
if (finished) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
finished = true;
|
|
108
|
+
done(value);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const { container, loader, signal } = createLoaderComponent(
|
|
112
|
+
tui,
|
|
113
|
+
theme,
|
|
114
|
+
config.title,
|
|
115
|
+
config.message,
|
|
116
|
+
config.cancellable ?? true,
|
|
117
|
+
() => finish(TASK_ABORTED)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
cleanupStartedTaskUI = () => {
|
|
121
|
+
if (loader instanceof CancellableLoader) {
|
|
122
|
+
loader.dispose();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
loader.stop();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
startedTask = Promise.resolve().then(() =>
|
|
130
|
+
task({
|
|
131
|
+
signal,
|
|
132
|
+
setMessage: (message) => {
|
|
133
|
+
loader.setMessage(message);
|
|
134
|
+
tui.requestRender();
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
void startedTask
|
|
140
|
+
.then((value) => finish({ type: "ok", value }))
|
|
141
|
+
.catch((error) => {
|
|
142
|
+
if (signal.aborted) {
|
|
143
|
+
finish(TASK_ABORTED);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
taskError = error;
|
|
148
|
+
finish(TASK_FAILED);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
render(width: number) {
|
|
153
|
+
return container.render(width);
|
|
154
|
+
},
|
|
155
|
+
invalidate() {
|
|
156
|
+
container.invalidate();
|
|
157
|
+
},
|
|
158
|
+
handleInput(data: string) {
|
|
159
|
+
if (loader instanceof CancellableLoader) {
|
|
160
|
+
loader.handleInput(data);
|
|
161
|
+
tui.requestRender();
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
dispose() {
|
|
165
|
+
if (loader instanceof CancellableLoader) {
|
|
166
|
+
loader.dispose();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
loader.stop();
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (result === undefined) {
|
|
176
|
+
if (startedTask) {
|
|
177
|
+
return startedTask.finally(() => cleanupStartedTaskUI?.());
|
|
178
|
+
}
|
|
179
|
+
if (config.fallbackWithoutLoader) {
|
|
180
|
+
return runTaskWithoutLoader(task);
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (result === TASK_ABORTED) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (result === TASK_FAILED) {
|
|
190
|
+
throw taskError;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result.value;
|
|
194
|
+
}
|
package/src/ui/footer.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Footer helpers for the unified extension manager UI
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type State, type UnifiedItem } from "../types/index.js";
|
|
5
5
|
|
|
6
6
|
export interface FooterState {
|
|
7
|
-
hasToggleRows: boolean;
|
|
8
7
|
hasLocals: boolean;
|
|
9
8
|
hasPackages: boolean;
|
|
10
9
|
}
|
|
@@ -13,11 +12,8 @@ export interface FooterState {
|
|
|
13
12
|
* Build footer state from visible items.
|
|
14
13
|
*/
|
|
15
14
|
export function buildFooterState(items: UnifiedItem[]): FooterState {
|
|
16
|
-
const hasLocals = items.some((i) => i.type === "local");
|
|
17
|
-
|
|
18
15
|
return {
|
|
19
|
-
|
|
20
|
-
hasLocals,
|
|
16
|
+
hasLocals: items.some((i) => i.type === "local"),
|
|
21
17
|
hasPackages: items.some((i) => i.type === "package"),
|
|
22
18
|
};
|
|
23
19
|
}
|
|
@@ -47,8 +43,8 @@ export function buildFooterShortcuts(state: FooterState): string {
|
|
|
47
43
|
const parts: string[] = [];
|
|
48
44
|
parts.push("↑↓ Navigate");
|
|
49
45
|
|
|
50
|
-
if (state.
|
|
51
|
-
if (state.
|
|
46
|
+
if (state.hasLocals) parts.push("Space/Enter Toggle");
|
|
47
|
+
if (state.hasLocals) parts.push("S Save");
|
|
52
48
|
if (state.hasPackages) parts.push("Enter/A Actions");
|
|
53
49
|
if (state.hasPackages) parts.push("c Configure");
|
|
54
50
|
if (state.hasPackages) parts.push("u Update");
|
package/src/ui/help.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Help display
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
export function showHelp(ctx: ExtensionCommandContext): void {
|
|
7
7
|
const lines = [
|
|
@@ -17,7 +17,7 @@ export function showHelp(ctx: ExtensionCommandContext): void {
|
|
|
17
17
|
" Space/Enter Toggle local extension enabled/disabled",
|
|
18
18
|
" S Save changes to local extensions",
|
|
19
19
|
" Enter/A Open actions for selected package",
|
|
20
|
-
" c Configure selected package extensions (
|
|
20
|
+
" c Configure selected package extensions (reload after save)",
|
|
21
21
|
" u Update selected package",
|
|
22
22
|
" X Remove selected item (package or local extension)",
|
|
23
23
|
" i Quick install by source",
|