pi-extmgr 0.1.27 → 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.
Files changed (41) hide show
  1. package/README.md +21 -10
  2. package/package.json +21 -16
  3. package/src/commands/auto-update.ts +5 -5
  4. package/src/commands/cache.ts +1 -1
  5. package/src/commands/history.ts +5 -34
  6. package/src/commands/install.ts +2 -2
  7. package/src/commands/registry.ts +7 -7
  8. package/src/commands/types.ts +1 -1
  9. package/src/constants.ts +0 -8
  10. package/src/extensions/discovery.ts +125 -42
  11. package/src/index.ts +15 -15
  12. package/src/packages/catalog.ts +9 -8
  13. package/src/packages/discovery.ts +56 -19
  14. package/src/packages/extensions.ts +65 -103
  15. package/src/packages/install.ts +104 -74
  16. package/src/packages/management.ts +78 -65
  17. package/src/types/index.ts +20 -11
  18. package/src/ui/async-task.ts +101 -65
  19. package/src/ui/footer.ts +47 -31
  20. package/src/ui/help.ts +17 -13
  21. package/src/ui/package-config.ts +36 -48
  22. package/src/ui/remote.ts +714 -119
  23. package/src/ui/theme.ts +2 -2
  24. package/src/ui/unified.ts +964 -371
  25. package/src/utils/auto-update.ts +44 -39
  26. package/src/utils/cache.ts +208 -37
  27. package/src/utils/command.ts +1 -1
  28. package/src/utils/duration.ts +132 -0
  29. package/src/utils/format.ts +4 -33
  30. package/src/utils/fs.ts +8 -4
  31. package/src/utils/history.ts +47 -9
  32. package/src/utils/mode.ts +2 -2
  33. package/src/utils/notify.ts +1 -15
  34. package/src/utils/npm-exec.ts +1 -1
  35. package/src/utils/package-source.ts +35 -7
  36. package/src/utils/path-identity.ts +7 -0
  37. package/src/utils/relative-path-selection.ts +100 -0
  38. package/src/utils/settings.ts +11 -61
  39. package/src/utils/status.ts +12 -10
  40. package/src/utils/ui-helpers.ts +2 -2
  41. package/src/utils/retry.ts +0 -49
package/src/utils/fs.ts CHANGED
@@ -4,6 +4,10 @@
4
4
  import { access, readFile } from "node:fs/promises";
5
5
  import { truncate } from "./format.js";
6
6
 
7
+ function formatSummary(text: string): string {
8
+ return truncate(text.replace(/\s+/g, " ").trim(), 80);
9
+ }
10
+
7
11
  export async function fileExists(filePath: string): Promise<boolean> {
8
12
  try {
9
13
  await access(filePath);
@@ -28,7 +32,7 @@ export async function readSummary(filePath: string): Promise<string> {
28
32
  for (const pattern of descriptionPatterns) {
29
33
  const match = text.match(pattern);
30
34
  const value = match?.[1]?.trim();
31
- if (value) return truncate(value, 80);
35
+ if (value) return formatSummary(value);
32
36
  }
33
37
 
34
38
  // Look for block comments
@@ -45,7 +49,7 @@ export async function readSummary(filePath: string): Promise<string> {
45
49
  )
46
50
  .filter((s): s is string => Boolean(s));
47
51
  const firstLine = lines[0];
48
- if (firstLine) return truncate(firstLine, 80);
52
+ if (firstLine) return formatSummary(firstLine);
49
53
  }
50
54
 
51
55
  // Look for line comments
@@ -55,13 +59,13 @@ export async function readSummary(filePath: string): Promise<string> {
55
59
  .split("\n")
56
60
  .map((line) => line.replace(/^\s*\/\/\s?/, "").trim())
57
61
  .filter(Boolean)[0];
58
- if (first) return truncate(first, 80);
62
+ if (first) return formatSummary(first);
59
63
  }
60
64
 
61
65
  // First non-empty line
62
66
  for (const line of text.split("\n")) {
63
67
  const clean = line.trim();
64
- if (clean.length > 0) return truncate(clean, 80);
68
+ if (clean.length > 0) return formatSummary(clean);
65
69
  }
66
70
  } catch {
67
71
  // ignore
@@ -2,10 +2,12 @@
2
2
  * Extension change history tracking using pi.appendEntry()
3
3
  * This persists extension management actions to the session
4
4
  */
5
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
5
+
6
+ import { type Dirent } from "node:fs";
6
7
  import { readdir, readFile } from "node:fs/promises";
7
8
  import { homedir } from "node:os";
8
9
  import { join } from "node:path";
10
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
9
11
 
10
12
  export type ChangeAction =
11
13
  | "extension_toggle"
@@ -278,7 +280,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
278
280
  async function walkSessionFiles(dir: string): Promise<string[]> {
279
281
  const result: string[] = [];
280
282
 
281
- let entries;
283
+ let entries: Dirent<string>[];
282
284
  try {
283
285
  entries = await readdir(dir, { withFileTypes: true, encoding: "utf8" });
284
286
  } catch {
@@ -297,6 +299,41 @@ async function walkSessionFiles(dir: string): Promise<string[]> {
297
299
  return result;
298
300
  }
299
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
+
300
337
  /**
301
338
  * Query change history across all persisted pi sessions.
302
339
  */
@@ -305,7 +342,8 @@ export async function queryGlobalHistory(
305
342
  sessionDir = DEFAULT_SESSION_DIR
306
343
  ): Promise<GlobalHistoryEntry[]> {
307
344
  const files = await walkSessionFiles(sessionDir);
308
- const all: GlobalHistoryEntry[] = [];
345
+ const matchedEntries: GlobalHistoryEntry[] = [];
346
+ const limit = filters.limit ?? 20;
309
347
 
310
348
  for (const file of files) {
311
349
  let text: string;
@@ -335,16 +373,16 @@ export async function queryGlobalHistory(
335
373
  }
336
374
 
337
375
  const change = asChangeEntry(parsed.data);
338
- if (change) {
339
- all.push({ change, sessionFile: file });
376
+ if (!change || !matchesHistoryFilters(change, filters)) {
377
+ continue;
340
378
  }
379
+
380
+ pushGlobalHistoryEntry(matchedEntries, { change, sessionFile: file }, limit);
341
381
  }
342
382
  }
343
383
 
344
- all.sort((a, b) => a.change.timestamp - b.change.timestamp);
345
-
346
- const filtered = all.filter((entry) => matchesHistoryFilters(entry.change, filters));
347
- return applyHistoryLimit(filtered, filters);
384
+ matchedEntries.sort((a, b) => a.change.timestamp - b.change.timestamp);
385
+ return matchedEntries;
348
386
  }
349
387
 
350
388
  /**
package/src/utils/mode.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * UI capability helpers
3
3
  */
4
- import type { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
4
+ import { type ExtensionCommandContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
5
5
  import { notify } from "./notify.js";
6
6
 
7
7
  type AnyContext = ExtensionCommandContext | ExtensionContext;
@@ -66,7 +66,7 @@ export async function runCustomUI<T>(
66
66
  }
67
67
 
68
68
  const suffix = fallbackMessage ? ` ${fallbackMessage}` : "";
69
- notify(ctx, `${featureName} requires the full interactive TUI.${suffix}`, "warning");
69
+ notify(ctx, `${featureName} is unavailable in the current UI mode.${suffix}`, "warning");
70
70
  return undefined;
71
71
  }
72
72
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Centralized notification handling for UI and non-UI modes
3
3
  */
4
- import type { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
4
+ import { type ExtensionCommandContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
5
5
 
6
6
  export type NotifyLevel = "info" | "warning" | "error";
7
7
 
@@ -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
- }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { execPath, platform } from "node:process";
3
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
+ import { type ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
4
 
5
5
  interface NpmCommandResolutionOptions {
6
6
  platform?: NodeJS.Platform;
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * Package source parsing helpers shared across discovery/management flows.
3
3
  */
4
+ import { homedir } from "node:os";
5
+ import { join, resolve as resolvePath } from "node:path";
6
+ import { fileURLToPath } from "node:url";
4
7
  import { parseNpmSource } from "./format.js";
8
+ import { normalizePathIdentity } from "./path-identity.js";
5
9
 
6
10
  export type PackageSourceKind = "npm" | "git" | "local" | "unknown";
7
11
 
@@ -49,11 +53,7 @@ export function getPackageSourceKind(source: string): PackageSourceKind {
49
53
  }
50
54
 
51
55
  export function normalizeLocalSourceIdentity(source: string): string {
52
- const normalized = source.replace(/\\/g, "/");
53
- const looksWindowsPath =
54
- /^[a-zA-Z]:\//.test(normalized) || normalized.startsWith("//") || source.includes("\\");
55
-
56
- return looksWindowsPath ? normalized.toLowerCase() : normalized;
56
+ return normalizePathIdentity(source);
57
57
  }
58
58
 
59
59
  export function stripGitSourcePrefix(source: string): string {
@@ -61,9 +61,35 @@ export function stripGitSourcePrefix(source: string): string {
61
61
  return withoutGitPlus.startsWith("git:") ? withoutGitPlus.slice(4) : withoutGitPlus;
62
62
  }
63
63
 
64
+ function resolveLocalSourceForIdentity(source: string, cwd?: string): string {
65
+ if (source.startsWith("file://")) {
66
+ try {
67
+ return fileURLToPath(source);
68
+ } catch {
69
+ return source;
70
+ }
71
+ }
72
+
73
+ if (source.startsWith("~/")) {
74
+ return join(homedir(), source.slice(2));
75
+ }
76
+
77
+ if (
78
+ cwd &&
79
+ (source.startsWith("./") ||
80
+ source.startsWith("../") ||
81
+ source.startsWith(".\\") ||
82
+ source.startsWith("..\\"))
83
+ ) {
84
+ return resolvePath(cwd, source);
85
+ }
86
+
87
+ return source;
88
+ }
89
+
64
90
  export function normalizePackageIdentity(
65
91
  source: string,
66
- options?: { resolvedPath?: string }
92
+ options?: { resolvedPath?: string; cwd?: string }
67
93
  ): string {
68
94
  const normalized = sanitizeSource(source);
69
95
  const kind = getPackageSourceKind(normalized);
@@ -80,7 +106,9 @@ export function normalizePackageIdentity(
80
106
  }
81
107
 
82
108
  if (kind === "local") {
83
- return `local:${normalizeLocalSourceIdentity(options?.resolvedPath ?? normalized)}`;
109
+ const localSource =
110
+ options?.resolvedPath ?? resolveLocalSourceForIdentity(normalized, options?.cwd);
111
+ return `local:${normalizeLocalSourceIdentity(localSource)}`;
84
112
  }
85
113
 
86
114
  return `raw:${normalized.replace(/\\/g, "/").toLowerCase()}`;
@@ -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
+ }
@@ -2,14 +2,16 @@
2
2
  * Auto-update settings storage
3
3
  * Persists to disk so config survives across pi sessions.
4
4
  */
5
- import type {
6
- ExtensionAPI,
7
- ExtensionCommandContext,
8
- ExtensionContext,
9
- } from "@mariozechner/pi-coding-agent";
10
- import { readFile, writeFile, mkdir, rename, rm } from "node:fs/promises";
5
+
6
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
11
7
  import { homedir } from "node:os";
12
8
  import { join } from "node:path";
9
+ import {
10
+ type ExtensionAPI,
11
+ type ExtensionCommandContext,
12
+ type ExtensionContext,
13
+ } from "@mariozechner/pi-coding-agent";
14
+ import { parseScheduleDuration } from "./duration.js";
13
15
  import { fileExists } from "./fs.js";
14
16
  import { normalizePackageIdentity } from "./package-source.js";
15
17
 
@@ -303,63 +305,11 @@ export function clearUpdatesAvailable(
303
305
  }
304
306
 
305
307
  /**
306
- * Parse duration string to milliseconds
307
- * Supports: 1h, 2h, 1d, 7d, 1m, 3m, etc.
308
- * 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`.
309
310
  */
310
311
  export function parseDuration(input: string): { ms: number; display: string } | undefined {
311
- const normalized = input.toLowerCase().trim();
312
-
313
- // Special cases for disabling
314
- if (normalized === "never" || normalized === "off" || normalized === "disable") {
315
- return { ms: 0, display: "off" };
316
- }
317
-
318
- // Named schedules
319
- if (normalized === "daily" || normalized === "day" || normalized === "1d") {
320
- return { ms: 24 * 60 * 60 * 1000, display: "daily" };
321
- }
322
- if (normalized === "weekly" || normalized === "week" || normalized === "1w") {
323
- return { ms: 7 * 24 * 60 * 60 * 1000, display: "weekly" };
324
- }
325
-
326
- // Parse duration patterns: 1h, 2h, 3d, 7d, 1m, etc.
327
- const durationMatch = normalized.match(
328
- /^(\d+)\s*(h|hr|hrs|hour|hours|d|day|days|w|wk|wks|week|weeks|m|mo|mos|month|months)$/
329
- );
330
- if (durationMatch) {
331
- const value = parseInt(durationMatch[1]!, 10);
332
- const unit = durationMatch[2]![0]; // First character of unit
333
-
334
- let ms: number;
335
- let display: string;
336
-
337
- switch (unit) {
338
- case "h":
339
- ms = value * 60 * 60 * 1000;
340
- display = value === 1 ? "1 hour" : `${value} hours`;
341
- break;
342
- case "d":
343
- ms = value * 24 * 60 * 60 * 1000;
344
- display = value === 1 ? "1 day" : `${value} days`;
345
- break;
346
- case "w":
347
- ms = value * 7 * 24 * 60 * 60 * 1000;
348
- display = value === 1 ? "1 week" : `${value} weeks`;
349
- break;
350
- case "m":
351
- // Approximate months as 30 days
352
- ms = value * 30 * 24 * 60 * 60 * 1000;
353
- display = value === 1 ? "1 month" : `${value} months`;
354
- break;
355
- default:
356
- return undefined;
357
- }
358
-
359
- return { ms, display };
360
- }
361
-
362
- return undefined;
312
+ return parseScheduleDuration(input);
363
313
  }
364
314
 
365
315
  /**
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Status bar helpers for extmgr
3
3
  */
4
- import type {
5
- ExtensionAPI,
6
- ExtensionCommandContext,
7
- ExtensionContext,
4
+ import {
5
+ type ExtensionAPI,
6
+ type ExtensionCommandContext,
7
+ type ExtensionContext,
8
+ getAgentDir,
8
9
  } from "@mariozechner/pi-coding-agent";
9
10
  import { getPackageCatalog, type PackageCatalog } from "../packages/catalog.js";
10
11
  import { getAutoUpdateStatus } from "./auto-update.js";
@@ -15,14 +16,15 @@ type CatalogInstalledPackages = Awaited<ReturnType<PackageCatalog["listInstalled
15
16
 
16
17
  function filterStaleUpdates(
17
18
  knownUpdates: string[],
18
- installedPackages: CatalogInstalledPackages
19
+ installedPackages: CatalogInstalledPackages,
20
+ cwd: string
19
21
  ): string[] {
20
22
  const installedIdentities = new Set(
21
23
  installedPackages.map((pkg) =>
22
- normalizePackageIdentity(
23
- pkg.source,
24
- pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : undefined
25
- )
24
+ normalizePackageIdentity(pkg.source, {
25
+ ...(pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : {}),
26
+ cwd: pkg.scope === "project" ? cwd : getAgentDir(),
27
+ })
26
28
  )
27
29
  );
28
30
  return knownUpdates.filter((identity) => installedIdentities.has(identity));
@@ -52,7 +54,7 @@ export async function updateExtmgrStatus(
52
54
 
53
55
  // Validate updates against actually installed packages (handles external pi update)
54
56
  const knownUpdates = autoUpdateConfig.updatesAvailable ?? [];
55
- const validUpdates = filterStaleUpdates(knownUpdates, packages);
57
+ const validUpdates = filterStaleUpdates(knownUpdates, packages, ctx.cwd);
56
58
 
57
59
  // If stale updates were filtered, persist the correction
58
60
  if (validUpdates.length !== knownUpdates.length) {
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Common UI helper patterns
3
3
  */
4
- import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
5
- import { notify } from "./notify.js";
4
+ import { type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
6
5
  import { UI } from "../constants.js";
6
+ import { notify } from "./notify.js";
7
7
 
8
8
  /**
9
9
  * Confirm and trigger reload
@@ -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 * Math.pow(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
- }