@evomap/evolver 1.89.3 → 1.89.4
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/.cursor/BUGBOT.md +182 -0
- package/.env.example +68 -0
- package/.git-commit-guard-token +1 -0
- package/.github/CODEOWNERS +63 -0
- package/.github/ISSUE_TEMPLATE/good_first_issue.md +23 -0
- package/.github/pull_request_template.md +45 -0
- package/.github/workflows/test.yml +75 -0
- package/CHANGELOG.md +1237 -0
- package/README.md +86 -528
- package/README.public.md +569 -0
- package/SECURITY.md +108 -0
- package/assets/gep/events.jsonl +3 -0
- package/examples/atp-consumer-quickstart.md +100 -0
- package/examples/hello-world.md +38 -0
- package/index.js +30 -1
- package/package.json +6 -17
- package/proxy-package.json +39 -0
- package/public.manifest.json +143 -0
- package/src/config.js +23 -0
- package/src/evolve/guards.js +721 -1
- package/src/evolve/pipeline/collect.js +1283 -1
- package/src/evolve/pipeline/dispatch.js +421 -1
- package/src/evolve/pipeline/enrich.js +440 -1
- package/src/evolve/pipeline/hub.js +319 -1
- package/src/evolve/pipeline/select.js +274 -1
- package/src/evolve/pipeline/signals.js +206 -1
- package/src/evolve/utils.js +264 -1
- package/src/evolve.js +350 -1
- package/src/experiment/agentRunner.js +229 -0
- package/src/experiment/cli.js +159 -0
- package/src/experiment/comparison.js +233 -0
- package/src/experiment/metrics.js +75 -0
- package/src/forceUpdate.js +147 -59
- package/src/gep/a2aProtocol.js +4455 -1
- package/src/gep/antiAbuseTelemetry.js +233 -0
- package/src/gep/autoDistillConv.js +205 -1
- package/src/gep/autoDistillLlm.js +315 -1
- package/src/gep/candidateEval.js +92 -1
- package/src/gep/candidates.js +198 -1
- package/src/gep/contentHash.js +30 -1
- package/src/gep/conversationSniffer.js +266 -1
- package/src/gep/crypto.js +89 -1
- package/src/gep/curriculum.js +163 -1
- package/src/gep/deviceId.js +218 -1
- package/src/gep/envFingerprint.js +118 -1
- package/src/gep/epigenetics.js +31 -1
- package/src/gep/execBridge.js +711 -1
- package/src/gep/explore.js +289 -1
- package/src/gep/hash.js +15 -1
- package/src/gep/hubFetch.js +359 -1
- package/src/gep/hubReview.js +207 -1
- package/src/gep/hubSearch.js +526 -1
- package/src/gep/hubVerify.js +306 -1
- package/src/gep/learningSignals.js +89 -1
- package/src/gep/memoryGraph.js +1374 -1
- package/src/gep/memoryGraphAdapter.js +203 -1
- package/src/gep/mutation.js +203 -1
- package/src/gep/narrativeMemory.js +108 -1
- package/src/gep/openPRRegistry.js +205 -1
- package/src/gep/personality.js +423 -1
- package/src/gep/policyCheck.js +599 -1
- package/src/gep/prompt.js +836 -1
- package/src/gep/recallInject.js +409 -1
- package/src/gep/recallVerifier.js +318 -1
- package/src/gep/reflection.js +177 -1
- package/src/gep/sanitize.js +9 -0
- package/src/gep/selector.js +602 -1
- package/src/gep/skillDistiller.js +1294 -1
- package/src/gep/solidify.js +1699 -1
- package/src/gep/strategy.js +136 -1
- package/src/gep/tokenSavings.js +88 -1
- package/src/gep/validator/sandboxExecutor.js +29 -1
- package/src/gep/workspaceKeychain.js +174 -1
- package/src/proxy/extensions/traceControl.js +99 -1
- package/src/proxy/index.js +10 -1
- package/src/proxy/inject.js +52 -1
- package/src/proxy/lifecycle/manager.js +19 -0
- package/src/proxy/mailbox/store.js +2 -1
- package/src/proxy/router/messages_route.js +5 -2
- package/src/proxy/trace/extractor.js +646 -1
- package/src/proxy/trace/usage.js +105 -1
- package/CONTRIBUTING.md +0 -19
- package/assets/cover.png +0 -0
- package/scripts/a2a_export.js +0 -63
- package/scripts/a2a_ingest.js +0 -79
- package/scripts/a2a_promote.js +0 -118
- package/scripts/analyze_by_skill.js +0 -121
- package/scripts/build_binaries.js +0 -479
- package/scripts/check-changelog.js +0 -166
- package/scripts/extract_log.js +0 -85
- package/scripts/generate_history.js +0 -75
- package/scripts/gep_append_event.js +0 -96
- package/scripts/gep_personality_report.js +0 -234
- package/scripts/human_report.js +0 -147
- package/scripts/recall-verify-report.js +0 -234
- package/scripts/recover_loop.js +0 -61
- package/scripts/refresh_stars_badge.js +0 -168
- package/scripts/seed-merchants.js +0 -91
- package/scripts/suggest_version.js +0 -89
- package/scripts/validate-modules.js +0 -38
- package/scripts/validate-suite.js +0 -78
- package/skills/index.json +0 -14
- /package/assets/gep/{genes.seed.json → genes.json} +0 -0
- /package/{skills → bundled-skills}/_meta/SKILL.md +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/experiment/metrics.js
|
|
2
|
+
//
|
|
3
|
+
// Pure, table-driven mapping from a human metric label (e.g. "完成耗时 (s)",
|
|
4
|
+
// "轮次", "token", "通过率") onto a per-arm field + comparison direction.
|
|
5
|
+
// No I/O, no side effects -- safe to unit-test in isolation.
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
function num(v, fallback) {
|
|
9
|
+
const n = Number(v);
|
|
10
|
+
return Number.isFinite(n) ? n : (fallback === undefined ? 0 : fallback);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function round(n, digits) {
|
|
14
|
+
const f = Math.pow(10, digits);
|
|
15
|
+
return Math.round((num(n) + Number.EPSILON) * f) / f;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Ordered rules. The FIRST rule whose any keyword is a (case-insensitive)
|
|
19
|
+
// substring of the metric label wins. Order matters: pass-rate / rounds /
|
|
20
|
+
// tokens / cost are checked before duration so a label like "通过率" is not
|
|
21
|
+
// swallowed by a looser rule.
|
|
22
|
+
const METRIC_RULES = [
|
|
23
|
+
{ keys: ['通过率', 'pass', 'success', 'accuracy', '准确', '正确率'], field: 'passRate', lowerIsBetter: false },
|
|
24
|
+
{ keys: ['轮次', 'turn', 'round', 'step', 'iteration', '迭代'], field: 'rounds', lowerIsBetter: true },
|
|
25
|
+
{ keys: ['token', '令牌'], field: 'tokensTotal', lowerIsBetter: true },
|
|
26
|
+
{ keys: ['成本', 'cost', 'usd', '价格', '费用'], field: 'costUsd', lowerIsBetter: true },
|
|
27
|
+
{ keys: ['耗时', 'duration', 'latency', '延迟', '秒', 'second', '(s)', 'time'], field: 'durationMs', lowerIsBetter: true },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a metric label to the per-arm field used for scoring, the
|
|
32
|
+
* comparison direction, and the display unit.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} metricStr
|
|
35
|
+
* @returns {{ metricField: string, lowerIsBetter: boolean, scoreUnit: string, recognized: boolean }}
|
|
36
|
+
*/
|
|
37
|
+
function deriveMetric(metricStr) {
|
|
38
|
+
const m = String(metricStr || '').toLowerCase();
|
|
39
|
+
for (const rule of METRIC_RULES) {
|
|
40
|
+
if (rule.keys.some((k) => m.includes(String(k).toLowerCase()))) {
|
|
41
|
+
if (rule.field === 'durationMs') {
|
|
42
|
+
// Seconds-flavoured labels ("(s)", "秒", "seconds") -> report in seconds.
|
|
43
|
+
if (/\(s\)|秒|second/.test(m)) {
|
|
44
|
+
return { metricField: 'durationSec', lowerIsBetter: true, scoreUnit: 'seconds', recognized: true };
|
|
45
|
+
}
|
|
46
|
+
return { metricField: 'durationMs', lowerIsBetter: true, scoreUnit: 'ms', recognized: true };
|
|
47
|
+
}
|
|
48
|
+
return { metricField: rule.field, lowerIsBetter: rule.lowerIsBetter, scoreUnit: 'raw', recognized: true };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Unrecognized -> degrade to pass-rate (higher is better). Caller records a warning.
|
|
52
|
+
return { metricField: 'passRate', lowerIsBetter: false, scoreUnit: 'raw', recognized: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Pull the scalar score for one arm given the resolved metric field.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} arm a normalized arm (see comparison.normalizeArm)
|
|
59
|
+
* @param {string} metricField
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
function scoreArm(arm, metricField) {
|
|
63
|
+
if (!arm) return 0;
|
|
64
|
+
switch (metricField) {
|
|
65
|
+
case 'durationSec': return round(num(arm.durationMs) / 1000, 2);
|
|
66
|
+
case 'durationMs': return num(arm.durationMs);
|
|
67
|
+
case 'rounds': return num(arm.rounds);
|
|
68
|
+
case 'tokensTotal': return num(arm.tokensTotal);
|
|
69
|
+
case 'costUsd': return round(num(arm.costUsd), 4);
|
|
70
|
+
case 'passRate': return round(num(arm.passRate), 4);
|
|
71
|
+
default: return round(num(arm.passRate), 4);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { deriveMetric, scoreArm, METRIC_RULES, round, num };
|
package/src/forceUpdate.js
CHANGED
|
@@ -58,6 +58,7 @@ const FORCE_UPDATE_FAIL_CODES = Object.freeze({
|
|
|
58
58
|
DEGIT_FAILED: 'degit_failed',
|
|
59
59
|
DOWNLOAD_INCOMPLETE: 'download_incomplete',
|
|
60
60
|
DOWNLOADED_VERSION_MISMATCH: 'downloaded_version_mismatch',
|
|
61
|
+
DELETE_FAILED: 'delete_failed',
|
|
61
62
|
COPY_FAILED: 'copy_failed',
|
|
62
63
|
ALL_CHANNELS_EXHAUSTED: 'all_channels_exhausted',
|
|
63
64
|
});
|
|
@@ -92,6 +93,10 @@ function _errStr(e) {
|
|
|
92
93
|
// 'parse' -> degit exited 0 but the downloaded package.json is missing/invalid
|
|
93
94
|
// 'copy' -> the staged tree downloaded fine but cpSync into INSTALL_ROOT failed
|
|
94
95
|
function _classifyChannel1Error(e, phase) {
|
|
96
|
+
if (phase === 'delete') {
|
|
97
|
+
var deleteEntry = e && e._evolverEntry ? String(e._evolverEntry) + ': ' : '';
|
|
98
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.DELETE_FAILED, deleteEntry + _errStr(e));
|
|
99
|
+
}
|
|
95
100
|
if (phase === 'copy') {
|
|
96
101
|
var entry = e && e._evolverEntry ? String(e._evolverEntry) + ': ' : '';
|
|
97
102
|
return _fail(FORCE_UPDATE_FAIL_CODES.COPY_FAILED, entry + _errStr(e));
|
|
@@ -134,6 +139,72 @@ function _classifyChannel1Error(e, phase) {
|
|
|
134
139
|
return _fail(FORCE_UPDATE_FAIL_CODES.DEGIT_FAILED, detail);
|
|
135
140
|
}
|
|
136
141
|
|
|
142
|
+
function _isRetryableFsLockError(e) {
|
|
143
|
+
var code = e && e.code;
|
|
144
|
+
return code === 'EPERM' || code === 'EBUSY' || code === 'EACCES' ||
|
|
145
|
+
code === 'ENOTEMPTY' || code === 'EMFILE' || code === 'ENFILE';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function _waitForFsLockRetry() {
|
|
149
|
+
var until = Date.now() + 200;
|
|
150
|
+
while (Date.now() < until) { /* spin */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _retryFsLockOperation(fn) {
|
|
154
|
+
var err = null;
|
|
155
|
+
for (var attempt = 0; attempt < 3; attempt++) {
|
|
156
|
+
try {
|
|
157
|
+
return fn();
|
|
158
|
+
} catch (e) {
|
|
159
|
+
err = e;
|
|
160
|
+
if (!_isRetryableFsLockError(e)) break;
|
|
161
|
+
if (attempt < 2) _waitForFsLockRetry();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function _recoverPackageCommitMarkerIfMissing(installRoot) {
|
|
168
|
+
var pkgDst = path.join(installRoot, 'package.json');
|
|
169
|
+
if (fs.existsSync(pkgDst)) return false;
|
|
170
|
+
var entries;
|
|
171
|
+
try {
|
|
172
|
+
entries = fs.readdirSync(installRoot);
|
|
173
|
+
} catch (_) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
var backups = entries
|
|
177
|
+
.filter(function (name) { return /^package\.json\.\d+\.evolver-old$/.test(name); })
|
|
178
|
+
.sort();
|
|
179
|
+
for (var i = backups.length - 1; i >= 0; i--) {
|
|
180
|
+
try {
|
|
181
|
+
fs.renameSync(path.join(installRoot, backups[i]), pkgDst);
|
|
182
|
+
console.warn('[ForceUpdate] Recovered package.json commit marker from ' + backups[i]);
|
|
183
|
+
return true;
|
|
184
|
+
} catch (_) {}
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
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
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
fs.renameSync(pkgBackup, pkgDst);
|
|
197
|
+
return true;
|
|
198
|
+
} catch (_) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
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';
|
|
206
|
+
}
|
|
207
|
+
|
|
137
208
|
// Module-level mutex: shared by every caller that requires('../forceUpdate'),
|
|
138
209
|
// so the heartbeat-thread trigger in a2aProtocol.js and the evolve-tick path
|
|
139
210
|
// in enrich/pipeline cannot run executeForceUpdate concurrently. This is a
|
|
@@ -263,10 +334,29 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
263
334
|
'install root package.json name="' + (pkg && pkg.name) + '", expected "@evomap/evolver"');
|
|
264
335
|
}
|
|
265
336
|
} catch (e) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
337
|
+
if (_recoverPackageCommitMarkerIfMissing(INSTALL_ROOT)) {
|
|
338
|
+
try {
|
|
339
|
+
const recoveredPkg = JSON.parse(fs.readFileSync(path.join(INSTALL_ROOT, 'package.json'), 'utf8'));
|
|
340
|
+
if (!recoveredPkg || (recoveredPkg.name !== '@evomap/evolver' && recoveredPkg.name !== 'evolver')) {
|
|
341
|
+
console.warn('[ForceUpdate] Refusing — recovered ' + INSTALL_ROOT +
|
|
342
|
+
'/package.json has name="' + (recoveredPkg && recoveredPkg.name) +
|
|
343
|
+
'", expected "@evomap/evolver". Aborting to avoid data loss.');
|
|
344
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_NAME_MISMATCH,
|
|
345
|
+
'recovered install root package.json name="' + (recoveredPkg && recoveredPkg.name) +
|
|
346
|
+
'", expected "@evomap/evolver"');
|
|
347
|
+
}
|
|
348
|
+
} catch (recoverReadErr) {
|
|
349
|
+
console.warn('[ForceUpdate] Refusing — cannot read recovered ' + INSTALL_ROOT +
|
|
350
|
+
'/package.json: ' + (recoverReadErr && recoverReadErr.message || recoverReadErr));
|
|
351
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
|
|
352
|
+
'cannot read recovered install root package.json: ' + _errStr(recoverReadErr));
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
console.warn('[ForceUpdate] Refusing — cannot read ' + INSTALL_ROOT +
|
|
356
|
+
'/package.json: ' + (e && e.message || e));
|
|
357
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
|
|
358
|
+
'cannot read install root package.json: ' + _errStr(e));
|
|
359
|
+
}
|
|
270
360
|
}
|
|
271
361
|
|
|
272
362
|
const requiredVersion = normalizeRequiredVersion(forceUpdate.required_version);
|
|
@@ -335,7 +425,7 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
335
425
|
// terminal `return` can surface it instead of a bare `false`. `phase` tracks
|
|
336
426
|
// how far we got before any throw, so _classifyChannel1Error can tell a
|
|
337
427
|
// degit-spawn failure (phase 'degit') from a truncated download (phase
|
|
338
|
-
// 'parse') from a copy-into-INSTALL_ROOT failure
|
|
428
|
+
// 'parse') from a delete/copy-into-INSTALL_ROOT failure.
|
|
339
429
|
var channel1Failure = null;
|
|
340
430
|
var phase = 'degit';
|
|
341
431
|
try {
|
|
@@ -353,7 +443,6 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
353
443
|
// Require exact version match — a ">=" check would allow a compromised hub to
|
|
354
444
|
// request version "0.0.1" and install any version including unreleased HEAD code.
|
|
355
445
|
if (tmpPkg.version && tmpPkg.version === requiredVersion) {
|
|
356
|
-
phase = 'copy';
|
|
357
446
|
var entries = fs.readdirSync(INSTALL_ROOT, { withFileTypes: true });
|
|
358
447
|
for (var ei = 0; ei < entries.length; ei++) {
|
|
359
448
|
var eName = entries[ei].name;
|
|
@@ -366,37 +455,38 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
366
455
|
// refuses on an unreadable package.json, wedging the node in
|
|
367
456
|
// install_guard_unreadable on every subsequent attempt with no path
|
|
368
457
|
// that ever re-copies it. Deferring it keeps the install self-healing.
|
|
369
|
-
if (eName
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
+
}
|
|
373
473
|
}
|
|
474
|
+
phase = 'copy';
|
|
374
475
|
var newEntries = fs.readdirSync(TMP_TARGET, { withFileTypes: true });
|
|
375
476
|
for (var ni = 0; ni < newEntries.length; ni++) {
|
|
376
|
-
// Deferred: package.json is the commit marker, written last
|
|
377
|
-
//
|
|
378
|
-
|
|
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;
|
|
379
481
|
var src = path.join(TMP_TARGET, newEntries[ni].name);
|
|
380
482
|
var dst = path.join(INSTALL_ROOT, newEntries[ni].name);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
break;
|
|
389
|
-
} catch (cpErr) {
|
|
390
|
-
copyErr = cpErr;
|
|
391
|
-
var code = cpErr && cpErr.code;
|
|
392
|
-
if (code !== 'EPERM' && code !== 'EBUSY' && code !== 'EACCES') break;
|
|
393
|
-
// Brief busy-wait — execFileSync has already blocked the event loop,
|
|
394
|
-
// so a synchronous spin is acceptable here.
|
|
395
|
-
var until = Date.now() + 200;
|
|
396
|
-
while (Date.now() < until) { /* spin */ }
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
if (copyErr) {
|
|
483
|
+
try {
|
|
484
|
+
(function (copySrc, copyDst) {
|
|
485
|
+
_retryFsLockOperation(function () {
|
|
486
|
+
fs.cpSync(copySrc, copyDst, { recursive: true });
|
|
487
|
+
});
|
|
488
|
+
})(src, dst);
|
|
489
|
+
} catch (copyErr) {
|
|
400
490
|
console.warn('[ForceUpdate] cpSync failed for ' + newEntries[ni].name + ': ' + (copyErr.message || copyErr));
|
|
401
491
|
// Tag the failing entry so _classifyChannel1Error can name it in the
|
|
402
492
|
// copy_failed detail. phase is already 'copy' here.
|
|
@@ -405,40 +495,38 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
405
495
|
}
|
|
406
496
|
}
|
|
407
497
|
// Commit marker: every other entry copied successfully, so swap in the
|
|
408
|
-
// new package.json LAST
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
// force-update simply retries (no install_guard_unreadable wedge);
|
|
413
|
-
// - the new version becomes "current" only once the tree is fully in
|
|
414
|
-
// place, so a partial install never reports as already-satisfied.
|
|
415
|
-
// tmp + rename in INSTALL_ROOT (same filesystem) is an atomic replace on
|
|
416
|
-
// POSIX; Windows renameSync throws EPERM over an existing dest, so unlink
|
|
417
|
-
// first there. Mirrors src/proxy/mailbox/store.js _persistState.
|
|
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.
|
|
418
502
|
var pkgSrc = path.join(TMP_TARGET, 'package.json');
|
|
419
503
|
var pkgDst = path.join(INSTALL_ROOT, 'package.json');
|
|
420
504
|
var pkgTmp = pkgDst + '.' + process.pid + '.evolver-tmp';
|
|
421
|
-
var
|
|
422
|
-
|
|
423
|
-
|
|
505
|
+
var pkgBackup = pkgDst + '.' + process.pid + '.evolver-old';
|
|
506
|
+
try {
|
|
507
|
+
_retryFsLockOperation(function () {
|
|
508
|
+
try { fs.rmSync(pkgTmp, { force: true }); } catch (_) {}
|
|
424
509
|
fs.cpSync(pkgSrc, pkgTmp);
|
|
425
510
|
if (process.platform === 'win32') {
|
|
426
|
-
|
|
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);
|
|
427
523
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
} catch (
|
|
432
|
-
pkgErr = pErr;
|
|
433
|
-
try { fs.rmSync(pkgTmp, { force: true }); } catch (_) {}
|
|
434
|
-
var pcode = pErr && pErr.code;
|
|
435
|
-
if (pcode !== 'EPERM' && pcode !== 'EBUSY' && pcode !== 'EACCES') break;
|
|
436
|
-
var puntil = Date.now() + 200;
|
|
437
|
-
while (Date.now() < puntil) { /* spin */ }
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
if (pkgErr) {
|
|
524
|
+
});
|
|
525
|
+
} catch (pkgErr) {
|
|
526
|
+
_restorePackageBackup(pkgBackup, pkgDst);
|
|
527
|
+
try { fs.rmSync(pkgTmp, { force: true }); } catch (_) {}
|
|
441
528
|
console.warn('[ForceUpdate] package.json commit (atomic replace) failed: ' + (pkgErr.message || pkgErr));
|
|
529
|
+
try { pkgErr._evolverEntry = 'package.json commit'; } catch (_) {}
|
|
442
530
|
throw pkgErr;
|
|
443
531
|
}
|
|
444
532
|
try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
|