pi-extmgr 0.1.28 → 0.2.0
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 +18 -8
- package/package.json +10 -2
- package/src/commands/auto-update.ts +1 -1
- package/src/commands/history.ts +2 -31
- package/src/constants.ts +0 -8
- package/src/extensions/discovery.ts +121 -39
- package/src/packages/discovery.ts +34 -0
- package/src/packages/extensions.ts +55 -98
- package/src/packages/install.ts +85 -56
- package/src/packages/management.ts +25 -38
- package/src/types/index.ts +4 -2
- package/src/ui/footer.ts +49 -29
- package/src/ui/help.ts +15 -11
- package/src/ui/remote.ts +704 -112
- package/src/ui/unified.ts +922 -311
- package/src/utils/auto-update.ts +34 -29
- package/src/utils/cache.ts +205 -34
- package/src/utils/duration.ts +132 -0
- package/src/utils/format.ts +0 -30
- package/src/utils/fs.ts +8 -4
- package/src/utils/history.ts +43 -7
- package/src/utils/mode.ts +1 -1
- package/src/utils/notify.ts +0 -14
- package/src/utils/package-source.ts +2 -5
- package/src/utils/path-identity.ts +7 -0
- package/src/utils/relative-path-selection.ts +100 -0
- package/src/utils/settings.ts +4 -63
- package/src/utils/retry.ts +0 -49
package/src/utils/history.ts
CHANGED
|
@@ -299,6 +299,41 @@ async function walkSessionFiles(dir: string): Promise<string[]> {
|
|
|
299
299
|
return result;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
function pushGlobalHistoryEntry(
|
|
303
|
+
entries: GlobalHistoryEntry[],
|
|
304
|
+
nextEntry: GlobalHistoryEntry,
|
|
305
|
+
limit: number | undefined
|
|
306
|
+
): void {
|
|
307
|
+
if (!limit || limit <= 0) {
|
|
308
|
+
entries.push(nextEntry);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (entries.length < limit) {
|
|
313
|
+
entries.push(nextEntry);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let oldestIndex = 0;
|
|
318
|
+
let oldestEntry = entries[oldestIndex];
|
|
319
|
+
if (!oldestEntry) {
|
|
320
|
+
entries.push(nextEntry);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (let index = 1; index < entries.length; index++) {
|
|
325
|
+
const entry = entries[index];
|
|
326
|
+
if (entry && entry.change.timestamp < oldestEntry.change.timestamp) {
|
|
327
|
+
oldestIndex = index;
|
|
328
|
+
oldestEntry = entry;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (oldestEntry.change.timestamp <= nextEntry.change.timestamp) {
|
|
333
|
+
entries[oldestIndex] = nextEntry;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
302
337
|
/**
|
|
303
338
|
* Query change history across all persisted pi sessions.
|
|
304
339
|
*/
|
|
@@ -307,7 +342,8 @@ export async function queryGlobalHistory(
|
|
|
307
342
|
sessionDir = DEFAULT_SESSION_DIR
|
|
308
343
|
): Promise<GlobalHistoryEntry[]> {
|
|
309
344
|
const files = await walkSessionFiles(sessionDir);
|
|
310
|
-
const
|
|
345
|
+
const matchedEntries: GlobalHistoryEntry[] = [];
|
|
346
|
+
const limit = filters.limit ?? 20;
|
|
311
347
|
|
|
312
348
|
for (const file of files) {
|
|
313
349
|
let text: string;
|
|
@@ -337,16 +373,16 @@ export async function queryGlobalHistory(
|
|
|
337
373
|
}
|
|
338
374
|
|
|
339
375
|
const change = asChangeEntry(parsed.data);
|
|
340
|
-
if (change) {
|
|
341
|
-
|
|
376
|
+
if (!change || !matchesHistoryFilters(change, filters)) {
|
|
377
|
+
continue;
|
|
342
378
|
}
|
|
379
|
+
|
|
380
|
+
pushGlobalHistoryEntry(matchedEntries, { change, sessionFile: file }, limit);
|
|
343
381
|
}
|
|
344
382
|
}
|
|
345
383
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const filtered = all.filter((entry) => matchesHistoryFilters(entry.change, filters));
|
|
349
|
-
return applyHistoryLimit(filtered, filters);
|
|
384
|
+
matchedEntries.sort((a, b) => a.change.timestamp - b.change.timestamp);
|
|
385
|
+
return matchedEntries;
|
|
350
386
|
}
|
|
351
387
|
|
|
352
388
|
/**
|
package/src/utils/mode.ts
CHANGED
|
@@ -66,7 +66,7 @@ export async function runCustomUI<T>(
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const suffix = fallbackMessage ? ` ${fallbackMessage}` : "";
|
|
69
|
-
notify(ctx, `${featureName}
|
|
69
|
+
notify(ctx, `${featureName} is unavailable in the current UI mode.${suffix}`, "warning");
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
72
|
|
package/src/utils/notify.ts
CHANGED
|
@@ -34,17 +34,3 @@ export function success(ctx: ExtensionCommandContext | ExtensionContext, message
|
|
|
34
34
|
export function error(ctx: ExtensionCommandContext | ExtensionContext, message: string): void {
|
|
35
35
|
notify(ctx, message, "error");
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Show warning message
|
|
40
|
-
*/
|
|
41
|
-
export function warning(ctx: ExtensionCommandContext | ExtensionContext, message: string): void {
|
|
42
|
-
notify(ctx, message, "warning");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Show info message
|
|
47
|
-
*/
|
|
48
|
-
export function info(ctx: ExtensionCommandContext | ExtensionContext, message: string): void {
|
|
49
|
-
notify(ctx, message, "info");
|
|
50
|
-
}
|
|
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { join, resolve as resolvePath } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { parseNpmSource } from "./format.js";
|
|
8
|
+
import { normalizePathIdentity } from "./path-identity.js";
|
|
8
9
|
|
|
9
10
|
export type PackageSourceKind = "npm" | "git" | "local" | "unknown";
|
|
10
11
|
|
|
@@ -52,11 +53,7 @@ export function getPackageSourceKind(source: string): PackageSourceKind {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export function normalizeLocalSourceIdentity(source: string): string {
|
|
55
|
-
|
|
56
|
-
const looksWindowsPath =
|
|
57
|
-
/^[a-zA-Z]:\//.test(normalized) || normalized.startsWith("//") || source.includes("\\");
|
|
58
|
-
|
|
59
|
-
return looksWindowsPath ? normalized.toLowerCase() : normalized;
|
|
56
|
+
return normalizePathIdentity(source);
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
export function stripGitSourcePrefix(source: string): string {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function normalizePathIdentity(value: string): string {
|
|
2
|
+
const normalized = value.replace(/\\/g, "/");
|
|
3
|
+
const looksWindowsPath =
|
|
4
|
+
/^[a-zA-Z]:\//.test(normalized) || normalized.startsWith("//") || value.includes("\\");
|
|
5
|
+
|
|
6
|
+
return looksWindowsPath ? normalized.toLowerCase() : normalized;
|
|
7
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { matchesGlob } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function normalizeRelativePath(value: string): string {
|
|
4
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function hasGlobMagic(path: string): boolean {
|
|
8
|
+
return /[*?{}[\]]/.test(path);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isSafeRelativePath(path: string): boolean {
|
|
12
|
+
const normalizedPath = path.replace(/\\/g, "/");
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
normalizedPath !== "" &&
|
|
16
|
+
normalizedPath !== ".." &&
|
|
17
|
+
!normalizedPath.startsWith("/") &&
|
|
18
|
+
!path.startsWith("\\") &&
|
|
19
|
+
!/^[A-Za-z]:/.test(normalizedPath) &&
|
|
20
|
+
!normalizedPath.startsWith("../") &&
|
|
21
|
+
!normalizedPath.includes("/../") &&
|
|
22
|
+
!normalizedPath.endsWith("/..")
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function safeMatchesGlob(targetPath: string, pattern: string): boolean {
|
|
27
|
+
try {
|
|
28
|
+
return matchesGlob(targetPath, pattern);
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function matchesFilterPattern(targetPath: string, pattern: string): boolean {
|
|
35
|
+
const normalizedPattern = normalizeRelativePath(pattern.trim());
|
|
36
|
+
if (!normalizedPattern) return false;
|
|
37
|
+
if (targetPath === normalizedPattern) return true;
|
|
38
|
+
|
|
39
|
+
return safeMatchesGlob(targetPath, normalizedPattern);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function selectDirectoryFiles(allFiles: readonly string[], directoryPath: string): string[] {
|
|
43
|
+
const prefix = `${directoryPath}/`;
|
|
44
|
+
return allFiles.filter((file) => file.startsWith(prefix));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function applySelection(
|
|
48
|
+
selected: Set<string>,
|
|
49
|
+
files: Iterable<string>,
|
|
50
|
+
exclude: boolean
|
|
51
|
+
): void {
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
if (exclude) {
|
|
54
|
+
selected.delete(file);
|
|
55
|
+
} else {
|
|
56
|
+
selected.add(file);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveRelativePathSelection(
|
|
62
|
+
allFiles: readonly string[],
|
|
63
|
+
entries: readonly string[],
|
|
64
|
+
isExactPathSelectable: (path: string, allFiles: readonly string[]) => boolean
|
|
65
|
+
): string[] {
|
|
66
|
+
const selected = new Set<string>();
|
|
67
|
+
|
|
68
|
+
for (const rawToken of entries) {
|
|
69
|
+
const token = rawToken.trim();
|
|
70
|
+
if (!token) continue;
|
|
71
|
+
|
|
72
|
+
const exclude = token.startsWith("!");
|
|
73
|
+
const normalizedToken = normalizeRelativePath(exclude ? token.slice(1) : token);
|
|
74
|
+
const pattern = normalizedToken.replace(/[\\/]+$/g, "");
|
|
75
|
+
if (!isSafeRelativePath(pattern)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (hasGlobMagic(pattern)) {
|
|
80
|
+
applySelection(
|
|
81
|
+
selected,
|
|
82
|
+
allFiles.filter((file) => matchesFilterPattern(file, pattern)),
|
|
83
|
+
exclude
|
|
84
|
+
);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const directoryFiles = selectDirectoryFiles(allFiles, pattern);
|
|
89
|
+
if (directoryFiles.length > 0) {
|
|
90
|
+
applySelection(selected, directoryFiles, exclude);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (isExactPathSelectable(pattern, allFiles)) {
|
|
95
|
+
applySelection(selected, [pattern], exclude);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Array.from(selected).sort((a, b) => a.localeCompare(b));
|
|
100
|
+
}
|
package/src/utils/settings.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ExtensionCommandContext,
|
|
12
12
|
type ExtensionContext,
|
|
13
13
|
} from "@mariozechner/pi-coding-agent";
|
|
14
|
+
import { parseScheduleDuration } from "./duration.js";
|
|
14
15
|
import { fileExists } from "./fs.js";
|
|
15
16
|
import { normalizePackageIdentity } from "./package-source.js";
|
|
16
17
|
|
|
@@ -304,71 +305,11 @@ export function clearUpdatesAvailable(
|
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
/**
|
|
307
|
-
* Parse duration
|
|
308
|
-
* Supports
|
|
309
|
-
* Also supports: never, off, disable, daily, weekly
|
|
308
|
+
* Parse schedule duration strings for auto-update settings.
|
|
309
|
+
* Supports hours/days/weeks/months plus schedule aliases like `daily`, `weekly`, and `never`.
|
|
310
310
|
*/
|
|
311
311
|
export function parseDuration(input: string): { ms: number; display: string } | undefined {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// Special cases for disabling
|
|
315
|
-
if (normalized === "never" || normalized === "off" || normalized === "disable") {
|
|
316
|
-
return { ms: 0, display: "off" };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Named schedules
|
|
320
|
-
if (normalized === "daily" || normalized === "day" || normalized === "1d") {
|
|
321
|
-
return { ms: 24 * 60 * 60 * 1000, display: "daily" };
|
|
322
|
-
}
|
|
323
|
-
if (normalized === "weekly" || normalized === "week" || normalized === "1w") {
|
|
324
|
-
return { ms: 7 * 24 * 60 * 60 * 1000, display: "weekly" };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Parse duration patterns: 1h, 2h, 3d, 7d, 1m, etc.
|
|
328
|
-
const durationMatch = normalized.match(
|
|
329
|
-
/^(\d+)\s*(h|hr|hrs|hour|hours|d|day|days|w|wk|wks|week|weeks|m|mo|mos|month|months)$/
|
|
330
|
-
);
|
|
331
|
-
if (durationMatch) {
|
|
332
|
-
const [, rawValue, rawUnit] = durationMatch;
|
|
333
|
-
if (!rawValue || !rawUnit) {
|
|
334
|
-
return undefined;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const value = Number.parseInt(rawValue, 10);
|
|
338
|
-
const unit = rawUnit[0];
|
|
339
|
-
if (!unit) {
|
|
340
|
-
return undefined;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
let ms: number;
|
|
344
|
-
let display: string;
|
|
345
|
-
|
|
346
|
-
switch (unit) {
|
|
347
|
-
case "h":
|
|
348
|
-
ms = value * 60 * 60 * 1000;
|
|
349
|
-
display = value === 1 ? "1 hour" : `${value} hours`;
|
|
350
|
-
break;
|
|
351
|
-
case "d":
|
|
352
|
-
ms = value * 24 * 60 * 60 * 1000;
|
|
353
|
-
display = value === 1 ? "1 day" : `${value} days`;
|
|
354
|
-
break;
|
|
355
|
-
case "w":
|
|
356
|
-
ms = value * 7 * 24 * 60 * 60 * 1000;
|
|
357
|
-
display = value === 1 ? "1 week" : `${value} weeks`;
|
|
358
|
-
break;
|
|
359
|
-
case "m":
|
|
360
|
-
// Approximate months as 30 days
|
|
361
|
-
ms = value * 30 * 24 * 60 * 60 * 1000;
|
|
362
|
-
display = value === 1 ? "1 month" : `${value} months`;
|
|
363
|
-
break;
|
|
364
|
-
default:
|
|
365
|
-
return undefined;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return { ms, display };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return undefined;
|
|
312
|
+
return parseScheduleDuration(input);
|
|
372
313
|
}
|
|
373
314
|
|
|
374
315
|
/**
|
package/src/utils/retry.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Retry utilities for async operations
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface RetryOptions {
|
|
6
|
-
maxAttempts?: number;
|
|
7
|
-
delayMs?: number;
|
|
8
|
-
backoff?: "fixed" | "linear" | "exponential";
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function retryWithBackoff<T>(
|
|
12
|
-
operation: () => Promise<T | undefined>,
|
|
13
|
-
options: RetryOptions = {}
|
|
14
|
-
): Promise<T | undefined> {
|
|
15
|
-
const { maxAttempts = 5, delayMs = 100, backoff = "exponential" } = options;
|
|
16
|
-
|
|
17
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
18
|
-
const result = await operation();
|
|
19
|
-
if (result !== undefined) {
|
|
20
|
-
return result;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (attempt < maxAttempts) {
|
|
24
|
-
const delay =
|
|
25
|
-
backoff === "exponential"
|
|
26
|
-
? delayMs * 2 ** (attempt - 1)
|
|
27
|
-
: backoff === "linear"
|
|
28
|
-
? delayMs * attempt
|
|
29
|
-
: delayMs;
|
|
30
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Wait for a condition to be true with timeout
|
|
39
|
-
*/
|
|
40
|
-
export async function waitForCondition(
|
|
41
|
-
condition: () => Promise<boolean> | boolean,
|
|
42
|
-
options: RetryOptions = {}
|
|
43
|
-
): Promise<boolean> {
|
|
44
|
-
const result = await retryWithBackoff(async () => {
|
|
45
|
-
const value = await condition();
|
|
46
|
-
return value ? true : undefined;
|
|
47
|
-
}, options);
|
|
48
|
-
return result === true;
|
|
49
|
-
}
|