@evomap/evolver 1.89.20 → 2.0.0-beta.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.
- package/README.md +27 -564
- package/bin/evolver-llm-proxy.js +3 -0
- package/bin/evolver-mcp.js +2 -0
- package/bin/evolver-proxy.js +3 -0
- package/bin/evolver.js +4 -0
- package/index.js +1 -3586
- package/package.json +32 -62
- package/CONTRIBUTING.md +0 -19
- package/LICENSE +0 -641
- package/README.ja-JP.md +0 -521
- package/README.ko-KR.md +0 -520
- package/README.zh-CN.md +0 -531
- package/SKILL.md +0 -365
- package/assets/cover.png +0 -0
- package/assets/gep/genes.seed.json +0 -496
- package/conformance/savings-core/constants.json +0 -30
- package/conformance/savings-core/golden-vectors.json +0 -333
- 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/skill2recipes.js +0 -118
- package/scripts/suggest_version.js +0 -89
- package/scripts/validate-modules.js +0 -38
- package/scripts/validate-suite.js +0 -78
- package/skills/_meta/SKILL.md +0 -41
- package/skills/index.json +0 -14
- package/src/adapters/claudeCode.js +0 -194
- package/src/adapters/codex.js +0 -216
- package/src/adapters/cursor.js +0 -91
- package/src/adapters/hookAdapter.js +0 -469
- package/src/adapters/kiro.js +0 -195
- package/src/adapters/opencode.js +0 -326
- package/src/adapters/scripts/_lockPaths.js +0 -74
- package/src/adapters/scripts/_memoryFiltering.js +0 -35
- package/src/adapters/scripts/_runtimePaths.js +0 -440
- package/src/adapters/scripts/evolver-session-end.js +0 -321
- package/src/adapters/scripts/evolver-session-start.js +0 -587
- package/src/adapters/scripts/evolver-signal-detect.js +0 -98
- package/src/adapters/scripts/evolver-task-recall.js +0 -173
- package/src/atp/atpExecute.js +0 -283
- package/src/atp/atpTaskPickup.js +0 -233
- package/src/atp/autoBuyer.js +0 -382
- package/src/atp/autoDeliver.js +0 -215
- package/src/atp/cli.js +0 -354
- package/src/atp/cliAutobuyPrompt.js +0 -154
- package/src/atp/consumerAgent.js +0 -157
- package/src/atp/defaultHandler.js +0 -69
- package/src/atp/heartbeatSignalsHandler.js +0 -254
- package/src/atp/hubClient.js +0 -317
- package/src/atp/index.js +0 -38
- package/src/atp/merchantAgent.js +0 -118
- package/src/atp/protocol.js +0 -41
- package/src/atp/questionComposer.js +0 -133
- package/src/atp/serviceHelper.js +0 -92
- package/src/canary.js +0 -13
- package/src/config.js +0 -351
- package/src/evolve/guards.js +0 -1
- package/src/evolve/pipeline/collect.js +0 -1
- package/src/evolve/pipeline/dispatch.js +0 -1
- package/src/evolve/pipeline/enrich.js +0 -1
- package/src/evolve/pipeline/hub.js +0 -1
- package/src/evolve/pipeline/select.js +0 -1
- package/src/evolve/pipeline/signals.js +0 -1
- package/src/evolve/utils.js +0 -1
- package/src/evolve.js +0 -1
- package/src/experiment/agentRunner.js +0 -229
- package/src/experiment/cli.js +0 -159
- package/src/experiment/comparison.js +0 -233
- package/src/experiment/metrics.js +0 -75
- package/src/forceUpdate.js +0 -991
- package/src/gep/a2a.js +0 -173
- package/src/gep/a2aProtocol.js +0 -1
- package/src/gep/analyzer.js +0 -35
- package/src/gep/antiAbuseTelemetry.js +0 -1
- package/src/gep/assetCallLog.js +0 -197
- package/src/gep/assetStore.js +0 -723
- package/src/gep/assets.js +0 -36
- package/src/gep/autoDistillConv.js +0 -1
- package/src/gep/autoDistillLlm.js +0 -1
- package/src/gep/bridge.js +0 -138
- package/src/gep/candidateEval.js +0 -1
- package/src/gep/candidates.js +0 -1
- package/src/gep/claimNudge.js +0 -121
- package/src/gep/cliContracts.js +0 -1190
- package/src/gep/contentHash.js +0 -1
- package/src/gep/conversationDistiller.js +0 -1
- package/src/gep/conversationSniffer.js +0 -1
- package/src/gep/crypto.js +0 -1
- package/src/gep/curriculum.js +0 -1
- package/src/gep/deviceId.js +0 -1
- package/src/gep/directoryClient.js +0 -115
- package/src/gep/envFingerprint.js +0 -1
- package/src/gep/epigenetics.js +0 -1
- package/src/gep/execBridge.js +0 -1
- package/src/gep/executionTrace.js +0 -291
- package/src/gep/explore.js +0 -1
- package/src/gep/featureFlags.js +0 -121
- package/src/gep/gitOps.js +0 -265
- package/src/gep/hash.js +0 -1
- package/src/gep/hostErrorClassifier.js +0 -34
- package/src/gep/hubFetch.js +0 -1
- package/src/gep/hubReview.js +0 -1
- package/src/gep/hubSearch.js +0 -1
- package/src/gep/hubVerify.js +0 -1
- package/src/gep/idleScheduler.js +0 -400
- package/src/gep/issueReporter.js +0 -416
- package/src/gep/learningSignals.js +0 -1
- package/src/gep/llmReview.js +0 -92
- package/src/gep/localStateAwareness.js +0 -243
- package/src/gep/mailboxTransport.js +0 -119
- package/src/gep/memoryGraph.js +0 -1
- package/src/gep/memoryGraphAdapter.js +0 -1
- package/src/gep/mutation.js +0 -1
- package/src/gep/narrativeMemory.js +0 -1
- package/src/gep/oauthLogin.js +0 -181
- package/src/gep/openPRRegistry.js +0 -1
- package/src/gep/paths.js +0 -522
- package/src/gep/personality.js +0 -1
- package/src/gep/policyCheck.js +0 -1
- package/src/gep/portable.js +0 -103
- package/src/gep/privacyClient.js +0 -235
- package/src/gep/prompt.js +0 -1
- package/src/gep/questionGenerator.js +0 -518
- package/src/gep/recallInject.js +0 -1
- package/src/gep/recallVerifier.js +0 -1
- package/src/gep/reflection.js +0 -1
- package/src/gep/sanitize.js +0 -264
- package/src/gep/savingsCore.js +0 -1
- package/src/gep/schemas/capsule.js +0 -170
- package/src/gep/schemas/gene.js +0 -154
- package/src/gep/schemas/index.js +0 -8
- package/src/gep/schemas/protocol.js +0 -51
- package/src/gep/schemas/task.js +0 -74
- package/src/gep/selector.js +0 -1
- package/src/gep/selfPR.js +0 -469
- package/src/gep/signals.js +0 -776
- package/src/gep/skill2gep.js +0 -1056
- package/src/gep/skill2gepAudit.js +0 -303
- package/src/gep/skill2recipes.js +0 -511
- package/src/gep/skillDistiller.js +0 -1
- package/src/gep/skillPublisher.js +0 -358
- package/src/gep/solidify.js +0 -1
- package/src/gep/strategy.js +0 -1
- package/src/gep/taskReceiver.js +0 -575
- package/src/gep/tokenSavings.js +0 -1
- package/src/gep/trajectoryExport.js +0 -1
- package/src/gep/validationReport.js +0 -55
- package/src/gep/validator/index.js +0 -411
- package/src/gep/validator/reporter.js +0 -210
- package/src/gep/validator/sandboxExecutor.js +0 -480
- package/src/gep/validator/stakeBootstrap.js +0 -357
- package/src/gep/workspaceKeychain.js +0 -1
- package/src/ops/cleanup.js +0 -80
- package/src/ops/commentary.js +0 -60
- package/src/ops/health_check.js +0 -104
- package/src/ops/index.js +0 -11
- package/src/ops/innovation.js +0 -67
- package/src/ops/lifecycle.js +0 -798
- package/src/ops/self_repair.js +0 -76
- package/src/ops/skills_monitor.js +0 -147
- package/src/ops/trigger.js +0 -33
- package/src/proxy/clientSettings.js +0 -405
- package/src/proxy/envelope.js +0 -59
- package/src/proxy/extensions/dmHandler.js +0 -45
- package/src/proxy/extensions/sessionHandler.js +0 -141
- package/src/proxy/extensions/skillUpdater.js +0 -64
- package/src/proxy/extensions/traceControl.js +0 -1
- package/src/proxy/index.js +0 -1395
- package/src/proxy/inject.js +0 -1
- package/src/proxy/lifecycle/manager.js +0 -1568
- package/src/proxy/mailbox/state.js +0 -207
- package/src/proxy/mailbox/store.js +0 -590
- package/src/proxy/router/cache_passthrough.js +0 -26
- package/src/proxy/router/features.js +0 -84
- package/src/proxy/router/gemini_route.js +0 -154
- package/src/proxy/router/messages_route.js +0 -535
- package/src/proxy/router/model_router.js +0 -113
- package/src/proxy/router/models_route.js +0 -52
- package/src/proxy/router/ollama_route.js +0 -103
- package/src/proxy/router/responses_route.js +0 -170
- package/src/proxy/router/vertex_route.js +0 -110
- package/src/proxy/server/http.js +0 -363
- package/src/proxy/server/routes.js +0 -558
- package/src/proxy/server/settings.js +0 -115
- package/src/proxy/sync/engine.js +0 -179
- package/src/proxy/sync/inbound.js +0 -211
- package/src/proxy/sync/outbound.js +0 -320
- package/src/proxy/task/monitor.js +0 -131
- package/src/proxy/trace/extractor.js +0 -1
- package/src/proxy/trace/usage.js +0 -1
- package/src/webui/client/clientJs/assets.js +0 -111
- package/src/webui/client/clientJs/bootstrap.js +0 -92
- package/src/webui/client/clientJs/common.js +0 -77
- package/src/webui/client/clientJs/i18n.js +0 -366
- package/src/webui/client/clientJs/index.js +0 -35
- package/src/webui/client/clientJs/interactions.js +0 -351
- package/src/webui/client/clientJs/overview.js +0 -152
- package/src/webui/client/clientJs/personality.js +0 -285
- package/src/webui/client/clientJs/pipelines.js +0 -330
- package/src/webui/client/indexHtml.js +0 -221
- package/src/webui/client/static.js +0 -23
- package/src/webui/client/stylesCss.js +0 -639
- package/src/webui/client/vendor/README.md +0 -15
- package/src/webui/client/vendor/echarts.min.js +0 -45
- package/src/webui/index.js +0 -14
- package/src/webui/observer/assets.js +0 -146
- package/src/webui/observer/index.js +0 -37
- package/src/webui/observer/interactions.js +0 -127
- package/src/webui/observer/jsonl.js +0 -75
- package/src/webui/observer/paths.js +0 -46
- package/src/webui/observer/personality.js +0 -43
- package/src/webui/observer/pipelineEvents.js +0 -58
- package/src/webui/observer/redact.js +0 -63
- package/src/webui/observer/runs.js +0 -356
- package/src/webui/observer/safety.js +0 -57
- package/src/webui/observer/skills.js +0 -70
- package/src/webui/observer/status.js +0 -71
- package/src/webui/server/http.js +0 -138
- package/src/webui/server/routes.js +0 -41
package/src/forceUpdate.js
DELETED
|
@@ -1,991 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const zlib = require('zlib');
|
|
5
|
-
const { execFileSync } = require('child_process');
|
|
6
|
-
const { getEvolverInstallRoot } = require('./gep/paths');
|
|
7
|
-
|
|
8
|
-
const MAX_EXEC_BUFFER = 10 * 1024 * 1024;
|
|
9
|
-
|
|
10
|
-
const SEMVER_NUMERIC_IDENTIFIER = '0|[1-9]\\d*';
|
|
11
|
-
const SEMVER_PRERELEASE_IDENTIFIER = '(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)';
|
|
12
|
-
const SEMVER_BUILD_IDENTIFIER = '[0-9A-Za-z-]+';
|
|
13
|
-
const CONCRETE_SEMVER_RE = new RegExp(
|
|
14
|
-
'^(' + SEMVER_NUMERIC_IDENTIFIER + ')\\.(' + SEMVER_NUMERIC_IDENTIFIER + ')\\.(' +
|
|
15
|
-
SEMVER_NUMERIC_IDENTIFIER + ')(?:-(' + SEMVER_PRERELEASE_IDENTIFIER +
|
|
16
|
-
'(?:\\.' + SEMVER_PRERELEASE_IDENTIFIER + ')*))?(?:\\+(' +
|
|
17
|
-
SEMVER_BUILD_IDENTIFIER + '(?:\\.' + SEMVER_BUILD_IDENTIFIER + ')*))?$'
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
// Sentinel returned by executeForceUpdate when the no-op short-circuit fires
|
|
21
|
-
// (current installed version already satisfies required_version). Distinct from
|
|
22
|
-
// `true` so callers can suppress phantom "success" telemetry and avoid the
|
|
23
|
-
// gratuitous process.exit(78) restart that follows a real upgrade. Callers
|
|
24
|
-
// MUST detect this with === identity comparison; do not use truthy/falsy
|
|
25
|
-
// checks (the sentinel IS truthy).
|
|
26
|
-
const FORCE_UPDATE_NOOP = Symbol('FORCE_UPDATE_NOOP');
|
|
27
|
-
|
|
28
|
-
// Sentinel returned by executeForceUpdate when a concurrent invocation is
|
|
29
|
-
// already running in this process. The two callers (enrich.js's evolve tick
|
|
30
|
-
// and a2aProtocol's heartbeat-thread trigger) can both observe a pending
|
|
31
|
-
// force_update directive in the same scheduler tick. Without a shared
|
|
32
|
-
// in-process mutex, both would call executeForceUpdate and both would fire
|
|
33
|
-
// reportForceUpdateOutcome, causing two atomic-rename writes to the same
|
|
34
|
-
// state file -- last writer wins, the first attempt's telemetry is lost.
|
|
35
|
-
//
|
|
36
|
-
// Callers MUST detect this with === identity comparison and treat it as a
|
|
37
|
-
// no-op (do NOT write a state file, do NOT trigger process.exit(78), do NOT
|
|
38
|
-
// emit failure telemetry). The in-flight invocation owns the outcome and will
|
|
39
|
-
// fire its own reportForceUpdateOutcome. See test/forceUpdateConcurrencyGuard.test.js.
|
|
40
|
-
const FORCE_UPDATE_BUSY = Symbol('FORCE_UPDATE_BUSY');
|
|
41
|
-
|
|
42
|
-
// Structured failure taxonomy. Historically every failing branch of
|
|
43
|
-
// _executeForceUpdateInner just `return false`, so the only thing that ever
|
|
44
|
-
// reached the hub (via reportForceUpdateOutcome) was the literal string
|
|
45
|
-
// "executeForceUpdate returned false" — degit-missing, tag-404, version
|
|
46
|
-
// mismatch and copy-EPERM were all indistinguishable in EvolverUpgradeAttempt.
|
|
47
|
-
// Each branch now returns _fail(code, detail); the reporter encodes it as
|
|
48
|
-
// `error = code + ': ' + detail`, so operators can GROUP BY the code prefix
|
|
49
|
-
// without any hub schema / DB migration. Codes are a small stable set — keep
|
|
50
|
-
// new ones coarse and additive so historical `error LIKE 'code%'` queries
|
|
51
|
-
// don't churn.
|
|
52
|
-
const FORCE_UPDATE_FAIL_CODES = Object.freeze({
|
|
53
|
-
INSTALL_GUARD_NAME_MISMATCH: 'install_guard_name_mismatch',
|
|
54
|
-
INSTALL_GUARD_UNREADABLE: 'install_guard_unreadable',
|
|
55
|
-
BAD_REQUIRED_VERSION: 'bad_required_version',
|
|
56
|
-
CURRENT_VERSION_UNPARSABLE: 'current_version_unparsable',
|
|
57
|
-
NPX_NOT_FOUND: 'npx_not_found',
|
|
58
|
-
DEGIT_TIMEOUT: 'degit_timeout',
|
|
59
|
-
DEGIT_FAILED: 'degit_failed',
|
|
60
|
-
DOWNLOAD_INCOMPLETE: 'download_incomplete',
|
|
61
|
-
DOWNLOADED_PACKAGE_NAME_MISMATCH: 'downloaded_package_name_mismatch',
|
|
62
|
-
DOWNLOADED_VERSION_MISMATCH: 'downloaded_version_mismatch',
|
|
63
|
-
DELETE_FAILED: 'delete_failed',
|
|
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',
|
|
70
|
-
ALL_CHANNELS_EXHAUSTED: 'all_channels_exhausted',
|
|
71
|
-
});
|
|
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
|
-
|
|
96
|
-
// Build the structured failure result that replaces a bare `return false`.
|
|
97
|
-
// Shape: { ok:false, code, detail }. Distinct from `true`, FORCE_UPDATE_NOOP
|
|
98
|
-
// and FORCE_UPDATE_BUSY, so the three call sites' `result === true` /
|
|
99
|
-
// `result === SENTINEL` checks keep classifying it as "failed" unchanged —
|
|
100
|
-
// this is backward compatible. Frozen so a downstream consumer cannot mutate
|
|
101
|
-
// the code/detail before it is reported. detail is best-effort context (an
|
|
102
|
-
// errno, a version delta, an entry name); it is redacted + truncated to
|
|
103
|
-
// ERROR_MAX by the reporter before it leaves the process.
|
|
104
|
-
function _fail(code, detail) {
|
|
105
|
-
return Object.freeze({
|
|
106
|
-
ok: false,
|
|
107
|
-
code: String(code),
|
|
108
|
-
detail: detail == null ? '' : String(detail),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Compact "CODE: message" rendering of a thrown error for the detail field.
|
|
113
|
-
function _errStr(e) {
|
|
114
|
-
if (!e) return 'unknown';
|
|
115
|
-
var code = e.code ? String(e.code) + ': ' : '';
|
|
116
|
-
return code + (e.message != null ? String(e.message) : String(e));
|
|
117
|
-
}
|
|
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
|
-
|
|
151
|
-
// Map a Channel 1 (GitHub Release / degit) throw to a structured failure.
|
|
152
|
-
// `phase` records how far the try block got before throwing, so a readFileSync
|
|
153
|
-
// ENOENT (truncated download) is not misread as an npx ENOENT (npx missing):
|
|
154
|
-
// 'degit' -> the npx/degit spawn itself
|
|
155
|
-
// 'parse' -> degit exited 0 but the downloaded package.json is missing/invalid
|
|
156
|
-
// 'copy' -> the staged tree downloaded fine but cpSync into INSTALL_ROOT failed
|
|
157
|
-
function _classifyChannel1Error(e, phase) {
|
|
158
|
-
if (phase === 'delete') {
|
|
159
|
-
var deleteEntry = e && e._evolverEntry ? String(e._evolverEntry) + ': ' : '';
|
|
160
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.DELETE_FAILED, deleteEntry + _errStr(e));
|
|
161
|
-
}
|
|
162
|
-
if (phase === 'copy') {
|
|
163
|
-
var entry = e && e._evolverEntry ? String(e._evolverEntry) + ': ' : '';
|
|
164
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.COPY_FAILED, entry + _errStr(e));
|
|
165
|
-
}
|
|
166
|
-
if (phase === 'parse') {
|
|
167
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
|
|
168
|
-
'missing/invalid package.json in downloaded tree: ' + _errStr(e));
|
|
169
|
-
}
|
|
170
|
-
// phase === 'degit' (the spawn). ENOENT here is the npx binary itself, not a
|
|
171
|
-
// file inside the download — that distinction is exactly why `phase` exists.
|
|
172
|
-
if (e && e.code === 'ENOENT') {
|
|
173
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.NPX_NOT_FOUND, _errStr(e));
|
|
174
|
-
}
|
|
175
|
-
// execFileSync timeout kills the child with SIGTERM (and sets .killed); some
|
|
176
|
-
// platforms surface ETIMEDOUT instead. Either way it is a 60s timeout.
|
|
177
|
-
if (e && (e.killed || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT')) {
|
|
178
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.DEGIT_TIMEOUT,
|
|
179
|
-
'degit timed out after 60s' + (e.signal ? ' (signal=' + e.signal + ')' : ''));
|
|
180
|
-
}
|
|
181
|
-
// Generic degit/network/tag-not-found failure. degit prints the real reason
|
|
182
|
-
// ("could not find commit hash for v…", "could not resolve host") to stderr,
|
|
183
|
-
// so keep a tail of it. Redact + strip control chars HERE, before the tail
|
|
184
|
-
// slice: the downstream reporter redact (a2aProtocol.reportForceUpdateOutcome)
|
|
185
|
-
// runs after this, so slicing first could chop a token's prefix anchor and
|
|
186
|
-
// let the bare value slip past the prefix-anchored redact patterns. Stripping
|
|
187
|
-
// ANSI/NUL/newlines also keeps the persisted error free of terminal-injection
|
|
188
|
-
// sequences and log-line noise.
|
|
189
|
-
var detail = _errStr(e);
|
|
190
|
-
var stderr = '';
|
|
191
|
-
if (e && e.stderr != null) {
|
|
192
|
-
try {
|
|
193
|
-
var redactString = require('./gep/sanitize').redactString;
|
|
194
|
-
stderr = redactString(String(e.stderr)).replace(/[\x00-\x1f\x7f]/g, ' ').trim();
|
|
195
|
-
} catch (_) {
|
|
196
|
-
// sanitize unavailable — still strip control chars so logs stay clean.
|
|
197
|
-
stderr = String(e.stderr).replace(/[\x00-\x1f\x7f]/g, ' ').trim();
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (stderr) detail += ' | stderr=' + stderr.slice(-300);
|
|
201
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.DEGIT_FAILED, detail);
|
|
202
|
-
}
|
|
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
|
-
|
|
216
|
-
function _isRetryableFsLockError(e) {
|
|
217
|
-
var code = e && e.code;
|
|
218
|
-
return code === 'EPERM' || code === 'EBUSY' || code === 'EACCES' ||
|
|
219
|
-
code === 'ENOTEMPTY' || code === 'EMFILE' || code === 'ENFILE';
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function _waitForFsLockRetry() {
|
|
223
|
-
var until = Date.now() + 200;
|
|
224
|
-
while (Date.now() < until) { /* spin */ }
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function _retryFsLockOperation(fn) {
|
|
228
|
-
var err = null;
|
|
229
|
-
for (var attempt = 0; attempt < 3; attempt++) {
|
|
230
|
-
try {
|
|
231
|
-
return fn();
|
|
232
|
-
} catch (e) {
|
|
233
|
-
err = e;
|
|
234
|
-
if (!_isRetryableFsLockError(e)) break;
|
|
235
|
-
if (attempt < 2) _waitForFsLockRetry();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
throw err;
|
|
239
|
-
}
|
|
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
|
-
|
|
379
|
-
function _recoverPackageCommitMarkerIfMissing(installRoot) {
|
|
380
|
-
var pkgDst = path.join(installRoot, 'package.json');
|
|
381
|
-
if (fs.existsSync(pkgDst)) return false;
|
|
382
|
-
var entries;
|
|
383
|
-
try {
|
|
384
|
-
entries = fs.readdirSync(installRoot);
|
|
385
|
-
} catch (_) {
|
|
386
|
-
return false;
|
|
387
|
-
}
|
|
388
|
-
var backups = entries
|
|
389
|
-
.filter(function (name) { return /^package\.json\.\d+\.evolver-old$/.test(name); })
|
|
390
|
-
.sort();
|
|
391
|
-
for (var i = backups.length - 1; i >= 0; i--) {
|
|
392
|
-
try {
|
|
393
|
-
fs.renameSync(path.join(installRoot, backups[i]), pkgDst);
|
|
394
|
-
console.warn('[ForceUpdate] Recovered package.json commit marker from ' + backups[i]);
|
|
395
|
-
return true;
|
|
396
|
-
} catch (_) {}
|
|
397
|
-
}
|
|
398
|
-
return false;
|
|
399
|
-
}
|
|
400
|
-
|
|
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) {
|
|
416
|
-
try {
|
|
417
|
-
var pkg = JSON.parse(fs.readFileSync(path.join(installRoot, 'package.json'), 'utf8'));
|
|
418
|
-
return pkg && pkg.version ? String(pkg.version) : '';
|
|
419
|
-
} catch (_) {
|
|
420
|
-
return '';
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
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
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Module-level mutex: shared by every caller that requires('../forceUpdate'),
|
|
654
|
-
// so the heartbeat-thread trigger in a2aProtocol.js and the evolve-tick path
|
|
655
|
-
// in enrich/pipeline cannot run executeForceUpdate concurrently. This is a
|
|
656
|
-
// process-local guard only; it does not protect against two separate node
|
|
657
|
-
// processes upgrading the same install root simultaneously (out of scope --
|
|
658
|
-
// distinct processes have distinct install layouts in practice).
|
|
659
|
-
let _inFlight = false;
|
|
660
|
-
|
|
661
|
-
function parseConcreteSemver(version) {
|
|
662
|
-
var match = CONCRETE_SEMVER_RE.exec(normalizeConcreteSemver(version));
|
|
663
|
-
if (!match) return null;
|
|
664
|
-
return {
|
|
665
|
-
major: match[1],
|
|
666
|
-
minor: match[2],
|
|
667
|
-
patch: match[3],
|
|
668
|
-
prerelease: match[4] ? match[4].split('.') : [],
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function normalizeConcreteSemver(version) {
|
|
673
|
-
var normalized = String(version || '').replace(/^v(?=\d)/, '');
|
|
674
|
-
return CONCRETE_SEMVER_RE.test(normalized) ? normalized : '';
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function normalizeRequiredVersion(raw) {
|
|
678
|
-
return normalizeConcreteSemver(String(raw || '').replace(/^[>=^~\s]+/, ''));
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function isNumericPrereleaseIdentifier(value) {
|
|
682
|
-
return /^\d+$/.test(value);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function compareNumericIdentifierStrings(left, right) {
|
|
686
|
-
if (left.length !== right.length) return left.length - right.length;
|
|
687
|
-
if (left < right) return -1;
|
|
688
|
-
if (left > right) return 1;
|
|
689
|
-
return 0;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function comparePrereleaseIdentifiers(left, right) {
|
|
693
|
-
var leftNumeric = isNumericPrereleaseIdentifier(left);
|
|
694
|
-
var rightNumeric = isNumericPrereleaseIdentifier(right);
|
|
695
|
-
if (leftNumeric && rightNumeric) return compareNumericIdentifierStrings(left, right);
|
|
696
|
-
if (leftNumeric) return -1;
|
|
697
|
-
if (rightNumeric) return 1;
|
|
698
|
-
if (left < right) return -1;
|
|
699
|
-
if (left > right) return 1;
|
|
700
|
-
return 0;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function compareConcreteSemver(left, right) {
|
|
704
|
-
var a = parseConcreteSemver(left);
|
|
705
|
-
var b = parseConcreteSemver(right);
|
|
706
|
-
if (!a || !b) return null;
|
|
707
|
-
var majorCmp = compareNumericIdentifierStrings(a.major, b.major);
|
|
708
|
-
if (majorCmp !== 0) return majorCmp;
|
|
709
|
-
var minorCmp = compareNumericIdentifierStrings(a.minor, b.minor);
|
|
710
|
-
if (minorCmp !== 0) return minorCmp;
|
|
711
|
-
var patchCmp = compareNumericIdentifierStrings(a.patch, b.patch);
|
|
712
|
-
if (patchCmp !== 0) return patchCmp;
|
|
713
|
-
if (!a.prerelease.length && !b.prerelease.length) return 0;
|
|
714
|
-
if (!a.prerelease.length) return 1;
|
|
715
|
-
if (!b.prerelease.length) return -1;
|
|
716
|
-
var max = Math.max(a.prerelease.length, b.prerelease.length);
|
|
717
|
-
for (var i = 0; i < max; i++) {
|
|
718
|
-
if (a.prerelease[i] === undefined) return -1;
|
|
719
|
-
if (b.prerelease[i] === undefined) return 1;
|
|
720
|
-
var cmp = comparePrereleaseIdentifiers(a.prerelease[i], b.prerelease[i]);
|
|
721
|
-
if (cmp !== 0) return cmp;
|
|
722
|
-
}
|
|
723
|
-
return 0;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Force Update: triggered by Hub when version is critically outdated.
|
|
727
|
-
// Extracted from src/evolve.js so both the evolve main loop and heartbeat
|
|
728
|
-
// thread can trigger it independently (heartbeat-only workers need this
|
|
729
|
-
// because they never reach the evolve run() loop that consumes the pending
|
|
730
|
-
// force_update directive).
|
|
731
|
-
//
|
|
732
|
-
// CRITICAL (issue #51): this function MUST operate on the evolver INSTALL
|
|
733
|
-
// directory, NOT getRepoRoot(). getRepoRoot() preferentially returns the
|
|
734
|
-
// user's surrounding project (process.cwd()'s nearest .git ancestor).
|
|
735
|
-
// Using it here would delete the user's project files and copy the
|
|
736
|
-
// evolver package on top of them. Always use getEvolverInstallRoot(),
|
|
737
|
-
// which resolves to the package containing this file regardless of
|
|
738
|
-
// install layout (global npm / local node_modules / dev clone).
|
|
739
|
-
function executeForceUpdate(forceUpdate) {
|
|
740
|
-
// Concurrency guard: if a prior invocation is still in flight, refuse and
|
|
741
|
-
// return the BUSY sentinel. The in-flight caller owns the outcome (state
|
|
742
|
-
// file write, process.exit(78) on success); a second concurrent attempt
|
|
743
|
-
// would (a) race the atomic-rename state-file writes and clobber the first
|
|
744
|
-
// attempt's telemetry row, and (b) potentially double-exit. See
|
|
745
|
-
// FORCE_UPDATE_BUSY docstring above for context.
|
|
746
|
-
if (_inFlight) {
|
|
747
|
-
console.log('[ForceUpdate] BUSY: another invocation already in flight, skipping');
|
|
748
|
-
return FORCE_UPDATE_BUSY;
|
|
749
|
-
}
|
|
750
|
-
_inFlight = true;
|
|
751
|
-
try {
|
|
752
|
-
return _executeForceUpdateInner(forceUpdate);
|
|
753
|
-
} finally {
|
|
754
|
-
// Always release the mutex, even on throw. Callers may rely on retrying
|
|
755
|
-
// after a failure (e.g. heartbeat cooldown), so the flag MUST NOT remain
|
|
756
|
-
// set after the function returns/throws. Note: on a successful upgrade,
|
|
757
|
-
// _executeForceUpdateInner returns true and the caller invokes
|
|
758
|
-
// process.exit(78); the finally still runs before exit -- which is fine,
|
|
759
|
-
// there is nothing else to coordinate with at that point.
|
|
760
|
-
_inFlight = false;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function _executeForceUpdateInner(forceUpdate) {
|
|
765
|
-
const INSTALL_ROOT = getEvolverInstallRoot();
|
|
766
|
-
|
|
767
|
-
// Defense in depth: if a future refactor breaks path resolution and
|
|
768
|
-
// INSTALL_ROOT no longer points at the evolver package (no package.json
|
|
769
|
-
// / wrong package name), refuse the update rather than risk
|
|
770
|
-
// overwriting an unrelated directory. This is the last guard between
|
|
771
|
-
// the deletion loop and the user's data.
|
|
772
|
-
try {
|
|
773
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(INSTALL_ROOT, 'package.json'), 'utf8'));
|
|
774
|
-
if (!pkg || !_isEvolverPackageName(pkg.name)) {
|
|
775
|
-
console.warn('[ForceUpdate] Refusing — ' + INSTALL_ROOT +
|
|
776
|
-
'/package.json has name="' + (pkg && pkg.name) +
|
|
777
|
-
'", expected "@evomap/evolver". Aborting to avoid data loss.');
|
|
778
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_NAME_MISMATCH,
|
|
779
|
-
'install root package.json name="' + (pkg && pkg.name) + '", expected "@evomap/evolver"');
|
|
780
|
-
}
|
|
781
|
-
} catch (e) {
|
|
782
|
-
if (_recoverPackageCommitMarkerIfMissing(INSTALL_ROOT)) {
|
|
783
|
-
try {
|
|
784
|
-
const recoveredPkg = JSON.parse(fs.readFileSync(path.join(INSTALL_ROOT, 'package.json'), 'utf8'));
|
|
785
|
-
if (!recoveredPkg || !_isEvolverPackageName(recoveredPkg.name)) {
|
|
786
|
-
console.warn('[ForceUpdate] Refusing — recovered ' + INSTALL_ROOT +
|
|
787
|
-
'/package.json has name="' + (recoveredPkg && recoveredPkg.name) +
|
|
788
|
-
'", expected "@evomap/evolver". Aborting to avoid data loss.');
|
|
789
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_NAME_MISMATCH,
|
|
790
|
-
'recovered install root package.json name="' + (recoveredPkg && recoveredPkg.name) +
|
|
791
|
-
'", expected "@evomap/evolver"');
|
|
792
|
-
}
|
|
793
|
-
} catch (recoverReadErr) {
|
|
794
|
-
console.warn('[ForceUpdate] Refusing — cannot read recovered ' + INSTALL_ROOT +
|
|
795
|
-
'/package.json: ' + (recoverReadErr && recoverReadErr.message || recoverReadErr));
|
|
796
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
|
|
797
|
-
'cannot read recovered install root package.json: ' + _errStr(recoverReadErr));
|
|
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');
|
|
801
|
-
} else {
|
|
802
|
-
console.warn('[ForceUpdate] Refusing — cannot read ' + INSTALL_ROOT +
|
|
803
|
-
'/package.json: ' + (e && e.message || e));
|
|
804
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
|
|
805
|
-
'cannot read install root package.json: ' + _errStr(e));
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const requiredVersion = normalizeRequiredVersion(forceUpdate.required_version);
|
|
810
|
-
if (!requiredVersion) {
|
|
811
|
-
console.warn('[ForceUpdate] Refusing — required_version "' +
|
|
812
|
-
String(forceUpdate.required_version || '').replace(/^[>=^~\s]+/, '') +
|
|
813
|
-
'" is not a concrete semver (ranges not accepted).');
|
|
814
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.BAD_REQUIRED_VERSION,
|
|
815
|
-
'required_version=' + JSON.stringify(forceUpdate && forceUpdate.required_version) + ' is not a concrete semver');
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function getCurrentVersion() {
|
|
819
|
-
try {
|
|
820
|
-
var pkg = JSON.parse(fs.readFileSync(path.join(INSTALL_ROOT, 'package.json'), 'utf8'));
|
|
821
|
-
return pkg.version || '0.0.0';
|
|
822
|
-
} catch (_) { return '0.0.0'; }
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Idempotency / anti-downgrade short-circuit: the hub keeps re-issuing the
|
|
826
|
-
// same force_update directive until the node reports success. After a
|
|
827
|
-
// successful upgrade + restart (process.exit(78)), the next heartbeat may
|
|
828
|
-
// still carry the same directive. Without this early return, a transient
|
|
829
|
-
// Channel 1 failure (npx unavailable, network blip, EBUSY) would cause
|
|
830
|
-
// executeForceUpdate to return false and overwrite the previous successful
|
|
831
|
-
// run's state file with a bogus "failed" -- even though we are already at or
|
|
832
|
-
// above the target version.
|
|
833
|
-
//
|
|
834
|
-
// Compare the ACTUAL current running version (which reflects the new
|
|
835
|
-
// version post-restart) against the parsed requiredVersion. Force-update is
|
|
836
|
-
// a minimum-version floor, not an exact-version pin: a node running 1.88.4
|
|
837
|
-
// must not be downgraded to satisfy a 1.88.3 floor. Only reached after the
|
|
838
|
-
// strip+validate above, so a garbage / unparseable required_version will NOT
|
|
839
|
-
// short-circuit -- it falls into the validation failure branch above and
|
|
840
|
-
// returns false safely.
|
|
841
|
-
var currentVersion = getCurrentVersion();
|
|
842
|
-
var versionCmp = compareConcreteSemver(currentVersion, requiredVersion);
|
|
843
|
-
if (versionCmp === null) {
|
|
844
|
-
console.warn('[ForceUpdate] Refusing — current installed version "' +
|
|
845
|
-
currentVersion + '" is not a concrete semver.');
|
|
846
|
-
return _fail(FORCE_UPDATE_FAIL_CODES.CURRENT_VERSION_UNPARSABLE,
|
|
847
|
-
'current installed version "' + currentVersion + '" is not a concrete semver');
|
|
848
|
-
}
|
|
849
|
-
if (versionCmp >= 0) {
|
|
850
|
-
console.log('[ForceUpdate] already satisfies required version, no-op (current=' +
|
|
851
|
-
currentVersion + ', required=' + requiredVersion + ')');
|
|
852
|
-
// Return the dedicated sentinel rather than `true`. Callers use this to
|
|
853
|
-
// (a) emit status="skipped" telemetry instead of a phantom "success"
|
|
854
|
-
// row in EvolverUpgradeAttempt with from_version == to_version, and
|
|
855
|
-
// (b) skip the process.exit(78) restart — there is nothing to restart
|
|
856
|
-
// for when the binary didn't change.
|
|
857
|
-
return FORCE_UPDATE_NOOP;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
console.log('[ForceUpdate] Starting update (target: ' + requiredVersion +
|
|
861
|
-
', install root: ' + INSTALL_ROOT + ')');
|
|
862
|
-
|
|
863
|
-
// Use os.tmpdir() for staging — INSTALL_ROOT's parent (e.g.
|
|
864
|
-
// /usr/lib/node_modules/@evomap when globally installed) is often not
|
|
865
|
-
// writable, unlike the previous user-project parent.
|
|
866
|
-
// mkdtempSync produces a random suffix, preventing predictable-path pre-population.
|
|
867
|
-
const TMP_TARGET = fs.mkdtempSync(path.join(os.tmpdir(), '.evolver-update-tmp-'));
|
|
868
|
-
|
|
869
|
-
// Channel 1: GitHub Release (via degit pinned to exact version tag)
|
|
870
|
-
//
|
|
871
|
-
// channel1Failure captures the structured reason this channel failed, so the
|
|
872
|
-
// terminal `return` can surface it instead of a bare `false`. `phase` tracks
|
|
873
|
-
// how far we got before any throw, so _classifyChannel1Error can tell a
|
|
874
|
-
// degit-spawn failure (phase 'degit') from a truncated download (phase
|
|
875
|
-
// 'parse') from a delete/copy-into-INSTALL_ROOT failure.
|
|
876
|
-
var channel1Failure = null;
|
|
877
|
-
var phase = 'degit';
|
|
878
|
-
try {
|
|
879
|
-
console.log('[ForceUpdate] Channel 1: GitHub Release download (v' + requiredVersion + ')...');
|
|
880
|
-
var npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
881
|
-
// Pin to exact git tag so we download a specific published release, not
|
|
882
|
-
// whatever is currently at HEAD (which could be a different, unreviewed commit).
|
|
883
|
-
// --force: mkdtempSync pre-creates TMP_TARGET; some degit versions refuse a pre-existing dest.
|
|
884
|
-
execFileSync(npxBin, ['-y', 'degit', '--force', 'EvoMap/evolver#v' + requiredVersion, TMP_TARGET], {
|
|
885
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
886
|
-
timeout: 60000, windowsHide: true, maxBuffer: MAX_EXEC_BUFFER,
|
|
887
|
-
});
|
|
888
|
-
var installResult = _installDownloadedTree(
|
|
889
|
-
INSTALL_ROOT, TMP_TARGET, requiredVersion, 'GitHub Release update successful',
|
|
890
|
-
);
|
|
891
|
-
if (installResult.ok === true) {
|
|
892
|
-
return true;
|
|
893
|
-
}
|
|
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);
|
|
913
|
-
}
|
|
914
|
-
try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
|
|
915
|
-
} catch (e) {
|
|
916
|
-
channel1Failure = _classifyChannel1Error(e, phase);
|
|
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
|
-
}
|
|
933
|
-
try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
|
|
934
|
-
// Fall through to Channel 2 (manual download URL hint) instead of
|
|
935
|
-
// returning. A Channel 1 error (degit missing, network down, tag not
|
|
936
|
-
// found) still leaves the user a path forward via the release_url.
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// Channel 2: GitHub release (manual download URL only)
|
|
940
|
-
try {
|
|
941
|
-
var releaseUrl = forceUpdate.release_url;
|
|
942
|
-
if (releaseUrl) {
|
|
943
|
-
console.log('[ForceUpdate] Channel 2: GitHub release -- manual download required');
|
|
944
|
-
console.log('[ForceUpdate] Visit: ' + releaseUrl);
|
|
945
|
-
}
|
|
946
|
-
} catch (_) {}
|
|
947
|
-
|
|
948
|
-
console.warn('[ForceUpdate] All automatic channels exhausted. Current version: ' + getCurrentVersion());
|
|
949
|
-
// Surface the concrete Channel 1 failure when we have one (the common case:
|
|
950
|
-
// degit/network/copy/version-mismatch). channel1Failure is null only when
|
|
951
|
-
// Channel 1 was never entered, which cannot happen here — but fall back to a
|
|
952
|
-
// terminal code so the reporter never lands on the legacy "returned false".
|
|
953
|
-
return channel1Failure || _fail(FORCE_UPDATE_FAIL_CODES.ALL_CHANNELS_EXHAUSTED,
|
|
954
|
-
'no automatic channel succeeded; current=' + getCurrentVersion() + ' target=' + requiredVersion);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// Test-only hook: re-implements the EXACT same operator-strip + semver
|
|
958
|
-
// validation as the runtime force_update check. Exists
|
|
959
|
-
// so test/forceUpdateLastUpdateReport.test.js can build a parity sweep
|
|
960
|
-
// proving that _extractTargetVersion's (a2aProtocol.js) verdict matches
|
|
961
|
-
// forceUpdate.js's verdict byte-for-byte on any input -- the comment at
|
|
962
|
-
// a2aProtocol.js:823-833 claims this invariant but a hand-maintained
|
|
963
|
-
// regex copy can silently drift. Anything that changes this function
|
|
964
|
-
// MUST also update _extractTargetVersion (and vice versa) or the
|
|
965
|
-
// parity test breaks.
|
|
966
|
-
function _isAcceptedRequiredVersionForTesting(raw) {
|
|
967
|
-
if (typeof raw !== 'string') return false;
|
|
968
|
-
return normalizeRequiredVersion(raw) !== '';
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// Type guard: is `result` a structured failure (vs true / NOOP / BUSY)?
|
|
972
|
-
// Call sites use this to decide whether to forward result as opts.failure to
|
|
973
|
-
// reportForceUpdateOutcome. Kept tiny and dependency-free so all three
|
|
974
|
-
// duplicated triggers (a2aProtocol heartbeat, proxy manager, enrich tick) can
|
|
975
|
-
// share one definition.
|
|
976
|
-
function isForceUpdateFailure(result) {
|
|
977
|
-
return !!result && typeof result === 'object' && result.ok === false && typeof result.code === 'string';
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
module.exports = {
|
|
981
|
-
executeForceUpdate,
|
|
982
|
-
FORCE_UPDATE_NOOP,
|
|
983
|
-
FORCE_UPDATE_BUSY,
|
|
984
|
-
FORCE_UPDATE_FAIL_CODES,
|
|
985
|
-
isForceUpdateFailure,
|
|
986
|
-
// Test-only hook: reset the in-flight mutex so unit tests do not leak state
|
|
987
|
-
// across cases. Production callers must NOT touch this -- the mutex is the
|
|
988
|
-
// load-bearing invariant that prevents concurrent state-file writes.
|
|
989
|
-
_resetInFlightForTesting: function () { _inFlight = false; },
|
|
990
|
-
_isAcceptedRequiredVersionForTesting: _isAcceptedRequiredVersionForTesting,
|
|
991
|
-
};
|