pi-extmgr 0.1.21 → 0.1.23
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 +25 -17
- package/package.json +2 -2
- package/src/index.ts +6 -1
- package/src/packages/discovery.ts +17 -8
- package/src/packages/extensions.ts +252 -31
- package/src/packages/install.ts +10 -23
- package/src/packages/management.ts +60 -70
- package/src/types/index.ts +7 -6
- package/src/ui/footer.ts +3 -6
- package/src/ui/help.ts +1 -0
- package/src/ui/package-config.ts +361 -0
- package/src/ui/remote.ts +2 -2
- package/src/ui/unified.ts +88 -111
- package/src/utils/auto-update.ts +2 -2
- package/src/utils/command.ts +77 -1
- package/src/utils/format.ts +23 -3
- package/src/utils/npm-exec.ts +47 -0
- package/src/utils/package-source.ts +12 -0
|
@@ -10,8 +10,12 @@ import {
|
|
|
10
10
|
isSourceInstalled,
|
|
11
11
|
} from "./discovery.js";
|
|
12
12
|
import { waitForCondition } from "../utils/retry.js";
|
|
13
|
-
import { formatInstalledPackageLabel,
|
|
14
|
-
import {
|
|
13
|
+
import { formatInstalledPackageLabel, parseNpmSource } from "../utils/format.js";
|
|
14
|
+
import {
|
|
15
|
+
getPackageSourceKind,
|
|
16
|
+
normalizeLocalSourceIdentity,
|
|
17
|
+
splitGitRepoAndRef,
|
|
18
|
+
} from "../utils/package-source.js";
|
|
15
19
|
import { logPackageUpdate, logPackageRemove } from "../utils/history.js";
|
|
16
20
|
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
17
21
|
import { notify, error as notifyError, success } from "../utils/notify.js";
|
|
@@ -33,12 +37,19 @@ const NO_PACKAGE_MUTATION_OUTCOME: PackageMutationOutcome = {
|
|
|
33
37
|
reloaded: false,
|
|
34
38
|
};
|
|
35
39
|
|
|
40
|
+
const BULK_UPDATE_LABEL = "all packages";
|
|
41
|
+
|
|
36
42
|
function packageMutationOutcome(
|
|
37
43
|
overrides: Partial<PackageMutationOutcome>
|
|
38
44
|
): PackageMutationOutcome {
|
|
39
45
|
return { ...NO_PACKAGE_MUTATION_OUTCOME, ...overrides };
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
function isUpToDateOutput(stdout: string): boolean {
|
|
49
|
+
const pinnedAsStatus = /^\s*pinned\b(?!\s+dependency\b)(?:\s*$|\s*[:(-])/im.test(stdout);
|
|
50
|
+
return /already\s+up\s+to\s+date/i.test(stdout) || pinnedAsStatus;
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
async function updatePackageInternal(
|
|
43
54
|
source: string,
|
|
44
55
|
ctx: ExtensionCommandContext,
|
|
@@ -60,9 +71,10 @@ async function updatePackageInternal(
|
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
const stdout = res.stdout || "";
|
|
63
|
-
if (
|
|
74
|
+
if (isUpToDateOutput(stdout)) {
|
|
64
75
|
notify(ctx, `${source} is already up to date (or pinned).`, "info");
|
|
65
76
|
logPackageUpdate(pi, source, source, undefined, true);
|
|
77
|
+
clearUpdatesAvailable(pi, ctx);
|
|
66
78
|
void updateExtmgrStatus(ctx, pi);
|
|
67
79
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
68
80
|
}
|
|
@@ -87,18 +99,23 @@ async function updatePackagesInternal(
|
|
|
87
99
|
const res = await pi.exec("pi", ["update"], { timeout: TIMEOUTS.packageUpdateAll, cwd: ctx.cwd });
|
|
88
100
|
|
|
89
101
|
if (res.code !== 0) {
|
|
90
|
-
|
|
102
|
+
const errorMsg = `Update failed: ${res.stderr || res.stdout || `exit ${res.code}`}`;
|
|
103
|
+
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, false, errorMsg);
|
|
104
|
+
notifyError(ctx, errorMsg);
|
|
91
105
|
void updateExtmgrStatus(ctx, pi);
|
|
92
106
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
const stdout = res.stdout || "";
|
|
96
|
-
if (stdout
|
|
110
|
+
if (isUpToDateOutput(stdout) || stdout.trim() === "") {
|
|
97
111
|
notify(ctx, "All packages are already up to date.", "info");
|
|
112
|
+
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
|
|
113
|
+
clearUpdatesAvailable(pi, ctx);
|
|
98
114
|
void updateExtmgrStatus(ctx, pi);
|
|
99
115
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
100
116
|
}
|
|
101
117
|
|
|
118
|
+
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
|
|
102
119
|
success(ctx, "Packages updated");
|
|
103
120
|
clearUpdatesAvailable(pi, ctx);
|
|
104
121
|
|
|
@@ -145,12 +162,18 @@ function packageIdentity(source: string, fallbackName?: string): string {
|
|
|
145
162
|
return `npm:${npm.name}`;
|
|
146
163
|
}
|
|
147
164
|
|
|
148
|
-
|
|
165
|
+
const sourceKind = getPackageSourceKind(source);
|
|
166
|
+
|
|
167
|
+
if (sourceKind === "git") {
|
|
149
168
|
const gitSpec = source.startsWith("git:") ? source.slice(4) : source;
|
|
150
169
|
const { repo } = splitGitRepoAndRef(gitSpec);
|
|
151
170
|
return `git:${repo}`;
|
|
152
171
|
}
|
|
153
172
|
|
|
173
|
+
if (sourceKind === "local") {
|
|
174
|
+
return `src:${normalizeLocalSourceIdentity(source)}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
154
177
|
if (fallbackName) {
|
|
155
178
|
return `name:${fallbackName}`;
|
|
156
179
|
}
|
|
@@ -238,12 +261,18 @@ function formatRemovalTargets(targets: RemovalTarget[]): string {
|
|
|
238
261
|
return targets.map((t) => `${t.scope}: ${t.source}`).join("\n");
|
|
239
262
|
}
|
|
240
263
|
|
|
264
|
+
interface RemovalExecutionResult {
|
|
265
|
+
target: RemovalTarget;
|
|
266
|
+
success: boolean;
|
|
267
|
+
error?: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
241
270
|
async function executeRemovalTargets(
|
|
242
271
|
targets: RemovalTarget[],
|
|
243
272
|
ctx: ExtensionCommandContext,
|
|
244
273
|
pi: ExtensionAPI
|
|
245
|
-
): Promise<
|
|
246
|
-
const
|
|
274
|
+
): Promise<RemovalExecutionResult[]> {
|
|
275
|
+
const results: RemovalExecutionResult[] = [];
|
|
247
276
|
|
|
248
277
|
for (const target of targets) {
|
|
249
278
|
showProgress(ctx, "Removing", `${target.source} (${target.scope})`);
|
|
@@ -254,14 +283,15 @@ async function executeRemovalTargets(
|
|
|
254
283
|
if (res.code !== 0) {
|
|
255
284
|
const errorMsg = `Remove failed (${target.scope}): ${res.stderr || res.stdout || `exit ${res.code}`}`;
|
|
256
285
|
logPackageRemove(pi, target.source, target.name, false, errorMsg);
|
|
257
|
-
|
|
286
|
+
results.push({ target, success: false, error: errorMsg });
|
|
258
287
|
continue;
|
|
259
288
|
}
|
|
260
289
|
|
|
261
290
|
logPackageRemove(pi, target.source, target.name, true);
|
|
291
|
+
results.push({ target, success: true });
|
|
262
292
|
}
|
|
263
293
|
|
|
264
|
-
return
|
|
294
|
+
return results;
|
|
265
295
|
}
|
|
266
296
|
|
|
267
297
|
function notifyRemovalSummary(
|
|
@@ -326,9 +356,18 @@ async function removePackageInternal(
|
|
|
326
356
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
327
357
|
}
|
|
328
358
|
|
|
329
|
-
const
|
|
359
|
+
const results = await executeRemovalTargets(targets, ctx, pi);
|
|
330
360
|
clearSearchCache();
|
|
331
361
|
|
|
362
|
+
const failures = results
|
|
363
|
+
.filter((result): result is RemovalExecutionResult & { success: false; error: string } =>
|
|
364
|
+
Boolean(!result.success && result.error)
|
|
365
|
+
)
|
|
366
|
+
.map((result) => result.error);
|
|
367
|
+
const successfulTargets = results
|
|
368
|
+
.filter((result) => result.success)
|
|
369
|
+
.map((result) => result.target);
|
|
370
|
+
|
|
332
371
|
const remaining = (await getInstalledPackagesAllScopes(ctx, pi)).filter(
|
|
333
372
|
(p) => packageIdentity(p.source, p.name) === identity
|
|
334
373
|
);
|
|
@@ -338,13 +377,15 @@ async function removePackageInternal(
|
|
|
338
377
|
clearUpdatesAvailable(pi, ctx);
|
|
339
378
|
}
|
|
340
379
|
|
|
341
|
-
|
|
342
|
-
|
|
380
|
+
const successfulRemovalCount = successfulTargets.length;
|
|
381
|
+
|
|
382
|
+
// Wait for successfully removed targets to disappear from their target scopes before reloading.
|
|
383
|
+
if (successfulTargets.length > 0) {
|
|
343
384
|
notify(ctx, "Waiting for removal to complete...", "info");
|
|
344
385
|
const isRemoved = await waitForCondition(
|
|
345
386
|
async () => {
|
|
346
387
|
const installedChecks = await Promise.all(
|
|
347
|
-
|
|
388
|
+
successfulTargets.map((target) =>
|
|
348
389
|
isSourceInstalled(target.source, ctx, pi, {
|
|
349
390
|
scope: target.scope,
|
|
350
391
|
})
|
|
@@ -360,6 +401,11 @@ async function removePackageInternal(
|
|
|
360
401
|
}
|
|
361
402
|
}
|
|
362
403
|
|
|
404
|
+
if (successfulRemovalCount === 0) {
|
|
405
|
+
void updateExtmgrStatus(ctx, pi);
|
|
406
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
407
|
+
}
|
|
408
|
+
|
|
363
409
|
const reloaded = await confirmReload(ctx, "Removal complete.");
|
|
364
410
|
if (!reloaded) {
|
|
365
411
|
void updateExtmgrStatus(ctx, pi);
|
|
@@ -408,62 +454,6 @@ export async function promptRemove(ctx: ExtensionCommandContext, pi: ExtensionAP
|
|
|
408
454
|
}
|
|
409
455
|
}
|
|
410
456
|
|
|
411
|
-
export async function showPackageActions(
|
|
412
|
-
pkg: InstalledPackage,
|
|
413
|
-
ctx: ExtensionCommandContext,
|
|
414
|
-
pi: ExtensionAPI
|
|
415
|
-
): Promise<boolean> {
|
|
416
|
-
if (!requireUI(ctx, "Package actions")) {
|
|
417
|
-
console.log(`Package: ${pkg.name}`);
|
|
418
|
-
console.log(`Version: ${pkg.version || "unknown"}`);
|
|
419
|
-
console.log(`Source: ${pkg.source}`);
|
|
420
|
-
console.log(`Scope: ${pkg.scope}`);
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const choice = await ctx.ui.select(pkg.name, [
|
|
425
|
-
`Remove ${pkg.name}`,
|
|
426
|
-
`Update ${pkg.name}`,
|
|
427
|
-
"View details",
|
|
428
|
-
"Back to manager",
|
|
429
|
-
]);
|
|
430
|
-
|
|
431
|
-
if (!choice || choice.includes("Back")) {
|
|
432
|
-
return false;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const action = choice.startsWith("Remove")
|
|
436
|
-
? "remove"
|
|
437
|
-
: choice.startsWith("Update")
|
|
438
|
-
? "update"
|
|
439
|
-
: choice.includes("details")
|
|
440
|
-
? "details"
|
|
441
|
-
: "back";
|
|
442
|
-
|
|
443
|
-
switch (action) {
|
|
444
|
-
case "remove": {
|
|
445
|
-
const outcome = await removePackageWithOutcome(pkg.source, ctx, pi);
|
|
446
|
-
return outcome.reloaded;
|
|
447
|
-
}
|
|
448
|
-
case "update": {
|
|
449
|
-
const outcome = await updatePackageWithOutcome(pkg.source, ctx, pi);
|
|
450
|
-
return outcome.reloaded;
|
|
451
|
-
}
|
|
452
|
-
case "details": {
|
|
453
|
-
const sizeStr = pkg.size !== undefined ? `\nSize: ${formatBytes(pkg.size)}` : "";
|
|
454
|
-
notify(
|
|
455
|
-
ctx,
|
|
456
|
-
`Name: ${pkg.name}\nVersion: ${pkg.version || "unknown"}\nSource: ${pkg.source}\nScope: ${pkg.scope}${sizeStr}`,
|
|
457
|
-
"info"
|
|
458
|
-
);
|
|
459
|
-
return showPackageActions(pkg, ctx, pi);
|
|
460
|
-
}
|
|
461
|
-
case "back":
|
|
462
|
-
default:
|
|
463
|
-
return false;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
457
|
export async function showInstalledPackagesList(
|
|
468
458
|
ctx: ExtensionCommandContext,
|
|
469
459
|
pi: ExtensionAPI
|
package/src/types/index.ts
CHANGED
|
@@ -47,11 +47,11 @@ export interface PackageExtensionEntry {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export interface UnifiedItem {
|
|
50
|
-
type: "local" | "package"
|
|
50
|
+
type: "local" | "package";
|
|
51
51
|
id: string;
|
|
52
52
|
displayName: string;
|
|
53
53
|
summary: string;
|
|
54
|
-
scope: Scope
|
|
54
|
+
scope: Scope;
|
|
55
55
|
// Local extension fields
|
|
56
56
|
state?: State | undefined;
|
|
57
57
|
activePath?: string | undefined;
|
|
@@ -63,9 +63,6 @@ export interface UnifiedItem {
|
|
|
63
63
|
description?: string | undefined;
|
|
64
64
|
size?: number | undefined; // Package size in bytes
|
|
65
65
|
updateAvailable?: boolean | undefined;
|
|
66
|
-
// Package extension fields
|
|
67
|
-
packageSource?: string | undefined;
|
|
68
|
-
extensionPath?: string | undefined;
|
|
69
66
|
}
|
|
70
67
|
|
|
71
68
|
export interface SearchCache {
|
|
@@ -82,7 +79,11 @@ export type UnifiedAction =
|
|
|
82
79
|
| { type: "help" }
|
|
83
80
|
| { type: "menu" }
|
|
84
81
|
| { type: "quick"; action: "install" | "search" | "update-all" | "auto-update" }
|
|
85
|
-
| {
|
|
82
|
+
| {
|
|
83
|
+
type: "action";
|
|
84
|
+
itemId: string;
|
|
85
|
+
action?: "menu" | "update" | "remove" | "details" | "configure";
|
|
86
|
+
};
|
|
86
87
|
|
|
87
88
|
export type BrowseAction =
|
|
88
89
|
| { type: "package"; name: string }
|
package/src/ui/footer.ts
CHANGED
|
@@ -14,10 +14,9 @@ export interface FooterState {
|
|
|
14
14
|
*/
|
|
15
15
|
export function buildFooterState(items: UnifiedItem[]): FooterState {
|
|
16
16
|
const hasLocals = items.some((i) => i.type === "local");
|
|
17
|
-
const hasPackageExtensions = items.some((i) => i.type === "package-extension");
|
|
18
17
|
|
|
19
18
|
return {
|
|
20
|
-
hasToggleRows: hasLocals
|
|
19
|
+
hasToggleRows: hasLocals,
|
|
21
20
|
hasLocals,
|
|
22
21
|
hasPackages: items.some((i) => i.type === "package"),
|
|
23
22
|
};
|
|
@@ -33,10 +32,7 @@ export function getPendingToggleChangeCount(
|
|
|
33
32
|
const item = byId.get(id);
|
|
34
33
|
if (!item) continue;
|
|
35
34
|
|
|
36
|
-
if (
|
|
37
|
-
(item.type === "local" || item.type === "package-extension") &&
|
|
38
|
-
item.originalState !== state
|
|
39
|
-
) {
|
|
35
|
+
if (item.type === "local" && item.originalState !== state) {
|
|
40
36
|
count += 1;
|
|
41
37
|
}
|
|
42
38
|
}
|
|
@@ -54,6 +50,7 @@ export function buildFooterShortcuts(state: FooterState): string {
|
|
|
54
50
|
if (state.hasToggleRows) parts.push("Space/Enter Toggle");
|
|
55
51
|
if (state.hasToggleRows) parts.push("S Save");
|
|
56
52
|
if (state.hasPackages) parts.push("Enter/A Actions");
|
|
53
|
+
if (state.hasPackages) parts.push("c Configure");
|
|
57
54
|
if (state.hasPackages) parts.push("u Update");
|
|
58
55
|
if (state.hasPackages || state.hasLocals) parts.push("X Remove");
|
|
59
56
|
|
package/src/ui/help.ts
CHANGED
|
@@ -17,6 +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 (restart required after save)",
|
|
20
21
|
" u Update selected package",
|
|
21
22
|
" X Remove selected item (package or local extension)",
|
|
22
23
|
" i Quick install by source",
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package extension configuration panel.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { DynamicBorder, getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
Container,
|
|
8
|
+
Key,
|
|
9
|
+
matchesKey,
|
|
10
|
+
SettingsList,
|
|
11
|
+
Spacer,
|
|
12
|
+
Text,
|
|
13
|
+
type SettingItem,
|
|
14
|
+
} from "@mariozechner/pi-tui";
|
|
15
|
+
import type { InstalledPackage, PackageExtensionEntry, State } from "../types/index.js";
|
|
16
|
+
import { discoverPackageExtensions, setPackageExtensionState } from "../packages/extensions.js";
|
|
17
|
+
import { notify } from "../utils/notify.js";
|
|
18
|
+
import { logExtensionToggle } from "../utils/history.js";
|
|
19
|
+
import { getPackageSourceKind } from "../utils/package-source.js";
|
|
20
|
+
import { fileExists } from "../utils/fs.js";
|
|
21
|
+
import { UI } from "../constants.js";
|
|
22
|
+
import { getChangeMarker, getPackageIcon, getScopeIcon, getStatusIcon } from "./theme.js";
|
|
23
|
+
|
|
24
|
+
interface SelectableList {
|
|
25
|
+
selectedIndex?: number;
|
|
26
|
+
handleInput?(data: string): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PackageConfigRow {
|
|
30
|
+
id: string;
|
|
31
|
+
extensionPath: string;
|
|
32
|
+
summary: string;
|
|
33
|
+
originalState: State;
|
|
34
|
+
available: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type ConfigurePanelAction = { type: "cancel" } | { type: "save" };
|
|
38
|
+
|
|
39
|
+
function getSelectedIndex(settingsList: unknown): number | undefined {
|
|
40
|
+
if (settingsList && typeof settingsList === "object") {
|
|
41
|
+
const selectable = settingsList as SelectableList;
|
|
42
|
+
if (typeof selectable.selectedIndex === "number") {
|
|
43
|
+
return selectable.selectedIndex;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function buildPackageConfigRows(
|
|
50
|
+
entries: PackageExtensionEntry[]
|
|
51
|
+
): Promise<PackageConfigRow[]> {
|
|
52
|
+
const dedupedEntries = new Map<string, PackageExtensionEntry>();
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (!dedupedEntries.has(entry.extensionPath)) {
|
|
55
|
+
dedupedEntries.set(entry.extensionPath, entry);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rows = await Promise.all(
|
|
60
|
+
Array.from(dedupedEntries.values()).map(async (entry) => ({
|
|
61
|
+
id: entry.id,
|
|
62
|
+
extensionPath: entry.extensionPath,
|
|
63
|
+
summary: entry.summary,
|
|
64
|
+
originalState: entry.state,
|
|
65
|
+
available: await fileExists(entry.absolutePath),
|
|
66
|
+
}))
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
rows.sort((a, b) => a.extensionPath.localeCompare(b.extensionPath));
|
|
70
|
+
return rows;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatConfigRowLabel(
|
|
74
|
+
row: PackageConfigRow,
|
|
75
|
+
state: State,
|
|
76
|
+
pkg: InstalledPackage,
|
|
77
|
+
theme: Theme,
|
|
78
|
+
changed: boolean
|
|
79
|
+
): string {
|
|
80
|
+
const statusIcon = getStatusIcon(theme, state);
|
|
81
|
+
const scopeIcon = getScopeIcon(theme, pkg.scope);
|
|
82
|
+
const sourceKind = getPackageSourceKind(pkg.source);
|
|
83
|
+
const pkgIcon = getPackageIcon(
|
|
84
|
+
theme,
|
|
85
|
+
sourceKind === "npm" || sourceKind === "git" || sourceKind === "local" ? sourceKind : "local"
|
|
86
|
+
);
|
|
87
|
+
const changeMarker = getChangeMarker(theme, changed);
|
|
88
|
+
const name = theme.bold(row.extensionPath);
|
|
89
|
+
const availability = row.available
|
|
90
|
+
? ""
|
|
91
|
+
: ` ${theme.fg("warning", "[missing]")}${theme.fg("dim", " (cannot toggle)")}`;
|
|
92
|
+
const summary = theme.fg("dim", row.summary);
|
|
93
|
+
|
|
94
|
+
return `${statusIcon} ${pkgIcon} [${scopeIcon}] ${name}${availability} - ${summary}${changeMarker}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildSettingItems(
|
|
98
|
+
rows: PackageConfigRow[],
|
|
99
|
+
staged: Map<string, State>,
|
|
100
|
+
pkg: InstalledPackage,
|
|
101
|
+
theme: Theme
|
|
102
|
+
): SettingItem[] {
|
|
103
|
+
return rows.map((row) => {
|
|
104
|
+
const current = staged.get(row.id) ?? row.originalState;
|
|
105
|
+
const changed = current !== row.originalState;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
id: row.id,
|
|
109
|
+
label: formatConfigRowLabel(row, current, pkg, theme, changed),
|
|
110
|
+
currentValue: current,
|
|
111
|
+
values: row.available ? ["enabled", "disabled"] : [current],
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getPendingChangeCount(rows: PackageConfigRow[], staged: Map<string, State>): number {
|
|
117
|
+
let count = 0;
|
|
118
|
+
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
const target = staged.get(row.id);
|
|
121
|
+
if (!target) continue;
|
|
122
|
+
if (target !== row.originalState) count += 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return count;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function showConfigurePanel(
|
|
129
|
+
pkg: InstalledPackage,
|
|
130
|
+
rows: PackageConfigRow[],
|
|
131
|
+
staged: Map<string, State>,
|
|
132
|
+
ctx: ExtensionCommandContext
|
|
133
|
+
): Promise<ConfigurePanelAction> {
|
|
134
|
+
return ctx.ui.custom<ConfigurePanelAction>((tui, theme, _keybindings, done) => {
|
|
135
|
+
const container = new Container();
|
|
136
|
+
|
|
137
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
138
|
+
container.addChild(
|
|
139
|
+
new Text(theme.fg("accent", theme.bold(`Configure extensions: ${pkg.name}`)), 2, 0)
|
|
140
|
+
);
|
|
141
|
+
container.addChild(
|
|
142
|
+
new Text(
|
|
143
|
+
theme.fg(
|
|
144
|
+
"muted",
|
|
145
|
+
`${rows.length} extension path${rows.length === 1 ? "" : "s"} • Space/Enter toggle • S save • Esc cancel`
|
|
146
|
+
),
|
|
147
|
+
2,
|
|
148
|
+
0
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
container.addChild(new Spacer(1));
|
|
152
|
+
|
|
153
|
+
const settingsItems = buildSettingItems(rows, staged, pkg, theme);
|
|
154
|
+
const rowById = new Map(rows.map((row) => [row.id, row]));
|
|
155
|
+
|
|
156
|
+
const settingsList = new SettingsList(
|
|
157
|
+
settingsItems,
|
|
158
|
+
Math.min(rows.length + 2, UI.maxListHeight),
|
|
159
|
+
getSettingsListTheme(),
|
|
160
|
+
(id: string, newValue: string) => {
|
|
161
|
+
const row = rowById.get(id);
|
|
162
|
+
if (!row || !row.available) return;
|
|
163
|
+
|
|
164
|
+
const state = newValue as State;
|
|
165
|
+
staged.set(id, state);
|
|
166
|
+
|
|
167
|
+
const settingsItem = settingsItems.find((item) => item.id === id);
|
|
168
|
+
if (settingsItem) {
|
|
169
|
+
settingsItem.label = formatConfigRowLabel(
|
|
170
|
+
row,
|
|
171
|
+
state,
|
|
172
|
+
pkg,
|
|
173
|
+
theme,
|
|
174
|
+
state !== row.originalState
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
tui.requestRender();
|
|
179
|
+
},
|
|
180
|
+
() => done({ type: "cancel" }),
|
|
181
|
+
{ enableSearch: rows.length > UI.searchThreshold }
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
container.addChild(settingsList);
|
|
185
|
+
container.addChild(new Spacer(1));
|
|
186
|
+
container.addChild(
|
|
187
|
+
new Text(theme.fg("dim", "↑↓ Navigate | Space/Enter Toggle | S Save | Esc Back"), 2, 0)
|
|
188
|
+
);
|
|
189
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
render(width: number) {
|
|
193
|
+
return container.render(width);
|
|
194
|
+
},
|
|
195
|
+
invalidate() {
|
|
196
|
+
container.invalidate();
|
|
197
|
+
},
|
|
198
|
+
handleInput(data: string) {
|
|
199
|
+
if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
|
|
200
|
+
done({ type: "save" });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const selectedIndex = getSelectedIndex(settingsList) ?? 0;
|
|
205
|
+
const selectedId = settingsItems[selectedIndex]?.id ?? settingsItems[0]?.id;
|
|
206
|
+
const selectedRow = selectedId ? rowById.get(selectedId) : undefined;
|
|
207
|
+
|
|
208
|
+
if (
|
|
209
|
+
selectedRow &&
|
|
210
|
+
!selectedRow.available &&
|
|
211
|
+
(data === " " || data === "\r" || data === "\n")
|
|
212
|
+
) {
|
|
213
|
+
notify(
|
|
214
|
+
ctx,
|
|
215
|
+
`${selectedRow.extensionPath} is missing on disk and cannot be toggled.`,
|
|
216
|
+
"warning"
|
|
217
|
+
);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
settingsList.handleInput?.(data);
|
|
222
|
+
tui.requestRender();
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function applyPackageExtensionChanges(
|
|
229
|
+
rows: PackageConfigRow[],
|
|
230
|
+
staged: Map<string, State>,
|
|
231
|
+
pkg: InstalledPackage,
|
|
232
|
+
cwd: string,
|
|
233
|
+
pi: ExtensionAPI
|
|
234
|
+
): Promise<{ changed: number; errors: string[] }> {
|
|
235
|
+
let changed = 0;
|
|
236
|
+
const errors: string[] = [];
|
|
237
|
+
|
|
238
|
+
const sortedRows = [...rows].sort((a, b) => a.extensionPath.localeCompare(b.extensionPath));
|
|
239
|
+
|
|
240
|
+
for (const row of sortedRows) {
|
|
241
|
+
const target = staged.get(row.id) ?? row.originalState;
|
|
242
|
+
if (target === row.originalState) continue;
|
|
243
|
+
|
|
244
|
+
if (!row.available) {
|
|
245
|
+
const error = `${row.extensionPath}: extension entrypoint is missing on disk`;
|
|
246
|
+
errors.push(error);
|
|
247
|
+
logExtensionToggle(pi, row.id, row.originalState, target, false, error);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const result = await setPackageExtensionState(
|
|
252
|
+
pkg.source,
|
|
253
|
+
row.extensionPath,
|
|
254
|
+
pkg.scope,
|
|
255
|
+
target,
|
|
256
|
+
cwd
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (result.ok) {
|
|
260
|
+
changed += 1;
|
|
261
|
+
logExtensionToggle(pi, row.id, row.originalState, target, true);
|
|
262
|
+
} else {
|
|
263
|
+
errors.push(`${row.extensionPath}: ${result.error}`);
|
|
264
|
+
logExtensionToggle(pi, row.id, row.originalState, target, false, result.error);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { changed, errors };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function promptRestartForPackageConfig(ctx: ExtensionCommandContext): Promise<boolean> {
|
|
272
|
+
if (!ctx.hasUI) {
|
|
273
|
+
notify(
|
|
274
|
+
ctx,
|
|
275
|
+
"Restart pi to apply package extension configuration changes. /reload may not be enough.",
|
|
276
|
+
"warning"
|
|
277
|
+
);
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const restartNow = await ctx.ui.confirm(
|
|
282
|
+
"Restart Required",
|
|
283
|
+
"Package extension configuration changed.\nA full pi restart is required to apply it.\nExit pi now?"
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (!restartNow) {
|
|
287
|
+
notify(
|
|
288
|
+
ctx,
|
|
289
|
+
"Restart pi manually to apply package extension configuration changes. /reload may not be enough.",
|
|
290
|
+
"warning"
|
|
291
|
+
);
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
notify(ctx, "Shutting down pi. Start it again to apply changes.", "info");
|
|
296
|
+
ctx.shutdown();
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function configurePackageExtensions(
|
|
301
|
+
pkg: InstalledPackage,
|
|
302
|
+
ctx: ExtensionCommandContext,
|
|
303
|
+
pi: ExtensionAPI
|
|
304
|
+
): Promise<{ changed: number; reloaded: boolean }> {
|
|
305
|
+
const discovered = await discoverPackageExtensions([pkg], ctx.cwd);
|
|
306
|
+
const rows = await buildPackageConfigRows(discovered);
|
|
307
|
+
|
|
308
|
+
if (rows.length === 0) {
|
|
309
|
+
notify(ctx, "No configurable extensions discovered for this package.", "info");
|
|
310
|
+
return { changed: 0, reloaded: false };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const staged = new Map<string, State>();
|
|
314
|
+
|
|
315
|
+
while (true) {
|
|
316
|
+
const action = await showConfigurePanel(pkg, rows, staged, ctx);
|
|
317
|
+
|
|
318
|
+
if (action.type === "cancel") {
|
|
319
|
+
const pending = getPendingChangeCount(rows, staged);
|
|
320
|
+
if (pending === 0) {
|
|
321
|
+
return { changed: 0, reloaded: false };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const choice = await ctx.ui.select(`Unsaved changes (${pending})`, [
|
|
325
|
+
"Save and back",
|
|
326
|
+
"Discard changes",
|
|
327
|
+
"Stay in configure",
|
|
328
|
+
]);
|
|
329
|
+
|
|
330
|
+
if (!choice || choice === "Stay in configure") {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (choice === "Discard changes") {
|
|
335
|
+
return { changed: 0, reloaded: false };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const apply = await applyPackageExtensionChanges(rows, staged, pkg, ctx.cwd, pi);
|
|
340
|
+
|
|
341
|
+
if (apply.errors.length > 0) {
|
|
342
|
+
notify(
|
|
343
|
+
ctx,
|
|
344
|
+
`Applied ${apply.changed} change(s), ${apply.errors.length} failed.\n${apply.errors.join("\n")}`,
|
|
345
|
+
"warning"
|
|
346
|
+
);
|
|
347
|
+
} else if (apply.changed === 0) {
|
|
348
|
+
notify(ctx, "No changes to apply.", "info");
|
|
349
|
+
return { changed: 0, reloaded: false };
|
|
350
|
+
} else {
|
|
351
|
+
notify(ctx, `Applied ${apply.changed} package extension change(s).`, "info");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (apply.changed === 0) {
|
|
355
|
+
return { changed: 0, reloaded: false };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const restarted = await promptRestartForPackageConfig(ctx);
|
|
359
|
+
return { changed: apply.changed, reloaded: restarted };
|
|
360
|
+
}
|
|
361
|
+
}
|
package/src/ui/remote.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
isCacheValid,
|
|
16
16
|
} from "../packages/discovery.js";
|
|
17
17
|
import { installPackage, installPackageLocally } from "../packages/install.js";
|
|
18
|
+
import { execNpm } from "../utils/npm-exec.js";
|
|
18
19
|
import { notify } from "../utils/notify.js";
|
|
19
20
|
|
|
20
21
|
interface PackageInfoCacheEntry {
|
|
@@ -143,9 +144,8 @@ async function buildPackageInfoText(
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
const [infoRes, weeklyDownloads] = await Promise.all([
|
|
146
|
-
pi
|
|
147
|
+
execNpm(pi, ["view", packageName, "--json"], ctx, {
|
|
147
148
|
timeout: TIMEOUTS.npmView,
|
|
148
|
-
cwd: ctx.cwd,
|
|
149
149
|
}),
|
|
150
150
|
fetchWeeklyDownloads(packageName),
|
|
151
151
|
]);
|