pi-extmgr 0.1.20 → 0.1.22
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/commands/registry.ts +12 -8
- package/src/constants.ts +48 -17
- package/src/packages/extensions.ts +252 -31
- package/src/packages/install.ts +17 -28
- package/src/packages/management.ts +2 -58
- package/src/types/index.ts +7 -6
- package/src/ui/footer.ts +67 -0
- package/src/ui/help.ts +1 -0
- package/src/ui/package-config.ts +361 -0
- package/src/ui/unified.ts +87 -163
- package/src/utils/auto-update.ts +10 -18
- package/src/utils/format.ts +31 -16
- package/src/utils/history.ts +22 -16
- package/src/utils/settings.ts +15 -23
- package/src/utils/timer.ts +36 -0
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
isSourceInstalled,
|
|
11
11
|
} from "./discovery.js";
|
|
12
12
|
import { waitForCondition } from "../utils/retry.js";
|
|
13
|
-
import { formatInstalledPackageLabel,
|
|
13
|
+
import { formatInstalledPackageLabel, parseNpmSource } from "../utils/format.js";
|
|
14
14
|
import { getPackageSourceKind, splitGitRepoAndRef } from "../utils/package-source.js";
|
|
15
15
|
import { logPackageUpdate, logPackageRemove } from "../utils/history.js";
|
|
16
16
|
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
@@ -84,7 +84,7 @@ async function updatePackagesInternal(
|
|
|
84
84
|
): Promise<PackageMutationOutcome> {
|
|
85
85
|
showProgress(ctx, "Updating", "all packages");
|
|
86
86
|
|
|
87
|
-
const res = await pi.exec("pi", ["update"], { timeout:
|
|
87
|
+
const res = await pi.exec("pi", ["update"], { timeout: TIMEOUTS.packageUpdateAll, cwd: ctx.cwd });
|
|
88
88
|
|
|
89
89
|
if (res.code !== 0) {
|
|
90
90
|
notifyError(ctx, `Update failed: ${res.stderr || res.stdout || `exit ${res.code}`}`);
|
|
@@ -408,62 +408,6 @@ export async function promptRemove(ctx: ExtensionCommandContext, pi: ExtensionAP
|
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
410
|
|
|
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
411
|
export async function showInstalledPackagesList(
|
|
468
412
|
ctx: ExtensionCommandContext,
|
|
469
413
|
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
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer helpers for the unified extension manager UI
|
|
3
|
+
*/
|
|
4
|
+
import type { UnifiedItem, State } from "../types/index.js";
|
|
5
|
+
|
|
6
|
+
export interface FooterState {
|
|
7
|
+
hasToggleRows: boolean;
|
|
8
|
+
hasLocals: boolean;
|
|
9
|
+
hasPackages: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build footer state from visible items.
|
|
14
|
+
*/
|
|
15
|
+
export function buildFooterState(items: UnifiedItem[]): FooterState {
|
|
16
|
+
const hasLocals = items.some((i) => i.type === "local");
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
hasToggleRows: hasLocals,
|
|
20
|
+
hasLocals,
|
|
21
|
+
hasPackages: items.some((i) => i.type === "package"),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getPendingToggleChangeCount(
|
|
26
|
+
staged: Map<string, State>,
|
|
27
|
+
byId: Map<string, UnifiedItem>
|
|
28
|
+
): number {
|
|
29
|
+
let count = 0;
|
|
30
|
+
|
|
31
|
+
for (const [id, state] of staged.entries()) {
|
|
32
|
+
const item = byId.get(id);
|
|
33
|
+
if (!item) continue;
|
|
34
|
+
|
|
35
|
+
if (item.type === "local" && item.originalState !== state) {
|
|
36
|
+
count += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return count;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build keyboard shortcuts text for the footer.
|
|
45
|
+
*/
|
|
46
|
+
export function buildFooterShortcuts(state: FooterState): string {
|
|
47
|
+
const parts: string[] = [];
|
|
48
|
+
parts.push("↑↓ Navigate");
|
|
49
|
+
|
|
50
|
+
if (state.hasToggleRows) parts.push("Space/Enter Toggle");
|
|
51
|
+
if (state.hasToggleRows) parts.push("S Save");
|
|
52
|
+
if (state.hasPackages) parts.push("Enter/A Actions");
|
|
53
|
+
if (state.hasPackages) parts.push("c Configure");
|
|
54
|
+
if (state.hasPackages) parts.push("u Update");
|
|
55
|
+
if (state.hasPackages || state.hasLocals) parts.push("X Remove");
|
|
56
|
+
|
|
57
|
+
parts.push("i Install");
|
|
58
|
+
parts.push("f Search");
|
|
59
|
+
parts.push("U Update all");
|
|
60
|
+
parts.push("t Auto-update");
|
|
61
|
+
parts.push("P Palette");
|
|
62
|
+
parts.push("R Browse");
|
|
63
|
+
parts.push("? Help");
|
|
64
|
+
parts.push("Esc Cancel");
|
|
65
|
+
|
|
66
|
+
return parts.join(" | ");
|
|
67
|
+
}
|
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
|
+
}
|