pi-extmgr 0.1.23 → 0.1.24
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 -12
- package/package.json +1 -1
- package/src/commands/auto-update.ts +1 -1
- package/src/commands/cache.ts +5 -1
- package/src/commands/history.ts +4 -2
- package/src/commands/registry.ts +1 -1
- package/src/packages/discovery.ts +45 -29
- package/src/packages/extensions.ts +171 -63
- package/src/packages/install.ts +116 -22
- package/src/packages/management.ts +12 -37
- package/src/ui/package-config.ts +157 -126
- package/src/ui/remote.ts +77 -52
- package/src/ui/unified.ts +217 -172
- package/src/utils/auto-update.ts +34 -29
- package/src/utils/history.ts +41 -2
- package/src/utils/mode.ts +56 -5
- package/src/utils/package-source.ts +31 -0
- package/src/utils/settings-list.ts +12 -0
- package/src/utils/settings.ts +35 -7
- package/src/utils/status.ts +10 -2
- package/src/utils/timer.ts +32 -8
- package/src/utils/ui-helpers.ts +2 -1
package/src/utils/auto-update.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
} from "./settings.js";
|
|
20
20
|
import { parseNpmSource } from "./format.js";
|
|
21
21
|
import { execNpm } from "./npm-exec.js";
|
|
22
|
+
import { normalizePackageIdentity } from "./package-source.js";
|
|
23
|
+
import { logAutoUpdateConfig } from "./history.js";
|
|
22
24
|
import { TIMEOUTS } from "../constants.js";
|
|
23
25
|
|
|
24
26
|
import { startTimer, stopTimer, isTimerRunning } from "./timer.js";
|
|
@@ -26,6 +28,10 @@ import { startTimer, stopTimer, isTimerRunning } from "./timer.js";
|
|
|
26
28
|
// Context provider for safe session handling
|
|
27
29
|
export type ContextProvider = () => (ExtensionCommandContext | ExtensionContext) | undefined;
|
|
28
30
|
|
|
31
|
+
function getUpdateIdentity(pkg: InstalledPackage): string {
|
|
32
|
+
return normalizePackageIdentity(pkg.source);
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
/**
|
|
30
36
|
* Start auto-update background checker
|
|
31
37
|
* Uses a context provider to avoid stale context issues when sessions switch
|
|
@@ -48,27 +54,23 @@ export function startAutoUpdateTimer(
|
|
|
48
54
|
const interval = getScheduleInterval(config);
|
|
49
55
|
if (!interval) return;
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
saveAutoUpdateConfig(pi, {
|
|
69
|
-
...config,
|
|
70
|
-
nextCheck: calculateNextCheck(config.intervalMs),
|
|
71
|
-
});
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const nextCheck = config.nextCheck;
|
|
59
|
+
const initialDelayMs =
|
|
60
|
+
typeof nextCheck === "number" && nextCheck > now ? Math.max(0, nextCheck - now) : 0;
|
|
61
|
+
|
|
62
|
+
startTimer(
|
|
63
|
+
interval,
|
|
64
|
+
() => {
|
|
65
|
+
const checkCtx = getCtx();
|
|
66
|
+
if (!checkCtx) {
|
|
67
|
+
stopAutoUpdateTimer();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
void checkForUpdates(pi, checkCtx, onUpdateAvailable);
|
|
71
|
+
},
|
|
72
|
+
{ initialDelayMs }
|
|
73
|
+
);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/**
|
|
@@ -98,28 +100,30 @@ export async function checkForUpdates(
|
|
|
98
100
|
const npmPackages = packages.filter((p) => p.source.startsWith("npm:"));
|
|
99
101
|
|
|
100
102
|
const updatesAvailable: string[] = [];
|
|
103
|
+
const updatedPackageNames: string[] = [];
|
|
101
104
|
|
|
102
105
|
for (const pkg of npmPackages) {
|
|
103
106
|
const hasUpdate = await checkPackageUpdate(pkg, ctx, pi);
|
|
104
107
|
if (hasUpdate) {
|
|
105
|
-
updatesAvailable.push(pkg
|
|
108
|
+
updatesAvailable.push(getUpdateIdentity(pkg));
|
|
109
|
+
updatedPackageNames.push(pkg.name);
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
const checkedAt = Date.now();
|
|
110
114
|
const config = getAutoUpdateConfig(ctx);
|
|
111
115
|
saveAutoUpdateConfig(pi, {
|
|
112
116
|
...config,
|
|
113
|
-
lastCheck:
|
|
117
|
+
lastCheck: checkedAt,
|
|
114
118
|
nextCheck: calculateNextCheck(config.intervalMs),
|
|
115
119
|
updatesAvailable,
|
|
116
120
|
});
|
|
117
121
|
|
|
118
|
-
if (
|
|
119
|
-
onUpdateAvailable(
|
|
122
|
+
if (updatedPackageNames.length > 0 && onUpdateAvailable) {
|
|
123
|
+
onUpdateAvailable(updatedPackageNames);
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
return
|
|
126
|
+
return updatedPackageNames;
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
/**
|
|
@@ -168,7 +172,7 @@ export function getAutoUpdateStatus(ctx: ExtensionCommandContext | ExtensionCont
|
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
/**
|
|
171
|
-
* Return package
|
|
175
|
+
* Return normalized package identities currently known to have updates available
|
|
172
176
|
* (from the latest background check).
|
|
173
177
|
*/
|
|
174
178
|
export function getKnownUpdates(ctx: ExtensionCommandContext | ExtensionContext): Set<string> {
|
|
@@ -251,12 +255,12 @@ export function enableAutoUpdate(
|
|
|
251
255
|
intervalMs,
|
|
252
256
|
enabled: true,
|
|
253
257
|
displayText,
|
|
254
|
-
lastCheck: Date.now(),
|
|
255
258
|
nextCheck: calculateNextCheck(intervalMs),
|
|
256
259
|
updatesAvailable: [],
|
|
257
260
|
};
|
|
258
261
|
|
|
259
262
|
saveAutoUpdateConfig(pi, config);
|
|
263
|
+
logAutoUpdateConfig(pi, `set to ${displayText}`, true);
|
|
260
264
|
|
|
261
265
|
const getCtx: ContextProvider = () => ctx;
|
|
262
266
|
|
|
@@ -280,6 +284,7 @@ export function disableAutoUpdate(
|
|
|
280
284
|
displayText: "off",
|
|
281
285
|
updatesAvailable: [],
|
|
282
286
|
});
|
|
287
|
+
logAutoUpdateConfig(pi, "disabled", true);
|
|
283
288
|
|
|
284
289
|
notify(ctx, "Auto-update disabled", "info");
|
|
285
290
|
}
|
package/src/utils/history.ts
CHANGED
|
@@ -9,10 +9,12 @@ import { join } from "node:path";
|
|
|
9
9
|
|
|
10
10
|
export type ChangeAction =
|
|
11
11
|
| "extension_toggle"
|
|
12
|
+
| "extension_delete"
|
|
12
13
|
| "package_install"
|
|
13
14
|
| "package_update"
|
|
14
15
|
| "package_remove"
|
|
15
|
-
| "cache_clear"
|
|
16
|
+
| "cache_clear"
|
|
17
|
+
| "auto_update_config";
|
|
16
18
|
|
|
17
19
|
export interface ExtensionChangeEntry {
|
|
18
20
|
action: ChangeAction;
|
|
@@ -26,6 +28,7 @@ export interface ExtensionChangeEntry {
|
|
|
26
28
|
packageName?: string | undefined;
|
|
27
29
|
version?: string | undefined;
|
|
28
30
|
scope?: "global" | "project" | undefined;
|
|
31
|
+
detail?: string | undefined;
|
|
29
32
|
// Result
|
|
30
33
|
success: boolean;
|
|
31
34
|
error?: string | undefined;
|
|
@@ -80,6 +83,34 @@ export function logExtensionToggle(
|
|
|
80
83
|
});
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
export function logExtensionDelete(
|
|
87
|
+
pi: ExtensionAPI,
|
|
88
|
+
extensionId: string,
|
|
89
|
+
success: boolean,
|
|
90
|
+
error?: string
|
|
91
|
+
): void {
|
|
92
|
+
logChange(pi, {
|
|
93
|
+
action: "extension_delete",
|
|
94
|
+
extensionId,
|
|
95
|
+
success,
|
|
96
|
+
error,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function logAutoUpdateConfig(
|
|
101
|
+
pi: ExtensionAPI,
|
|
102
|
+
detail: string,
|
|
103
|
+
success: boolean,
|
|
104
|
+
error?: string
|
|
105
|
+
): void {
|
|
106
|
+
logChange(pi, {
|
|
107
|
+
action: "auto_update_config",
|
|
108
|
+
detail,
|
|
109
|
+
success,
|
|
110
|
+
error,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
83
114
|
/**
|
|
84
115
|
* Log package installation
|
|
85
116
|
*/
|
|
@@ -180,10 +211,12 @@ function matchesHistoryFilters(change: ExtensionChangeEntry, filters: HistoryFil
|
|
|
180
211
|
const packageName = change.packageName?.toLowerCase() ?? "";
|
|
181
212
|
const packageSource = change.packageSource?.toLowerCase() ?? "";
|
|
182
213
|
const extensionId = change.extensionId?.toLowerCase() ?? "";
|
|
214
|
+
const detail = change.detail?.toLowerCase() ?? "";
|
|
183
215
|
if (
|
|
184
216
|
!packageName.includes(packageQuery) &&
|
|
185
217
|
!packageSource.includes(packageQuery) &&
|
|
186
|
-
!extensionId.includes(packageQuery)
|
|
218
|
+
!extensionId.includes(packageQuery) &&
|
|
219
|
+
!detail.includes(packageQuery)
|
|
187
220
|
) {
|
|
188
221
|
return false;
|
|
189
222
|
}
|
|
@@ -330,6 +363,9 @@ export function formatChangeEntry(entry: ExtensionChangeEntry): string {
|
|
|
330
363
|
case "extension_toggle":
|
|
331
364
|
return `[${time}] ${icon} ${entry.extensionId}: ${entry.fromState} → ${entry.toState}`;
|
|
332
365
|
|
|
366
|
+
case "extension_delete":
|
|
367
|
+
return `[${time}] ${icon} Deleted ${entry.extensionId ?? "extension"}`;
|
|
368
|
+
|
|
333
369
|
case "package_install":
|
|
334
370
|
return `[${time}] ${icon} Installed ${packageLabel}${entry.version ? `@${entry.version}` : ""}${sourceSuffix}`;
|
|
335
371
|
|
|
@@ -342,6 +378,9 @@ export function formatChangeEntry(entry: ExtensionChangeEntry): string {
|
|
|
342
378
|
case "cache_clear":
|
|
343
379
|
return `[${time}] ${icon} Cache cleared`;
|
|
344
380
|
|
|
381
|
+
case "auto_update_config":
|
|
382
|
+
return `[${time}] ${icon} Auto-update ${entry.detail ?? "configuration changed"}`;
|
|
383
|
+
|
|
345
384
|
default:
|
|
346
385
|
return `[${time}] ${icon} Unknown action`;
|
|
347
386
|
}
|
package/src/utils/mode.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* UI
|
|
2
|
+
* UI capability helpers
|
|
3
3
|
*/
|
|
4
|
-
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import type { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
import { notify } from "./notify.js";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
type AnyContext = ExtensionCommandContext | ExtensionContext;
|
|
8
|
+
|
|
9
|
+
export type UICapability = "none" | "dialog" | "custom";
|
|
10
|
+
|
|
11
|
+
export function getUICapability(ctx: AnyContext): UICapability {
|
|
12
|
+
if (!ctx.hasUI) {
|
|
13
|
+
return "none";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return typeof ctx.ui?.custom === "function" ? "custom" : "dialog";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hasCustomUI(ctx: AnyContext): boolean {
|
|
20
|
+
return getUICapability(ctx) === "custom";
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
export function requireUI(ctx: ExtensionCommandContext, featureName: string): boolean {
|
|
11
24
|
if (!ctx.hasUI) {
|
|
12
25
|
notify(
|
|
@@ -19,6 +32,44 @@ export function requireUI(ctx: ExtensionCommandContext, featureName: string): bo
|
|
|
19
32
|
return true;
|
|
20
33
|
}
|
|
21
34
|
|
|
35
|
+
export function requireCustomUI(
|
|
36
|
+
ctx: AnyContext,
|
|
37
|
+
featureName: string,
|
|
38
|
+
fallbackMessage?: string
|
|
39
|
+
): boolean {
|
|
40
|
+
if (hasCustomUI(ctx)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const suffix = fallbackMessage ? ` ${fallbackMessage}` : "";
|
|
45
|
+
if (ctx.hasUI) {
|
|
46
|
+
notify(ctx, `${featureName} requires the full interactive TUI.${suffix}`, "warning");
|
|
47
|
+
} else {
|
|
48
|
+
notify(ctx, `${featureName} requires interactive mode.${suffix}`, "warning");
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function runCustomUI<T>(
|
|
54
|
+
ctx: AnyContext,
|
|
55
|
+
featureName: string,
|
|
56
|
+
open: () => Promise<T | undefined>,
|
|
57
|
+
fallbackMessage?: string
|
|
58
|
+
): Promise<T | undefined> {
|
|
59
|
+
if (!requireCustomUI(ctx, featureName, fallbackMessage)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = await open();
|
|
64
|
+
if (result !== undefined) {
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const suffix = fallbackMessage ? ` ${fallbackMessage}` : "";
|
|
69
|
+
notify(ctx, `${featureName} requires the full interactive TUI.${suffix}`, "warning");
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
22
73
|
/**
|
|
23
74
|
* Execute operation with automatic error handling
|
|
24
75
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package source parsing helpers shared across discovery/management flows.
|
|
3
3
|
*/
|
|
4
|
+
import { parseNpmSource } from "./format.js";
|
|
4
5
|
|
|
5
6
|
export type PackageSourceKind = "npm" | "git" | "local" | "unknown";
|
|
6
7
|
|
|
@@ -55,6 +56,36 @@ export function normalizeLocalSourceIdentity(source: string): string {
|
|
|
55
56
|
return looksWindowsPath ? normalized.toLowerCase() : normalized;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
export function stripGitSourcePrefix(source: string): string {
|
|
60
|
+
const withoutGitPlus = source.startsWith("git+") ? source.slice(4) : source;
|
|
61
|
+
return withoutGitPlus.startsWith("git:") ? withoutGitPlus.slice(4) : withoutGitPlus;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function normalizePackageIdentity(
|
|
65
|
+
source: string,
|
|
66
|
+
options?: { resolvedPath?: string }
|
|
67
|
+
): string {
|
|
68
|
+
const normalized = sanitizeSource(source);
|
|
69
|
+
const kind = getPackageSourceKind(normalized);
|
|
70
|
+
|
|
71
|
+
if (kind === "npm") {
|
|
72
|
+
const npm = parseNpmSource(normalized);
|
|
73
|
+
return `npm:${(npm?.name ?? normalized).toLowerCase()}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (kind === "git") {
|
|
77
|
+
const gitSpec = stripGitSourcePrefix(normalized);
|
|
78
|
+
const { repo } = splitGitRepoAndRef(gitSpec);
|
|
79
|
+
return `git:${repo.replace(/\\/g, "/").toLowerCase()}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (kind === "local") {
|
|
83
|
+
return `local:${normalizeLocalSourceIdentity(options?.resolvedPath ?? normalized)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return `raw:${normalized.replace(/\\/g, "/").toLowerCase()}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
58
89
|
export function splitGitRepoAndRef(gitSpec: string): { repo: string; ref?: string | undefined } {
|
|
59
90
|
const lastAt = gitSpec.lastIndexOf("@");
|
|
60
91
|
if (lastAt <= 0) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface SelectableListLike {
|
|
2
|
+
selectedIndex?: number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function getSettingsListSelectedIndex(settingsList: unknown): number | undefined {
|
|
6
|
+
if (!settingsList || typeof settingsList !== "object") {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const selectable = settingsList as SelectableListLike;
|
|
11
|
+
return Number.isInteger(selectable.selectedIndex) ? selectable.selectedIndex : undefined;
|
|
12
|
+
}
|
package/src/utils/settings.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { readFile, writeFile, mkdir, rename, rm } from "node:fs/promises";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { fileExists } from "./fs.js";
|
|
14
|
+
import { normalizePackageIdentity } from "./package-source.js";
|
|
14
15
|
|
|
15
16
|
export interface AutoUpdateConfig {
|
|
16
17
|
intervalMs: number;
|
|
@@ -49,6 +50,20 @@ function sanitizeStringArray(value: unknown): string[] | undefined {
|
|
|
49
50
|
return sanitized.length > 0 ? sanitized : undefined;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
function isUpdateIdentity(value: string): boolean {
|
|
54
|
+
return /^(npm|git|local|raw):/i.test(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sanitizeUpdateIdentities(value: unknown): string[] | undefined {
|
|
58
|
+
const updates = sanitizeStringArray(value);
|
|
59
|
+
if (!updates) return undefined;
|
|
60
|
+
|
|
61
|
+
const sanitized = updates
|
|
62
|
+
.filter(isUpdateIdentity)
|
|
63
|
+
.map((entry) => normalizePackageIdentity(entry));
|
|
64
|
+
return sanitized.length > 0 ? sanitized : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
function sanitizeAutoUpdateConfig(input: unknown): AutoUpdateConfig {
|
|
53
68
|
if (!isRecord(input)) {
|
|
54
69
|
return { ...DEFAULT_CONFIG };
|
|
@@ -85,7 +100,7 @@ function sanitizeAutoUpdateConfig(input: unknown): AutoUpdateConfig {
|
|
|
85
100
|
config.nextCheck = input.nextCheck;
|
|
86
101
|
}
|
|
87
102
|
|
|
88
|
-
const updates =
|
|
103
|
+
const updates = sanitizeUpdateIdentities(input.updatesAvailable);
|
|
89
104
|
if (updates) {
|
|
90
105
|
config.updatesAvailable = updates;
|
|
91
106
|
}
|
|
@@ -263,15 +278,28 @@ export function saveAutoUpdateConfig(pi: ExtensionAPI, config: Partial<AutoUpdat
|
|
|
263
278
|
*/
|
|
264
279
|
export function clearUpdatesAvailable(
|
|
265
280
|
pi: ExtensionAPI,
|
|
266
|
-
ctx: ExtensionCommandContext | ExtensionContext
|
|
281
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
282
|
+
identities?: Iterable<string>
|
|
267
283
|
): void {
|
|
268
284
|
const config = getAutoUpdateConfig(ctx);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
285
|
+
const currentUpdates = config.updatesAvailable ?? [];
|
|
286
|
+
if (currentUpdates.length === 0) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const clearedIdentities = identities ? new Set(identities) : undefined;
|
|
291
|
+
const updatesAvailable = clearedIdentities
|
|
292
|
+
? currentUpdates.filter((identity) => !clearedIdentities.has(identity))
|
|
293
|
+
: [];
|
|
294
|
+
|
|
295
|
+
if (updatesAvailable.length === currentUpdates.length) {
|
|
296
|
+
return;
|
|
274
297
|
}
|
|
298
|
+
|
|
299
|
+
saveAutoUpdateConfig(pi, {
|
|
300
|
+
...config,
|
|
301
|
+
updatesAvailable,
|
|
302
|
+
});
|
|
275
303
|
}
|
|
276
304
|
|
|
277
305
|
/**
|
package/src/utils/status.ts
CHANGED
|
@@ -8,14 +8,22 @@ import type {
|
|
|
8
8
|
} from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import { getInstalledPackages } from "../packages/discovery.js";
|
|
10
10
|
import { getAutoUpdateStatus } from "./auto-update.js";
|
|
11
|
+
import { normalizePackageIdentity } from "./package-source.js";
|
|
11
12
|
import { getAutoUpdateConfigAsync, saveAutoUpdateConfig } from "./settings.js";
|
|
12
13
|
|
|
13
14
|
function filterStaleUpdates(
|
|
14
15
|
knownUpdates: string[],
|
|
15
16
|
installedPackages: Awaited<ReturnType<typeof getInstalledPackages>>
|
|
16
17
|
): string[] {
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const installedIdentities = new Set(
|
|
19
|
+
installedPackages.map((pkg) =>
|
|
20
|
+
normalizePackageIdentity(
|
|
21
|
+
pkg.source,
|
|
22
|
+
pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : undefined
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
return knownUpdates.filter((identity) => installedIdentities.has(identity));
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export async function updateExtmgrStatus(
|
package/src/utils/timer.ts
CHANGED
|
@@ -4,33 +4,57 @@
|
|
|
4
4
|
|
|
5
5
|
export type TimerCallback = () => void;
|
|
6
6
|
|
|
7
|
-
let
|
|
7
|
+
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
8
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Start a recurring timer with the given interval and callback.
|
|
11
12
|
* Clears any existing timer first.
|
|
12
13
|
*/
|
|
13
|
-
export function startTimer(
|
|
14
|
+
export function startTimer(
|
|
15
|
+
intervalMs: number,
|
|
16
|
+
callback: TimerCallback,
|
|
17
|
+
options?: { initialDelayMs?: number }
|
|
18
|
+
): void {
|
|
14
19
|
stopTimer();
|
|
15
20
|
|
|
16
21
|
if (intervalMs <= 0) return;
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
const runAndReschedule = (): void => {
|
|
24
|
+
intervalId = setInterval(callback, intervalMs);
|
|
25
|
+
callback();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const initialDelayMs = options?.initialDelayMs ?? 0;
|
|
29
|
+
if (initialDelayMs <= 0) {
|
|
30
|
+
runAndReschedule();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
timeoutId = setTimeout(() => {
|
|
35
|
+
timeoutId = null;
|
|
36
|
+
runAndReschedule();
|
|
37
|
+
}, initialDelayMs);
|
|
19
38
|
}
|
|
20
39
|
|
|
21
40
|
/**
|
|
22
41
|
* Stop the current timer if running.
|
|
23
42
|
*/
|
|
24
43
|
export function stopTimer(): void {
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
if (timeoutId) {
|
|
45
|
+
clearTimeout(timeoutId);
|
|
46
|
+
timeoutId = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (intervalId) {
|
|
50
|
+
clearInterval(intervalId);
|
|
51
|
+
intervalId = null;
|
|
52
|
+
}
|
|
29
53
|
}
|
|
30
54
|
|
|
31
55
|
/**
|
|
32
56
|
* Check if a timer is currently running.
|
|
33
57
|
*/
|
|
34
58
|
export function isTimerRunning(): boolean {
|
|
35
|
-
return
|
|
59
|
+
return timeoutId !== null || intervalId !== null;
|
|
36
60
|
}
|
package/src/utils/ui-helpers.ts
CHANGED
|
@@ -67,9 +67,10 @@ export function formatListOutput(
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const output = items.join("\n");
|
|
70
|
+
const titledOutput = `${title}:\n${output}`;
|
|
70
71
|
|
|
71
72
|
if (ctx.hasUI) {
|
|
72
|
-
ctx.ui.notify(
|
|
73
|
+
ctx.ui.notify(titledOutput, "info");
|
|
73
74
|
} else {
|
|
74
75
|
console.log(`${title}:`);
|
|
75
76
|
console.log(output);
|