@evomap/evolver 1.89.14 → 1.89.17

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 (72) hide show
  1. package/index.js +219 -29
  2. package/package.json +1 -1
  3. package/src/adapters/claudeCode.js +2 -2
  4. package/src/adapters/codex.js +2 -2
  5. package/src/adapters/hookAdapter.js +14 -2
  6. package/src/adapters/scripts/evolver-session-start.js +182 -2
  7. package/src/config.js +11 -3
  8. package/src/evolve/guards.js +1 -1
  9. package/src/evolve/pipeline/collect.js +1 -1
  10. package/src/evolve/pipeline/dispatch.js +1 -1
  11. package/src/evolve/pipeline/enrich.js +1 -1
  12. package/src/evolve/pipeline/hub.js +1 -1
  13. package/src/evolve/pipeline/select.js +1 -1
  14. package/src/evolve/pipeline/signals.js +1 -1
  15. package/src/evolve/utils.js +1 -1
  16. package/src/evolve.js +1 -1
  17. package/src/forceUpdate.js +499 -119
  18. package/src/gep/a2aProtocol.js +1 -1
  19. package/src/gep/antiAbuseTelemetry.js +1 -1
  20. package/src/gep/autoDistillConv.js +1 -1
  21. package/src/gep/autoDistillLlm.js +1 -1
  22. package/src/gep/candidateEval.js +1 -1
  23. package/src/gep/candidates.js +1 -1
  24. package/src/gep/cliContracts.js +1154 -0
  25. package/src/gep/contentHash.js +1 -1
  26. package/src/gep/conversationDistiller.js +1 -1
  27. package/src/gep/conversationSniffer.js +1 -1
  28. package/src/gep/crypto.js +1 -1
  29. package/src/gep/curriculum.js +1 -1
  30. package/src/gep/deviceId.js +1 -1
  31. package/src/gep/envFingerprint.js +1 -1
  32. package/src/gep/epigenetics.js +1 -1
  33. package/src/gep/execBridge.js +1 -1
  34. package/src/gep/explore.js +1 -1
  35. package/src/gep/hash.js +1 -1
  36. package/src/gep/hostErrorClassifier.js +34 -0
  37. package/src/gep/hubFetch.js +1 -1
  38. package/src/gep/hubReview.js +1 -1
  39. package/src/gep/hubSearch.js +1 -1
  40. package/src/gep/hubVerify.js +1 -1
  41. package/src/gep/issueReporter.js +87 -0
  42. package/src/gep/learningSignals.js +1 -1
  43. package/src/gep/memoryGraph.js +1 -1
  44. package/src/gep/memoryGraphAdapter.js +1 -1
  45. package/src/gep/mutation.js +1 -1
  46. package/src/gep/narrativeMemory.js +1 -1
  47. package/src/gep/openPRRegistry.js +1 -1
  48. package/src/gep/paths.js +20 -0
  49. package/src/gep/personality.js +1 -1
  50. package/src/gep/policyCheck.js +1 -1
  51. package/src/gep/prompt.js +1 -1
  52. package/src/gep/recallInject.js +1 -1
  53. package/src/gep/recallVerifier.js +1 -1
  54. package/src/gep/reflection.js +1 -1
  55. package/src/gep/sanitize.js +20 -4
  56. package/src/gep/savingsCore.js +1 -1
  57. package/src/gep/selector.js +1 -1
  58. package/src/gep/signals.js +70 -24
  59. package/src/gep/skillDistiller.js +1 -1
  60. package/src/gep/solidify.js +1 -1
  61. package/src/gep/strategy.js +1 -1
  62. package/src/gep/tokenSavings.js +1 -1
  63. package/src/gep/workspaceKeychain.js +1 -1
  64. package/src/ops/lifecycle.js +501 -31
  65. package/src/proxy/extensions/traceControl.js +1 -1
  66. package/src/proxy/index.js +4 -4
  67. package/src/proxy/inject.js +1 -1
  68. package/src/proxy/lifecycle/manager.js +233 -33
  69. package/src/proxy/sync/inbound.js +5 -4
  70. package/src/proxy/sync/outbound.js +3 -2
  71. package/src/proxy/trace/extractor.js +1 -1
  72. package/src/proxy/trace/usage.js +1 -1
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
+ const zlib = require('zlib');
4
5
  const { execFileSync } = require('child_process');
5
6
  const { getEvolverInstallRoot } = require('./gep/paths');
6
7
 
@@ -57,12 +58,41 @@ const FORCE_UPDATE_FAIL_CODES = Object.freeze({
57
58
  DEGIT_TIMEOUT: 'degit_timeout',
58
59
  DEGIT_FAILED: 'degit_failed',
59
60
  DOWNLOAD_INCOMPLETE: 'download_incomplete',
61
+ DOWNLOADED_PACKAGE_NAME_MISMATCH: 'downloaded_package_name_mismatch',
60
62
  DOWNLOADED_VERSION_MISMATCH: 'downloaded_version_mismatch',
61
63
  DELETE_FAILED: 'delete_failed',
62
64
  COPY_FAILED: 'copy_failed',
65
+ FALLBACK_DOWNLOAD_INCOMPLETE: 'fallback_download_incomplete',
66
+ FALLBACK_DELETE_FAILED: 'fallback_delete_failed',
67
+ FALLBACK_COPY_FAILED: 'fallback_copy_failed',
68
+ FALLBACK_DOWNLOADED_PACKAGE_NAME_MISMATCH: 'fallback_downloaded_package_name_mismatch',
69
+ FALLBACK_DOWNLOADED_VERSION_MISMATCH: 'fallback_downloaded_version_mismatch',
63
70
  ALL_CHANNELS_EXHAUSTED: 'all_channels_exhausted',
64
71
  });
65
72
 
73
+ const EVOLVER_INSTALL_MARKERS = Object.freeze([
74
+ Object.freeze({
75
+ relPath: path.join('src', 'forceUpdate.js'),
76
+ required: true,
77
+ tokens: Object.freeze(['executeForceUpdate', 'FORCE_UPDATE_FAIL_CODES']),
78
+ }),
79
+ Object.freeze({
80
+ relPath: path.join('src', 'gep', 'paths.js'),
81
+ tokens: Object.freeze(['getRepoRoot', 'getEvolverInstallRoot']),
82
+ }),
83
+ Object.freeze({
84
+ relPath: path.join('src', 'gep', 'a2aProtocol.js'),
85
+ tokens: Object.freeze(['GEP A2A Protocol', 'reportForceUpdateOutcome']),
86
+ }),
87
+ Object.freeze({
88
+ relPath: 'index.js',
89
+ tokens: Object.freeze(['proxy-token', './src/evolve']),
90
+ }),
91
+ ]);
92
+ const MAX_INSTALL_MARKER_BYTES = 1024 * 1024;
93
+ const FORCE_UPDATE_BACKUP_PREFIX = '.evolver-force-update-backup-';
94
+ const FORCE_UPDATE_JOURNAL_FILE = '.evolver-force-update-journal.json';
95
+
66
96
  // Build the structured failure result that replaces a bare `return false`.
67
97
  // Shape: { ok:false, code, detail }. Distinct from `true`, FORCE_UPDATE_NOOP
68
98
  // and FORCE_UPDATE_BUSY, so the three call sites' `result === true` /
@@ -86,6 +116,38 @@ function _errStr(e) {
86
116
  return code + (e.message != null ? String(e.message) : String(e));
87
117
  }
88
118
 
119
+ function _isEvolverPackageName(name) {
120
+ return name === '@evomap/evolver' || name === 'evolver';
121
+ }
122
+
123
+ function _fileMatchesInstallMarker(root, marker) {
124
+ try {
125
+ var markerPath = path.join(root, marker.relPath);
126
+ var st = fs.statSync(markerPath);
127
+ if (!st.isFile()) return false;
128
+ if (st.size > MAX_INSTALL_MARKER_BYTES) return false;
129
+ var content = fs.readFileSync(markerPath, 'utf8');
130
+ for (var i = 0; i < marker.tokens.length; i++) {
131
+ if (!content.includes(marker.tokens[i])) return false;
132
+ }
133
+ return true;
134
+ } catch (_) {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ function _hasStrongEvolverInstallMarkers(root) {
140
+ var matched = 0;
141
+ var requiredMatched = false;
142
+ for (var i = 0; i < EVOLVER_INSTALL_MARKERS.length; i++) {
143
+ var marker = EVOLVER_INSTALL_MARKERS[i];
144
+ if (!_fileMatchesInstallMarker(root, marker)) continue;
145
+ matched++;
146
+ if (marker.required) requiredMatched = true;
147
+ }
148
+ return requiredMatched && matched >= 3;
149
+ }
150
+
89
151
  // Map a Channel 1 (GitHub Release / degit) throw to a structured failure.
90
152
  // `phase` records how far the try block got before throwing, so a readFileSync
91
153
  // ENOENT (truncated download) is not misread as an npx ENOENT (npx missing):
@@ -139,6 +201,18 @@ function _classifyChannel1Error(e, phase) {
139
201
  return _fail(FORCE_UPDATE_FAIL_CODES.DEGIT_FAILED, detail);
140
202
  }
141
203
 
204
+ function _withFallbackFailure(primaryFailure, fallbackFailure) {
205
+ if (!primaryFailure) return fallbackFailure;
206
+ if (!fallbackFailure) return primaryFailure;
207
+ var primaryCode = String(primaryFailure.code || FORCE_UPDATE_FAIL_CODES.ALL_CHANNELS_EXHAUSTED);
208
+ var fallbackCode = String(fallbackFailure.code || FORCE_UPDATE_FAIL_CODES.ALL_CHANNELS_EXHAUSTED);
209
+ var terminalCode = 'fallback_' + fallbackCode.replace(/[^a-z0-9_]/gi, '_').toLowerCase();
210
+ var detail = 'primary_failed=' + primaryCode + ' | fallback_failed=' + fallbackCode;
211
+ if (fallbackFailure.detail) detail += ': ' + fallbackFailure.detail;
212
+ if (primaryFailure.detail) detail += ' | primary_detail=' + primaryFailure.detail;
213
+ return _fail(terminalCode, detail);
214
+ }
215
+
142
216
  function _isRetryableFsLockError(e) {
143
217
  var code = e && e.code;
144
218
  return code === 'EPERM' || code === 'EBUSY' || code === 'EACCES' ||
@@ -164,6 +238,144 @@ function _retryFsLockOperation(fn) {
164
238
  throw err;
165
239
  }
166
240
 
241
+ function _downloadUrlWithNode(url, destPath) {
242
+ var script = [
243
+ "const fs=require('fs');",
244
+ "const https=require('https');",
245
+ "const url=process.argv[1];",
246
+ "const dest=process.argv[2];",
247
+ "function fail(msg){try{fs.rmSync(dest,{force:true});}catch(_){};console.error(msg);process.exit(1);}",
248
+ "function get(u, redirects){",
249
+ " const parsed=new URL(u);",
250
+ " if(parsed.protocol!=='https:') fail('refusing non-https download URL');",
251
+ " const req=https.get(parsed,{headers:{'User-Agent':'evomap-evolver-force-update'}},(res)=>{",
252
+ " if(res.statusCode>=300&&res.statusCode<400&&res.headers.location){",
253
+ " if(redirects<=0) fail('too many redirects');",
254
+ " res.resume();",
255
+ " return get(new URL(res.headers.location, parsed).toString(), redirects-1);",
256
+ " }",
257
+ " if(res.statusCode!==200) fail('download status '+res.statusCode);",
258
+ " const out=fs.createWriteStream(dest,{flags:'wx',mode:0o600});",
259
+ " res.pipe(out);",
260
+ " out.on('finish',()=>out.close(()=>process.exit(0)));",
261
+ " out.on('error',(e)=>fail(e&&e.message||e));",
262
+ " });",
263
+ " req.setTimeout(60000,()=>req.destroy(new Error('download timeout')));",
264
+ " req.on('error',(e)=>fail(e&&e.message||e));",
265
+ "}",
266
+ "get(url,5);",
267
+ ].join('');
268
+ execFileSync(process.execPath, ['-e', script, url, destPath], {
269
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
270
+ timeout: 70000, windowsHide: true, maxBuffer: MAX_EXEC_BUFFER,
271
+ });
272
+ }
273
+
274
+ function _readTarString(block, start, length) {
275
+ var end = start;
276
+ var max = start + length;
277
+ while (end < max && block[end] !== 0) end++;
278
+ return block.toString('utf8', start, end);
279
+ }
280
+
281
+ function _readTarOctal(block, start, length) {
282
+ var raw = _readTarString(block, start, length).trim();
283
+ if (!raw) return 0;
284
+ var parsed = parseInt(raw, 8);
285
+ if (!Number.isFinite(parsed) || parsed < 0) throw new Error('invalid tar numeric field');
286
+ return parsed;
287
+ }
288
+
289
+ function _isZeroTarBlock(block) {
290
+ for (var i = 0; i < block.length; i++) {
291
+ if (block[i] !== 0) return false;
292
+ }
293
+ return true;
294
+ }
295
+
296
+ function _stripTarPathComponents(entryName, count) {
297
+ var normalized = String(entryName || '').replace(/\\/g, '/');
298
+ if (!normalized || normalized.startsWith('/')) return '';
299
+ var parts = normalized.split('/').filter(function (part) { return part !== '' && part !== '.'; });
300
+ if (parts.length <= count) return '';
301
+ return parts.slice(count).join('/');
302
+ }
303
+
304
+ function _safeTarOutputPath(root, relativeName) {
305
+ var parts = String(relativeName || '').split('/');
306
+ for (var i = 0; i < parts.length; i++) {
307
+ if (!parts[i] || parts[i] === '.' || parts[i] === '..') return '';
308
+ }
309
+ var rootPath = path.resolve(root);
310
+ var outPath = path.resolve(rootPath, relativeName);
311
+ if (outPath !== rootPath && outPath.startsWith(rootPath + path.sep)) return outPath;
312
+ return '';
313
+ }
314
+
315
+ function _extractTarGzWithNode(archivePath, tempTarget) {
316
+ var archive = zlib.gunzipSync(fs.readFileSync(archivePath));
317
+ var offset = 0;
318
+ while (offset + 512 <= archive.length) {
319
+ var header = archive.subarray(offset, offset + 512);
320
+ offset += 512;
321
+ if (_isZeroTarBlock(header)) break;
322
+
323
+ var name = _readTarString(header, 0, 100);
324
+ var prefix = _readTarString(header, 345, 155);
325
+ if (prefix) name = prefix + '/' + name;
326
+ var size = _readTarOctal(header, 124, 12);
327
+ var typeFlag = _readTarString(header, 156, 1) || '0';
328
+ if (offset + size > archive.length) throw new Error('truncated tar entry: ' + name);
329
+ var mode = 0;
330
+ try { mode = _readTarOctal(header, 100, 8) & 0o777; } catch (_) { mode = 0; }
331
+
332
+ var stripped = _stripTarPathComponents(name, 1);
333
+ var outPath = _safeTarOutputPath(tempTarget, stripped);
334
+ if (outPath) {
335
+ if (typeFlag === '5') {
336
+ fs.mkdirSync(outPath, { recursive: true });
337
+ if (mode && process.platform !== 'win32') {
338
+ try { fs.chmodSync(outPath, mode); } catch (_) {}
339
+ }
340
+ } else if (typeFlag === '0' || typeFlag === '\0') {
341
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
342
+ fs.writeFileSync(outPath, archive.subarray(offset, offset + size));
343
+ if (mode && process.platform !== 'win32') {
344
+ try { fs.chmodSync(outPath, mode); } catch (_) {}
345
+ }
346
+ }
347
+ }
348
+
349
+ offset += Math.ceil(size / 512) * 512;
350
+ }
351
+ }
352
+
353
+ function _clearTempTarget(tempTarget) {
354
+ fs.rmSync(tempTarget, { recursive: true, force: true });
355
+ fs.mkdirSync(tempTarget, { recursive: true });
356
+ }
357
+
358
+ function _tryDownloadReleaseTarball(requiredVersion, tempTarget) {
359
+ var archiveDir = null;
360
+ var archivePath = null;
361
+ var url = 'https://codeload.github.com/EvoMap/evolver/tar.gz/refs/tags/v' + requiredVersion;
362
+ try {
363
+ archiveDir = fs.mkdtempSync(path.join(os.tmpdir(), '.evolver-update-archive-'));
364
+ archivePath = path.join(archiveDir, 'archive.tar.gz');
365
+ _clearTempTarget(tempTarget);
366
+ console.log('[ForceUpdate] Channel 1b: GitHub tarball fallback (v' + requiredVersion + ')...');
367
+ _downloadUrlWithNode(url, archivePath);
368
+ _extractTarGzWithNode(archivePath, tempTarget);
369
+ return null;
370
+ } catch (e) {
371
+ return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE, _errStr(e));
372
+ } finally {
373
+ if (archiveDir) {
374
+ try { fs.rmSync(archiveDir, { recursive: true, force: true }); } catch (_) {}
375
+ }
376
+ }
377
+ }
378
+
167
379
  function _recoverPackageCommitMarkerIfMissing(installRoot) {
168
380
  var pkgDst = path.join(installRoot, 'package.json');
169
381
  if (fs.existsSync(pkgDst)) return false;
@@ -186,23 +398,256 @@ function _recoverPackageCommitMarkerIfMissing(installRoot) {
186
398
  return false;
187
399
  }
188
400
 
189
- function _restorePackageBackup(pkgBackup, pkgDst) {
190
- if (!fs.existsSync(pkgBackup)) return false;
191
- if (fs.existsSync(pkgDst)) {
192
- try { fs.rmSync(pkgBackup, { force: true }); } catch (_) {}
193
- return false;
194
- }
401
+ function _isForceUpdateKeepEntry(name) {
402
+ return name === 'node_modules' || name === 'memory' || name === '.git' || name === 'MEMORY.md' ||
403
+ name === '.env' || name === '.env.local' || name === 'USER.md' || name === '.evolver' ||
404
+ name === 'logs';
405
+ }
406
+
407
+ function _isForceUpdateInternalEntry(name) {
408
+ return name === FORCE_UPDATE_JOURNAL_FILE || String(name || '').startsWith(FORCE_UPDATE_BACKUP_PREFIX);
409
+ }
410
+
411
+ function _isForceUpdateBootstrapEntry(name) {
412
+ return name === 'index.js';
413
+ }
414
+
415
+ function _readInstallPackageVersion(installRoot) {
195
416
  try {
196
- fs.renameSync(pkgBackup, pkgDst);
197
- return true;
417
+ var pkg = JSON.parse(fs.readFileSync(path.join(installRoot, 'package.json'), 'utf8'));
418
+ return pkg && pkg.version ? String(pkg.version) : '';
198
419
  } catch (_) {
199
- return false;
420
+ return '';
200
421
  }
201
422
  }
202
423
 
203
- function _isForceUpdateKeepEntry(name) {
204
- return name === 'node_modules' || name === 'memory' || name === '.git' || name === 'MEMORY.md' ||
205
- name === '.env' || name === '.env.local' || name === 'USER.md' || name === '.evolver';
424
+ function _writeForceUpdateRecoveryJournal(backupRoot, requiredVersion, previousVersion) {
425
+ var journal = {
426
+ state: 'precommit',
427
+ requiredVersion: String(requiredVersion || ''),
428
+ previousVersion: String(previousVersion || ''),
429
+ createdAt: Date.now(),
430
+ };
431
+ fs.writeFileSync(
432
+ path.join(backupRoot, FORCE_UPDATE_JOURNAL_FILE),
433
+ JSON.stringify(journal),
434
+ { encoding: 'utf8', mode: 0o600 },
435
+ );
436
+ }
437
+
438
+ function _removeInstallEntryIfPresent(entryPath) {
439
+ _retryFsLockOperation(function () {
440
+ fs.rmSync(entryPath, {
441
+ recursive: true, force: true, maxRetries: 3, retryDelay: 200,
442
+ });
443
+ });
444
+ }
445
+
446
+ function _copyFileIfPresent(src, dst) {
447
+ if (!fs.existsSync(src)) return false;
448
+ fs.copyFileSync(src, dst);
449
+ return true;
450
+ }
451
+
452
+ function _restoreFileBackupIfPresent(backupPath, dstPath) {
453
+ if (!backupPath || !fs.existsSync(backupPath)) return false;
454
+ try { fs.rmSync(dstPath, { recursive: true, force: true }); } catch (_) {}
455
+ fs.copyFileSync(backupPath, dstPath);
456
+ return true;
457
+ }
458
+
459
+ function _commitAtomicFileReplacement(srcPath, dstPath, tmpPath, backupPath) {
460
+ try { fs.rmSync(tmpPath, { force: true }); } catch (_) {}
461
+ try { fs.rmSync(backupPath, { force: true }); } catch (_) {}
462
+ fs.copyFileSync(srcPath, tmpPath);
463
+ _copyFileIfPresent(dstPath, backupPath);
464
+ _retryFsLockOperation(function () {
465
+ fs.renameSync(tmpPath, dstPath);
466
+ });
467
+ }
468
+
469
+ function _restoreMovedInstallEntries(installRoot, movedEntries, copiedEntryNames) {
470
+ var ok = true;
471
+ var seenCopied = Object.create(null);
472
+ for (var ci = copiedEntryNames.length - 1; ci >= 0; ci--) {
473
+ var copiedName = copiedEntryNames[ci];
474
+ if (seenCopied[copiedName]) continue;
475
+ seenCopied[copiedName] = true;
476
+ try {
477
+ _removeInstallEntryIfPresent(path.join(installRoot, copiedName));
478
+ } catch (copyCleanupErr) {
479
+ ok = false;
480
+ console.warn('[ForceUpdate] rollback cleanup failed for ' + copiedName + ': ' +
481
+ (copyCleanupErr.message || copyCleanupErr));
482
+ }
483
+ }
484
+
485
+ for (var mi = movedEntries.length - 1; mi >= 0; mi--) {
486
+ var moved = movedEntries[mi];
487
+ try {
488
+ if (fs.existsSync(moved.livePath)) _removeInstallEntryIfPresent(moved.livePath);
489
+ fs.renameSync(moved.backupPath, moved.livePath);
490
+ } catch (restoreErr) {
491
+ ok = false;
492
+ console.warn('[ForceUpdate] rollback restore failed for ' + moved.name + ': ' +
493
+ (restoreErr.message || restoreErr));
494
+ }
495
+ }
496
+ return ok;
497
+ }
498
+
499
+ function _installDownloadedTree(installRoot, tempTarget, requiredVersion, successLabel) {
500
+ var phase = 'parse';
501
+ var backupRoot = null;
502
+ var movedEntries = [];
503
+ var copiedEntryNames = [];
504
+ var committedIndex = false;
505
+ var indexBackup = null;
506
+ var packageBackup = null;
507
+ try {
508
+ var tmpPkg = JSON.parse(fs.readFileSync(path.join(tempTarget, 'package.json'), 'utf8'));
509
+ if (!_isEvolverPackageName(tmpPkg && tmpPkg.name)) {
510
+ return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOADED_PACKAGE_NAME_MISMATCH,
511
+ 'downloaded package.json name="' + (tmpPkg && tmpPkg.name) + '", expected "@evomap/evolver"');
512
+ }
513
+ if (!tmpPkg.version) {
514
+ return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
515
+ 'downloaded package.json has no version field');
516
+ }
517
+ if (tmpPkg.version !== requiredVersion) {
518
+ return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOADED_VERSION_MISMATCH,
519
+ 'downloaded version=' + JSON.stringify(tmpPkg.version) + ', expected ' + requiredVersion);
520
+ }
521
+ try {
522
+ if (!fs.statSync(path.join(tempTarget, 'index.js')).isFile()) {
523
+ return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
524
+ 'downloaded index.js is not a file');
525
+ }
526
+ } catch (indexReadErr) {
527
+ return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
528
+ 'missing/unreadable index.js in downloaded tree: ' + _errStr(indexReadErr));
529
+ }
530
+
531
+ phase = 'delete';
532
+ var entries = fs.readdirSync(installRoot, { withFileTypes: true });
533
+ backupRoot = fs.mkdtempSync(path.join(installRoot, FORCE_UPDATE_BACKUP_PREFIX));
534
+ _writeForceUpdateRecoveryJournal(backupRoot, requiredVersion, _readInstallPackageVersion(installRoot));
535
+ var indexSrc = path.join(tempTarget, 'index.js');
536
+ var indexDst = path.join(installRoot, 'index.js');
537
+ var indexTmp = indexDst + '.' + process.pid + '.evolver-tmp';
538
+ indexBackup = path.join(backupRoot, 'index.js');
539
+ try {
540
+ phase = 'copy';
541
+ _commitAtomicFileReplacement(indexSrc, indexDst, indexTmp, indexBackup);
542
+ committedIndex = true;
543
+ } catch (indexErr) {
544
+ _restoreFileBackupIfPresent(indexBackup, indexDst);
545
+ try { fs.rmSync(indexTmp, { force: true }); } catch (_) {}
546
+ console.warn('[ForceUpdate] index.js commit (atomic replace) failed: ' + (indexErr.message || indexErr));
547
+ try { indexErr._evolverEntry = 'index.js commit'; } catch (_) {}
548
+ throw indexErr;
549
+ }
550
+
551
+ for (var ei = 0; ei < entries.length; ei++) {
552
+ var eName = entries[ei].name;
553
+ // package.json is the install's commit marker: keep the OLD one in
554
+ // place through the entire delete+copy below and swap in the new one
555
+ // atomically at the very end. index.js was already atomically replaced
556
+ // after the recovery journal was written, before moving payload entries,
557
+ // so an interrupted update restarts through the recovery-capable
558
+ // bootstrap. Keep-list entries are local state and must not be deleted or
559
+ // overwritten by a downloaded release.
560
+ if (_isForceUpdateKeepEntry(eName) || _isForceUpdateBootstrapEntry(eName) ||
561
+ _isForceUpdateInternalEntry(eName) || eName === 'package.json') continue;
562
+ try {
563
+ (function (entryName) {
564
+ phase = 'delete';
565
+ var livePath = path.join(installRoot, entryName);
566
+ var backupPath = path.join(backupRoot, entryName);
567
+ _retryFsLockOperation(function () {
568
+ fs.renameSync(livePath, backupPath);
569
+ });
570
+ movedEntries.push({ name: entryName, livePath: livePath, backupPath: backupPath });
571
+ })(eName);
572
+ } catch (rmErr) {
573
+ console.warn('[ForceUpdate] backup move failed for ' + eName + ': ' + (rmErr.message || rmErr));
574
+ try { rmErr._evolverEntry = eName; } catch (_) {}
575
+ throw rmErr;
576
+ }
577
+ }
578
+
579
+ phase = 'copy';
580
+ var newEntries = fs.readdirSync(tempTarget, { withFileTypes: true });
581
+ for (var ni = 0; ni < newEntries.length; ni++) {
582
+ if (newEntries[ni].name === 'package.json' || _isForceUpdateKeepEntry(newEntries[ni].name) ||
583
+ _isForceUpdateBootstrapEntry(newEntries[ni].name) || _isForceUpdateInternalEntry(newEntries[ni].name)) continue;
584
+ var src = path.join(tempTarget, newEntries[ni].name);
585
+ var dst = path.join(installRoot, newEntries[ni].name);
586
+ try {
587
+ (function (copySrc, copyDst) {
588
+ copiedEntryNames.push(path.basename(copyDst));
589
+ _retryFsLockOperation(function () {
590
+ fs.cpSync(copySrc, copyDst, { recursive: true });
591
+ });
592
+ })(src, dst);
593
+ } catch (copyErr) {
594
+ console.warn('[ForceUpdate] cpSync failed for ' + newEntries[ni].name + ': ' +
595
+ (copyErr.message || copyErr));
596
+ try { copyErr._evolverEntry = newEntries[ni].name; } catch (_) {}
597
+ throw copyErr;
598
+ }
599
+ }
600
+
601
+ var pkgSrc = path.join(tempTarget, 'package.json');
602
+ var pkgDst = path.join(installRoot, 'package.json');
603
+ var pkgTmp = pkgDst + '.' + process.pid + '.evolver-tmp';
604
+ packageBackup = path.join(backupRoot, 'package.json');
605
+
606
+ try {
607
+ _commitAtomicFileReplacement(pkgSrc, pkgDst, pkgTmp, packageBackup);
608
+ } catch (pkgErr) {
609
+ _restoreFileBackupIfPresent(packageBackup, pkgDst);
610
+ if (committedIndex) _restoreFileBackupIfPresent(indexBackup, indexDst);
611
+ try { fs.rmSync(pkgTmp, { force: true }); } catch (_) {}
612
+ console.warn('[ForceUpdate] package.json commit (atomic replace) failed: ' + (pkgErr.message || pkgErr));
613
+ try { pkgErr._evolverEntry = 'package.json commit'; } catch (_) {}
614
+ throw pkgErr;
615
+ }
616
+
617
+ try { fs.rmSync(tempTarget, { recursive: true, force: true }); } catch (_) {}
618
+ if (backupRoot) {
619
+ try { fs.rmSync(backupRoot, { recursive: true, force: true }); } catch (backupCleanupErr) {
620
+ console.warn('[ForceUpdate] backup cleanup failed: ' + (backupCleanupErr.message || backupCleanupErr));
621
+ }
622
+ }
623
+ console.log('[ForceUpdate] ' + successLabel + ': ' + tmpPkg.version);
624
+ return { ok: true, version: tmpPkg.version };
625
+ } catch (e) {
626
+ if (backupRoot) {
627
+ var rollbackOk = true;
628
+ if (committedIndex) {
629
+ try {
630
+ if (!_restoreFileBackupIfPresent(indexBackup, path.join(installRoot, 'index.js'))) rollbackOk = false;
631
+ } catch (indexRestoreErr) {
632
+ rollbackOk = false;
633
+ console.warn('[ForceUpdate] rollback restore failed for index.js: ' +
634
+ (indexRestoreErr.message || indexRestoreErr));
635
+ }
636
+ }
637
+ try {
638
+ _restoreFileBackupIfPresent(packageBackup, path.join(installRoot, 'package.json'));
639
+ } catch (packageRestoreErr) {
640
+ rollbackOk = false;
641
+ console.warn('[ForceUpdate] rollback restore failed for package.json: ' +
642
+ (packageRestoreErr.message || packageRestoreErr));
643
+ }
644
+ var restored = _restoreMovedInstallEntries(installRoot, movedEntries, copiedEntryNames);
645
+ if (rollbackOk && restored) {
646
+ try { fs.rmSync(backupRoot, { recursive: true, force: true }); } catch (_) {}
647
+ }
648
+ }
649
+ return _classifyChannel1Error(e, phase);
650
+ }
206
651
  }
207
652
 
208
653
  // Module-level mutex: shared by every caller that requires('../forceUpdate'),
@@ -326,7 +771,7 @@ function _executeForceUpdateInner(forceUpdate) {
326
771
  // the deletion loop and the user's data.
327
772
  try {
328
773
  const pkg = JSON.parse(fs.readFileSync(path.join(INSTALL_ROOT, 'package.json'), 'utf8'));
329
- if (!pkg || (pkg.name !== '@evomap/evolver' && pkg.name !== 'evolver')) {
774
+ if (!pkg || !_isEvolverPackageName(pkg.name)) {
330
775
  console.warn('[ForceUpdate] Refusing — ' + INSTALL_ROOT +
331
776
  '/package.json has name="' + (pkg && pkg.name) +
332
777
  '", expected "@evomap/evolver". Aborting to avoid data loss.');
@@ -337,7 +782,7 @@ function _executeForceUpdateInner(forceUpdate) {
337
782
  if (_recoverPackageCommitMarkerIfMissing(INSTALL_ROOT)) {
338
783
  try {
339
784
  const recoveredPkg = JSON.parse(fs.readFileSync(path.join(INSTALL_ROOT, 'package.json'), 'utf8'));
340
- if (!recoveredPkg || (recoveredPkg.name !== '@evomap/evolver' && recoveredPkg.name !== 'evolver')) {
785
+ if (!recoveredPkg || !_isEvolverPackageName(recoveredPkg.name)) {
341
786
  console.warn('[ForceUpdate] Refusing — recovered ' + INSTALL_ROOT +
342
787
  '/package.json has name="' + (recoveredPkg && recoveredPkg.name) +
343
788
  '", expected "@evomap/evolver". Aborting to avoid data loss.');
@@ -351,6 +796,8 @@ function _executeForceUpdateInner(forceUpdate) {
351
796
  return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
352
797
  'cannot read recovered install root package.json: ' + _errStr(recoverReadErr));
353
798
  }
799
+ } else if (_hasStrongEvolverInstallMarkers(INSTALL_ROOT)) {
800
+ console.warn('[ForceUpdate] install package.json is unreadable, but strong evolver install markers are present; bootstrap recovery allowed');
354
801
  } else {
355
802
  console.warn('[ForceUpdate] Refusing — cannot read ' + INSTALL_ROOT +
356
803
  '/package.json: ' + (e && e.message || e));
@@ -438,118 +885,51 @@ function _executeForceUpdateInner(forceUpdate) {
438
885
  encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
439
886
  timeout: 60000, windowsHide: true, maxBuffer: MAX_EXEC_BUFFER,
440
887
  });
441
- phase = 'parse';
442
- var tmpPkg = JSON.parse(fs.readFileSync(path.join(TMP_TARGET, 'package.json'), 'utf8'));
443
- // Require exact version match — a ">=" check would allow a compromised hub to
444
- // request version "0.0.1" and install any version including unreleased HEAD code.
445
- if (tmpPkg.version && tmpPkg.version === requiredVersion) {
446
- var entries = fs.readdirSync(INSTALL_ROOT, { withFileTypes: true });
447
- for (var ei = 0; ei < entries.length; ei++) {
448
- var eName = entries[ei].name;
449
- // package.json is the install's commit marker: keep the OLD one in
450
- // place through the entire delete+copy below and swap in the new one
451
- // atomically at the very end (see "commit marker" block). If it were
452
- // deleted here and any later cpSync threw (ENOSPC, a Windows lock that
453
- // outlasts the retries, a kill), the install root would be left with
454
- // no package.json — and the install-guard at the top of this function
455
- // refuses on an unreadable package.json, wedging the node in
456
- // install_guard_unreadable on every subsequent attempt with no path
457
- // that ever re-copies it. Deferring it keeps the install self-healing.
458
- if (_isForceUpdateKeepEntry(eName) || eName === 'package.json') continue;
459
- try {
460
- (function (entryName) {
461
- phase = 'delete';
462
- _retryFsLockOperation(function () {
463
- fs.rmSync(path.join(INSTALL_ROOT, entryName), {
464
- recursive: true, force: true, maxRetries: 3, retryDelay: 200,
465
- });
466
- });
467
- })(eName);
468
- } catch (rmErr) {
469
- console.warn('[ForceUpdate] rmSync failed for ' + eName + ': ' + (rmErr.message || rmErr));
470
- try { rmErr._evolverEntry = eName; } catch (_) {}
471
- throw rmErr;
472
- }
473
- }
474
- phase = 'copy';
475
- var newEntries = fs.readdirSync(TMP_TARGET, { withFileTypes: true });
476
- for (var ni = 0; ni < newEntries.length; ni++) {
477
- // Deferred: package.json is the commit marker, written last after every
478
- // other entry has copied successfully (see below). Keep-list entries are
479
- // local state and must not be overwritten by the downloaded release.
480
- if (newEntries[ni].name === 'package.json' || _isForceUpdateKeepEntry(newEntries[ni].name)) continue;
481
- var src = path.join(TMP_TARGET, newEntries[ni].name);
482
- var dst = path.join(INSTALL_ROOT, newEntries[ni].name);
483
- try {
484
- (function (copySrc, copyDst) {
485
- _retryFsLockOperation(function () {
486
- fs.cpSync(copySrc, copyDst, { recursive: true });
487
- });
488
- })(src, dst);
489
- } catch (copyErr) {
490
- console.warn('[ForceUpdate] cpSync failed for ' + newEntries[ni].name + ': ' + (copyErr.message || copyErr));
491
- // Tag the failing entry so _classifyChannel1Error can name it in the
492
- // copy_failed detail. phase is already 'copy' here.
493
- try { copyErr._evolverEntry = newEntries[ni].name; } catch (_) {}
494
- throw copyErr;
495
- }
496
- }
497
- // Commit marker: every other entry copied successfully, so swap in the
498
- // new package.json LAST. POSIX gets an atomic rename-over-existing. Windows
499
- // cannot rename over an existing destination, so it first renames the old
500
- // package.json to a recoverable same-directory backup; the install guard
501
- // restores that backup on the next tick if the process dies mid-commit.
502
- var pkgSrc = path.join(TMP_TARGET, 'package.json');
503
- var pkgDst = path.join(INSTALL_ROOT, 'package.json');
504
- var pkgTmp = pkgDst + '.' + process.pid + '.evolver-tmp';
505
- var pkgBackup = pkgDst + '.' + process.pid + '.evolver-old';
506
- try {
507
- _retryFsLockOperation(function () {
508
- try { fs.rmSync(pkgTmp, { force: true }); } catch (_) {}
509
- fs.cpSync(pkgSrc, pkgTmp);
510
- if (process.platform === 'win32') {
511
- if (!fs.existsSync(pkgDst)) _recoverPackageCommitMarkerIfMissing(INSTALL_ROOT);
512
- try { fs.rmSync(pkgBackup, { force: true }); } catch (_) {}
513
- fs.renameSync(pkgDst, pkgBackup);
514
- try {
515
- fs.renameSync(pkgTmp, pkgDst);
516
- } catch (commitErr) {
517
- _restorePackageBackup(pkgBackup, pkgDst);
518
- throw commitErr;
519
- }
520
- try { fs.rmSync(pkgBackup, { force: true }); } catch (_) {}
521
- } else {
522
- fs.renameSync(pkgTmp, pkgDst);
523
- }
524
- });
525
- } catch (pkgErr) {
526
- _restorePackageBackup(pkgBackup, pkgDst);
527
- try { fs.rmSync(pkgTmp, { force: true }); } catch (_) {}
528
- console.warn('[ForceUpdate] package.json commit (atomic replace) failed: ' + (pkgErr.message || pkgErr));
529
- try { pkgErr._evolverEntry = 'package.json commit'; } catch (_) {}
530
- throw pkgErr;
531
- }
532
- try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
533
- console.log('[ForceUpdate] GitHub Release update successful: ' + tmpPkg.version);
888
+ var installResult = _installDownloadedTree(
889
+ INSTALL_ROOT, TMP_TARGET, requiredVersion, 'GitHub Release update successful',
890
+ );
891
+ if (installResult.ok === true) {
534
892
  return true;
535
893
  }
536
- // degit succeeded and produced a parseable package.json, but it did not
537
- // satisfy the exact-version check above. Two distinct causes, two codes:
538
- if (!tmpPkg.version) {
539
- // degit produced a parseable package.json with no version field — a
540
- // malformed/incomplete download, not a stale/tampered tag mismatch.
541
- channel1Failure = _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
542
- 'downloaded package.json has no version field');
543
- } else {
544
- // version present but not the exact tag we asked for (stale tag, mirror
545
- // lag, or a tampered/redirected tag). Refuse and record the delta.
546
- channel1Failure = _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOADED_VERSION_MISMATCH,
547
- 'downloaded version=' + JSON.stringify(tmpPkg.version) + ', expected ' + requiredVersion);
894
+ channel1Failure = installResult;
895
+ if (
896
+ channel1Failure.code === FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE ||
897
+ channel1Failure.code === FORCE_UPDATE_FAIL_CODES.DOWNLOADED_PACKAGE_NAME_MISMATCH ||
898
+ channel1Failure.code === FORCE_UPDATE_FAIL_CODES.DOWNLOADED_VERSION_MISMATCH
899
+ ) {
900
+ var postDownloadFallbackFailure = _tryDownloadReleaseTarball(requiredVersion, TMP_TARGET);
901
+ if (!postDownloadFallbackFailure) {
902
+ var postDownloadFallbackResult = _installDownloadedTree(
903
+ INSTALL_ROOT, TMP_TARGET, requiredVersion, 'GitHub tarball fallback update successful',
904
+ );
905
+ if (postDownloadFallbackResult.ok === true) {
906
+ return true;
907
+ }
908
+ postDownloadFallbackFailure = postDownloadFallbackResult;
909
+ }
910
+ console.warn('[ForceUpdate] GitHub tarball fallback failed (' + postDownloadFallbackFailure.code + '): ' +
911
+ postDownloadFallbackFailure.detail);
912
+ channel1Failure = _withFallbackFailure(channel1Failure, postDownloadFallbackFailure);
548
913
  }
549
914
  try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
550
915
  } catch (e) {
551
916
  channel1Failure = _classifyChannel1Error(e, phase);
552
917
  console.warn('[ForceUpdate] GitHub Release failed (' + channel1Failure.code + '):', e && e.message || e);
918
+ var fallbackFailure = _tryDownloadReleaseTarball(requiredVersion, TMP_TARGET);
919
+ if (!fallbackFailure) {
920
+ var fallbackInstallResult = _installDownloadedTree(
921
+ INSTALL_ROOT, TMP_TARGET, requiredVersion, 'GitHub tarball fallback update successful',
922
+ );
923
+ if (fallbackInstallResult.ok === true) {
924
+ return true;
925
+ }
926
+ fallbackFailure = fallbackInstallResult;
927
+ }
928
+ if (fallbackFailure) {
929
+ console.warn('[ForceUpdate] GitHub tarball fallback failed (' + fallbackFailure.code + '): ' +
930
+ fallbackFailure.detail);
931
+ channel1Failure = _withFallbackFailure(channel1Failure, fallbackFailure);
932
+ }
553
933
  try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
554
934
  // Fall through to Channel 2 (manual download URL hint) instead of
555
935
  // returning. A Channel 1 error (degit missing, network down, tag not