()
| 99 | } |
| 100 | |
| 101 | export function fetchAdapters() { |
| 102 | const currentVersion = getPackageVersion(); |
| 103 | const oldManifest = readManifest(); |
| 104 | |
| 105 | // Skip if already installed at the same version (unless forced via OPENCLI_FETCH=1) |
| 106 | const isForced = process.env.OPENCLI_FETCH === '1'; |
| 107 | if (!isForced && currentVersion !== 'unknown' && oldManifest?.version === currentVersion) { |
| 108 | log(`Adapters already up to date (v${currentVersion})`); |
| 109 | return; |
| 110 | } |
| 111 | |
| 112 | if (!existsSync(BUILTIN_CLIS)) { |
| 113 | log('Warning: clis/ not found in package — skipping adapter copy'); |
| 114 | return; |
| 115 | } |
| 116 | |
| 117 | const newOfficialFiles = new Set(walkFiles(BUILTIN_CLIS)); |
| 118 | const oldOfficialFiles = new Set(oldManifest?.files ?? []); |
| 119 | const rawHashes = oldManifest?.hashes; |
| 120 | // Guard against corrupted manifest: if hashes is a non-object type (string, number, |
| 121 | // array), skip sync to avoid false-positive "changed" detection that deletes overrides. |
| 122 | // null/undefined are treated as empty (old manifests may lack the field). |
| 123 | if (rawHashes != null && (typeof rawHashes !== 'object' || Array.isArray(rawHashes))) { |
| 124 | log('Warning: adapter-manifest.json has corrupted hashes — skipping sync. Will fix on next run.'); |
| 125 | return; |
| 126 | } |
| 127 | const oldHashes = rawHashes ?? {}; |
| 128 | mkdirSync(USER_CLIS_DIR, { recursive: true }); |
| 129 | |
| 130 | // 1. Compute new hashes and detect which sites have changes |
| 131 | const newHashes = {}; |
| 132 | const siteFiles = new Map(); // site -> [relPath, ...] |
| 133 | for (const relPath of newOfficialFiles) { |
| 134 | const src = join(BUILTIN_CLIS, relPath); |
| 135 | const srcHash = fileHash(src); |
| 136 | newHashes[relPath] = srcHash; |
| 137 | |
| 138 | const site = relPath.split('/')[0]; |
| 139 | if (!siteFiles.has(site)) siteFiles.set(site, []); |
| 140 | siteFiles.get(site).push(relPath); |
| 141 | } |
| 142 | |
| 143 | // Determine which sites have any changed/new/removed files |
| 144 | const changedSites = new Set(); |
| 145 | for (const [site, files] of siteFiles) { |
| 146 | for (const relPath of files) { |
| 147 | if (oldHashes[relPath] !== newHashes[relPath]) { |
| 148 | changedSites.add(site); |
| 149 | break; |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | // Also mark sites that had files removed |
| 154 | for (const relPath of oldOfficialFiles) { |
| 155 | if (!newOfficialFiles.has(relPath)) { |
| 156 | changedSites.add(relPath.split('/')[0]); |
| 157 | } |
| 158 | } |
no test coverage detected