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
package/src/ui/package-config.ts
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package extension configuration panel.
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
DynamicBorder,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionCommandContext,
|
|
8
|
+
getSettingsListTheme,
|
|
9
|
+
type Theme,
|
|
10
|
+
} from "@mariozechner/pi-coding-agent";
|
|
6
11
|
import {
|
|
7
12
|
Container,
|
|
8
13
|
Key,
|
|
9
14
|
matchesKey,
|
|
15
|
+
type SettingItem,
|
|
10
16
|
SettingsList,
|
|
11
17
|
Spacer,
|
|
12
18
|
Text,
|
|
13
|
-
type SettingItem,
|
|
14
19
|
} from "@mariozechner/pi-tui";
|
|
15
|
-
import
|
|
20
|
+
import { UI } from "../constants.js";
|
|
16
21
|
import {
|
|
17
22
|
applyPackageExtensionStateChanges,
|
|
18
23
|
discoverPackageExtensions,
|
|
19
24
|
validatePackageExtensionSettings,
|
|
20
25
|
} from "../packages/extensions.js";
|
|
21
|
-
import {
|
|
26
|
+
import { type InstalledPackage, type PackageExtensionEntry, type State } from "../types/index.js";
|
|
27
|
+
import { fileExists } from "../utils/fs.js";
|
|
22
28
|
import { logExtensionToggle } from "../utils/history.js";
|
|
23
29
|
import { requireCustomUI, runCustomUI } from "../utils/mode.js";
|
|
30
|
+
import { notify } from "../utils/notify.js";
|
|
24
31
|
import { getPackageSourceKind } from "../utils/package-source.js";
|
|
25
32
|
import { getSettingsListSelectedIndex } from "../utils/settings-list.js";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
33
|
+
import { confirmReload } from "../utils/ui-helpers.js";
|
|
34
|
+
import { runTaskWithLoader } from "./async-task.js";
|
|
28
35
|
import { getChangeMarker, getPackageIcon, getScopeIcon, getStatusIcon } from "./theme.js";
|
|
29
36
|
|
|
30
37
|
export interface PackageConfigRow {
|
|
@@ -167,10 +174,14 @@ async function showConfigurePanel(
|
|
|
167
174
|
getSettingsListTheme(),
|
|
168
175
|
(id: string, newValue: string) => {
|
|
169
176
|
const row = rowById.get(id);
|
|
170
|
-
if (!row
|
|
177
|
+
if (!row?.available) return;
|
|
171
178
|
|
|
172
179
|
const state = newValue as State;
|
|
173
|
-
|
|
180
|
+
if (state === row.originalState) {
|
|
181
|
+
staged.delete(id);
|
|
182
|
+
} else {
|
|
183
|
+
staged.set(id, state);
|
|
184
|
+
}
|
|
174
185
|
|
|
175
186
|
const settingsItem = settingsItems.find((item) => item.id === id);
|
|
176
187
|
if (settingsItem) {
|
|
@@ -285,35 +296,6 @@ export async function applyPackageExtensionChanges(
|
|
|
285
296
|
return { changed: changedRows.length, errors };
|
|
286
297
|
}
|
|
287
298
|
|
|
288
|
-
async function promptRestartForPackageConfig(ctx: ExtensionCommandContext): Promise<boolean> {
|
|
289
|
-
if (!ctx.hasUI) {
|
|
290
|
-
notify(
|
|
291
|
-
ctx,
|
|
292
|
-
"Restart pi to apply package extension configuration changes. /reload may not be enough.",
|
|
293
|
-
"warning"
|
|
294
|
-
);
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const restartNow = await ctx.ui.confirm(
|
|
299
|
-
"Restart Required",
|
|
300
|
-
"Package extension configuration changed.\nA full pi restart is required to apply it.\nExit pi now?"
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
if (!restartNow) {
|
|
304
|
-
notify(
|
|
305
|
-
ctx,
|
|
306
|
-
"Restart pi manually to apply package extension configuration changes. /reload may not be enough.",
|
|
307
|
-
"warning"
|
|
308
|
-
);
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
notify(ctx, "Shutting down pi. Start it again to apply changes.", "info");
|
|
313
|
-
ctx.shutdown();
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
299
|
export async function configurePackageExtensions(
|
|
318
300
|
pkg: InstalledPackage,
|
|
319
301
|
ctx: ExtensionCommandContext,
|
|
@@ -329,8 +311,32 @@ export async function configurePackageExtensions(
|
|
|
329
311
|
return { changed: 0, reloaded: false };
|
|
330
312
|
}
|
|
331
313
|
|
|
332
|
-
|
|
333
|
-
|
|
314
|
+
let initialData: { rows: PackageConfigRow[] } | undefined;
|
|
315
|
+
try {
|
|
316
|
+
initialData = await runTaskWithLoader(
|
|
317
|
+
ctx,
|
|
318
|
+
{
|
|
319
|
+
title: `Configure ${pkg.name}`,
|
|
320
|
+
message: "Discovering package extensions...",
|
|
321
|
+
cancellable: false,
|
|
322
|
+
},
|
|
323
|
+
async () => {
|
|
324
|
+
const discovered = await discoverPackageExtensions([pkg], ctx.cwd);
|
|
325
|
+
const rows = await buildPackageConfigRows(discovered);
|
|
326
|
+
return { rows };
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
331
|
+
return { changed: 0, reloaded: false };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!initialData) {
|
|
335
|
+
notify(ctx, "Package extension configuration requires the full interactive TUI.", "warning");
|
|
336
|
+
return { changed: 0, reloaded: false };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const { rows } = initialData;
|
|
334
340
|
|
|
335
341
|
if (rows.length === 0) {
|
|
336
342
|
notify(ctx, "No configurable extensions discovered for this package.", "info");
|
|
@@ -368,24 +374,31 @@ export async function configurePackageExtensions(
|
|
|
368
374
|
|
|
369
375
|
const apply = await applyPackageExtensionChanges(rows, staged, pkg, ctx.cwd, pi);
|
|
370
376
|
|
|
377
|
+
if (apply.changed === 0) {
|
|
378
|
+
if (apply.errors.length > 0) {
|
|
379
|
+
notify(
|
|
380
|
+
ctx,
|
|
381
|
+
`Applied ${apply.changed} change(s), ${apply.errors.length} failed.\n${apply.errors.join("\n")}`,
|
|
382
|
+
"warning"
|
|
383
|
+
);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
notify(ctx, "No changes to apply.", "info");
|
|
388
|
+
return { changed: 0, reloaded: false };
|
|
389
|
+
}
|
|
390
|
+
|
|
371
391
|
if (apply.errors.length > 0) {
|
|
372
392
|
notify(
|
|
373
393
|
ctx,
|
|
374
394
|
`Applied ${apply.changed} change(s), ${apply.errors.length} failed.\n${apply.errors.join("\n")}`,
|
|
375
395
|
"warning"
|
|
376
396
|
);
|
|
377
|
-
} else if (apply.changed === 0) {
|
|
378
|
-
notify(ctx, "No changes to apply.", "info");
|
|
379
|
-
return { changed: 0, reloaded: false };
|
|
380
397
|
} else {
|
|
381
398
|
notify(ctx, `Applied ${apply.changed} package extension change(s).`, "info");
|
|
382
399
|
}
|
|
383
400
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const restarted = await promptRestartForPackageConfig(ctx);
|
|
389
|
-
return { changed: apply.changed, reloaded: restarted };
|
|
401
|
+
const reloaded = await confirmReload(ctx, "Package extension configuration changed.");
|
|
402
|
+
return { changed: apply.changed, reloaded };
|
|
390
403
|
}
|
|
391
404
|
}
|
package/src/ui/remote.ts
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Remote package browsing UI
|
|
3
3
|
*/
|
|
4
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
6
|
-
import { Container, SelectList, Text, type SelectItem } from "@mariozechner/pi-tui";
|
|
7
|
-
import type { BrowseAction, NpmPackage } from "../types/index.js";
|
|
8
|
-
import { PAGE_SIZE, TIMEOUTS, CACHE_LIMITS } from "../constants.js";
|
|
9
|
-
import { truncate, dynamicTruncate, formatBytes } from "../utils/format.js";
|
|
10
|
-
import { parseChoiceByLabel, splitCommandArgs } from "../utils/command.js";
|
|
11
4
|
import {
|
|
12
|
-
|
|
5
|
+
DynamicBorder,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionCommandContext,
|
|
8
|
+
} from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
|
|
10
|
+
import { CACHE_LIMITS, PAGE_SIZE, TIMEOUTS } from "../constants.js";
|
|
11
|
+
import {
|
|
13
12
|
getSearchCache,
|
|
14
|
-
setSearchCache,
|
|
15
13
|
isCacheValid,
|
|
14
|
+
searchNpmPackages,
|
|
15
|
+
setSearchCache,
|
|
16
16
|
} from "../packages/discovery.js";
|
|
17
17
|
import { installPackage, installPackageLocally } from "../packages/install.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { type BrowseAction, type NpmPackage } from "../types/index.js";
|
|
19
|
+
import { parseChoiceByLabel, splitCommandArgs } from "../utils/command.js";
|
|
20
|
+
import { dynamicTruncate, formatBytes, truncate } from "../utils/format.js";
|
|
20
21
|
import { requireCustomUI, runCustomUI } from "../utils/mode.js";
|
|
22
|
+
import { notify } from "../utils/notify.js";
|
|
23
|
+
import { execNpm } from "../utils/npm-exec.js";
|
|
24
|
+
import { runTaskWithLoader } from "./async-task.js";
|
|
21
25
|
|
|
22
26
|
interface PackageInfoCacheEntry {
|
|
23
27
|
timestamp: number;
|
|
@@ -109,25 +113,44 @@ const PACKAGE_DETAILS_CHOICES = {
|
|
|
109
113
|
back: "Back to results",
|
|
110
114
|
} as const;
|
|
111
115
|
|
|
116
|
+
function createAbortError(): Error {
|
|
117
|
+
const error = new Error("Operation cancelled");
|
|
118
|
+
error.name = "AbortError";
|
|
119
|
+
return error;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
123
|
+
if (signal?.aborted) {
|
|
124
|
+
throw createAbortError();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
function formatCount(value: number | undefined): string {
|
|
113
129
|
if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
|
|
114
130
|
return new Intl.NumberFormat().format(value);
|
|
115
131
|
}
|
|
116
132
|
|
|
117
|
-
async function fetchWeeklyDownloads(
|
|
133
|
+
async function fetchWeeklyDownloads(
|
|
134
|
+
packageName: string,
|
|
135
|
+
signal?: AbortSignal
|
|
136
|
+
): Promise<number | undefined> {
|
|
118
137
|
const controller = new AbortController();
|
|
119
138
|
const timer = setTimeout(() => controller.abort(), TIMEOUTS.weeklyDownloads);
|
|
139
|
+
const combinedSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
120
140
|
|
|
121
141
|
try {
|
|
122
142
|
const encoded = encodeURIComponent(packageName);
|
|
123
143
|
const res = await fetch(`https://api.npmjs.org/downloads/point/last-week/${encoded}`, {
|
|
124
|
-
signal:
|
|
144
|
+
signal: combinedSignal,
|
|
125
145
|
});
|
|
126
146
|
|
|
127
147
|
if (!res.ok) return undefined;
|
|
128
148
|
const data = (await res.json()) as NpmDownloadsPoint;
|
|
129
149
|
return typeof data.downloads === "number" ? data.downloads : undefined;
|
|
130
|
-
} catch {
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (signal?.aborted && error instanceof Error && error.name === "AbortError") {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
131
154
|
return undefined;
|
|
132
155
|
} finally {
|
|
133
156
|
clearTimeout(timer);
|
|
@@ -137,7 +160,8 @@ async function fetchWeeklyDownloads(packageName: string): Promise<number | undef
|
|
|
137
160
|
async function buildPackageInfoText(
|
|
138
161
|
packageName: string,
|
|
139
162
|
ctx: ExtensionCommandContext,
|
|
140
|
-
pi: ExtensionAPI
|
|
163
|
+
pi: ExtensionAPI,
|
|
164
|
+
signal?: AbortSignal
|
|
141
165
|
): Promise<string> {
|
|
142
166
|
// Check cache first
|
|
143
167
|
const cached = packageInfoCache.get(packageName);
|
|
@@ -148,10 +172,13 @@ async function buildPackageInfoText(
|
|
|
148
172
|
const [infoRes, weeklyDownloads] = await Promise.all([
|
|
149
173
|
execNpm(pi, ["view", packageName, "--json"], ctx, {
|
|
150
174
|
timeout: TIMEOUTS.npmView,
|
|
175
|
+
...(signal ? { signal } : {}),
|
|
151
176
|
}),
|
|
152
|
-
fetchWeeklyDownloads(packageName),
|
|
177
|
+
fetchWeeklyDownloads(packageName, signal),
|
|
153
178
|
]);
|
|
154
179
|
|
|
180
|
+
throwIfAborted(signal);
|
|
181
|
+
|
|
155
182
|
if (infoRes.code !== 0) {
|
|
156
183
|
throw new Error(infoRes.stderr || infoRes.stdout || `npm view failed (exit ${infoRes.code})`);
|
|
157
184
|
}
|
|
@@ -179,7 +206,7 @@ async function buildPackageInfoText(
|
|
|
179
206
|
|
|
180
207
|
const text = lines.join("\n");
|
|
181
208
|
|
|
182
|
-
|
|
209
|
+
throwIfAborted(signal);
|
|
183
210
|
packageInfoCache.set(packageName, { text });
|
|
184
211
|
|
|
185
212
|
return text;
|
|
@@ -346,20 +373,34 @@ export async function browseRemotePackages(
|
|
|
346
373
|
return;
|
|
347
374
|
}
|
|
348
375
|
|
|
349
|
-
|
|
350
|
-
let allPackages: NpmPackage[] = [];
|
|
376
|
+
let allPackages: NpmPackage[] | undefined;
|
|
351
377
|
|
|
352
|
-
if (isCacheValid(query)
|
|
378
|
+
if (isCacheValid(query)) {
|
|
353
379
|
const cache = getSearchCache();
|
|
354
|
-
if (cache)
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
380
|
+
if (cache) {
|
|
381
|
+
allPackages = cache.results;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
358
384
|
|
|
359
|
-
|
|
360
|
-
|
|
385
|
+
if (!allPackages) {
|
|
386
|
+
const results = await runTaskWithLoader(
|
|
387
|
+
ctx,
|
|
388
|
+
{
|
|
389
|
+
title: "Remote Packages",
|
|
390
|
+
message: `Searching npm for ${truncate(query, 40)}...`,
|
|
391
|
+
},
|
|
392
|
+
async ({ signal, setMessage }) => {
|
|
393
|
+
setMessage(`Searching npm for ${truncate(query, 40)}...`);
|
|
394
|
+
return searchNpmPackages(query, ctx, { signal });
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
if (!results) {
|
|
399
|
+
notify(ctx, "Remote package search was cancelled.", "info");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
361
402
|
|
|
362
|
-
|
|
403
|
+
allPackages = results;
|
|
363
404
|
setSearchCache({
|
|
364
405
|
query,
|
|
365
406
|
results: allPackages,
|
|
@@ -451,7 +492,21 @@ async function showPackageDetails(
|
|
|
451
492
|
return;
|
|
452
493
|
case "viewInfo":
|
|
453
494
|
try {
|
|
454
|
-
const text = await
|
|
495
|
+
const text = await runTaskWithLoader(
|
|
496
|
+
ctx,
|
|
497
|
+
{
|
|
498
|
+
title: packageName,
|
|
499
|
+
message: `Fetching package details for ${packageName}...`,
|
|
500
|
+
},
|
|
501
|
+
({ signal }) => buildPackageInfoText(packageName, ctx, pi, signal)
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
if (!text) {
|
|
505
|
+
notify(ctx, `Loading ${packageName} details was cancelled.`, "info");
|
|
506
|
+
await showPackageDetails(packageName, ctx, pi, previousQuery, previousOffset);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
455
510
|
ctx.ui.notify(text, "info");
|
|
456
511
|
} catch (error) {
|
|
457
512
|
const message = error instanceof Error ? error.message : String(error);
|
package/src/ui/theme.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Theme utilities for consistent UI styling across dark/light themes
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type Theme } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Status icons that work across themes
|
|
@@ -62,7 +62,7 @@ export function getScopeIcon(
|
|
|
62
62
|
*/
|
|
63
63
|
export function getChangeMarker(theme: Theme, hasChanges: boolean): string {
|
|
64
64
|
if (!hasChanges) return "";
|
|
65
|
-
return
|
|
65
|
+
return ` ${theme.fg("warning", "*")}`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|