@link-assistant/hive-mind 1.58.0 → 1.59.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.
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Retry wrapper for `use-m` package loading.
5
+ *
6
+ * Issue #1710: Hosted CI runners occasionally hand back a truncated or
7
+ * partially-installed global package after `npm install -g <pkg>`. Two
8
+ * surface symptoms have been observed:
9
+ *
10
+ * 1. `import` throws a SyntaxError ("Unexpected end of input") wrapped
11
+ * in use-m's `Failed to import module from '<path>'.` — the file on
12
+ * disk is cut off mid-line.
13
+ * 2. use-m throws `Failed to resolve the path to '<pkg>' from '<dir>'`
14
+ * — the install completed without error but the package tree is
15
+ * missing files that the `main`/`exports` entry depends on.
16
+ *
17
+ * The recovery is identical for both: delete the broken alias install
18
+ * directory and ask use-m to re-fetch. A clean reinstall almost always
19
+ * succeeds. This helper centralises that retry so every call site picks
20
+ * it up.
21
+ */
22
+
23
+ /**
24
+ * @param {(specifier: string) => Promise<unknown>} use - the use-m loader.
25
+ * @param {string} specifier - the npm specifier to load (e.g. `'getenv'`).
26
+ * @param {object} [options]
27
+ * @param {number} [options.attempts=3] - total attempts including the first try.
28
+ * @param {(path: string) => Promise<void>} [options.cleanup] - injectable cleanup
29
+ * for the corrupted install directory (defaults to recursive `rm`).
30
+ * @returns {Promise<unknown>} the module returned by use-m.
31
+ */
32
+ export const useWithRetry = async (use, specifier, options = {}) => {
33
+ const attempts = options.attempts ?? 3;
34
+ const cleanup = options.cleanup ?? defaultCleanup;
35
+ let lastError;
36
+ for (let attempt = 1; attempt <= attempts; attempt++) {
37
+ try {
38
+ return await use(specifier);
39
+ } catch (error) {
40
+ lastError = error;
41
+ if (attempt === attempts || !isCorruptInstallError(error)) {
42
+ throw error;
43
+ }
44
+ const corruptedPath = extractCorruptedFilePath(error);
45
+ if (corruptedPath) {
46
+ try {
47
+ // Two failure modes:
48
+ // * "Failed to import module from '<file>'" — corruptedPath is a file
49
+ // inside the use-m alias dir (e.g. /.../getenv-v-latest/index.js).
50
+ // * "Failed to resolve the path to 'pkg' from '<dir>'" — corruptedPath
51
+ // is the alias dir itself (e.g. /.../links-notation-v-latest).
52
+ // For files, walk up to the alias dir; otherwise remove the dir as-is.
53
+ const { dirname } = await import('node:path');
54
+ const target = corruptedPath.endsWith('-v-latest') || /-v-\d/.test(corruptedPath) ? corruptedPath : dirname(corruptedPath);
55
+ await cleanup(target);
56
+ } catch {
57
+ // Best-effort cleanup; fall through to retry regardless.
58
+ }
59
+ }
60
+ }
61
+ }
62
+ // Unreachable — the loop either returns or throws.
63
+ throw lastError;
64
+ };
65
+
66
+ export const isCorruptInstallError = error => {
67
+ const cause = error?.cause;
68
+ if (cause instanceof SyntaxError) return true;
69
+ const causeMessage = typeof cause?.message === 'string' ? cause.message : '';
70
+ if (/Unexpected end of input|Unexpected token/.test(causeMessage)) return true;
71
+ // Mode 2 (also seen on hosted CI): npm install completes but the package
72
+ // tree is incomplete, so use-m can't resolve the entry point.
73
+ const message = typeof error?.message === 'string' ? error.message : '';
74
+ return /^Failed to resolve the path to /.test(message);
75
+ };
76
+
77
+ export const extractCorruptedFilePath = error => {
78
+ const message = typeof error?.message === 'string' ? error.message : '';
79
+ const importMatch = message.match(/Failed to import module from '([^']+)'/);
80
+ if (importMatch) return importMatch[1];
81
+ // For "Failed to resolve the path to 'pkg' from '<dir>'" the second path
82
+ // is already the alias install directory — return it directly so callers
83
+ // can clean it up (cleanup() handles both files and directories).
84
+ const resolveMatch = message.match(/Failed to resolve the path to '[^']+' from '([^']+)'/);
85
+ return resolveMatch ? resolveMatch[1] : null;
86
+ };
87
+
88
+ const defaultCleanup = async path => {
89
+ const { rm } = await import('node:fs/promises');
90
+ await rm(path, { recursive: true, force: true });
91
+ };