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
package/src/ui/unified.ts
CHANGED
|
@@ -14,22 +14,14 @@ import {
|
|
|
14
14
|
matchesKey,
|
|
15
15
|
Key,
|
|
16
16
|
} from "@mariozechner/pi-tui";
|
|
17
|
-
import type {
|
|
18
|
-
UnifiedItem,
|
|
19
|
-
State,
|
|
20
|
-
UnifiedAction,
|
|
21
|
-
InstalledPackage,
|
|
22
|
-
PackageExtensionEntry,
|
|
23
|
-
} from "../types/index.js";
|
|
17
|
+
import type { UnifiedItem, State, UnifiedAction, InstalledPackage } from "../types/index.js";
|
|
24
18
|
import {
|
|
25
19
|
discoverExtensions,
|
|
26
20
|
removeLocalExtension,
|
|
27
21
|
setExtensionState,
|
|
28
22
|
} from "../extensions/discovery.js";
|
|
29
23
|
import { getInstalledPackages } from "../packages/discovery.js";
|
|
30
|
-
import { discoverPackageExtensions, setPackageExtensionState } from "../packages/extensions.js";
|
|
31
24
|
import {
|
|
32
|
-
showPackageActions,
|
|
33
25
|
updatePackageWithOutcome,
|
|
34
26
|
removePackageWithOutcome,
|
|
35
27
|
updatePackagesWithOutcome,
|
|
@@ -44,12 +36,14 @@ import {
|
|
|
44
36
|
getChangeMarker,
|
|
45
37
|
formatSize,
|
|
46
38
|
} from "./theme.js";
|
|
39
|
+
import { buildFooterState, buildFooterShortcuts, getPendingToggleChangeCount } from "./footer.js";
|
|
47
40
|
import { logExtensionToggle } from "../utils/history.js";
|
|
48
41
|
import { getKnownUpdates, promptAutoUpdateWizard } from "../utils/auto-update.js";
|
|
49
42
|
import { updateExtmgrStatus } from "../utils/status.js";
|
|
50
43
|
import { parseChoiceByLabel } from "../utils/command.js";
|
|
51
44
|
import { getPackageSourceKind } from "../utils/package-source.js";
|
|
52
45
|
import { UI } from "../constants.js";
|
|
46
|
+
import { configurePackageExtensions } from "./package-config.js";
|
|
53
47
|
|
|
54
48
|
// Type guard for SettingsList with selectedIndex
|
|
55
49
|
interface SelectableList {
|
|
@@ -86,16 +80,15 @@ async function showInteractiveOnce(
|
|
|
86
80
|
ctx: ExtensionCommandContext,
|
|
87
81
|
pi: ExtensionAPI
|
|
88
82
|
): Promise<boolean> {
|
|
89
|
-
// Load local extensions and installed packages
|
|
83
|
+
// Load local extensions and installed packages.
|
|
90
84
|
const [localEntries, installedPackages] = await Promise.all([
|
|
91
85
|
discoverExtensions(ctx.cwd),
|
|
92
86
|
getInstalledPackages(ctx, pi),
|
|
93
87
|
]);
|
|
94
|
-
const packageExtensions = await discoverPackageExtensions(installedPackages, ctx.cwd);
|
|
95
88
|
|
|
96
|
-
// Build unified items list
|
|
89
|
+
// Build unified items list.
|
|
97
90
|
const knownUpdates = getKnownUpdates(ctx);
|
|
98
|
-
const items = buildUnifiedItems(localEntries, installedPackages,
|
|
91
|
+
const items = buildUnifiedItems(localEntries, installedPackages, knownUpdates);
|
|
99
92
|
|
|
100
93
|
// If nothing found, show quick actions
|
|
101
94
|
if (items.length === 0) {
|
|
@@ -111,16 +104,12 @@ async function showInteractiveOnce(
|
|
|
111
104
|
return true;
|
|
112
105
|
}
|
|
113
106
|
|
|
114
|
-
// Staged changes tracking for
|
|
107
|
+
// Staged changes tracking for local extensions.
|
|
115
108
|
const staged = new Map<string, State>();
|
|
116
109
|
const byId = new Map(items.map((item) => [item.id, item]));
|
|
117
110
|
|
|
118
111
|
const result = await ctx.ui.custom<UnifiedAction>((tui, theme, _keybindings, done) => {
|
|
119
112
|
const container = new Container();
|
|
120
|
-
const hasLocals = items.some((i) => i.type === "local");
|
|
121
|
-
const hasPackageExtensions = items.some((i) => i.type === "package-extension");
|
|
122
|
-
const hasToggleRows = hasLocals || hasPackageExtensions;
|
|
123
|
-
const hasPackages = items.some((i) => i.type === "package");
|
|
124
113
|
|
|
125
114
|
// Header
|
|
126
115
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
@@ -129,7 +118,7 @@ async function showInteractiveOnce(
|
|
|
129
118
|
new Text(
|
|
130
119
|
theme.fg(
|
|
131
120
|
"muted",
|
|
132
|
-
`${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter toggle
|
|
121
|
+
`${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter toggle local • Enter/A actions • c configure pkg extensions • u update pkg • x remove selected`
|
|
133
122
|
),
|
|
134
123
|
2,
|
|
135
124
|
0
|
|
@@ -153,7 +142,7 @@ async function showInteractiveOnce(
|
|
|
153
142
|
getSettingsListTheme(),
|
|
154
143
|
(id: string, newValue: string) => {
|
|
155
144
|
const item = byId.get(id);
|
|
156
|
-
if (!item ||
|
|
145
|
+
if (!item || item.type !== "local") return;
|
|
157
146
|
|
|
158
147
|
const state = newValue as State;
|
|
159
148
|
staged.set(id, state);
|
|
@@ -173,8 +162,8 @@ async function showInteractiveOnce(
|
|
|
173
162
|
container.addChild(new Spacer(1));
|
|
174
163
|
|
|
175
164
|
// Footer with keyboard shortcuts
|
|
176
|
-
const
|
|
177
|
-
container.addChild(new Text(theme.fg("dim",
|
|
165
|
+
const footerState = buildFooterState(items);
|
|
166
|
+
container.addChild(new Text(theme.fg("dim", buildFooterShortcuts(footerState)), 2, 0));
|
|
178
167
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
179
168
|
|
|
180
169
|
return {
|
|
@@ -239,6 +228,10 @@ async function showInteractiveOnce(
|
|
|
239
228
|
done({ type: "action", itemId: selectedId, action: "details" });
|
|
240
229
|
return;
|
|
241
230
|
}
|
|
231
|
+
if (data === "c" || data === "C") {
|
|
232
|
+
done({ type: "action", itemId: selectedId, action: "configure" });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
242
235
|
}
|
|
243
236
|
|
|
244
237
|
if (selectedId && selectedItem?.type === "local") {
|
|
@@ -269,35 +262,21 @@ async function showInteractiveOnce(
|
|
|
269
262
|
return await handleUnifiedAction(result, items, staged, byId, ctx, pi);
|
|
270
263
|
}
|
|
271
264
|
|
|
265
|
+
function normalizePathForDuplicateCheck(value: string): string {
|
|
266
|
+
return value.replace(/\\/g, "/").toLowerCase();
|
|
267
|
+
}
|
|
268
|
+
|
|
272
269
|
export function buildUnifiedItems(
|
|
273
270
|
localEntries: Awaited<ReturnType<typeof discoverExtensions>>,
|
|
274
271
|
installedPackages: InstalledPackage[],
|
|
275
|
-
packageExtensions: PackageExtensionEntry[],
|
|
276
272
|
knownUpdates: Set<string>
|
|
277
273
|
): UnifiedItem[] {
|
|
278
274
|
const items: UnifiedItem[] = [];
|
|
279
275
|
const localPaths = new Set<string>();
|
|
280
276
|
|
|
281
|
-
const packageExtensionGroups = new Map<string, PackageExtensionEntry[]>();
|
|
282
|
-
for (const entry of packageExtensions) {
|
|
283
|
-
const key = `${entry.packageScope}:${entry.packageSource.toLowerCase()}`;
|
|
284
|
-
const group = packageExtensionGroups.get(key) ?? [];
|
|
285
|
-
group.push(entry);
|
|
286
|
-
packageExtensionGroups.set(key, group);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const visiblePackageExtensions = packageExtensions.filter((entry) => {
|
|
290
|
-
const key = `${entry.packageScope}:${entry.packageSource.toLowerCase()}`;
|
|
291
|
-
const group = packageExtensionGroups.get(key) ?? [];
|
|
292
|
-
|
|
293
|
-
// Avoid duplicate-looking rows for packages that expose a single enabled extension entrypoint.
|
|
294
|
-
// Keep extension rows when there are multiple entrypoints, or when an entry is disabled so it can be re-enabled.
|
|
295
|
-
return group.length > 1 || entry.state === "disabled";
|
|
296
|
-
});
|
|
297
|
-
|
|
298
277
|
// Add local extensions
|
|
299
278
|
for (const entry of localEntries) {
|
|
300
|
-
localPaths.add(entry.activePath
|
|
279
|
+
localPaths.add(normalizePathForDuplicateCheck(entry.activePath));
|
|
301
280
|
items.push({
|
|
302
281
|
type: "local",
|
|
303
282
|
id: entry.id,
|
|
@@ -311,39 +290,28 @@ export function buildUnifiedItems(
|
|
|
311
290
|
});
|
|
312
291
|
}
|
|
313
292
|
|
|
314
|
-
for (const entry of visiblePackageExtensions) {
|
|
315
|
-
items.push({
|
|
316
|
-
type: "package-extension",
|
|
317
|
-
id: entry.id,
|
|
318
|
-
displayName: entry.displayName,
|
|
319
|
-
summary: entry.summary,
|
|
320
|
-
scope: entry.packageScope,
|
|
321
|
-
state: entry.state,
|
|
322
|
-
originalState: entry.state,
|
|
323
|
-
packageSource: entry.packageSource,
|
|
324
|
-
extensionPath: entry.extensionPath,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
293
|
for (const pkg of installedPackages) {
|
|
329
|
-
const
|
|
330
|
-
const
|
|
294
|
+
const pkgSourceNormalized = normalizePathForDuplicateCheck(pkg.source);
|
|
295
|
+
const pkgResolvedNormalized = pkg.resolvedPath
|
|
296
|
+
? normalizePathForDuplicateCheck(pkg.resolvedPath)
|
|
297
|
+
: "";
|
|
331
298
|
|
|
332
299
|
let isDuplicate = false;
|
|
333
300
|
for (const localPath of localPaths) {
|
|
334
|
-
if (
|
|
301
|
+
if (pkgSourceNormalized === localPath || pkgResolvedNormalized === localPath) {
|
|
335
302
|
isDuplicate = true;
|
|
336
303
|
break;
|
|
337
304
|
}
|
|
338
305
|
if (
|
|
339
|
-
|
|
340
|
-
(localPath.startsWith(
|
|
306
|
+
pkgResolvedNormalized &&
|
|
307
|
+
(localPath.startsWith(`${pkgResolvedNormalized}/`) ||
|
|
308
|
+
pkgResolvedNormalized.startsWith(localPath))
|
|
341
309
|
) {
|
|
342
310
|
isDuplicate = true;
|
|
343
311
|
break;
|
|
344
312
|
}
|
|
345
|
-
const localDir = localPath.
|
|
346
|
-
if (
|
|
313
|
+
const localDir = localPath.split("/").slice(0, -1).join("/");
|
|
314
|
+
if (pkgResolvedNormalized && pkgResolvedNormalized === localDir) {
|
|
347
315
|
isDuplicate = true;
|
|
348
316
|
break;
|
|
349
317
|
}
|
|
@@ -368,8 +336,7 @@ export function buildUnifiedItems(
|
|
|
368
336
|
items.sort((a, b) => {
|
|
369
337
|
const rank = (type: UnifiedItem["type"]): number => {
|
|
370
338
|
if (type === "local") return 0;
|
|
371
|
-
|
|
372
|
-
return 2;
|
|
339
|
+
return 1;
|
|
373
340
|
};
|
|
374
341
|
|
|
375
342
|
const diff = rank(a.type) - rank(b.type);
|
|
@@ -386,7 +353,7 @@ function buildSettingsItems(
|
|
|
386
353
|
theme: Theme
|
|
387
354
|
): SettingItem[] {
|
|
388
355
|
return items.map((item) => {
|
|
389
|
-
if (item.type === "local"
|
|
356
|
+
if (item.type === "local") {
|
|
390
357
|
const currentState = staged.get(item.id) ?? item.state!;
|
|
391
358
|
const changed = staged.has(item.id) && staged.get(item.id) !== item.originalState;
|
|
392
359
|
return {
|
|
@@ -406,34 +373,6 @@ function buildSettingsItems(
|
|
|
406
373
|
});
|
|
407
374
|
}
|
|
408
375
|
|
|
409
|
-
function buildFooter(
|
|
410
|
-
hasToggleRows: boolean,
|
|
411
|
-
hasLocals: boolean,
|
|
412
|
-
hasPackages: boolean,
|
|
413
|
-
staged: Map<string, State>,
|
|
414
|
-
byId: Map<string, UnifiedItem>
|
|
415
|
-
): string[] {
|
|
416
|
-
const hasChanges = getPendingToggleChangeCount(staged, byId) > 0;
|
|
417
|
-
|
|
418
|
-
const footerParts: string[] = [];
|
|
419
|
-
footerParts.push("↑↓ Navigate");
|
|
420
|
-
if (hasToggleRows) footerParts.push("Space/Enter Toggle");
|
|
421
|
-
if (hasToggleRows) footerParts.push(hasChanges ? "S Save*" : "S Save");
|
|
422
|
-
if (hasPackages) footerParts.push("Enter/A Actions");
|
|
423
|
-
if (hasPackages) footerParts.push("u Update");
|
|
424
|
-
if (hasPackages || hasLocals) footerParts.push("X Remove");
|
|
425
|
-
footerParts.push("i Install");
|
|
426
|
-
footerParts.push("f Search");
|
|
427
|
-
footerParts.push("U Update all");
|
|
428
|
-
footerParts.push("t Auto-update");
|
|
429
|
-
footerParts.push("P Palette");
|
|
430
|
-
footerParts.push("R Browse");
|
|
431
|
-
footerParts.push("? Help");
|
|
432
|
-
footerParts.push("Esc Cancel");
|
|
433
|
-
|
|
434
|
-
return footerParts;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
376
|
function formatUnifiedItemLabel(
|
|
438
377
|
item: UnifiedItem,
|
|
439
378
|
state: State,
|
|
@@ -442,34 +381,19 @@ function formatUnifiedItemLabel(
|
|
|
442
381
|
): string {
|
|
443
382
|
if (item.type === "local") {
|
|
444
383
|
const statusIcon = getStatusIcon(theme, state === "enabled" ? "enabled" : "disabled");
|
|
445
|
-
const scopeIcon = getScopeIcon(theme, item.scope
|
|
384
|
+
const scopeIcon = getScopeIcon(theme, item.scope);
|
|
446
385
|
const changeMarker = getChangeMarker(theme, changed);
|
|
447
386
|
const name = theme.bold(item.displayName);
|
|
448
387
|
const summary = theme.fg("dim", item.summary);
|
|
449
388
|
return `${statusIcon} [${scopeIcon}] ${name} - ${summary}${changeMarker}`;
|
|
450
389
|
}
|
|
451
390
|
|
|
452
|
-
if (item.type === "package-extension") {
|
|
453
|
-
const statusIcon = getStatusIcon(theme, state === "enabled" ? "enabled" : "disabled");
|
|
454
|
-
const scopeIcon = getScopeIcon(theme, item.scope as "global" | "project");
|
|
455
|
-
const sourceKind = getPackageSourceKind(item.packageSource ?? "");
|
|
456
|
-
const pkgIcon = getPackageIcon(
|
|
457
|
-
theme,
|
|
458
|
-
sourceKind === "npm" || sourceKind === "git" || sourceKind === "local" ? sourceKind : "local"
|
|
459
|
-
);
|
|
460
|
-
const sourceLabel = sourceKind === "unknown" ? "package" : `${sourceKind} package`;
|
|
461
|
-
const changeMarker = getChangeMarker(theme, changed);
|
|
462
|
-
const name = theme.bold(item.displayName);
|
|
463
|
-
const summary = theme.fg("dim", `${item.summary} • ${sourceLabel}`);
|
|
464
|
-
return `${statusIcon} ${pkgIcon} [${scopeIcon}] ${name} - ${summary}${changeMarker}`;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
391
|
const sourceKind = getPackageSourceKind(item.source ?? "");
|
|
468
392
|
const pkgIcon = getPackageIcon(
|
|
469
393
|
theme,
|
|
470
394
|
sourceKind === "npm" || sourceKind === "git" || sourceKind === "local" ? sourceKind : "local"
|
|
471
395
|
);
|
|
472
|
-
const scopeIcon = getScopeIcon(theme, item.scope
|
|
396
|
+
const scopeIcon = getScopeIcon(theme, item.scope);
|
|
473
397
|
const name = theme.bold(item.displayName);
|
|
474
398
|
const version = item.version ? theme.fg("dim", `@${item.version}`) : "";
|
|
475
399
|
const updateBadge = item.updateAvailable ? ` ${theme.fg("warning", "[update]")}` : "";
|
|
@@ -498,26 +422,8 @@ function formatUnifiedItemLabel(
|
|
|
498
422
|
return `${pkgIcon} [${scopeIcon}] ${name}${version}${updateBadge} - ${summary}`;
|
|
499
423
|
}
|
|
500
424
|
|
|
501
|
-
function getPendingToggleChangeCount(
|
|
502
|
-
staged: Map<string, State>,
|
|
503
|
-
byId: Map<string, UnifiedItem>
|
|
504
|
-
): number {
|
|
505
|
-
let count = 0;
|
|
506
|
-
for (const [id, state] of staged.entries()) {
|
|
507
|
-
const item = byId.get(id);
|
|
508
|
-
if (!item) continue;
|
|
509
|
-
if (
|
|
510
|
-
(item.type === "local" || item.type === "package-extension") &&
|
|
511
|
-
item.originalState !== state
|
|
512
|
-
) {
|
|
513
|
-
count += 1;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return count;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
425
|
function getToggleItemsForApply(items: UnifiedItem[]): UnifiedItem[] {
|
|
520
|
-
return items.filter((item) => item.type === "local"
|
|
426
|
+
return items.filter((item) => item.type === "local");
|
|
521
427
|
}
|
|
522
428
|
|
|
523
429
|
async function applyToggleChangesFromManager(
|
|
@@ -528,7 +434,7 @@ async function applyToggleChangesFromManager(
|
|
|
528
434
|
options?: { promptReload?: boolean }
|
|
529
435
|
): Promise<{ changed: number; reloaded: boolean }> {
|
|
530
436
|
const toggleItems = getToggleItemsForApply(items);
|
|
531
|
-
const apply = await applyStagedChanges(toggleItems, staged, pi
|
|
437
|
+
const apply = await applyStagedChanges(toggleItems, staged, pi);
|
|
532
438
|
|
|
533
439
|
if (apply.errors.length > 0) {
|
|
534
440
|
ctx.ui.notify(
|
|
@@ -538,7 +444,7 @@ async function applyToggleChangesFromManager(
|
|
|
538
444
|
} else if (apply.changed === 0) {
|
|
539
445
|
ctx.ui.notify("No changes to apply.", "info");
|
|
540
446
|
} else {
|
|
541
|
-
ctx.ui.notify(`Applied ${apply.changed} extension change(s).`, "info");
|
|
447
|
+
ctx.ui.notify(`Applied ${apply.changed} local extension change(s).`, "info");
|
|
542
448
|
}
|
|
543
449
|
|
|
544
450
|
if (apply.changed > 0) {
|
|
@@ -547,7 +453,7 @@ async function applyToggleChangesFromManager(
|
|
|
547
453
|
if (shouldPromptReload) {
|
|
548
454
|
const shouldReload = await ctx.ui.confirm(
|
|
549
455
|
"Reload Required",
|
|
550
|
-
"
|
|
456
|
+
"Local extensions changed. Reload pi now?"
|
|
551
457
|
);
|
|
552
458
|
|
|
553
459
|
if (shouldReload) {
|
|
@@ -619,6 +525,34 @@ const QUICK_DESTINATION_LABELS: Record<QuickDestination, string> = {
|
|
|
619
525
|
help: "Help",
|
|
620
526
|
};
|
|
621
527
|
|
|
528
|
+
const PACKAGE_ACTION_OPTIONS = {
|
|
529
|
+
configure: "Configure extensions",
|
|
530
|
+
update: "Update package",
|
|
531
|
+
remove: "Remove package",
|
|
532
|
+
details: "View details",
|
|
533
|
+
back: "Back to manager",
|
|
534
|
+
} as const;
|
|
535
|
+
|
|
536
|
+
type PackageActionKey = keyof typeof PACKAGE_ACTION_OPTIONS;
|
|
537
|
+
|
|
538
|
+
type PackageActionSelection = Exclude<PackageActionKey, "back"> | "cancel";
|
|
539
|
+
|
|
540
|
+
async function promptPackageActionSelection(
|
|
541
|
+
pkg: InstalledPackage,
|
|
542
|
+
ctx: ExtensionCommandContext
|
|
543
|
+
): Promise<PackageActionSelection> {
|
|
544
|
+
const selection = parseChoiceByLabel(
|
|
545
|
+
PACKAGE_ACTION_OPTIONS,
|
|
546
|
+
await ctx.ui.select(pkg.name, Object.values(PACKAGE_ACTION_OPTIONS))
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
if (!selection || selection === "back") {
|
|
550
|
+
return "cancel";
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return selection;
|
|
554
|
+
}
|
|
555
|
+
|
|
622
556
|
async function navigateWithPendingGuard(
|
|
623
557
|
destination: QuickDestination,
|
|
624
558
|
items: UnifiedItem[],
|
|
@@ -803,20 +737,29 @@ async function handleUnifiedAction(
|
|
|
803
737
|
return false;
|
|
804
738
|
}
|
|
805
739
|
|
|
806
|
-
if (item.type === "package-extension") {
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
740
|
const pkg: InstalledPackage = {
|
|
811
741
|
source: item.source!,
|
|
812
742
|
name: item.displayName,
|
|
813
743
|
...(item.version ? { version: item.version } : {}),
|
|
814
|
-
scope: item.scope
|
|
744
|
+
scope: item.scope,
|
|
815
745
|
...(item.description ? { description: item.description } : {}),
|
|
816
746
|
...(item.size !== undefined ? { size: item.size } : {}),
|
|
817
747
|
};
|
|
818
748
|
|
|
819
|
-
|
|
749
|
+
const selection =
|
|
750
|
+
!result.action || result.action === "menu"
|
|
751
|
+
? await promptPackageActionSelection(pkg, ctx)
|
|
752
|
+
: result.action;
|
|
753
|
+
|
|
754
|
+
if (selection === "cancel") {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
switch (selection) {
|
|
759
|
+
case "configure": {
|
|
760
|
+
const outcome = await configurePackageExtensions(pkg, ctx, pi);
|
|
761
|
+
return outcome.reloaded;
|
|
762
|
+
}
|
|
820
763
|
case "update": {
|
|
821
764
|
const outcome = await updatePackageWithOutcome(pkg.source, ctx, pi);
|
|
822
765
|
return outcome.reloaded;
|
|
@@ -833,11 +776,6 @@ async function handleUnifiedAction(
|
|
|
833
776
|
);
|
|
834
777
|
return false;
|
|
835
778
|
}
|
|
836
|
-
case "menu":
|
|
837
|
-
default: {
|
|
838
|
-
const exitManager = await showPackageActions(pkg, ctx, pi);
|
|
839
|
-
return exitManager;
|
|
840
|
-
}
|
|
841
779
|
}
|
|
842
780
|
}
|
|
843
781
|
|
|
@@ -848,37 +786,23 @@ async function handleUnifiedAction(
|
|
|
848
786
|
async function applyStagedChanges(
|
|
849
787
|
items: UnifiedItem[],
|
|
850
788
|
staged: Map<string, State>,
|
|
851
|
-
pi: ExtensionAPI
|
|
852
|
-
cwd: string
|
|
789
|
+
pi: ExtensionAPI
|
|
853
790
|
) {
|
|
854
791
|
let changed = 0;
|
|
855
792
|
const errors: string[] = [];
|
|
856
793
|
|
|
857
794
|
for (const item of items) {
|
|
858
|
-
if (
|
|
795
|
+
if (item.type !== "local" || !item.originalState || !item.activePath || !item.disabledPath) {
|
|
859
796
|
continue;
|
|
860
797
|
}
|
|
861
798
|
|
|
862
799
|
const target = staged.get(item.id) ?? item.originalState;
|
|
863
800
|
if (target === item.originalState) continue;
|
|
864
801
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
{ activePath: item.activePath, disabledPath: item.disabledPath },
|
|
870
|
-
target
|
|
871
|
-
);
|
|
872
|
-
} else {
|
|
873
|
-
if (!item.packageSource || !item.extensionPath) continue;
|
|
874
|
-
result = await setPackageExtensionState(
|
|
875
|
-
item.packageSource,
|
|
876
|
-
item.extensionPath,
|
|
877
|
-
item.scope as "global" | "project",
|
|
878
|
-
target,
|
|
879
|
-
cwd
|
|
880
|
-
);
|
|
881
|
-
}
|
|
802
|
+
const result = await setExtensionState(
|
|
803
|
+
{ activePath: item.activePath, disabledPath: item.disabledPath },
|
|
804
|
+
target
|
|
805
|
+
);
|
|
882
806
|
|
|
883
807
|
if (result.ok) {
|
|
884
808
|
changed++;
|
package/src/utils/auto-update.ts
CHANGED
|
@@ -20,8 +20,7 @@ import {
|
|
|
20
20
|
import { parseNpmSource } from "./format.js";
|
|
21
21
|
import { TIMEOUTS } from "../constants.js";
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
let autoUpdateTimer: ReturnType<typeof setInterval> | null = null;
|
|
23
|
+
import { startTimer, stopTimer, isTimerRunning } from "./timer.js";
|
|
25
24
|
|
|
26
25
|
// Context provider for safe session handling
|
|
27
26
|
export type ContextProvider = () => (ExtensionCommandContext | ExtensionContext) | undefined;
|
|
@@ -35,10 +34,8 @@ export function startAutoUpdateTimer(
|
|
|
35
34
|
getCtx: ContextProvider,
|
|
36
35
|
onUpdateAvailable?: (packages: string[]) => void
|
|
37
36
|
): void {
|
|
38
|
-
// Clear existing timer
|
|
39
37
|
stopAutoUpdateTimer();
|
|
40
38
|
|
|
41
|
-
// Get fresh config from current context
|
|
42
39
|
const ctx = getCtx();
|
|
43
40
|
if (!ctx) return;
|
|
44
41
|
|
|
@@ -51,22 +48,20 @@ export function startAutoUpdateTimer(
|
|
|
51
48
|
if (!interval) return;
|
|
52
49
|
|
|
53
50
|
// Run an initial check immediately.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
})();
|
|
51
|
+
const initialCtx = getCtx();
|
|
52
|
+
if (initialCtx) {
|
|
53
|
+
void checkForUpdates(pi, initialCtx, onUpdateAvailable);
|
|
54
|
+
}
|
|
59
55
|
|
|
60
|
-
// Set up
|
|
61
|
-
|
|
56
|
+
// Set up recurring checks
|
|
57
|
+
startTimer(interval, () => {
|
|
62
58
|
const checkCtx = getCtx();
|
|
63
59
|
if (!checkCtx) {
|
|
64
|
-
// Session ended, stop timer
|
|
65
60
|
stopAutoUpdateTimer();
|
|
66
61
|
return;
|
|
67
62
|
}
|
|
68
63
|
void checkForUpdates(pi, checkCtx, onUpdateAvailable);
|
|
69
|
-
}
|
|
64
|
+
});
|
|
70
65
|
|
|
71
66
|
// Persist that timer is running
|
|
72
67
|
saveAutoUpdateConfig(pi, {
|
|
@@ -79,17 +74,14 @@ export function startAutoUpdateTimer(
|
|
|
79
74
|
* Stop auto-update background checker
|
|
80
75
|
*/
|
|
81
76
|
export function stopAutoUpdateTimer(): void {
|
|
82
|
-
|
|
83
|
-
clearInterval(autoUpdateTimer);
|
|
84
|
-
autoUpdateTimer = null;
|
|
85
|
-
}
|
|
77
|
+
stopTimer();
|
|
86
78
|
}
|
|
87
79
|
|
|
88
80
|
/**
|
|
89
81
|
* Check if auto-update timer is running
|
|
90
82
|
*/
|
|
91
83
|
export function isAutoUpdateRunning(): boolean {
|
|
92
|
-
return
|
|
84
|
+
return isTimerRunning();
|
|
93
85
|
}
|
|
94
86
|
|
|
95
87
|
/**
|
package/src/utils/format.ts
CHANGED
|
@@ -57,28 +57,43 @@ export function formatBytes(bytes: number): string {
|
|
|
57
57
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const GIT_PATTERNS = {
|
|
61
|
+
gitPrefix: /^git:/,
|
|
62
|
+
httpPrefix: /^https?:\/\//,
|
|
63
|
+
sshPrefix: /^ssh:\/\//,
|
|
64
|
+
gitProtoPrefix: /^git:\/\//,
|
|
65
|
+
gitSsh: /^git@[^\s:]+:.+/,
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
const LOCAL_PATH_PATTERNS = {
|
|
69
|
+
unixAbsolute: /^\//,
|
|
70
|
+
unixRelative: /^\.\.?\//,
|
|
71
|
+
windowsRelative: /^\.\.?\\/,
|
|
72
|
+
homeRelative: /^~\//,
|
|
73
|
+
fileProto: /^file:\/\//,
|
|
74
|
+
windowsDrive: /^[a-zA-Z]:[\\/]/,
|
|
75
|
+
uncPath: /^\\\\/,
|
|
76
|
+
} as const;
|
|
77
|
+
|
|
60
78
|
function isGitLikeSource(source: string): boolean {
|
|
61
79
|
return (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
/^git@[^\s:]+:.+/.test(source)
|
|
80
|
+
GIT_PATTERNS.gitPrefix.test(source) ||
|
|
81
|
+
GIT_PATTERNS.httpPrefix.test(source) ||
|
|
82
|
+
GIT_PATTERNS.sshPrefix.test(source) ||
|
|
83
|
+
GIT_PATTERNS.gitProtoPrefix.test(source) ||
|
|
84
|
+
GIT_PATTERNS.gitSsh.test(source)
|
|
68
85
|
);
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
function isLocalPathSource(source: string): boolean {
|
|
72
89
|
return (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/^[a-zA-Z]:[\\/]/.test(source) ||
|
|
81
|
-
source.startsWith("\\\\")
|
|
90
|
+
LOCAL_PATH_PATTERNS.unixAbsolute.test(source) ||
|
|
91
|
+
LOCAL_PATH_PATTERNS.unixRelative.test(source) ||
|
|
92
|
+
LOCAL_PATH_PATTERNS.windowsRelative.test(source) ||
|
|
93
|
+
LOCAL_PATH_PATTERNS.homeRelative.test(source) ||
|
|
94
|
+
LOCAL_PATH_PATTERNS.fileProto.test(source) ||
|
|
95
|
+
LOCAL_PATH_PATTERNS.windowsDrive.test(source) ||
|
|
96
|
+
LOCAL_PATH_PATTERNS.uncPath.test(source)
|
|
82
97
|
);
|
|
83
98
|
}
|
|
84
99
|
|
|
@@ -93,7 +108,7 @@ export function normalizePackageSource(source: string): string {
|
|
|
93
108
|
const trimmed = source.trim();
|
|
94
109
|
if (!trimmed) return trimmed;
|
|
95
110
|
|
|
96
|
-
if (
|
|
111
|
+
if (GIT_PATTERNS.gitSsh.test(trimmed)) {
|
|
97
112
|
return `git:${trimmed}`;
|
|
98
113
|
}
|
|
99
114
|
|
package/src/utils/history.ts
CHANGED
|
@@ -119,7 +119,6 @@ export function logPackageUpdate(
|
|
|
119
119
|
packageSource: source,
|
|
120
120
|
packageName: name,
|
|
121
121
|
version: toVersion,
|
|
122
|
-
scope: source.includes("node_modules") ? "global" : "project",
|
|
123
122
|
success,
|
|
124
123
|
error,
|
|
125
124
|
});
|
|
@@ -155,15 +154,19 @@ export function logCacheClear(pi: ExtensionAPI, success: boolean, error?: string
|
|
|
155
154
|
});
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
function
|
|
159
|
-
if (!
|
|
157
|
+
function isExtensionChangeEntry(value: unknown): value is ExtensionChangeEntry {
|
|
158
|
+
if (!value || typeof value !== "object") return false;
|
|
160
159
|
|
|
161
|
-
const maybe =
|
|
162
|
-
if (typeof maybe.action !== "string") return
|
|
163
|
-
if (typeof maybe.timestamp !== "number") return
|
|
164
|
-
if (typeof maybe.success !== "boolean") return
|
|
160
|
+
const maybe = value as Partial<ExtensionChangeEntry>;
|
|
161
|
+
if (typeof maybe.action !== "string") return false;
|
|
162
|
+
if (typeof maybe.timestamp !== "number") return false;
|
|
163
|
+
if (typeof maybe.success !== "boolean") return false;
|
|
165
164
|
|
|
166
|
-
return
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function asChangeEntry(data: unknown): ExtensionChangeEntry | undefined {
|
|
169
|
+
return isExtensionChangeEntry(data) ? data : undefined;
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
function matchesHistoryFilters(change: ExtensionChangeEntry, filters: HistoryFilters): boolean {
|
|
@@ -235,6 +238,10 @@ export function querySessionChanges(
|
|
|
235
238
|
return applyHistoryFilters(getAllSessionChanges(ctx), filters);
|
|
236
239
|
}
|
|
237
240
|
|
|
241
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
242
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
243
|
+
}
|
|
244
|
+
|
|
238
245
|
async function walkSessionFiles(dir: string): Promise<string[]> {
|
|
239
246
|
const result: string[] = [];
|
|
240
247
|
|
|
@@ -284,18 +291,17 @@ export async function queryGlobalHistory(
|
|
|
284
291
|
continue;
|
|
285
292
|
}
|
|
286
293
|
|
|
287
|
-
if (!parsed
|
|
288
|
-
const entry = parsed as {
|
|
289
|
-
type?: string;
|
|
290
|
-
customType?: string;
|
|
291
|
-
data?: unknown;
|
|
292
|
-
};
|
|
294
|
+
if (!isRecord(parsed)) continue;
|
|
293
295
|
|
|
294
|
-
if (
|
|
296
|
+
if (
|
|
297
|
+
parsed.type !== "custom" ||
|
|
298
|
+
parsed.customType !== EXT_CHANGE_CUSTOM_TYPE ||
|
|
299
|
+
!parsed.data
|
|
300
|
+
) {
|
|
295
301
|
continue;
|
|
296
302
|
}
|
|
297
303
|
|
|
298
|
-
const change = asChangeEntry(
|
|
304
|
+
const change = asChangeEntry(parsed.data);
|
|
299
305
|
if (change) {
|
|
300
306
|
all.push({ change, sessionFile: file });
|
|
301
307
|
}
|