blun-king-cli 9.1.602 → 9.1.604
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/CHANGELOG.md +118 -99
- package/LIESMICH.txt +74 -57
- package/README.md +119 -76
- package/bin/agentspine-king-goal-inbox.mjs +127 -127
- package/bin/agentspine-king-goal-intake.mjs +106 -106
- package/bin/agentspine-king-host-runner.mjs +109 -109
- package/bin/agentspine-king-snapshot-policy.cjs +80 -80
- package/bin/agentspine-king-status-policy.mjs +226 -226
- package/bin/agentspine-king-worker-host.mjs +160 -160
- package/bin/core-bootstrap.js +77 -77
- package/bin/launcher-mode.js +93 -93
- package/bin/launcher-restart-policy.cjs +131 -131
- package/bin/launcher-runtime.js +491 -491
- package/bin/managed-node.js +186 -186
- package/bin/managed-plugin-selection.cjs +115 -115
- package/bin/native-module-repair.js +201 -201
- package/bin/node-runtime.js +34 -34
- package/bin/node-version.js +49 -49
- package/bin/plugin-bootstrap.js +162 -162
- package/bin/private-paths.js +1606 -1606
- package/bin/runtime-exit-ledger.cjs +144 -144
- package/bin/runtime-exit-ledger.d.cts +23 -23
- package/bin/standard-tools-bootstrap.js +272 -272
- package/bin/turn-thinking-policy.cjs +82 -82
- package/bin/update-copy.js +200 -200
- package/bin/update-lease.js +597 -597
- package/bin/update-notice.js +1568 -1568
- package/bin/verify-agent-behavior.cjs +122 -122
- package/bin/verify-agent-components.cjs +104 -104
- package/bin/verify-bundled-agent-sources.cjs +98 -76
- package/bin/windows-node-crash-dump.cjs +289 -289
- package/blun.mjs +8041 -6407
- package/codebase-index/codebase_index.py +470 -470
- package/dist-web/assets/{CodeBlockNode-CXnTLMNr.js → CodeBlockNode-TKKkJX8L.js} +1 -1
- package/dist-web/assets/{CodeBlockShell.vue_vue_type_style_index_0_lang-CCF8Ntnq.js → CodeBlockShell.vue_vue_type_style_index_0_lang-CUj2Ds5x.js} +1 -1
- package/dist-web/assets/DesignSystemView-DwK1aQMo.js +12 -0
- package/dist-web/assets/DesignSystemView-szjTj7ns.css +1 -0
- package/dist-web/assets/{Tooltip-CFQEzqC-.js → Tooltip-CTWv7WCU.js} +1 -1
- package/dist-web/assets/{abnfDiagram-N423BO3Z-Z1APZG5m.js → abnfDiagram-N423BO3Z-DA8V8Zjh.js} +1 -1
- package/dist-web/assets/{arc-C4VyWloT.js → arc-YtddKbdY.js} +1 -1
- package/dist-web/assets/{architectureDiagram-T3A2C74G-DpddbUcM.js → architectureDiagram-T3A2C74G-CGEiObmo.js} +1 -1
- package/dist-web/assets/{blockDiagram-VBNYF7ZC-BqccOSMr.js → blockDiagram-VBNYF7ZC-Bd5Vt50e.js} +1 -1
- package/dist-web/assets/{c4Diagram-5PPSVZJV-OHAcJrTy.js → c4Diagram-5PPSVZJV-D2ApPM54.js} +1 -1
- package/dist-web/assets/channel-BnluexuN.js +1 -0
- package/dist-web/assets/{chunk-2GRJ4B5K-DqJHs8Fw.js → chunk-2GRJ4B5K-BDcIEZSl.js} +1 -1
- package/dist-web/assets/{chunk-2Q5K7J3B-B1nCd91o.js → chunk-2Q5K7J3B-6aSabCX7.js} +1 -1
- package/dist-web/assets/{chunk-5RXB4S5H-neG70kZx.js → chunk-5RXB4S5H-BFRQvIJS.js} +1 -1
- package/dist-web/assets/{chunk-5VM5RSS4-l7biGqeH.js → chunk-5VM5RSS4-ChAa-DNY.js} +1 -1
- package/dist-web/assets/{chunk-6Q2QTUOP-BO-F11Gs.js → chunk-6Q2QTUOP-BIfrb2Jc.js} +1 -1
- package/dist-web/assets/{chunk-GF5L2VYU-BtC69fw0.js → chunk-GF5L2VYU-BR0gvtWI.js} +1 -1
- package/dist-web/assets/{chunk-JWPE2WC7-DIOubEqr.js → chunk-JWPE2WC7-BgXKsx5E.js} +1 -1
- package/dist-web/assets/{chunk-KBJHAD2P-DDUeH_Ba.js → chunk-KBJHAD2P-I8mrZhpz.js} +1 -1
- package/dist-web/assets/{chunk-RYQCIY6F-B9gFMVbS.js → chunk-RYQCIY6F-BMPaem92.js} +1 -1
- package/dist-web/assets/{chunk-XXDRQBXY-Bn1Xz6TQ.js → chunk-XXDRQBXY-COy0J-0j.js} +1 -1
- package/dist-web/assets/classDiagram-JCYQIIEL-BliFdSD9.js +1 -0
- package/dist-web/assets/classDiagram-v2-OCEON4UE-BliFdSD9.js +1 -0
- package/dist-web/assets/{cose-bilkent-JH36ORCC-DqigO3T4.js → cose-bilkent-JH36ORCC-MosTTRdo.js} +1 -1
- package/dist-web/assets/{cynefin-VYW2F7L2-rpD93tQ-.js → cynefin-VYW2F7L2-5tjPgrkP.js} +1 -1
- package/dist-web/assets/{cynefinDiagram-MW4NZA55-DbTqnkVJ.js → cynefinDiagram-MW4NZA55-CbDcdInR.js} +1 -1
- package/dist-web/assets/{dagre-VZM6K2ZE-DcM8aEVY.js → dagre-VZM6K2ZE-YaBom1ZH.js} +1 -1
- package/dist-web/assets/{diagram-7IWD3JNH-UymgsBNa.js → diagram-7IWD3JNH-Da4m-Hgz.js} +1 -1
- package/dist-web/assets/{diagram-B4RE2ZJO-BiYu5WPF.js → diagram-B4RE2ZJO-CAUUegxg.js} +1 -1
- package/dist-web/assets/{diagram-LBJQPF4R-BD3kkgOO.js → diagram-LBJQPF4R-o21mWKG4.js} +1 -1
- package/dist-web/assets/{diagram-Q27KOJAE-D1mYQ_cI.js → diagram-Q27KOJAE-BA3z3sXf.js} +1 -1
- package/dist-web/assets/{diagram-UB23O5K3-BtX8u6HG.js → diagram-UB23O5K3-CGRdSSDK.js} +1 -1
- package/dist-web/assets/{ebnfDiagram-BXEA7PRR-CBAnFkRF.js → ebnfDiagram-BXEA7PRR-Bw-iU9FP.js} +1 -1
- package/dist-web/assets/{erDiagram-JOGREHBK-3vKcWsxm.js → erDiagram-JOGREHBK-BwM1yiR0.js} +1 -1
- package/dist-web/assets/{flowDiagram-UKHOOZJN-D9wBbHm2.js → flowDiagram-UKHOOZJN-z05eOR1Q.js} +1 -1
- package/dist-web/assets/{ganttDiagram-PKOTCBZU-Cd_dViF-.js → ganttDiagram-PKOTCBZU-BMUubNTl.js} +1 -1
- package/dist-web/assets/{gitGraphDiagram-DS77QQ5N-BiEEtTo7.js → gitGraphDiagram-DS77QQ5N-Dccbi1wU.js} +1 -1
- package/dist-web/assets/{index-DdEakFJM.js → index-BBbC5LRF.js} +1 -1
- package/dist-web/assets/{index-BmKj7CB1.css → index-C9eDH3u-.css} +1 -1
- package/dist-web/assets/{index-Ba1OvLjf.js → index-D48p0M2S.js} +2 -2
- package/dist-web/assets/{index-CiPA8Kex.js → index-h0KEOmkU.js} +14 -14
- package/dist-web/assets/{index10-CNr4U-Nw.js → index10-Qbuw1N6u.js} +1 -1
- package/dist-web/assets/{index11-Dto_yZNz.js → index11-g8v025wN.js} +1 -1
- package/dist-web/assets/{index5-B2PldaO2.js → index5-BYNcV8BY.js} +1 -1
- package/dist-web/assets/{index6-CBdZmmyx.js → index6-BetANtnV.js} +1 -1
- package/dist-web/assets/{index7-Dz9oCsUF.js → index7-BcPbgCKN.js} +1 -1
- package/dist-web/assets/{index8-B6k25jtU.js → index8-B1lp-RvF.js} +2 -2
- package/dist-web/assets/{infoDiagram-6WML65LV-D4ph2sIH.js → infoDiagram-6WML65LV-BF_pbUnN.js} +1 -1
- package/dist-web/assets/{ishikawaDiagram-WSZJBQD7-DjKjbMm9.js → ishikawaDiagram-WSZJBQD7-Dedv4urS.js} +1 -1
- package/dist-web/assets/{journeyDiagram-NVQOT4AX-DttHDfPQ.js → journeyDiagram-NVQOT4AX-B1Ex3b6o.js} +1 -1
- package/dist-web/assets/{kanban-definition-27J2QSJJ-C5gB-W0d.js → kanban-definition-27J2QSJJ-BtCGliBH.js} +1 -1
- package/dist-web/assets/{linear-BRGe4-d6.js → linear-ByItc8Qr.js} +1 -1
- package/dist-web/assets/{mermaid.core-D-9Sx1No.js → mermaid.core-B1M2O7nN.js} +4 -4
- package/dist-web/assets/{mindmap-definition-FAOFIHXS-Cg_hoVIq.js → mindmap-definition-FAOFIHXS-D0UHKUyj.js} +1 -1
- package/dist-web/assets/{pegDiagram-VL7TDLO6-CSX9hitD.js → pegDiagram-VL7TDLO6-D7zYCVmG.js} +1 -1
- package/dist-web/assets/{pieDiagram-7S7Q4E2Y-0L0mxahZ.js → pieDiagram-7S7Q4E2Y-Bd0i9CHa.js} +1 -1
- package/dist-web/assets/{quadrantDiagram-CIZ2JOQS-U483z0er.js → quadrantDiagram-CIZ2JOQS-MG-Ug47d.js} +1 -1
- package/dist-web/assets/{railroadDiagram-AXF67PYL-D80a2_DJ.js → railroadDiagram-AXF67PYL-AZdl90Gj.js} +1 -1
- package/dist-web/assets/{requirementDiagram-LRYGKXZP-CeyHfq8t.js → requirementDiagram-LRYGKXZP-DjplF9kt.js} +1 -1
- package/dist-web/assets/{sankeyDiagram-W5VNT64P-CCISkkrR.js → sankeyDiagram-W5VNT64P-Bi9Zgfjs.js} +1 -1
- package/dist-web/assets/{sequenceDiagram-SI44F4Z6-jQfm2XS7.js → sequenceDiagram-SI44F4Z6-ChEpdCvb.js} +1 -1
- package/dist-web/assets/{sizeCapture-X5ZJPWSS-lRZsmCoS.js → sizeCapture-X5ZJPWSS-B5RFUdaa.js} +1 -1
- package/dist-web/assets/{stateDiagram-OKZ733FA-ClCgTdUm.js → stateDiagram-OKZ733FA-BkRblSKp.js} +1 -1
- package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fVkQcACW.js +1 -0
- package/dist-web/assets/{swimlanes-SLNWSIFB-BCdOUN05.js → swimlanes-SLNWSIFB-DLXH6Zhc.js} +2 -2
- package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-Bg-BiZJQ.js +8 -0
- package/dist-web/assets/{timeline-definition-Z64GVDOM-DpWutSKa.js → timeline-definition-Z64GVDOM-C_N88siO.js} +1 -1
- package/dist-web/assets/{vennDiagram-T6HMQDX7-DVC5JVsc.js → vennDiagram-T6HMQDX7-DMsnvDt0.js} +1 -1
- package/dist-web/assets/{vue.runtime.esm-bundler-BwwRtQwQ.js → vue.runtime.esm-bundler-B-3uIy85.js} +1 -1
- package/dist-web/assets/{wardleyDiagram-T6FBY63Y-BK2mrq7L.js → wardleyDiagram-T6FBY63Y-C9SJIPzq.js} +1 -1
- package/dist-web/assets/{xychartDiagram-ELKLHX3M-DUJu9F7-.js → xychartDiagram-ELKLHX3M-DZLWA6Go.js} +1 -1
- package/dist-web/index.html +32 -32
- package/dist-web/vis/index.html.gz +0 -0
- package/package.json +1 -1
- package/scripts/fix-node-pty-perms.js +53 -53
- package/standard-skills/blun-app-design-system/SKILL.md +30 -30
- package/standard-skills/blun-app-design-system/provenance.json +17 -17
- package/standard-skills/blun-app-design-system/references/tokens.md +48 -48
- package/standard-skills/blun-app-design-system/scripts/check-blun-design.mjs +49 -49
- package/telegram-plugin/blun.plugin.json +20 -20
- package/telegram-plugin/commands/access.md +37 -37
- package/telegram-plugin/dist/bridge.mjs +117 -100
- package/telegram-plugin/dist/mcp-server.mjs +185 -161
- package/worker-host.mjs +1458 -1022
- package/dist-web/assets/DesignSystemView-6HgAUEmC.css +0 -1
- package/dist-web/assets/DesignSystemView-BbMOu-XO.js +0 -12
- package/dist-web/assets/channel-B2VOQOI-.js +0 -1
- package/dist-web/assets/classDiagram-JCYQIIEL-BA1TjAxj.js +0 -1
- package/dist-web/assets/classDiagram-v2-OCEON4UE-BA1TjAxj.js +0 -1
- package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fi_lrTAx.js +0 -1
- package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-BUd_uYK3.js +0 -8
package/bin/update-notice.js
CHANGED
|
@@ -1,1568 +1,1568 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { randomUUID } = require('node:crypto');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const https = require('node:https');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
const { fork } = require('node:child_process');
|
|
8
|
-
const { StringDecoder } = require('node:string_decoder');
|
|
9
|
-
const { types } = require('node:util');
|
|
10
|
-
|
|
11
|
-
const { tryAcquireUpdateLease } = require('./update-lease');
|
|
12
|
-
const { resolveUpdateLocale, updateText } = require('./update-copy');
|
|
13
|
-
|
|
14
|
-
const {
|
|
15
|
-
ensurePrivateDirectory,
|
|
16
|
-
securePrivateFile,
|
|
17
|
-
writePrivateFile,
|
|
18
|
-
} = require('./private-paths');
|
|
19
|
-
|
|
20
|
-
const PACKAGE_NAME = 'blun-king-cli';
|
|
21
|
-
const REGISTRY_URL = 'https://registry.npmjs.org/blun-king-cli';
|
|
22
|
-
const INSTALL_REGISTRY_URL = 'https://registry.npmjs.org/';
|
|
23
|
-
const FALLBACK_MANIFEST_URL = 'https://chat.blun.ai/blun-code-version.json';
|
|
24
|
-
const SNOOZE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
25
|
-
const REQUEST_TIMEOUT_MS = 2_500;
|
|
26
|
-
const STATE_FILE = 'update-notice.json';
|
|
27
|
-
const UPDATE_SKIP_FILE = 'update-skip-version.json';
|
|
28
|
-
const PENDING_UPDATE_FILE = 'update-pending.json';
|
|
29
|
-
const UPDATE_DIRECTORY = 'updates';
|
|
30
|
-
const CORE_INSTALL_STATE_FILE = 'install.json';
|
|
31
|
-
const UPDATE_KEY_ESCAPE_TIMEOUT_MS = 500;
|
|
32
|
-
const UPDATE_SIGNAL_EXIT_CODES = Object.freeze({
|
|
33
|
-
SIGHUP: 129,
|
|
34
|
-
SIGINT: 130,
|
|
35
|
-
SIGTERM: 143,
|
|
36
|
-
});
|
|
37
|
-
const INTERACTIVE_START_MODE_FLAGS = new Set([
|
|
38
|
-
'-y',
|
|
39
|
-
'--auto',
|
|
40
|
-
'--auto-approve',
|
|
41
|
-
'--plan',
|
|
42
|
-
'--yolo',
|
|
43
|
-
]);
|
|
44
|
-
const SEMVER_IDENTIFIER = '(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)';
|
|
45
|
-
const SEMVER_PATTERN = new RegExp(
|
|
46
|
-
`^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)`
|
|
47
|
-
+ `(?:-(${SEMVER_IDENTIFIER}(?:\\.${SEMVER_IDENTIFIER})*))?`
|
|
48
|
-
+ '(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$',
|
|
49
|
-
);
|
|
50
|
-
const LEGACY_MANIFEST_KEYS = Object.freeze([
|
|
51
|
-
'install',
|
|
52
|
-
'latest',
|
|
53
|
-
'minSupported',
|
|
54
|
-
'name',
|
|
55
|
-
'notesUrl',
|
|
56
|
-
'releasedAt',
|
|
57
|
-
]);
|
|
58
|
-
const RELEASE_NOTES_MANIFEST_KEYS = Object.freeze([
|
|
59
|
-
'install', 'latest', 'minSupported', 'name', 'notesUrl', 'releaseNotes', 'releasedAt',
|
|
60
|
-
]);
|
|
61
|
-
const MANIFEST_KEYS = Object.freeze([
|
|
62
|
-
'install', 'integrity', 'latest', 'minSupported', 'name', 'notesUrl', 'releaseNotes', 'releasedAt',
|
|
63
|
-
]);
|
|
64
|
-
|
|
65
|
-
function parseReleaseNotes(value) {
|
|
66
|
-
if (!Array.isArray(value) || value.length > 8) return undefined;
|
|
67
|
-
const notes = [];
|
|
68
|
-
for (const note of value) {
|
|
69
|
-
if (!isBoundedText(note, 240)) return undefined;
|
|
70
|
-
notes.push(note);
|
|
71
|
-
}
|
|
72
|
-
return Object.freeze(notes);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function validIntegrity(value) {
|
|
76
|
-
if (typeof value !== 'string') return false;
|
|
77
|
-
const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/u.exec(value);
|
|
78
|
-
if (!match) return false;
|
|
79
|
-
return Buffer.from(match[1], 'base64').length === 64;
|
|
80
|
-
}
|
|
81
|
-
const ENDPOINT_LIMITS = new Map([
|
|
82
|
-
[REGISTRY_URL, 1024 * 1024],
|
|
83
|
-
[FALLBACK_MANIFEST_URL, 64 * 1024],
|
|
84
|
-
]);
|
|
85
|
-
|
|
86
|
-
function parseSemver(value) {
|
|
87
|
-
if (typeof value !== 'string' || value.length > 128) return undefined;
|
|
88
|
-
const match = SEMVER_PATTERN.exec(value);
|
|
89
|
-
if (!match) return undefined;
|
|
90
|
-
return {
|
|
91
|
-
raw: value,
|
|
92
|
-
core: [match[1], match[2], match[3]],
|
|
93
|
-
prerelease: match[4] === undefined ? undefined : match[4].split('.'),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function compareNumericIdentifier(left, right) {
|
|
98
|
-
if (left.length !== right.length) return left.length < right.length ? -1 : 1;
|
|
99
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function comparePrerelease(left, right) {
|
|
103
|
-
if (left === undefined && right === undefined) return 0;
|
|
104
|
-
if (left === undefined) return 1;
|
|
105
|
-
if (right === undefined) return -1;
|
|
106
|
-
const length = Math.max(left.length, right.length);
|
|
107
|
-
for (let index = 0; index < length; index += 1) {
|
|
108
|
-
if (left[index] === undefined) return -1;
|
|
109
|
-
if (right[index] === undefined) return 1;
|
|
110
|
-
const leftNumeric = /^\d+$/.test(left[index]);
|
|
111
|
-
const rightNumeric = /^\d+$/.test(right[index]);
|
|
112
|
-
if (leftNumeric && rightNumeric) {
|
|
113
|
-
const comparison = compareNumericIdentifier(left[index], right[index]);
|
|
114
|
-
if (comparison !== 0) return comparison;
|
|
115
|
-
} else if (leftNumeric !== rightNumeric) {
|
|
116
|
-
return leftNumeric ? -1 : 1;
|
|
117
|
-
} else if (left[index] !== right[index]) {
|
|
118
|
-
return left[index] < right[index] ? -1 : 1;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return 0;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function compareSemver(leftValue, rightValue) {
|
|
125
|
-
const left = parseSemver(leftValue);
|
|
126
|
-
const right = parseSemver(rightValue);
|
|
127
|
-
if (!left || !right) return undefined;
|
|
128
|
-
for (let index = 0; index < left.core.length; index += 1) {
|
|
129
|
-
const comparison = compareNumericIdentifier(left.core[index], right.core[index]);
|
|
130
|
-
if (comparison !== 0) return comparison;
|
|
131
|
-
}
|
|
132
|
-
return comparePrerelease(left.prerelease, right.prerelease);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function exactDataRecord(value, expectedKeys) {
|
|
136
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value) || types.isProxy(value)) {
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
const prototype = Object.getPrototypeOf(value);
|
|
140
|
-
if (prototype !== Object.prototype && prototype !== null) return undefined;
|
|
141
|
-
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
142
|
-
const keys = Object.keys(descriptors).toSorted();
|
|
143
|
-
if (keys.length !== expectedKeys.length
|
|
144
|
-
|| keys.some((key, index) => key !== expectedKeys[index])) {
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
for (const key of keys) {
|
|
148
|
-
if (!Object.hasOwn(descriptors[key], 'value') || descriptors[key].enumerable !== true) {
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return Object.fromEntries(keys.map((key) => [key, descriptors[key].value]));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function readDataProperty(value, key) {
|
|
156
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value) || types.isProxy(value)) {
|
|
157
|
-
return undefined;
|
|
158
|
-
}
|
|
159
|
-
const prototype = Object.getPrototypeOf(value);
|
|
160
|
-
if (prototype !== Object.prototype && prototype !== null) return undefined;
|
|
161
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
162
|
-
if (!descriptor || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
return descriptor.value;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function parseRegistryDocument(value) {
|
|
169
|
-
const tags = readDataProperty(value, 'dist-tags');
|
|
170
|
-
const latest = readDataProperty(tags, 'latest');
|
|
171
|
-
if (!parseSemver(latest)) return undefined;
|
|
172
|
-
const parsed = { latest };
|
|
173
|
-
const next = readDataProperty(tags, 'next');
|
|
174
|
-
if (parseSemver(next)) parsed.next = next;
|
|
175
|
-
return Object.freeze(parsed);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function isBoundedText(value, maxBytes) {
|
|
179
|
-
return typeof value === 'string'
|
|
180
|
-
&& value.length > 0
|
|
181
|
-
&& Buffer.byteLength(value, 'utf8') <= maxBytes
|
|
182
|
-
&& !/[\u0000-\u001F\u007F-\u009F\u061C\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/u.test(value);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function canonicalOfficialNotesUrl(value) {
|
|
186
|
-
if (!isBoundedText(value, 2_048)) return undefined;
|
|
187
|
-
try {
|
|
188
|
-
const url = new URL(value);
|
|
189
|
-
const officialHost = url.hostname === 'blun.ai' || url.hostname.endsWith('.blun.ai');
|
|
190
|
-
if (url.protocol !== 'https:'
|
|
191
|
-
|| !officialHost
|
|
192
|
-
|| url.username !== ''
|
|
193
|
-
|| url.password !== ''
|
|
194
|
-
|| url.port !== '') {
|
|
195
|
-
return undefined;
|
|
196
|
-
}
|
|
197
|
-
return url.href;
|
|
198
|
-
} catch {
|
|
199
|
-
return undefined;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function isIsoTimestamp(value) {
|
|
204
|
-
if (typeof value !== 'string') return false;
|
|
205
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?Z$/.exec(value);
|
|
206
|
-
if (!match) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
const timestamp = Date.parse(value);
|
|
210
|
-
if (!Number.isFinite(timestamp)) return false;
|
|
211
|
-
const date = new Date(timestamp);
|
|
212
|
-
return date.getUTCFullYear() === Number(match[1])
|
|
213
|
-
&& date.getUTCMonth() + 1 === Number(match[2])
|
|
214
|
-
&& date.getUTCDate() === Number(match[3])
|
|
215
|
-
&& date.getUTCHours() === Number(match[4])
|
|
216
|
-
&& date.getUTCMinutes() === Number(match[5])
|
|
217
|
-
&& date.getUTCSeconds() === Number(match[6]);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function parseFallbackManifest(value) {
|
|
221
|
-
const record = exactDataRecord(value, MANIFEST_KEYS)
|
|
222
|
-
|| exactDataRecord(value, RELEASE_NOTES_MANIFEST_KEYS)
|
|
223
|
-
|| exactDataRecord(value, LEGACY_MANIFEST_KEYS);
|
|
224
|
-
const notesUrl = record ? canonicalOfficialNotesUrl(record.notesUrl) : undefined;
|
|
225
|
-
const releaseNotes = record && Object.hasOwn(record, 'releaseNotes')
|
|
226
|
-
? parseReleaseNotes(record.releaseNotes)
|
|
227
|
-
: undefined;
|
|
228
|
-
if (!record
|
|
229
|
-
|| record.name !== PACKAGE_NAME
|
|
230
|
-
|| !parseSemver(record.latest)
|
|
231
|
-
|| !parseSemver(record.minSupported)
|
|
232
|
-
|| compareSemver(record.minSupported, record.latest) === 1
|
|
233
|
-
|| !isIsoTimestamp(record.releasedAt)
|
|
234
|
-
|| !notesUrl
|
|
235
|
-
|| (Object.hasOwn(record, 'integrity') && !validIntegrity(record.integrity))
|
|
236
|
-
|| (Object.hasOwn(record, 'releaseNotes') && releaseNotes === undefined)
|
|
237
|
-
|| !isBoundedText(record.install, 256)) {
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
return Object.freeze({
|
|
241
|
-
name: record.name,
|
|
242
|
-
latest: record.latest,
|
|
243
|
-
minSupported: record.minSupported,
|
|
244
|
-
releasedAt: record.releasedAt,
|
|
245
|
-
notesUrl,
|
|
246
|
-
...(Object.hasOwn(record, 'integrity') ? { integrity: record.integrity } : {}),
|
|
247
|
-
...(releaseNotes === undefined ? {} : { releaseNotes }),
|
|
248
|
-
install: record.install,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function resolveEffectiveRelease(currentVersion, registryTags, fallbackManifest) {
|
|
253
|
-
if (!parseSemver(currentVersion)) return undefined;
|
|
254
|
-
const registryLatest = readDataProperty(registryTags, 'latest');
|
|
255
|
-
const validRegistryLatest = parseSemver(registryLatest) ? registryLatest : undefined;
|
|
256
|
-
const registryNext = readDataProperty(registryTags, 'next');
|
|
257
|
-
const validRegistryNext = parseSemver(registryNext) ? registryNext : undefined;
|
|
258
|
-
const validFallback = parseFallbackManifest(fallbackManifest);
|
|
259
|
-
if (!validRegistryLatest && !validFallback) return undefined;
|
|
260
|
-
|
|
261
|
-
if (validRegistryLatest && compareSemver(currentVersion, validRegistryLatest) > 0) {
|
|
262
|
-
if (!validRegistryNext || compareSemver(validRegistryNext, currentVersion) <= 0) {
|
|
263
|
-
return undefined;
|
|
264
|
-
}
|
|
265
|
-
return { channel: 'next', version: validRegistryNext };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
let version;
|
|
269
|
-
if (validRegistryLatest && validFallback) {
|
|
270
|
-
version = compareSemver(validRegistryLatest, validFallback.latest) <= 0
|
|
271
|
-
? validRegistryLatest
|
|
272
|
-
: validFallback.latest;
|
|
273
|
-
} else {
|
|
274
|
-
version = validRegistryLatest || validFallback.latest;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (compareSemver(version, currentVersion) <= 0) return undefined;
|
|
278
|
-
|
|
279
|
-
if (validFallback && compareSemver(validFallback.latest, version) === 0) {
|
|
280
|
-
return { channel: 'stable', version, notesUrl: validFallback.notesUrl,
|
|
281
|
-
...(validFallback.releaseNotes === undefined ? {} : { releaseNotes: validFallback.releaseNotes }),
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
return { channel: 'stable', version };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function requestTrustedJson(url, options = {}) {
|
|
288
|
-
const maxBytes = ENDPOINT_LIMITS.get(url);
|
|
289
|
-
if (maxBytes === undefined) {
|
|
290
|
-
return Promise.reject(new Error('UNTRUSTED_UPDATE_ENDPOINT'));
|
|
291
|
-
}
|
|
292
|
-
const httpsImpl = options.httpsImpl || https;
|
|
293
|
-
const timeoutMs = options.timeoutMs || REQUEST_TIMEOUT_MS;
|
|
294
|
-
|
|
295
|
-
return new Promise((resolve, reject) => {
|
|
296
|
-
let request;
|
|
297
|
-
let response;
|
|
298
|
-
let settled = false;
|
|
299
|
-
const finish = (error, value) => {
|
|
300
|
-
if (settled) return;
|
|
301
|
-
settled = true;
|
|
302
|
-
clearTimeout(timer);
|
|
303
|
-
if (error) reject(error);
|
|
304
|
-
else resolve(value);
|
|
305
|
-
};
|
|
306
|
-
const timer = setTimeout(() => {
|
|
307
|
-
const error = new Error('UPDATE_REQUEST_TIMEOUT');
|
|
308
|
-
finish(error);
|
|
309
|
-
if (response && typeof response.destroy === 'function') response.destroy();
|
|
310
|
-
if (request && typeof request.destroy === 'function') request.destroy();
|
|
311
|
-
}, timeoutMs);
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
request = httpsImpl.get(url, {
|
|
315
|
-
headers: { accept: 'application/vnd.npm.install-v1+json, application/json' },
|
|
316
|
-
}, (incoming) => {
|
|
317
|
-
response = incoming;
|
|
318
|
-
if (incoming.statusCode !== 200) {
|
|
319
|
-
finish(new Error(`UPDATE_HTTP_STATUS_${incoming.statusCode || 0}`));
|
|
320
|
-
if (typeof incoming.destroy === 'function') incoming.destroy();
|
|
321
|
-
if (request && typeof request.destroy === 'function') request.destroy();
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const rawLength = incoming.headers && incoming.headers['content-length'];
|
|
326
|
-
const declaredLength = typeof rawLength === 'string' && /^\d+$/.test(rawLength)
|
|
327
|
-
? Number(rawLength)
|
|
328
|
-
: undefined;
|
|
329
|
-
if (declaredLength !== undefined && declaredLength > maxBytes) {
|
|
330
|
-
finish(new Error('UPDATE_BODY_TOO_LARGE'));
|
|
331
|
-
if (typeof incoming.destroy === 'function') incoming.destroy();
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const chunks = [];
|
|
336
|
-
let total = 0;
|
|
337
|
-
incoming.on('data', (chunk) => {
|
|
338
|
-
if (settled) return;
|
|
339
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
340
|
-
total += buffer.length;
|
|
341
|
-
if (total > maxBytes) {
|
|
342
|
-
finish(new Error('UPDATE_BODY_TOO_LARGE'));
|
|
343
|
-
if (typeof incoming.destroy === 'function') incoming.destroy();
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
chunks.push(buffer);
|
|
347
|
-
});
|
|
348
|
-
incoming.on('error', (error) => finish(error));
|
|
349
|
-
incoming.on('end', () => {
|
|
350
|
-
if (settled) return;
|
|
351
|
-
try {
|
|
352
|
-
finish(undefined, JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
353
|
-
} catch {
|
|
354
|
-
finish(new Error('UPDATE_INVALID_JSON'));
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
request.on('error', (error) => finish(error));
|
|
359
|
-
} catch (error) {
|
|
360
|
-
finish(error);
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
async function loadReleaseSources() {
|
|
366
|
-
const [registry, fallback] = await Promise.allSettled([
|
|
367
|
-
requestTrustedJson(REGISTRY_URL),
|
|
368
|
-
requestTrustedJson(FALLBACK_MANIFEST_URL),
|
|
369
|
-
]);
|
|
370
|
-
return {
|
|
371
|
-
registryDocument: registry.status === 'fulfilled' ? registry.value : undefined,
|
|
372
|
-
fallbackDocument: fallback.status === 'fulfilled' ? fallback.value : undefined,
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function statePath(blunDir) {
|
|
377
|
-
return path.join(blunDir, STATE_FILE);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function readSnoozeState(blunDir, now = Date.now()) {
|
|
381
|
-
try {
|
|
382
|
-
const filePath = statePath(blunDir);
|
|
383
|
-
if (!fs.existsSync(filePath)) return false;
|
|
384
|
-
securePrivateFile(filePath);
|
|
385
|
-
const stat = fs.lstatSync(filePath);
|
|
386
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_024) return false;
|
|
387
|
-
const record = exactDataRecord(JSON.parse(fs.readFileSync(filePath, 'utf8')), [
|
|
388
|
-
'snoozedUntil',
|
|
389
|
-
'version',
|
|
390
|
-
]);
|
|
391
|
-
if (!record
|
|
392
|
-
|| record.version !== 1
|
|
393
|
-
|| !Number.isSafeInteger(record.snoozedUntil)
|
|
394
|
-
|| record.snoozedUntil <= now
|
|
395
|
-
|| record.snoozedUntil > now + SNOOZE_MS) {
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
return true;
|
|
399
|
-
} catch {
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function writeSnoozeState(blunDir, now = Date.now()) {
|
|
405
|
-
ensurePrivateDirectory(blunDir);
|
|
406
|
-
const filePath = statePath(blunDir);
|
|
407
|
-
const temporaryPath = path.join(
|
|
408
|
-
blunDir,
|
|
409
|
-
`.${STATE_FILE}.${process.pid}.${randomUUID()}.tmp`,
|
|
410
|
-
);
|
|
411
|
-
try {
|
|
412
|
-
writePrivateFile(temporaryPath, `${JSON.stringify({
|
|
413
|
-
version: 1,
|
|
414
|
-
snoozedUntil: now + SNOOZE_MS,
|
|
415
|
-
})}\n`);
|
|
416
|
-
const handle = fs.openSync(temporaryPath, 'r+');
|
|
417
|
-
try {
|
|
418
|
-
fs.fsyncSync(handle);
|
|
419
|
-
} finally {
|
|
420
|
-
fs.closeSync(handle);
|
|
421
|
-
}
|
|
422
|
-
fs.renameSync(temporaryPath, filePath);
|
|
423
|
-
securePrivateFile(filePath);
|
|
424
|
-
} catch (error) {
|
|
425
|
-
fs.rmSync(temporaryPath, { force: true });
|
|
426
|
-
throw error;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function skippedVersionPath(blunDir) {
|
|
431
|
-
return path.join(blunDir, UPDATE_SKIP_FILE);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function readSkippedVersion(blunDir) {
|
|
435
|
-
try {
|
|
436
|
-
const filePath = skippedVersionPath(blunDir);
|
|
437
|
-
if (!fs.existsSync(filePath)) return undefined;
|
|
438
|
-
securePrivateFile(filePath);
|
|
439
|
-
const stat = fs.lstatSync(filePath);
|
|
440
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_024) return undefined;
|
|
441
|
-
const record = exactDataRecord(JSON.parse(fs.readFileSync(filePath, 'utf8')), [
|
|
442
|
-
'skippedVersion',
|
|
443
|
-
'version',
|
|
444
|
-
]);
|
|
445
|
-
if (!record || record.version !== 1 || !parseSemver(record.skippedVersion)) {
|
|
446
|
-
return undefined;
|
|
447
|
-
}
|
|
448
|
-
return record.skippedVersion;
|
|
449
|
-
} catch {
|
|
450
|
-
return undefined;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function writeSkippedVersion(blunDir, skippedVersion) {
|
|
455
|
-
if (!parseSemver(skippedVersion)) throw new Error('INVALID_UPDATE_VERSION');
|
|
456
|
-
ensurePrivateDirectory(blunDir);
|
|
457
|
-
const filePath = skippedVersionPath(blunDir);
|
|
458
|
-
const temporaryPath = path.join(
|
|
459
|
-
blunDir,
|
|
460
|
-
`.${UPDATE_SKIP_FILE}.${process.pid}.${randomUUID()}.tmp`,
|
|
461
|
-
);
|
|
462
|
-
try {
|
|
463
|
-
writePrivateFile(temporaryPath, `${JSON.stringify({
|
|
464
|
-
version: 1,
|
|
465
|
-
skippedVersion,
|
|
466
|
-
})}\n`);
|
|
467
|
-
const handle = fs.openSync(temporaryPath, 'r+');
|
|
468
|
-
try {
|
|
469
|
-
fs.fsyncSync(handle);
|
|
470
|
-
} finally {
|
|
471
|
-
fs.closeSync(handle);
|
|
472
|
-
}
|
|
473
|
-
fs.renameSync(temporaryPath, filePath);
|
|
474
|
-
securePrivateFile(filePath);
|
|
475
|
-
} catch (error) {
|
|
476
|
-
fs.rmSync(temporaryPath, { force: true });
|
|
477
|
-
throw error;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function pendingUpdatePath(blunDir) {
|
|
482
|
-
return path.join(blunDir, PENDING_UPDATE_FILE);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function readPendingUpdate(blunDir) {
|
|
486
|
-
try {
|
|
487
|
-
const filePath = pendingUpdatePath(blunDir);
|
|
488
|
-
if (!fs.existsSync(filePath)) return undefined;
|
|
489
|
-
securePrivateFile(filePath);
|
|
490
|
-
const stat = fs.lstatSync(filePath);
|
|
491
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_024) return undefined;
|
|
492
|
-
const record = exactDataRecord(JSON.parse(fs.readFileSync(filePath, 'utf8')), [
|
|
493
|
-
'targetVersion',
|
|
494
|
-
'version',
|
|
495
|
-
]);
|
|
496
|
-
if (!record || record.version !== 1 || !parseSemver(record.targetVersion)) return undefined;
|
|
497
|
-
return record.targetVersion;
|
|
498
|
-
} catch {
|
|
499
|
-
return undefined;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function writePendingUpdate(blunDir, targetVersion) {
|
|
504
|
-
if (!parseSemver(targetVersion)) throw new Error('INVALID_UPDATE_VERSION');
|
|
505
|
-
ensurePrivateDirectory(blunDir);
|
|
506
|
-
const filePath = pendingUpdatePath(blunDir);
|
|
507
|
-
const temporaryPath = path.join(
|
|
508
|
-
blunDir,
|
|
509
|
-
`.${PENDING_UPDATE_FILE}.${process.pid}.${randomUUID()}.tmp`,
|
|
510
|
-
);
|
|
511
|
-
try {
|
|
512
|
-
writePrivateFile(temporaryPath, `${JSON.stringify({
|
|
513
|
-
version: 1,
|
|
514
|
-
targetVersion,
|
|
515
|
-
})}\n`);
|
|
516
|
-
const handle = fs.openSync(temporaryPath, 'r+');
|
|
517
|
-
try {
|
|
518
|
-
fs.fsyncSync(handle);
|
|
519
|
-
} finally {
|
|
520
|
-
fs.closeSync(handle);
|
|
521
|
-
}
|
|
522
|
-
fs.renameSync(temporaryPath, filePath);
|
|
523
|
-
securePrivateFile(filePath);
|
|
524
|
-
} catch (error) {
|
|
525
|
-
fs.rmSync(temporaryPath, { force: true });
|
|
526
|
-
throw error;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function clearPendingUpdate(blunDir) {
|
|
531
|
-
fs.rmSync(pendingUpdatePath(blunDir), { force: true });
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function writeCoreInstallSuccess(blunDir, fromVersion, version, now = Date.now()) {
|
|
535
|
-
if (!parseSemver(fromVersion) || !parseSemver(version)) {
|
|
536
|
-
throw new Error('INVALID_UPDATE_VERSION');
|
|
537
|
-
}
|
|
538
|
-
const updateDirectory = path.join(blunDir, UPDATE_DIRECTORY);
|
|
539
|
-
ensurePrivateDirectory(blunDir);
|
|
540
|
-
ensurePrivateDirectory(updateDirectory);
|
|
541
|
-
const filePath = path.join(updateDirectory, CORE_INSTALL_STATE_FILE);
|
|
542
|
-
const temporaryPath = path.join(
|
|
543
|
-
updateDirectory,
|
|
544
|
-
`.${CORE_INSTALL_STATE_FILE}.${process.pid}.${randomUUID()}.tmp`,
|
|
545
|
-
);
|
|
546
|
-
try {
|
|
547
|
-
writePrivateFile(temporaryPath, `${JSON.stringify({
|
|
548
|
-
active: null,
|
|
549
|
-
lastFailure: null,
|
|
550
|
-
lastSuccess: {
|
|
551
|
-
version,
|
|
552
|
-
fromVersion,
|
|
553
|
-
installedAt: new Date(now).toISOString(),
|
|
554
|
-
notifiedAt: null,
|
|
555
|
-
},
|
|
556
|
-
})}\n`);
|
|
557
|
-
const handle = fs.openSync(temporaryPath, 'r+');
|
|
558
|
-
try {
|
|
559
|
-
fs.fsyncSync(handle);
|
|
560
|
-
} finally {
|
|
561
|
-
fs.closeSync(handle);
|
|
562
|
-
}
|
|
563
|
-
fs.renameSync(temporaryPath, filePath);
|
|
564
|
-
securePrivateFile(filePath);
|
|
565
|
-
} catch (error) {
|
|
566
|
-
fs.rmSync(temporaryPath, { force: true });
|
|
567
|
-
throw error;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function isInteractiveUpdateStart({ argv, env, stdin, stdout }) {
|
|
572
|
-
const isOrdinaryStartup = Array.isArray(argv)
|
|
573
|
-
&& (argv.length === 0
|
|
574
|
-
|| (argv.length === 1 && INTERACTIVE_START_MODE_FLAGS.has(argv[0])));
|
|
575
|
-
return isOrdinaryStartup
|
|
576
|
-
&& (!env || env.BLUN_NO_AUTO_UPDATE !== '1')
|
|
577
|
-
&& stdin?.isTTY === true
|
|
578
|
-
&& stdout?.isTTY === true;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function installCommandText(version) {
|
|
582
|
-
return `npm i -g ${PACKAGE_NAME}@${version}`;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function createUpdateProgress(output) {
|
|
586
|
-
const enabled = output?.isTTY === true;
|
|
587
|
-
const phaseCount = 4;
|
|
588
|
-
const width = 24;
|
|
589
|
-
const cyan = '\u001B[36m';
|
|
590
|
-
const dim = '\u001B[2m';
|
|
591
|
-
const reset = '\u001B[0m';
|
|
592
|
-
let active = false;
|
|
593
|
-
const update = (phase, label) => {
|
|
594
|
-
if (!enabled) return;
|
|
595
|
-
const completed = Math.max(0, Math.min(phaseCount, phase));
|
|
596
|
-
const filled = Math.round((completed / phaseCount) * width);
|
|
597
|
-
const bar = `${'━'.repeat(filled)}${'─'.repeat(width - filled)}`;
|
|
598
|
-
output.write(`\r\u001B[2K${cyan}${bar}${reset} ${dim}${completed}/${phaseCount}${reset} ${label}`);
|
|
599
|
-
active = completed < phaseCount;
|
|
600
|
-
if (!active) output.write('\n');
|
|
601
|
-
};
|
|
602
|
-
update.finish = () => {
|
|
603
|
-
if (!enabled || !active) return;
|
|
604
|
-
active = false;
|
|
605
|
-
output.write('\n');
|
|
606
|
-
};
|
|
607
|
-
return update;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function promptUpdateAction({ currentVersion, release, input, output, locale = 'en', processRef = process }) {
|
|
611
|
-
return new Promise((resolve, reject) => {
|
|
612
|
-
let selected = 0;
|
|
613
|
-
let settled = false;
|
|
614
|
-
let cursorHidden = false;
|
|
615
|
-
let escapeTimer;
|
|
616
|
-
let pendingInput = '';
|
|
617
|
-
const decoder = new StringDecoder('utf8');
|
|
618
|
-
const wasRaw = input.isRaw === true;
|
|
619
|
-
const cyan = '\u001B[36m';
|
|
620
|
-
const dim = '\u001B[2m';
|
|
621
|
-
const underline = '\u001B[4m';
|
|
622
|
-
const reset = '\u001B[0m';
|
|
623
|
-
const actions = [
|
|
624
|
-
{
|
|
625
|
-
choice: 'install',
|
|
626
|
-
label: updateText(locale, 'install', { command: installCommandText(release.version) }),
|
|
627
|
-
},
|
|
628
|
-
{ choice: 'skip-once', label: updateText(locale, 'skipOnce') },
|
|
629
|
-
{ choice: 'skip-version', label: updateText(locale, 'skipVersion') },
|
|
630
|
-
];
|
|
631
|
-
const signalHandlers = new Map();
|
|
632
|
-
|
|
633
|
-
const renderActions = (refresh) => {
|
|
634
|
-
if (refresh) output.write(`\u001B[${actions.length + 1}F`);
|
|
635
|
-
for (let index = 0; index < actions.length; index += 1) {
|
|
636
|
-
const line = `${selected === index ? '›' : ' '} ${index + 1}. ${actions[index].label}`;
|
|
637
|
-
output.write(`\r\u001B[2K${selected === index ? cyan : ''}${line}${selected === index ? reset : ''}\n`);
|
|
638
|
-
}
|
|
639
|
-
output.write(`\r\u001B[2K${dim}${updateText(locale, 'confirm')}${reset}\n`);
|
|
640
|
-
};
|
|
641
|
-
const cleanup = () => {
|
|
642
|
-
if (escapeTimer !== undefined) clearTimeout(escapeTimer);
|
|
643
|
-
input.removeListener('data', onData);
|
|
644
|
-
input.removeListener('end', skipOnce);
|
|
645
|
-
input.removeListener('close', skipOnce);
|
|
646
|
-
input.removeListener('error', skipOnce);
|
|
647
|
-
for (const [signal, handler] of signalHandlers) {
|
|
648
|
-
processRef.removeListener(signal, handler);
|
|
649
|
-
}
|
|
650
|
-
signalHandlers.clear();
|
|
651
|
-
try {
|
|
652
|
-
if (typeof input.setRawMode === 'function') input.setRawMode(wasRaw);
|
|
653
|
-
} catch {}
|
|
654
|
-
try {
|
|
655
|
-
if (typeof input.pause === 'function') input.pause();
|
|
656
|
-
} catch {}
|
|
657
|
-
if (cursorHidden) {
|
|
658
|
-
try {
|
|
659
|
-
output.write('\u001B[?25h');
|
|
660
|
-
} catch {}
|
|
661
|
-
cursorHidden = false;
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
const finish = (choice) => {
|
|
665
|
-
if (settled) return;
|
|
666
|
-
settled = true;
|
|
667
|
-
cleanup();
|
|
668
|
-
try {
|
|
669
|
-
output.write('\n');
|
|
670
|
-
} catch {}
|
|
671
|
-
resolve(choice);
|
|
672
|
-
};
|
|
673
|
-
const fail = (error) => {
|
|
674
|
-
if (settled) return;
|
|
675
|
-
settled = true;
|
|
676
|
-
cleanup();
|
|
677
|
-
reject(error);
|
|
678
|
-
};
|
|
679
|
-
const handleSignal = (signal) => {
|
|
680
|
-
if (settled) return;
|
|
681
|
-
settled = true;
|
|
682
|
-
cleanup();
|
|
683
|
-
try {
|
|
684
|
-
output.write('\n');
|
|
685
|
-
} catch {}
|
|
686
|
-
const error = new Error(`UPDATE_PROMPT_${signal}`);
|
|
687
|
-
error.code = `UPDATE_PROMPT_${signal}`;
|
|
688
|
-
const exitCode = UPDATE_SIGNAL_EXIT_CODES[signal];
|
|
689
|
-
processRef.exitCode = exitCode;
|
|
690
|
-
if (typeof processRef.exit === 'function') processRef.exit(exitCode);
|
|
691
|
-
reject(error);
|
|
692
|
-
};
|
|
693
|
-
function skipOnce() {
|
|
694
|
-
finish('skip-once');
|
|
695
|
-
}
|
|
696
|
-
const waitForEscapeSuffix = () => {
|
|
697
|
-
if (escapeTimer !== undefined) return;
|
|
698
|
-
escapeTimer = setTimeout(() => {
|
|
699
|
-
escapeTimer = undefined;
|
|
700
|
-
skipOnce();
|
|
701
|
-
}, UPDATE_KEY_ESCAPE_TIMEOUT_MS);
|
|
702
|
-
};
|
|
703
|
-
const clearEscapeTimer = () => {
|
|
704
|
-
if (escapeTimer === undefined) return;
|
|
705
|
-
clearTimeout(escapeTimer);
|
|
706
|
-
escapeTimer = undefined;
|
|
707
|
-
};
|
|
708
|
-
const readEscapeSequence = () => {
|
|
709
|
-
if (pendingInput.length < 2) return undefined;
|
|
710
|
-
if (pendingInput[1] === '[') {
|
|
711
|
-
for (let index = 2; index < pendingInput.length; index += 1) {
|
|
712
|
-
const code = pendingInput.codePointAt(index);
|
|
713
|
-
if (code >= 0x40 && code <= 0x7e) return pendingInput.slice(0, index + 1);
|
|
714
|
-
const isParameter = code >= 0x30 && code <= 0x3f;
|
|
715
|
-
const isIntermediate = code >= 0x20 && code <= 0x2f;
|
|
716
|
-
if (!isParameter && !isIntermediate) return pendingInput.slice(0, index + 1);
|
|
717
|
-
}
|
|
718
|
-
return pendingInput.length > 64 ? pendingInput : undefined;
|
|
719
|
-
}
|
|
720
|
-
if (pendingInput[1] === 'O') {
|
|
721
|
-
return pendingInput.length >= 3 ? pendingInput.slice(0, 3) : undefined;
|
|
722
|
-
}
|
|
723
|
-
return pendingInput.slice(0, 2);
|
|
724
|
-
};
|
|
725
|
-
const processPendingInput = () => {
|
|
726
|
-
while (pendingInput.length > 0) {
|
|
727
|
-
const first = pendingInput[0];
|
|
728
|
-
if (first === '\u0003') {
|
|
729
|
-
skipOnce();
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
if (first === '\r' || first === '\n') {
|
|
733
|
-
pendingInput = pendingInput.slice(1);
|
|
734
|
-
finish(actions[selected].choice);
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
if (first !== '\u001B') {
|
|
738
|
-
const numericSelection = Number(first) - 1;
|
|
739
|
-
pendingInput = pendingInput.slice(1);
|
|
740
|
-
if (Number.isInteger(numericSelection)
|
|
741
|
-
&& numericSelection >= 0
|
|
742
|
-
&& numericSelection < actions.length) {
|
|
743
|
-
selected = numericSelection;
|
|
744
|
-
renderActions(true);
|
|
745
|
-
}
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
if (pendingInput.length > 1
|
|
749
|
-
&& (pendingInput[1] === '\r'
|
|
750
|
-
|| pendingInput[1] === '\n'
|
|
751
|
-
|| pendingInput[1] === '\u0003')) {
|
|
752
|
-
skipOnce();
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
const sequence = readEscapeSequence();
|
|
756
|
-
if (sequence === undefined) {
|
|
757
|
-
waitForEscapeSuffix();
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
clearEscapeTimer();
|
|
761
|
-
if (sequence === '\u001B[A' || sequence === '\u001B[D') {
|
|
762
|
-
selected = (selected - 1 + actions.length) % actions.length;
|
|
763
|
-
pendingInput = pendingInput.slice(sequence.length);
|
|
764
|
-
renderActions(true);
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
if (sequence === '\u001B[B' || sequence === '\u001B[C') {
|
|
768
|
-
selected = (selected + 1) % actions.length;
|
|
769
|
-
pendingInput = pendingInput.slice(sequence.length);
|
|
770
|
-
renderActions(true);
|
|
771
|
-
continue;
|
|
772
|
-
}
|
|
773
|
-
pendingInput = pendingInput.slice(sequence.length);
|
|
774
|
-
}
|
|
775
|
-
};
|
|
776
|
-
const onData = (chunk) => {
|
|
777
|
-
try {
|
|
778
|
-
pendingInput += Buffer.isBuffer(chunk) ? decoder.write(chunk) : String(chunk);
|
|
779
|
-
processPendingInput();
|
|
780
|
-
} catch (error) {
|
|
781
|
-
fail(error);
|
|
782
|
-
}
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
try {
|
|
786
|
-
const supportedSignals = processRef.platform === 'win32'
|
|
787
|
-
? ['SIGINT', 'SIGTERM']
|
|
788
|
-
: Object.keys(UPDATE_SIGNAL_EXIT_CODES);
|
|
789
|
-
for (const signal of supportedSignals) {
|
|
790
|
-
const handler = () => handleSignal(signal);
|
|
791
|
-
signalHandlers.set(signal, handler);
|
|
792
|
-
processRef.once(signal, handler);
|
|
793
|
-
}
|
|
794
|
-
output.write(`\n${updateText(locale, 'available', { currentVersion, targetVersion: release.version })}\n`);
|
|
795
|
-
if (release.notesUrl) {
|
|
796
|
-
output.write(`${dim}${updateText(locale, 'releaseNotes')}${reset} ${underline}${release.notesUrl}${reset}\n`);
|
|
797
|
-
}
|
|
798
|
-
output.write('\n');
|
|
799
|
-
cursorHidden = true;
|
|
800
|
-
output.write('\u001B[?25l');
|
|
801
|
-
renderActions(false);
|
|
802
|
-
input.on('data', onData);
|
|
803
|
-
input.once('end', skipOnce);
|
|
804
|
-
input.once('close', skipOnce);
|
|
805
|
-
input.once('error', skipOnce);
|
|
806
|
-
if (typeof input.setRawMode === 'function') input.setRawMode(true);
|
|
807
|
-
if (typeof input.resume === 'function') input.resume();
|
|
808
|
-
} catch (error) {
|
|
809
|
-
fail(error);
|
|
810
|
-
}
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function resolveTrustedNpmCliPath(options = {}) {
|
|
815
|
-
const execPath = path.resolve(options.execPath || process.execPath);
|
|
816
|
-
const executableDirectory = path.dirname(execPath);
|
|
817
|
-
const packageRoot = path.resolve(__dirname, '..');
|
|
818
|
-
const candidates = options.npmCliPath ? [options.npmCliPath] : [
|
|
819
|
-
path.join(executableDirectory, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
820
|
-
path.resolve(executableDirectory, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
821
|
-
path.resolve(executableDirectory, '..', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
822
|
-
path.resolve(packageRoot, '..', 'npm', 'bin', 'npm-cli.js'),
|
|
823
|
-
];
|
|
824
|
-
for (const candidate of candidates) {
|
|
825
|
-
try {
|
|
826
|
-
if (!path.isAbsolute(candidate)) continue;
|
|
827
|
-
const resolved = fs.realpathSync(candidate);
|
|
828
|
-
const parts = resolved.replaceAll('\\', '/').split('/');
|
|
829
|
-
const npmIndex = parts.length - 3;
|
|
830
|
-
if (parts.at(-1) !== 'npm-cli.js'
|
|
831
|
-
|| parts.at(-2) !== 'bin'
|
|
832
|
-
|| parts[npmIndex]?.toLowerCase() !== 'npm'
|
|
833
|
-
|| !fs.statSync(resolved).isFile()) {
|
|
834
|
-
continue;
|
|
835
|
-
}
|
|
836
|
-
return resolved;
|
|
837
|
-
} catch {}
|
|
838
|
-
}
|
|
839
|
-
throw new Error('TRUSTED_NPM_CLI_NOT_FOUND');
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function createInstallInvocation(version, options = {}) {
|
|
843
|
-
if (!parseSemver(version)) throw new Error('INVALID_UPDATE_VERSION');
|
|
844
|
-
const packageSpec = `${PACKAGE_NAME}@${version}`;
|
|
845
|
-
const execPath = fs.realpathSync(path.resolve(options.execPath || process.execPath));
|
|
846
|
-
if (!fs.statSync(execPath).isFile()) throw new Error('INVALID_NODE_EXECUTABLE');
|
|
847
|
-
const npmCliPath = resolveTrustedNpmCliPath(options);
|
|
848
|
-
const packageRoot = fs.realpathSync(path.resolve(options.packageRoot));
|
|
849
|
-
const modulesRoot = path.dirname(packageRoot);
|
|
850
|
-
if (path.basename(modulesRoot).toLowerCase() !== 'node_modules') {
|
|
851
|
-
throw new Error('INVALID_GLOBAL_PACKAGE_ROOT');
|
|
852
|
-
}
|
|
853
|
-
let installPrefix = path.dirname(modulesRoot);
|
|
854
|
-
const platform = options.platform || process.platform;
|
|
855
|
-
if (platform !== 'win32' && path.basename(installPrefix) === 'lib') {
|
|
856
|
-
installPrefix = path.dirname(installPrefix);
|
|
857
|
-
}
|
|
858
|
-
let npmUserConfigPath;
|
|
859
|
-
let npmGlobalConfigPath;
|
|
860
|
-
if (options.npmUserConfigPath || options.npmGlobalConfigPath) {
|
|
861
|
-
if (!options.npmUserConfigPath || !options.npmGlobalConfigPath) {
|
|
862
|
-
throw new Error('INVALID_NPM_CONFIG_PATHS');
|
|
863
|
-
}
|
|
864
|
-
npmUserConfigPath = fs.realpathSync(path.resolve(options.npmUserConfigPath));
|
|
865
|
-
npmGlobalConfigPath = fs.realpathSync(path.resolve(options.npmGlobalConfigPath));
|
|
866
|
-
if (!fs.statSync(npmUserConfigPath).isFile()
|
|
867
|
-
|| !fs.statSync(npmGlobalConfigPath).isFile()
|
|
868
|
-
|| npmUserConfigPath === npmGlobalConfigPath) {
|
|
869
|
-
throw new Error('INVALID_NPM_CONFIG_PATHS');
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
return {
|
|
873
|
-
command: execPath,
|
|
874
|
-
args: [
|
|
875
|
-
npmCliPath,
|
|
876
|
-
'i',
|
|
877
|
-
'-g',
|
|
878
|
-
packageSpec,
|
|
879
|
-
`--registry=${INSTALL_REGISTRY_URL}`,
|
|
880
|
-
'--strict-ssl=true',
|
|
881
|
-
'--dry-run=false',
|
|
882
|
-
'--ignore-scripts=false',
|
|
883
|
-
'--package-lock-only=false',
|
|
884
|
-
`--prefix=${installPrefix}`,
|
|
885
|
-
...(npmUserConfigPath ? [
|
|
886
|
-
`--userconfig=${npmUserConfigPath}`,
|
|
887
|
-
`--globalconfig=${npmGlobalConfigPath}`,
|
|
888
|
-
] : []),
|
|
889
|
-
],
|
|
890
|
-
windowsVerbatimArguments: false,
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function createInstallEnvironment(
|
|
895
|
-
sourceEnv,
|
|
896
|
-
installDirectory,
|
|
897
|
-
npmUserConfigPath,
|
|
898
|
-
npmGlobalConfigPath,
|
|
899
|
-
) {
|
|
900
|
-
const allowedKeys = new Set([
|
|
901
|
-
'appdata',
|
|
902
|
-
'comspec',
|
|
903
|
-
'home',
|
|
904
|
-
'homedrive',
|
|
905
|
-
'homepath',
|
|
906
|
-
'lang',
|
|
907
|
-
'lc_all',
|
|
908
|
-
'lc_ctype',
|
|
909
|
-
'localappdata',
|
|
910
|
-
'logname',
|
|
911
|
-
'path',
|
|
912
|
-
'pathext',
|
|
913
|
-
'programdata',
|
|
914
|
-
'programfiles',
|
|
915
|
-
'programfiles(x86)',
|
|
916
|
-
'systemroot',
|
|
917
|
-
'term',
|
|
918
|
-
'tz',
|
|
919
|
-
'user',
|
|
920
|
-
'userprofile',
|
|
921
|
-
'windir',
|
|
922
|
-
]);
|
|
923
|
-
const result = {};
|
|
924
|
-
for (const [key, value] of Object.entries(sourceEnv || {})) {
|
|
925
|
-
const normalized = key.toLowerCase();
|
|
926
|
-
if (!allowedKeys.has(normalized) && !key.startsWith('BLUN_TEST_')) continue;
|
|
927
|
-
result[key] = value;
|
|
928
|
-
}
|
|
929
|
-
result.TEMP = installDirectory;
|
|
930
|
-
result.TMP = installDirectory;
|
|
931
|
-
result.TMPDIR = installDirectory;
|
|
932
|
-
result.npm_config_registry = INSTALL_REGISTRY_URL;
|
|
933
|
-
result.npm_config_strict_ssl = 'true';
|
|
934
|
-
result.npm_config_dry_run = 'false';
|
|
935
|
-
result.npm_config_ignore_scripts = 'false';
|
|
936
|
-
result.npm_config_package_lock_only = 'false';
|
|
937
|
-
result.npm_config_userconfig = npmUserConfigPath;
|
|
938
|
-
result.npm_config_globalconfig = npmGlobalConfigPath;
|
|
939
|
-
return result;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
function pathIsWithin(root, candidate) {
|
|
943
|
-
const relative = path.relative(root, candidate);
|
|
944
|
-
return relative === '' || (!relative.startsWith(`..${path.sep}`)
|
|
945
|
-
&& relative !== '..'
|
|
946
|
-
&& !path.isAbsolute(relative));
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function resolveInstallTempRoot(options) {
|
|
950
|
-
if (typeof options.blunDir !== 'string' || options.blunDir.trim().length === 0) {
|
|
951
|
-
throw new Error('INVALID_UPDATE_BLUN_HOME');
|
|
952
|
-
}
|
|
953
|
-
const updateRoot = path.join(path.resolve(options.blunDir), UPDATE_DIRECTORY);
|
|
954
|
-
if (options.tempRoot === undefined) return updateRoot;
|
|
955
|
-
const explicitRoot = path.resolve(options.tempRoot);
|
|
956
|
-
if (!pathIsWithin(updateRoot, explicitRoot)) {
|
|
957
|
-
throw new Error('INVALID_UPDATE_TEMP_ROOT');
|
|
958
|
-
}
|
|
959
|
-
return explicitRoot;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
function createPrivateInstallDirectory(tempRoot, blunDir) {
|
|
963
|
-
const updateRoot = path.join(path.resolve(blunDir), UPDATE_DIRECTORY);
|
|
964
|
-
let current = updateRoot;
|
|
965
|
-
ensurePrivateDirectory(current);
|
|
966
|
-
const relative = path.relative(updateRoot, tempRoot);
|
|
967
|
-
if (relative !== '') {
|
|
968
|
-
for (const segment of relative.split(path.sep)) {
|
|
969
|
-
current = path.join(current, segment);
|
|
970
|
-
ensurePrivateDirectory(current);
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
let directory;
|
|
974
|
-
try {
|
|
975
|
-
directory = fs.mkdtempSync(path.join(tempRoot, 'blun-update-'));
|
|
976
|
-
ensurePrivateDirectory(directory);
|
|
977
|
-
return directory;
|
|
978
|
-
} catch (error) {
|
|
979
|
-
if (directory) {
|
|
980
|
-
try {
|
|
981
|
-
fs.rmSync(directory, { recursive: true, force: true });
|
|
982
|
-
} catch {}
|
|
983
|
-
}
|
|
984
|
-
throw error;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function verifyInstalledPackageVersion(packageRoot, version) {
|
|
989
|
-
try {
|
|
990
|
-
const manifestPath = path.join(packageRoot, 'package.json');
|
|
991
|
-
const stat = fs.lstatSync(manifestPath);
|
|
992
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024) return false;
|
|
993
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
994
|
-
return readDataProperty(manifest, 'name') === PACKAGE_NAME
|
|
995
|
-
&& readDataProperty(manifest, 'version') === version;
|
|
996
|
-
} catch {
|
|
997
|
-
return false;
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
function preparePinnedInstaller(version, options = {}) {
|
|
1002
|
-
const forkImpl = options.forkImpl || fork;
|
|
1003
|
-
const preparation = Promise.withResolvers();
|
|
1004
|
-
const resolvePreparation = preparation.resolve;
|
|
1005
|
-
let child;
|
|
1006
|
-
let invocation;
|
|
1007
|
-
let leaseBusy = false;
|
|
1008
|
-
let leaseUnavailable = false;
|
|
1009
|
-
let sendError;
|
|
1010
|
-
let childError;
|
|
1011
|
-
let preparationSettled = false;
|
|
1012
|
-
let closeSettled = false;
|
|
1013
|
-
let selectedAction;
|
|
1014
|
-
let installDirectory;
|
|
1015
|
-
let npmUserConfigPath;
|
|
1016
|
-
let npmGlobalConfigPath;
|
|
1017
|
-
let disconnectTimer;
|
|
1018
|
-
const close = Promise.withResolvers();
|
|
1019
|
-
const closePromise = close.promise;
|
|
1020
|
-
const resolveClose = close.resolve;
|
|
1021
|
-
const cleanupDirectory = () => {
|
|
1022
|
-
try {
|
|
1023
|
-
if (installDirectory) {
|
|
1024
|
-
(options.removeDirectory || fs.rmSync)(installDirectory, {
|
|
1025
|
-
recursive: true,
|
|
1026
|
-
force: true,
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
} catch {
|
|
1030
|
-
// Cleanup cannot change a completed npm result.
|
|
1031
|
-
}
|
|
1032
|
-
};
|
|
1033
|
-
const settlePreparation = (result) => {
|
|
1034
|
-
if (preparationSettled) return;
|
|
1035
|
-
preparationSettled = true;
|
|
1036
|
-
clearTimeout(prepareTimeout);
|
|
1037
|
-
resolvePreparation(result);
|
|
1038
|
-
};
|
|
1039
|
-
const finishClose = (result) => {
|
|
1040
|
-
if (closeSettled) return;
|
|
1041
|
-
closeSettled = true;
|
|
1042
|
-
clearTimeout(disconnectTimer);
|
|
1043
|
-
cleanupDirectory();
|
|
1044
|
-
resolveClose(result);
|
|
1045
|
-
if (!preparationSettled) {
|
|
1046
|
-
settlePreparation({
|
|
1047
|
-
ready: false,
|
|
1048
|
-
reason: leaseBusy
|
|
1049
|
-
? 'busy'
|
|
1050
|
-
: leaseUnavailable
|
|
1051
|
-
? 'unavailable'
|
|
1052
|
-
: result.code === 75 || result.unknownState === true
|
|
1053
|
-
? 'unknown'
|
|
1054
|
-
: 'failed',
|
|
1055
|
-
result,
|
|
1056
|
-
});
|
|
1057
|
-
}
|
|
1058
|
-
};
|
|
1059
|
-
const finishDisconnected = () => {
|
|
1060
|
-
if (closeSettled) return;
|
|
1061
|
-
closeSettled = true;
|
|
1062
|
-
const result = { code: undefined, error: childError, unknownState: true };
|
|
1063
|
-
resolveClose(result);
|
|
1064
|
-
if (!preparationSettled) {
|
|
1065
|
-
settlePreparation({ ready: false, reason: 'unknown', result });
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
const scheduleUnknownState = (error) => {
|
|
1069
|
-
if (error && childError === undefined) childError = error;
|
|
1070
|
-
if (disconnectTimer !== undefined) return;
|
|
1071
|
-
disconnectTimer = setTimeout(
|
|
1072
|
-
finishDisconnected,
|
|
1073
|
-
options.disconnectTimeoutMs || 1_000,
|
|
1074
|
-
);
|
|
1075
|
-
};
|
|
1076
|
-
const sendControl = (type) => {
|
|
1077
|
-
if (selectedAction) return closePromise;
|
|
1078
|
-
selectedAction = type;
|
|
1079
|
-
try {
|
|
1080
|
-
child.send({ type }, (error) => {
|
|
1081
|
-
if (!error) return;
|
|
1082
|
-
sendError = error;
|
|
1083
|
-
try {
|
|
1084
|
-
child.kill();
|
|
1085
|
-
} catch {}
|
|
1086
|
-
});
|
|
1087
|
-
} catch (error) {
|
|
1088
|
-
sendError = error;
|
|
1089
|
-
try {
|
|
1090
|
-
child.kill();
|
|
1091
|
-
} catch {}
|
|
1092
|
-
}
|
|
1093
|
-
return closePromise;
|
|
1094
|
-
};
|
|
1095
|
-
let prepareTimeout;
|
|
1096
|
-
try {
|
|
1097
|
-
installDirectory = createPrivateInstallDirectory(
|
|
1098
|
-
resolveInstallTempRoot(options),
|
|
1099
|
-
options.blunDir,
|
|
1100
|
-
);
|
|
1101
|
-
npmUserConfigPath = path.join(installDirectory, 'user-npmrc');
|
|
1102
|
-
npmGlobalConfigPath = path.join(installDirectory, 'global-npmrc');
|
|
1103
|
-
writePrivateFile(npmUserConfigPath, '');
|
|
1104
|
-
writePrivateFile(npmGlobalConfigPath, '');
|
|
1105
|
-
invocation = createInstallInvocation(version, {
|
|
1106
|
-
...options,
|
|
1107
|
-
npmUserConfigPath,
|
|
1108
|
-
npmGlobalConfigPath,
|
|
1109
|
-
});
|
|
1110
|
-
child = forkImpl(path.join(__dirname, 'update-lease.js'), ['--installer-worker'], {
|
|
1111
|
-
detached: true,
|
|
1112
|
-
execPath: invocation.command,
|
|
1113
|
-
cwd: installDirectory,
|
|
1114
|
-
env: createInstallEnvironment(
|
|
1115
|
-
options.env || process.env,
|
|
1116
|
-
installDirectory,
|
|
1117
|
-
npmUserConfigPath,
|
|
1118
|
-
npmGlobalConfigPath,
|
|
1119
|
-
),
|
|
1120
|
-
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
1121
|
-
windowsHide: true,
|
|
1122
|
-
});
|
|
1123
|
-
} catch (error) {
|
|
1124
|
-
cleanupDirectory();
|
|
1125
|
-
settlePreparation({ ready: false, reason: 'failed', result: { error } });
|
|
1126
|
-
return preparation.promise;
|
|
1127
|
-
}
|
|
1128
|
-
prepareTimeout = setTimeout(() => {
|
|
1129
|
-
sendError = new Error('UPDATE_INSTALLER_PREPARE_TIMEOUT');
|
|
1130
|
-
settlePreparation({ ready: false, reason: 'unknown', result: { error: sendError } });
|
|
1131
|
-
try {
|
|
1132
|
-
child.kill();
|
|
1133
|
-
} catch {}
|
|
1134
|
-
}, options.prepareTimeoutMs || 5_000);
|
|
1135
|
-
prepareTimeout.unref?.();
|
|
1136
|
-
child.once('error', (error) => {
|
|
1137
|
-
if (!preparationSettled && child.pid === undefined) {
|
|
1138
|
-
finishClose({ code: undefined, error });
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
scheduleUnknownState(error);
|
|
1142
|
-
});
|
|
1143
|
-
child.on('message', (message) => {
|
|
1144
|
-
if (message?.type === 'blun-update-lease-busy') leaseBusy = true;
|
|
1145
|
-
if (message?.type === 'blun-update-lease-unavailable') leaseUnavailable = true;
|
|
1146
|
-
if (message?.type === 'blun-update-lease-adopted') {
|
|
1147
|
-
settlePreparation(Object.freeze({
|
|
1148
|
-
ready: true,
|
|
1149
|
-
commit: () => sendControl('blun-update-lease-commit'),
|
|
1150
|
-
cancel: () => sendControl('blun-update-lease-cancel'),
|
|
1151
|
-
}));
|
|
1152
|
-
}
|
|
1153
|
-
});
|
|
1154
|
-
const finishInstallerProcess = (code, signal) => {
|
|
1155
|
-
if (sendError || childError) {
|
|
1156
|
-
finishClose({
|
|
1157
|
-
code: undefined,
|
|
1158
|
-
error: sendError || childError,
|
|
1159
|
-
unknownState: true,
|
|
1160
|
-
});
|
|
1161
|
-
return;
|
|
1162
|
-
}
|
|
1163
|
-
finishClose({
|
|
1164
|
-
code: Number.isInteger(code) ? code : undefined,
|
|
1165
|
-
signal: typeof signal === 'string' ? signal : undefined,
|
|
1166
|
-
leaseBusy,
|
|
1167
|
-
leaseUnavailable,
|
|
1168
|
-
unknownState: !Number.isInteger(code),
|
|
1169
|
-
verified: !leaseBusy
|
|
1170
|
-
&& !leaseUnavailable
|
|
1171
|
-
&& code === 0
|
|
1172
|
-
&& verifyInstalledPackageVersion(options.packageRoot, version),
|
|
1173
|
-
});
|
|
1174
|
-
};
|
|
1175
|
-
child.once('exit', finishInstallerProcess);
|
|
1176
|
-
child.once('close', finishInstallerProcess);
|
|
1177
|
-
child.once('disconnect', () => scheduleUnknownState());
|
|
1178
|
-
try {
|
|
1179
|
-
child.send({
|
|
1180
|
-
type: 'blun-update-lease-handoff',
|
|
1181
|
-
packageRoot: path.resolve(options.packageRoot),
|
|
1182
|
-
installer: {
|
|
1183
|
-
npmCliPath: invocation.args[0],
|
|
1184
|
-
npmArgs: invocation.args.slice(1),
|
|
1185
|
-
installCwd: installDirectory,
|
|
1186
|
-
},
|
|
1187
|
-
}, (error) => {
|
|
1188
|
-
if (!error) return;
|
|
1189
|
-
sendError = error;
|
|
1190
|
-
try {
|
|
1191
|
-
child.kill();
|
|
1192
|
-
} catch {
|
|
1193
|
-
finishClose({ code: undefined, error, unknownState: true });
|
|
1194
|
-
}
|
|
1195
|
-
});
|
|
1196
|
-
} catch (error) {
|
|
1197
|
-
sendError = error;
|
|
1198
|
-
try {
|
|
1199
|
-
child.kill();
|
|
1200
|
-
} catch {
|
|
1201
|
-
finishClose({ code: undefined, error, unknownState: true });
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
return preparation.promise;
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
async function installPinnedVersion(version, options = {}) {
|
|
1208
|
-
const prepared = await preparePinnedInstaller(version, options);
|
|
1209
|
-
if (!prepared.ready) {
|
|
1210
|
-
return {
|
|
1211
|
-
...prepared.result,
|
|
1212
|
-
leaseBusy: prepared.reason === 'busy',
|
|
1213
|
-
leaseUnavailable: prepared.reason === 'unavailable',
|
|
1214
|
-
unknownState: prepared.reason === 'unknown' || prepared.result?.unknownState === true,
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
|
-
return prepared.commit();
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
async function probeInstallLease(options, packageRoot) {
|
|
1221
|
-
let result;
|
|
1222
|
-
try {
|
|
1223
|
-
result = await (options.acquireInstallLease || tryAcquireUpdateLease)({
|
|
1224
|
-
packageRoot,
|
|
1225
|
-
role: 'install',
|
|
1226
|
-
});
|
|
1227
|
-
} catch {
|
|
1228
|
-
return 'unavailable';
|
|
1229
|
-
}
|
|
1230
|
-
if (!result?.acquired) return result?.reason === 'busy' ? 'busy' : 'unavailable';
|
|
1231
|
-
try {
|
|
1232
|
-
result.lease.assertOwned();
|
|
1233
|
-
} catch {
|
|
1234
|
-
return 'busy';
|
|
1235
|
-
} finally {
|
|
1236
|
-
try {
|
|
1237
|
-
await result.lease.release();
|
|
1238
|
-
} catch {}
|
|
1239
|
-
}
|
|
1240
|
-
return 'clear';
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
function noReleaseReason(currentVersion, registryTags, fallbackManifest) {
|
|
1244
|
-
const latest = readDataProperty(registryTags, 'latest');
|
|
1245
|
-
if (parseSemver(latest) && compareSemver(currentVersion, latest) > 0) {
|
|
1246
|
-
const next = readDataProperty(registryTags, 'next');
|
|
1247
|
-
return parseSemver(next) && compareSemver(next, currentVersion) === 0
|
|
1248
|
-
? 'current'
|
|
1249
|
-
: 'unavailable';
|
|
1250
|
-
}
|
|
1251
|
-
return parseSemver(latest) || fallbackManifest ? 'current' : 'unavailable';
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
function explicitNoTarget(stdout, reason, currentVersion, locale) {
|
|
1255
|
-
stdout.write(`${updateText(locale, reason === 'current' ? 'noUpdate' : 'noTarget', { currentVersion })}\n`);
|
|
1256
|
-
return { kind: 'current', reason };
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
function explicitFailure(stderr, reason, exitCode, locale) {
|
|
1260
|
-
const hasExitCode = Number.isInteger(exitCode) && exitCode !== 0;
|
|
1261
|
-
if (hasExitCode) {
|
|
1262
|
-
stderr.write(`${updateText(locale, 'failedCode', { exitCode })}\n`);
|
|
1263
|
-
} else {
|
|
1264
|
-
stderr.write(`${updateText(locale, 'verifyFailed')}\n`);
|
|
1265
|
-
}
|
|
1266
|
-
return {
|
|
1267
|
-
kind: 'failed',
|
|
1268
|
-
reason,
|
|
1269
|
-
exitCode: hasExitCode ? exitCode : 1,
|
|
1270
|
-
};
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
function queueExplicitUpdate(blunDir, release, stdout, stderr, locale) {
|
|
1274
|
-
try {
|
|
1275
|
-
ensurePrivateDirectory(blunDir);
|
|
1276
|
-
writePendingUpdate(blunDir, release.version);
|
|
1277
|
-
} catch {
|
|
1278
|
-
return explicitFailure(stderr, 'private_home_unavailable', undefined, locale);
|
|
1279
|
-
}
|
|
1280
|
-
stdout.write(
|
|
1281
|
-
`${updateText(locale, 'queued', { targetVersion: release.version })}\n`,
|
|
1282
|
-
);
|
|
1283
|
-
return { kind: 'queued', version: release.version };
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
async function runUpdateFlow(options, explicitUpdate) {
|
|
1287
|
-
const {
|
|
1288
|
-
currentVersion,
|
|
1289
|
-
argv,
|
|
1290
|
-
env,
|
|
1291
|
-
stdin,
|
|
1292
|
-
stdout,
|
|
1293
|
-
stderr,
|
|
1294
|
-
blunDir,
|
|
1295
|
-
} = options;
|
|
1296
|
-
const locale = resolveUpdateLocale(options);
|
|
1297
|
-
if (!explicitUpdate && !isInteractiveUpdateStart({ argv, env, stdin, stdout })) {
|
|
1298
|
-
return { kind: 'continue', reason: 'not_interactive' };
|
|
1299
|
-
}
|
|
1300
|
-
if (!parseSemver(currentVersion)) {
|
|
1301
|
-
return explicitUpdate
|
|
1302
|
-
? explicitFailure(stderr, 'invalid_current_version', undefined, locale)
|
|
1303
|
-
: { kind: 'continue', reason: 'invalid_current_version' };
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
let pendingVersion = readPendingUpdate(blunDir);
|
|
1307
|
-
if (pendingVersion !== undefined && compareSemver(currentVersion, pendingVersion) >= 0) {
|
|
1308
|
-
clearPendingUpdate(blunDir);
|
|
1309
|
-
pendingVersion = undefined;
|
|
1310
|
-
}
|
|
1311
|
-
const now = (options.now || Date.now)();
|
|
1312
|
-
if (!explicitUpdate && pendingVersion === undefined && readSnoozeState(blunDir, now)) {
|
|
1313
|
-
return { kind: 'continue', reason: 'snoozed' };
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
const packageRoot = path.resolve(options.packageRoot || path.resolve(__dirname, '..'));
|
|
1317
|
-
let leaseResult;
|
|
1318
|
-
const ownsNoticeLease = options.noticeLease === undefined;
|
|
1319
|
-
if (ownsNoticeLease) {
|
|
1320
|
-
try {
|
|
1321
|
-
leaseResult = await (options.acquireLease || tryAcquireUpdateLease)({ packageRoot });
|
|
1322
|
-
} catch {
|
|
1323
|
-
return explicitUpdate
|
|
1324
|
-
? explicitFailure(stderr, 'lease_unavailable', undefined, locale)
|
|
1325
|
-
: { kind: 'continue', reason: 'lease_unavailable' };
|
|
1326
|
-
}
|
|
1327
|
-
} else {
|
|
1328
|
-
leaseResult = { acquired: true, lease: options.noticeLease };
|
|
1329
|
-
}
|
|
1330
|
-
if (!leaseResult?.acquired) {
|
|
1331
|
-
if (explicitUpdate) {
|
|
1332
|
-
return explicitFailure(
|
|
1333
|
-
stderr,
|
|
1334
|
-
leaseResult?.reason === 'busy' ? 'update_in_progress' : 'lease_unavailable',
|
|
1335
|
-
undefined,
|
|
1336
|
-
locale,
|
|
1337
|
-
);
|
|
1338
|
-
}
|
|
1339
|
-
return leaseResult?.reason === 'busy'
|
|
1340
|
-
? { kind: 'update_in_progress' }
|
|
1341
|
-
: { kind: 'continue', reason: 'lease_unavailable' };
|
|
1342
|
-
}
|
|
1343
|
-
const { lease } = leaseResult;
|
|
1344
|
-
let installerSession;
|
|
1345
|
-
|
|
1346
|
-
try {
|
|
1347
|
-
if (!explicitUpdate
|
|
1348
|
-
&& pendingVersion === undefined
|
|
1349
|
-
&& readSnoozeState(blunDir, (options.now || Date.now)())) {
|
|
1350
|
-
return { kind: 'continue', reason: 'snoozed' };
|
|
1351
|
-
}
|
|
1352
|
-
let sources;
|
|
1353
|
-
try {
|
|
1354
|
-
sources = await (options.loadReleaseSources || loadReleaseSources)();
|
|
1355
|
-
} catch {
|
|
1356
|
-
return explicitUpdate
|
|
1357
|
-
? explicitNoTarget(stdout, 'unavailable', currentVersion, locale)
|
|
1358
|
-
: { kind: 'continue', reason: 'unavailable' };
|
|
1359
|
-
}
|
|
1360
|
-
const registryTags = parseRegistryDocument(sources?.registryDocument);
|
|
1361
|
-
const fallbackManifest = parseFallbackManifest(sources?.fallbackDocument);
|
|
1362
|
-
const release = resolveEffectiveRelease(currentVersion, registryTags, fallbackManifest);
|
|
1363
|
-
if (!release) {
|
|
1364
|
-
const reason = noReleaseReason(currentVersion, registryTags, fallbackManifest);
|
|
1365
|
-
return explicitUpdate
|
|
1366
|
-
? explicitNoTarget(stdout, reason, currentVersion, locale)
|
|
1367
|
-
: { kind: 'continue', reason };
|
|
1368
|
-
}
|
|
1369
|
-
const queuedForRelease = pendingVersion === release.version;
|
|
1370
|
-
const initialInstallLease = await probeInstallLease(options, packageRoot);
|
|
1371
|
-
if (initialInstallLease === 'busy') {
|
|
1372
|
-
return explicitUpdate
|
|
1373
|
-
? queueExplicitUpdate(blunDir, release, stdout, stderr, locale)
|
|
1374
|
-
: { kind: 'update_in_progress' };
|
|
1375
|
-
}
|
|
1376
|
-
if (initialInstallLease !== 'clear') {
|
|
1377
|
-
return explicitUpdate
|
|
1378
|
-
? explicitFailure(stderr, 'lease_unavailable', undefined, locale)
|
|
1379
|
-
: { kind: 'continue', reason: 'lease_unavailable' };
|
|
1380
|
-
}
|
|
1381
|
-
if (!explicitUpdate && !queuedForRelease && readSkippedVersion(blunDir) === release.version) {
|
|
1382
|
-
return { kind: 'continue', reason: 'skipped_version' };
|
|
1383
|
-
}
|
|
1384
|
-
if (explicitUpdate || queuedForRelease) {
|
|
1385
|
-
try {
|
|
1386
|
-
ensurePrivateDirectory(blunDir);
|
|
1387
|
-
} catch {
|
|
1388
|
-
return explicitFailure(stderr, 'private_home_unavailable', undefined, locale);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
try {
|
|
1392
|
-
lease.assertOwned();
|
|
1393
|
-
} catch {
|
|
1394
|
-
return explicitUpdate
|
|
1395
|
-
? explicitFailure(stderr, 'update_in_progress', undefined, locale)
|
|
1396
|
-
: { kind: 'update_in_progress' };
|
|
1397
|
-
}
|
|
1398
|
-
const prePromptInstallLease = await probeInstallLease(options, packageRoot);
|
|
1399
|
-
if (prePromptInstallLease === 'busy') {
|
|
1400
|
-
return explicitUpdate
|
|
1401
|
-
? queueExplicitUpdate(blunDir, release, stdout, stderr, locale)
|
|
1402
|
-
: { kind: 'update_in_progress' };
|
|
1403
|
-
}
|
|
1404
|
-
if (prePromptInstallLease !== 'clear') {
|
|
1405
|
-
return explicitUpdate
|
|
1406
|
-
? explicitFailure(stderr, 'lease_unavailable', undefined, locale)
|
|
1407
|
-
: { kind: 'continue', reason: 'lease_unavailable' };
|
|
1408
|
-
}
|
|
1409
|
-
if (!options.installVersion) {
|
|
1410
|
-
installerSession = await (options.prepareInstaller || preparePinnedInstaller)(
|
|
1411
|
-
release.version,
|
|
1412
|
-
{ blunDir, packageRoot },
|
|
1413
|
-
);
|
|
1414
|
-
if (!installerSession?.ready) {
|
|
1415
|
-
if (explicitUpdate) {
|
|
1416
|
-
return explicitFailure(stderr, 'installer_unavailable', undefined, locale);
|
|
1417
|
-
}
|
|
1418
|
-
return installerSession?.reason === 'failed'
|
|
1419
|
-
? { kind: 'continue', reason: 'installer_unavailable' }
|
|
1420
|
-
: { kind: 'update_in_progress' };
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
let action;
|
|
1424
|
-
if (explicitUpdate || queuedForRelease) {
|
|
1425
|
-
action = 'install';
|
|
1426
|
-
} else {
|
|
1427
|
-
try {
|
|
1428
|
-
action = await (options.promptAction || promptUpdateAction)({
|
|
1429
|
-
currentVersion,
|
|
1430
|
-
release,
|
|
1431
|
-
input: stdin,
|
|
1432
|
-
output: stdout,
|
|
1433
|
-
locale,
|
|
1434
|
-
});
|
|
1435
|
-
} catch {
|
|
1436
|
-
return { kind: 'continue', reason: 'prompt_failed' };
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
if (action !== 'install') {
|
|
1440
|
-
if (installerSession?.ready) await installerSession.cancel();
|
|
1441
|
-
if (action === 'skip-once') {
|
|
1442
|
-
return { kind: 'continue', reason: 'skipped_once' };
|
|
1443
|
-
}
|
|
1444
|
-
if (action === 'skip-version') {
|
|
1445
|
-
try {
|
|
1446
|
-
writeSkippedVersion(blunDir, release.version);
|
|
1447
|
-
} catch {
|
|
1448
|
-
// A failed preference write must not block the existing console.
|
|
1449
|
-
}
|
|
1450
|
-
return { kind: 'continue', reason: 'skipped_version' };
|
|
1451
|
-
}
|
|
1452
|
-
try {
|
|
1453
|
-
writeSnoozeState(blunDir, (options.now || Date.now)());
|
|
1454
|
-
} catch {
|
|
1455
|
-
// A failed preference write must not block the existing console.
|
|
1456
|
-
}
|
|
1457
|
-
return { kind: 'continue', reason: 'postponed' };
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
const updateProgress = createUpdateProgress(stdout);
|
|
1461
|
-
updateProgress(1, updateText(locale, 'progressChecked'));
|
|
1462
|
-
updateProgress(2, updateText(locale, 'installerReady'));
|
|
1463
|
-
|
|
1464
|
-
try {
|
|
1465
|
-
lease.assertOwned();
|
|
1466
|
-
} catch {
|
|
1467
|
-
return explicitUpdate
|
|
1468
|
-
? explicitFailure(stderr, 'update_in_progress', undefined, locale)
|
|
1469
|
-
: { kind: 'update_in_progress' };
|
|
1470
|
-
}
|
|
1471
|
-
let installResult;
|
|
1472
|
-
updateProgress(3, updateText(locale, 'installing'));
|
|
1473
|
-
try {
|
|
1474
|
-
installResult = options.installVersion
|
|
1475
|
-
? await options.installVersion(release.version)
|
|
1476
|
-
: await installerSession.commit();
|
|
1477
|
-
} catch {
|
|
1478
|
-
installResult = { code: undefined };
|
|
1479
|
-
}
|
|
1480
|
-
if (installResult?.leaseBusy === true
|
|
1481
|
-
|| installResult?.leaseUnavailable === true
|
|
1482
|
-
|| installResult?.unknownState === true) {
|
|
1483
|
-
updateProgress.finish();
|
|
1484
|
-
return explicitUpdate
|
|
1485
|
-
? explicitFailure(stderr, 'update_in_progress', undefined, locale)
|
|
1486
|
-
: { kind: 'update_in_progress' };
|
|
1487
|
-
}
|
|
1488
|
-
if (installResult?.code === 0 && installResult?.verified === true) {
|
|
1489
|
-
updateProgress(4, updateText(locale, 'installVerified'));
|
|
1490
|
-
clearPendingUpdate(blunDir);
|
|
1491
|
-
try {
|
|
1492
|
-
writeCoreInstallSuccess(
|
|
1493
|
-
blunDir,
|
|
1494
|
-
currentVersion,
|
|
1495
|
-
release.version,
|
|
1496
|
-
(options.now || Date.now)(),
|
|
1497
|
-
);
|
|
1498
|
-
} catch {
|
|
1499
|
-
stderr.write(
|
|
1500
|
-
`${updateText(locale, 'stateWarning', { targetVersion: release.version })}\n`,
|
|
1501
|
-
);
|
|
1502
|
-
}
|
|
1503
|
-
stdout.write(
|
|
1504
|
-
`${updateText(locale, 'success', { currentVersion, targetVersion: release.version })}\n`,
|
|
1505
|
-
);
|
|
1506
|
-
if (release.notesUrl) stdout.write(`${updateText(locale, 'releaseNotes')} ${release.notesUrl}\n`);
|
|
1507
|
-
return { kind: 'installed', version: release.version };
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
if (Number.isInteger(installResult?.code) && installResult.code !== 0) {
|
|
1511
|
-
updateProgress.finish();
|
|
1512
|
-
if (explicitUpdate) {
|
|
1513
|
-
return explicitFailure(stderr, 'install_failed', installResult.code, locale);
|
|
1514
|
-
}
|
|
1515
|
-
stderr.write(`${updateText(locale, 'failedContinue', { exitCode: installResult.code })}\n`);
|
|
1516
|
-
return { kind: 'continue', reason: 'install_failed', exitCode: installResult.code };
|
|
1517
|
-
}
|
|
1518
|
-
updateProgress.finish();
|
|
1519
|
-
if (explicitUpdate) return explicitFailure(stderr, 'verification_failed', undefined, locale);
|
|
1520
|
-
stderr.write(`${updateText(locale, 'verifyRestart')}\n`);
|
|
1521
|
-
return { kind: 'update_in_progress' };
|
|
1522
|
-
} finally {
|
|
1523
|
-
if (installerSession?.ready) {
|
|
1524
|
-
try {
|
|
1525
|
-
await installerSession.cancel();
|
|
1526
|
-
} catch {}
|
|
1527
|
-
}
|
|
1528
|
-
if (ownsNoticeLease) {
|
|
1529
|
-
try {
|
|
1530
|
-
await lease.release();
|
|
1531
|
-
} catch {}
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
function runExplicitUpdate(options) {
|
|
1537
|
-
return runUpdateFlow(options, true);
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
function runUpdateNotice(options) {
|
|
1541
|
-
return runUpdateFlow(options, false);
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
module.exports = {
|
|
1545
|
-
FALLBACK_MANIFEST_URL,
|
|
1546
|
-
PACKAGE_NAME,
|
|
1547
|
-
REGISTRY_URL,
|
|
1548
|
-
SNOOZE_MS,
|
|
1549
|
-
compareSemver,
|
|
1550
|
-
createInstallInvocation,
|
|
1551
|
-
installPinnedVersion,
|
|
1552
|
-
preparePinnedInstaller,
|
|
1553
|
-
isInteractiveUpdateStart,
|
|
1554
|
-
loadReleaseSources,
|
|
1555
|
-
parseFallbackManifest,
|
|
1556
|
-
parseRegistryDocument,
|
|
1557
|
-
promptUpdateAction,
|
|
1558
|
-
readSkippedVersion,
|
|
1559
|
-
readSnoozeState,
|
|
1560
|
-
requestTrustedJson,
|
|
1561
|
-
resolveEffectiveRelease,
|
|
1562
|
-
resolveUpdateLocale,
|
|
1563
|
-
runExplicitUpdate,
|
|
1564
|
-
runUpdateNotice,
|
|
1565
|
-
updateText,
|
|
1566
|
-
writeSkippedVersion,
|
|
1567
|
-
writeSnoozeState,
|
|
1568
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const https = require('node:https');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { fork } = require('node:child_process');
|
|
8
|
+
const { StringDecoder } = require('node:string_decoder');
|
|
9
|
+
const { types } = require('node:util');
|
|
10
|
+
|
|
11
|
+
const { tryAcquireUpdateLease } = require('./update-lease');
|
|
12
|
+
const { resolveUpdateLocale, updateText } = require('./update-copy');
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
ensurePrivateDirectory,
|
|
16
|
+
securePrivateFile,
|
|
17
|
+
writePrivateFile,
|
|
18
|
+
} = require('./private-paths');
|
|
19
|
+
|
|
20
|
+
const PACKAGE_NAME = 'blun-king-cli';
|
|
21
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/blun-king-cli';
|
|
22
|
+
const INSTALL_REGISTRY_URL = 'https://registry.npmjs.org/';
|
|
23
|
+
const FALLBACK_MANIFEST_URL = 'https://chat.blun.ai/blun-code-version.json';
|
|
24
|
+
const SNOOZE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
25
|
+
const REQUEST_TIMEOUT_MS = 2_500;
|
|
26
|
+
const STATE_FILE = 'update-notice.json';
|
|
27
|
+
const UPDATE_SKIP_FILE = 'update-skip-version.json';
|
|
28
|
+
const PENDING_UPDATE_FILE = 'update-pending.json';
|
|
29
|
+
const UPDATE_DIRECTORY = 'updates';
|
|
30
|
+
const CORE_INSTALL_STATE_FILE = 'install.json';
|
|
31
|
+
const UPDATE_KEY_ESCAPE_TIMEOUT_MS = 500;
|
|
32
|
+
const UPDATE_SIGNAL_EXIT_CODES = Object.freeze({
|
|
33
|
+
SIGHUP: 129,
|
|
34
|
+
SIGINT: 130,
|
|
35
|
+
SIGTERM: 143,
|
|
36
|
+
});
|
|
37
|
+
const INTERACTIVE_START_MODE_FLAGS = new Set([
|
|
38
|
+
'-y',
|
|
39
|
+
'--auto',
|
|
40
|
+
'--auto-approve',
|
|
41
|
+
'--plan',
|
|
42
|
+
'--yolo',
|
|
43
|
+
]);
|
|
44
|
+
const SEMVER_IDENTIFIER = '(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)';
|
|
45
|
+
const SEMVER_PATTERN = new RegExp(
|
|
46
|
+
`^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)`
|
|
47
|
+
+ `(?:-(${SEMVER_IDENTIFIER}(?:\\.${SEMVER_IDENTIFIER})*))?`
|
|
48
|
+
+ '(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$',
|
|
49
|
+
);
|
|
50
|
+
const LEGACY_MANIFEST_KEYS = Object.freeze([
|
|
51
|
+
'install',
|
|
52
|
+
'latest',
|
|
53
|
+
'minSupported',
|
|
54
|
+
'name',
|
|
55
|
+
'notesUrl',
|
|
56
|
+
'releasedAt',
|
|
57
|
+
]);
|
|
58
|
+
const RELEASE_NOTES_MANIFEST_KEYS = Object.freeze([
|
|
59
|
+
'install', 'latest', 'minSupported', 'name', 'notesUrl', 'releaseNotes', 'releasedAt',
|
|
60
|
+
]);
|
|
61
|
+
const MANIFEST_KEYS = Object.freeze([
|
|
62
|
+
'install', 'integrity', 'latest', 'minSupported', 'name', 'notesUrl', 'releaseNotes', 'releasedAt',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function parseReleaseNotes(value) {
|
|
66
|
+
if (!Array.isArray(value) || value.length > 8) return undefined;
|
|
67
|
+
const notes = [];
|
|
68
|
+
for (const note of value) {
|
|
69
|
+
if (!isBoundedText(note, 240)) return undefined;
|
|
70
|
+
notes.push(note);
|
|
71
|
+
}
|
|
72
|
+
return Object.freeze(notes);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validIntegrity(value) {
|
|
76
|
+
if (typeof value !== 'string') return false;
|
|
77
|
+
const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/u.exec(value);
|
|
78
|
+
if (!match) return false;
|
|
79
|
+
return Buffer.from(match[1], 'base64').length === 64;
|
|
80
|
+
}
|
|
81
|
+
const ENDPOINT_LIMITS = new Map([
|
|
82
|
+
[REGISTRY_URL, 1024 * 1024],
|
|
83
|
+
[FALLBACK_MANIFEST_URL, 64 * 1024],
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
function parseSemver(value) {
|
|
87
|
+
if (typeof value !== 'string' || value.length > 128) return undefined;
|
|
88
|
+
const match = SEMVER_PATTERN.exec(value);
|
|
89
|
+
if (!match) return undefined;
|
|
90
|
+
return {
|
|
91
|
+
raw: value,
|
|
92
|
+
core: [match[1], match[2], match[3]],
|
|
93
|
+
prerelease: match[4] === undefined ? undefined : match[4].split('.'),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function compareNumericIdentifier(left, right) {
|
|
98
|
+
if (left.length !== right.length) return left.length < right.length ? -1 : 1;
|
|
99
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function comparePrerelease(left, right) {
|
|
103
|
+
if (left === undefined && right === undefined) return 0;
|
|
104
|
+
if (left === undefined) return 1;
|
|
105
|
+
if (right === undefined) return -1;
|
|
106
|
+
const length = Math.max(left.length, right.length);
|
|
107
|
+
for (let index = 0; index < length; index += 1) {
|
|
108
|
+
if (left[index] === undefined) return -1;
|
|
109
|
+
if (right[index] === undefined) return 1;
|
|
110
|
+
const leftNumeric = /^\d+$/.test(left[index]);
|
|
111
|
+
const rightNumeric = /^\d+$/.test(right[index]);
|
|
112
|
+
if (leftNumeric && rightNumeric) {
|
|
113
|
+
const comparison = compareNumericIdentifier(left[index], right[index]);
|
|
114
|
+
if (comparison !== 0) return comparison;
|
|
115
|
+
} else if (leftNumeric !== rightNumeric) {
|
|
116
|
+
return leftNumeric ? -1 : 1;
|
|
117
|
+
} else if (left[index] !== right[index]) {
|
|
118
|
+
return left[index] < right[index] ? -1 : 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function compareSemver(leftValue, rightValue) {
|
|
125
|
+
const left = parseSemver(leftValue);
|
|
126
|
+
const right = parseSemver(rightValue);
|
|
127
|
+
if (!left || !right) return undefined;
|
|
128
|
+
for (let index = 0; index < left.core.length; index += 1) {
|
|
129
|
+
const comparison = compareNumericIdentifier(left.core[index], right.core[index]);
|
|
130
|
+
if (comparison !== 0) return comparison;
|
|
131
|
+
}
|
|
132
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function exactDataRecord(value, expectedKeys) {
|
|
136
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value) || types.isProxy(value)) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const prototype = Object.getPrototypeOf(value);
|
|
140
|
+
if (prototype !== Object.prototype && prototype !== null) return undefined;
|
|
141
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
142
|
+
const keys = Object.keys(descriptors).toSorted();
|
|
143
|
+
if (keys.length !== expectedKeys.length
|
|
144
|
+
|| keys.some((key, index) => key !== expectedKeys[index])) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
for (const key of keys) {
|
|
148
|
+
if (!Object.hasOwn(descriptors[key], 'value') || descriptors[key].enumerable !== true) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return Object.fromEntries(keys.map((key) => [key, descriptors[key].value]));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readDataProperty(value, key) {
|
|
156
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value) || types.isProxy(value)) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
const prototype = Object.getPrototypeOf(value);
|
|
160
|
+
if (prototype !== Object.prototype && prototype !== null) return undefined;
|
|
161
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
162
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
return descriptor.value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseRegistryDocument(value) {
|
|
169
|
+
const tags = readDataProperty(value, 'dist-tags');
|
|
170
|
+
const latest = readDataProperty(tags, 'latest');
|
|
171
|
+
if (!parseSemver(latest)) return undefined;
|
|
172
|
+
const parsed = { latest };
|
|
173
|
+
const next = readDataProperty(tags, 'next');
|
|
174
|
+
if (parseSemver(next)) parsed.next = next;
|
|
175
|
+
return Object.freeze(parsed);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isBoundedText(value, maxBytes) {
|
|
179
|
+
return typeof value === 'string'
|
|
180
|
+
&& value.length > 0
|
|
181
|
+
&& Buffer.byteLength(value, 'utf8') <= maxBytes
|
|
182
|
+
&& !/[\u0000-\u001F\u007F-\u009F\u061C\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/u.test(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function canonicalOfficialNotesUrl(value) {
|
|
186
|
+
if (!isBoundedText(value, 2_048)) return undefined;
|
|
187
|
+
try {
|
|
188
|
+
const url = new URL(value);
|
|
189
|
+
const officialHost = url.hostname === 'blun.ai' || url.hostname.endsWith('.blun.ai');
|
|
190
|
+
if (url.protocol !== 'https:'
|
|
191
|
+
|| !officialHost
|
|
192
|
+
|| url.username !== ''
|
|
193
|
+
|| url.password !== ''
|
|
194
|
+
|| url.port !== '') {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
return url.href;
|
|
198
|
+
} catch {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isIsoTimestamp(value) {
|
|
204
|
+
if (typeof value !== 'string') return false;
|
|
205
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?Z$/.exec(value);
|
|
206
|
+
if (!match) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const timestamp = Date.parse(value);
|
|
210
|
+
if (!Number.isFinite(timestamp)) return false;
|
|
211
|
+
const date = new Date(timestamp);
|
|
212
|
+
return date.getUTCFullYear() === Number(match[1])
|
|
213
|
+
&& date.getUTCMonth() + 1 === Number(match[2])
|
|
214
|
+
&& date.getUTCDate() === Number(match[3])
|
|
215
|
+
&& date.getUTCHours() === Number(match[4])
|
|
216
|
+
&& date.getUTCMinutes() === Number(match[5])
|
|
217
|
+
&& date.getUTCSeconds() === Number(match[6]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parseFallbackManifest(value) {
|
|
221
|
+
const record = exactDataRecord(value, MANIFEST_KEYS)
|
|
222
|
+
|| exactDataRecord(value, RELEASE_NOTES_MANIFEST_KEYS)
|
|
223
|
+
|| exactDataRecord(value, LEGACY_MANIFEST_KEYS);
|
|
224
|
+
const notesUrl = record ? canonicalOfficialNotesUrl(record.notesUrl) : undefined;
|
|
225
|
+
const releaseNotes = record && Object.hasOwn(record, 'releaseNotes')
|
|
226
|
+
? parseReleaseNotes(record.releaseNotes)
|
|
227
|
+
: undefined;
|
|
228
|
+
if (!record
|
|
229
|
+
|| record.name !== PACKAGE_NAME
|
|
230
|
+
|| !parseSemver(record.latest)
|
|
231
|
+
|| !parseSemver(record.minSupported)
|
|
232
|
+
|| compareSemver(record.minSupported, record.latest) === 1
|
|
233
|
+
|| !isIsoTimestamp(record.releasedAt)
|
|
234
|
+
|| !notesUrl
|
|
235
|
+
|| (Object.hasOwn(record, 'integrity') && !validIntegrity(record.integrity))
|
|
236
|
+
|| (Object.hasOwn(record, 'releaseNotes') && releaseNotes === undefined)
|
|
237
|
+
|| !isBoundedText(record.install, 256)) {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
return Object.freeze({
|
|
241
|
+
name: record.name,
|
|
242
|
+
latest: record.latest,
|
|
243
|
+
minSupported: record.minSupported,
|
|
244
|
+
releasedAt: record.releasedAt,
|
|
245
|
+
notesUrl,
|
|
246
|
+
...(Object.hasOwn(record, 'integrity') ? { integrity: record.integrity } : {}),
|
|
247
|
+
...(releaseNotes === undefined ? {} : { releaseNotes }),
|
|
248
|
+
install: record.install,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function resolveEffectiveRelease(currentVersion, registryTags, fallbackManifest) {
|
|
253
|
+
if (!parseSemver(currentVersion)) return undefined;
|
|
254
|
+
const registryLatest = readDataProperty(registryTags, 'latest');
|
|
255
|
+
const validRegistryLatest = parseSemver(registryLatest) ? registryLatest : undefined;
|
|
256
|
+
const registryNext = readDataProperty(registryTags, 'next');
|
|
257
|
+
const validRegistryNext = parseSemver(registryNext) ? registryNext : undefined;
|
|
258
|
+
const validFallback = parseFallbackManifest(fallbackManifest);
|
|
259
|
+
if (!validRegistryLatest && !validFallback) return undefined;
|
|
260
|
+
|
|
261
|
+
if (validRegistryLatest && compareSemver(currentVersion, validRegistryLatest) > 0) {
|
|
262
|
+
if (!validRegistryNext || compareSemver(validRegistryNext, currentVersion) <= 0) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
return { channel: 'next', version: validRegistryNext };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let version;
|
|
269
|
+
if (validRegistryLatest && validFallback) {
|
|
270
|
+
version = compareSemver(validRegistryLatest, validFallback.latest) <= 0
|
|
271
|
+
? validRegistryLatest
|
|
272
|
+
: validFallback.latest;
|
|
273
|
+
} else {
|
|
274
|
+
version = validRegistryLatest || validFallback.latest;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (compareSemver(version, currentVersion) <= 0) return undefined;
|
|
278
|
+
|
|
279
|
+
if (validFallback && compareSemver(validFallback.latest, version) === 0) {
|
|
280
|
+
return { channel: 'stable', version, notesUrl: validFallback.notesUrl,
|
|
281
|
+
...(validFallback.releaseNotes === undefined ? {} : { releaseNotes: validFallback.releaseNotes }),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return { channel: 'stable', version };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function requestTrustedJson(url, options = {}) {
|
|
288
|
+
const maxBytes = ENDPOINT_LIMITS.get(url);
|
|
289
|
+
if (maxBytes === undefined) {
|
|
290
|
+
return Promise.reject(new Error('UNTRUSTED_UPDATE_ENDPOINT'));
|
|
291
|
+
}
|
|
292
|
+
const httpsImpl = options.httpsImpl || https;
|
|
293
|
+
const timeoutMs = options.timeoutMs || REQUEST_TIMEOUT_MS;
|
|
294
|
+
|
|
295
|
+
return new Promise((resolve, reject) => {
|
|
296
|
+
let request;
|
|
297
|
+
let response;
|
|
298
|
+
let settled = false;
|
|
299
|
+
const finish = (error, value) => {
|
|
300
|
+
if (settled) return;
|
|
301
|
+
settled = true;
|
|
302
|
+
clearTimeout(timer);
|
|
303
|
+
if (error) reject(error);
|
|
304
|
+
else resolve(value);
|
|
305
|
+
};
|
|
306
|
+
const timer = setTimeout(() => {
|
|
307
|
+
const error = new Error('UPDATE_REQUEST_TIMEOUT');
|
|
308
|
+
finish(error);
|
|
309
|
+
if (response && typeof response.destroy === 'function') response.destroy();
|
|
310
|
+
if (request && typeof request.destroy === 'function') request.destroy();
|
|
311
|
+
}, timeoutMs);
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
request = httpsImpl.get(url, {
|
|
315
|
+
headers: { accept: 'application/vnd.npm.install-v1+json, application/json' },
|
|
316
|
+
}, (incoming) => {
|
|
317
|
+
response = incoming;
|
|
318
|
+
if (incoming.statusCode !== 200) {
|
|
319
|
+
finish(new Error(`UPDATE_HTTP_STATUS_${incoming.statusCode || 0}`));
|
|
320
|
+
if (typeof incoming.destroy === 'function') incoming.destroy();
|
|
321
|
+
if (request && typeof request.destroy === 'function') request.destroy();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const rawLength = incoming.headers && incoming.headers['content-length'];
|
|
326
|
+
const declaredLength = typeof rawLength === 'string' && /^\d+$/.test(rawLength)
|
|
327
|
+
? Number(rawLength)
|
|
328
|
+
: undefined;
|
|
329
|
+
if (declaredLength !== undefined && declaredLength > maxBytes) {
|
|
330
|
+
finish(new Error('UPDATE_BODY_TOO_LARGE'));
|
|
331
|
+
if (typeof incoming.destroy === 'function') incoming.destroy();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const chunks = [];
|
|
336
|
+
let total = 0;
|
|
337
|
+
incoming.on('data', (chunk) => {
|
|
338
|
+
if (settled) return;
|
|
339
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
340
|
+
total += buffer.length;
|
|
341
|
+
if (total > maxBytes) {
|
|
342
|
+
finish(new Error('UPDATE_BODY_TOO_LARGE'));
|
|
343
|
+
if (typeof incoming.destroy === 'function') incoming.destroy();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
chunks.push(buffer);
|
|
347
|
+
});
|
|
348
|
+
incoming.on('error', (error) => finish(error));
|
|
349
|
+
incoming.on('end', () => {
|
|
350
|
+
if (settled) return;
|
|
351
|
+
try {
|
|
352
|
+
finish(undefined, JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
353
|
+
} catch {
|
|
354
|
+
finish(new Error('UPDATE_INVALID_JSON'));
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
request.on('error', (error) => finish(error));
|
|
359
|
+
} catch (error) {
|
|
360
|
+
finish(error);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function loadReleaseSources() {
|
|
366
|
+
const [registry, fallback] = await Promise.allSettled([
|
|
367
|
+
requestTrustedJson(REGISTRY_URL),
|
|
368
|
+
requestTrustedJson(FALLBACK_MANIFEST_URL),
|
|
369
|
+
]);
|
|
370
|
+
return {
|
|
371
|
+
registryDocument: registry.status === 'fulfilled' ? registry.value : undefined,
|
|
372
|
+
fallbackDocument: fallback.status === 'fulfilled' ? fallback.value : undefined,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function statePath(blunDir) {
|
|
377
|
+
return path.join(blunDir, STATE_FILE);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function readSnoozeState(blunDir, now = Date.now()) {
|
|
381
|
+
try {
|
|
382
|
+
const filePath = statePath(blunDir);
|
|
383
|
+
if (!fs.existsSync(filePath)) return false;
|
|
384
|
+
securePrivateFile(filePath);
|
|
385
|
+
const stat = fs.lstatSync(filePath);
|
|
386
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_024) return false;
|
|
387
|
+
const record = exactDataRecord(JSON.parse(fs.readFileSync(filePath, 'utf8')), [
|
|
388
|
+
'snoozedUntil',
|
|
389
|
+
'version',
|
|
390
|
+
]);
|
|
391
|
+
if (!record
|
|
392
|
+
|| record.version !== 1
|
|
393
|
+
|| !Number.isSafeInteger(record.snoozedUntil)
|
|
394
|
+
|| record.snoozedUntil <= now
|
|
395
|
+
|| record.snoozedUntil > now + SNOOZE_MS) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
return true;
|
|
399
|
+
} catch {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function writeSnoozeState(blunDir, now = Date.now()) {
|
|
405
|
+
ensurePrivateDirectory(blunDir);
|
|
406
|
+
const filePath = statePath(blunDir);
|
|
407
|
+
const temporaryPath = path.join(
|
|
408
|
+
blunDir,
|
|
409
|
+
`.${STATE_FILE}.${process.pid}.${randomUUID()}.tmp`,
|
|
410
|
+
);
|
|
411
|
+
try {
|
|
412
|
+
writePrivateFile(temporaryPath, `${JSON.stringify({
|
|
413
|
+
version: 1,
|
|
414
|
+
snoozedUntil: now + SNOOZE_MS,
|
|
415
|
+
})}\n`);
|
|
416
|
+
const handle = fs.openSync(temporaryPath, 'r+');
|
|
417
|
+
try {
|
|
418
|
+
fs.fsyncSync(handle);
|
|
419
|
+
} finally {
|
|
420
|
+
fs.closeSync(handle);
|
|
421
|
+
}
|
|
422
|
+
fs.renameSync(temporaryPath, filePath);
|
|
423
|
+
securePrivateFile(filePath);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function skippedVersionPath(blunDir) {
|
|
431
|
+
return path.join(blunDir, UPDATE_SKIP_FILE);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function readSkippedVersion(blunDir) {
|
|
435
|
+
try {
|
|
436
|
+
const filePath = skippedVersionPath(blunDir);
|
|
437
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
438
|
+
securePrivateFile(filePath);
|
|
439
|
+
const stat = fs.lstatSync(filePath);
|
|
440
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_024) return undefined;
|
|
441
|
+
const record = exactDataRecord(JSON.parse(fs.readFileSync(filePath, 'utf8')), [
|
|
442
|
+
'skippedVersion',
|
|
443
|
+
'version',
|
|
444
|
+
]);
|
|
445
|
+
if (!record || record.version !== 1 || !parseSemver(record.skippedVersion)) {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
return record.skippedVersion;
|
|
449
|
+
} catch {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function writeSkippedVersion(blunDir, skippedVersion) {
|
|
455
|
+
if (!parseSemver(skippedVersion)) throw new Error('INVALID_UPDATE_VERSION');
|
|
456
|
+
ensurePrivateDirectory(blunDir);
|
|
457
|
+
const filePath = skippedVersionPath(blunDir);
|
|
458
|
+
const temporaryPath = path.join(
|
|
459
|
+
blunDir,
|
|
460
|
+
`.${UPDATE_SKIP_FILE}.${process.pid}.${randomUUID()}.tmp`,
|
|
461
|
+
);
|
|
462
|
+
try {
|
|
463
|
+
writePrivateFile(temporaryPath, `${JSON.stringify({
|
|
464
|
+
version: 1,
|
|
465
|
+
skippedVersion,
|
|
466
|
+
})}\n`);
|
|
467
|
+
const handle = fs.openSync(temporaryPath, 'r+');
|
|
468
|
+
try {
|
|
469
|
+
fs.fsyncSync(handle);
|
|
470
|
+
} finally {
|
|
471
|
+
fs.closeSync(handle);
|
|
472
|
+
}
|
|
473
|
+
fs.renameSync(temporaryPath, filePath);
|
|
474
|
+
securePrivateFile(filePath);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function pendingUpdatePath(blunDir) {
|
|
482
|
+
return path.join(blunDir, PENDING_UPDATE_FILE);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function readPendingUpdate(blunDir) {
|
|
486
|
+
try {
|
|
487
|
+
const filePath = pendingUpdatePath(blunDir);
|
|
488
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
489
|
+
securePrivateFile(filePath);
|
|
490
|
+
const stat = fs.lstatSync(filePath);
|
|
491
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_024) return undefined;
|
|
492
|
+
const record = exactDataRecord(JSON.parse(fs.readFileSync(filePath, 'utf8')), [
|
|
493
|
+
'targetVersion',
|
|
494
|
+
'version',
|
|
495
|
+
]);
|
|
496
|
+
if (!record || record.version !== 1 || !parseSemver(record.targetVersion)) return undefined;
|
|
497
|
+
return record.targetVersion;
|
|
498
|
+
} catch {
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function writePendingUpdate(blunDir, targetVersion) {
|
|
504
|
+
if (!parseSemver(targetVersion)) throw new Error('INVALID_UPDATE_VERSION');
|
|
505
|
+
ensurePrivateDirectory(blunDir);
|
|
506
|
+
const filePath = pendingUpdatePath(blunDir);
|
|
507
|
+
const temporaryPath = path.join(
|
|
508
|
+
blunDir,
|
|
509
|
+
`.${PENDING_UPDATE_FILE}.${process.pid}.${randomUUID()}.tmp`,
|
|
510
|
+
);
|
|
511
|
+
try {
|
|
512
|
+
writePrivateFile(temporaryPath, `${JSON.stringify({
|
|
513
|
+
version: 1,
|
|
514
|
+
targetVersion,
|
|
515
|
+
})}\n`);
|
|
516
|
+
const handle = fs.openSync(temporaryPath, 'r+');
|
|
517
|
+
try {
|
|
518
|
+
fs.fsyncSync(handle);
|
|
519
|
+
} finally {
|
|
520
|
+
fs.closeSync(handle);
|
|
521
|
+
}
|
|
522
|
+
fs.renameSync(temporaryPath, filePath);
|
|
523
|
+
securePrivateFile(filePath);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function clearPendingUpdate(blunDir) {
|
|
531
|
+
fs.rmSync(pendingUpdatePath(blunDir), { force: true });
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function writeCoreInstallSuccess(blunDir, fromVersion, version, now = Date.now()) {
|
|
535
|
+
if (!parseSemver(fromVersion) || !parseSemver(version)) {
|
|
536
|
+
throw new Error('INVALID_UPDATE_VERSION');
|
|
537
|
+
}
|
|
538
|
+
const updateDirectory = path.join(blunDir, UPDATE_DIRECTORY);
|
|
539
|
+
ensurePrivateDirectory(blunDir);
|
|
540
|
+
ensurePrivateDirectory(updateDirectory);
|
|
541
|
+
const filePath = path.join(updateDirectory, CORE_INSTALL_STATE_FILE);
|
|
542
|
+
const temporaryPath = path.join(
|
|
543
|
+
updateDirectory,
|
|
544
|
+
`.${CORE_INSTALL_STATE_FILE}.${process.pid}.${randomUUID()}.tmp`,
|
|
545
|
+
);
|
|
546
|
+
try {
|
|
547
|
+
writePrivateFile(temporaryPath, `${JSON.stringify({
|
|
548
|
+
active: null,
|
|
549
|
+
lastFailure: null,
|
|
550
|
+
lastSuccess: {
|
|
551
|
+
version,
|
|
552
|
+
fromVersion,
|
|
553
|
+
installedAt: new Date(now).toISOString(),
|
|
554
|
+
notifiedAt: null,
|
|
555
|
+
},
|
|
556
|
+
})}\n`);
|
|
557
|
+
const handle = fs.openSync(temporaryPath, 'r+');
|
|
558
|
+
try {
|
|
559
|
+
fs.fsyncSync(handle);
|
|
560
|
+
} finally {
|
|
561
|
+
fs.closeSync(handle);
|
|
562
|
+
}
|
|
563
|
+
fs.renameSync(temporaryPath, filePath);
|
|
564
|
+
securePrivateFile(filePath);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function isInteractiveUpdateStart({ argv, env, stdin, stdout }) {
|
|
572
|
+
const isOrdinaryStartup = Array.isArray(argv)
|
|
573
|
+
&& (argv.length === 0
|
|
574
|
+
|| (argv.length === 1 && INTERACTIVE_START_MODE_FLAGS.has(argv[0])));
|
|
575
|
+
return isOrdinaryStartup
|
|
576
|
+
&& (!env || env.BLUN_NO_AUTO_UPDATE !== '1')
|
|
577
|
+
&& stdin?.isTTY === true
|
|
578
|
+
&& stdout?.isTTY === true;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function installCommandText(version) {
|
|
582
|
+
return `npm i -g ${PACKAGE_NAME}@${version}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function createUpdateProgress(output) {
|
|
586
|
+
const enabled = output?.isTTY === true;
|
|
587
|
+
const phaseCount = 4;
|
|
588
|
+
const width = 24;
|
|
589
|
+
const cyan = '\u001B[36m';
|
|
590
|
+
const dim = '\u001B[2m';
|
|
591
|
+
const reset = '\u001B[0m';
|
|
592
|
+
let active = false;
|
|
593
|
+
const update = (phase, label) => {
|
|
594
|
+
if (!enabled) return;
|
|
595
|
+
const completed = Math.max(0, Math.min(phaseCount, phase));
|
|
596
|
+
const filled = Math.round((completed / phaseCount) * width);
|
|
597
|
+
const bar = `${'━'.repeat(filled)}${'─'.repeat(width - filled)}`;
|
|
598
|
+
output.write(`\r\u001B[2K${cyan}${bar}${reset} ${dim}${completed}/${phaseCount}${reset} ${label}`);
|
|
599
|
+
active = completed < phaseCount;
|
|
600
|
+
if (!active) output.write('\n');
|
|
601
|
+
};
|
|
602
|
+
update.finish = () => {
|
|
603
|
+
if (!enabled || !active) return;
|
|
604
|
+
active = false;
|
|
605
|
+
output.write('\n');
|
|
606
|
+
};
|
|
607
|
+
return update;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function promptUpdateAction({ currentVersion, release, input, output, locale = 'en', processRef = process }) {
|
|
611
|
+
return new Promise((resolve, reject) => {
|
|
612
|
+
let selected = 0;
|
|
613
|
+
let settled = false;
|
|
614
|
+
let cursorHidden = false;
|
|
615
|
+
let escapeTimer;
|
|
616
|
+
let pendingInput = '';
|
|
617
|
+
const decoder = new StringDecoder('utf8');
|
|
618
|
+
const wasRaw = input.isRaw === true;
|
|
619
|
+
const cyan = '\u001B[36m';
|
|
620
|
+
const dim = '\u001B[2m';
|
|
621
|
+
const underline = '\u001B[4m';
|
|
622
|
+
const reset = '\u001B[0m';
|
|
623
|
+
const actions = [
|
|
624
|
+
{
|
|
625
|
+
choice: 'install',
|
|
626
|
+
label: updateText(locale, 'install', { command: installCommandText(release.version) }),
|
|
627
|
+
},
|
|
628
|
+
{ choice: 'skip-once', label: updateText(locale, 'skipOnce') },
|
|
629
|
+
{ choice: 'skip-version', label: updateText(locale, 'skipVersion') },
|
|
630
|
+
];
|
|
631
|
+
const signalHandlers = new Map();
|
|
632
|
+
|
|
633
|
+
const renderActions = (refresh) => {
|
|
634
|
+
if (refresh) output.write(`\u001B[${actions.length + 1}F`);
|
|
635
|
+
for (let index = 0; index < actions.length; index += 1) {
|
|
636
|
+
const line = `${selected === index ? '›' : ' '} ${index + 1}. ${actions[index].label}`;
|
|
637
|
+
output.write(`\r\u001B[2K${selected === index ? cyan : ''}${line}${selected === index ? reset : ''}\n`);
|
|
638
|
+
}
|
|
639
|
+
output.write(`\r\u001B[2K${dim}${updateText(locale, 'confirm')}${reset}\n`);
|
|
640
|
+
};
|
|
641
|
+
const cleanup = () => {
|
|
642
|
+
if (escapeTimer !== undefined) clearTimeout(escapeTimer);
|
|
643
|
+
input.removeListener('data', onData);
|
|
644
|
+
input.removeListener('end', skipOnce);
|
|
645
|
+
input.removeListener('close', skipOnce);
|
|
646
|
+
input.removeListener('error', skipOnce);
|
|
647
|
+
for (const [signal, handler] of signalHandlers) {
|
|
648
|
+
processRef.removeListener(signal, handler);
|
|
649
|
+
}
|
|
650
|
+
signalHandlers.clear();
|
|
651
|
+
try {
|
|
652
|
+
if (typeof input.setRawMode === 'function') input.setRawMode(wasRaw);
|
|
653
|
+
} catch {}
|
|
654
|
+
try {
|
|
655
|
+
if (typeof input.pause === 'function') input.pause();
|
|
656
|
+
} catch {}
|
|
657
|
+
if (cursorHidden) {
|
|
658
|
+
try {
|
|
659
|
+
output.write('\u001B[?25h');
|
|
660
|
+
} catch {}
|
|
661
|
+
cursorHidden = false;
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
const finish = (choice) => {
|
|
665
|
+
if (settled) return;
|
|
666
|
+
settled = true;
|
|
667
|
+
cleanup();
|
|
668
|
+
try {
|
|
669
|
+
output.write('\n');
|
|
670
|
+
} catch {}
|
|
671
|
+
resolve(choice);
|
|
672
|
+
};
|
|
673
|
+
const fail = (error) => {
|
|
674
|
+
if (settled) return;
|
|
675
|
+
settled = true;
|
|
676
|
+
cleanup();
|
|
677
|
+
reject(error);
|
|
678
|
+
};
|
|
679
|
+
const handleSignal = (signal) => {
|
|
680
|
+
if (settled) return;
|
|
681
|
+
settled = true;
|
|
682
|
+
cleanup();
|
|
683
|
+
try {
|
|
684
|
+
output.write('\n');
|
|
685
|
+
} catch {}
|
|
686
|
+
const error = new Error(`UPDATE_PROMPT_${signal}`);
|
|
687
|
+
error.code = `UPDATE_PROMPT_${signal}`;
|
|
688
|
+
const exitCode = UPDATE_SIGNAL_EXIT_CODES[signal];
|
|
689
|
+
processRef.exitCode = exitCode;
|
|
690
|
+
if (typeof processRef.exit === 'function') processRef.exit(exitCode);
|
|
691
|
+
reject(error);
|
|
692
|
+
};
|
|
693
|
+
function skipOnce() {
|
|
694
|
+
finish('skip-once');
|
|
695
|
+
}
|
|
696
|
+
const waitForEscapeSuffix = () => {
|
|
697
|
+
if (escapeTimer !== undefined) return;
|
|
698
|
+
escapeTimer = setTimeout(() => {
|
|
699
|
+
escapeTimer = undefined;
|
|
700
|
+
skipOnce();
|
|
701
|
+
}, UPDATE_KEY_ESCAPE_TIMEOUT_MS);
|
|
702
|
+
};
|
|
703
|
+
const clearEscapeTimer = () => {
|
|
704
|
+
if (escapeTimer === undefined) return;
|
|
705
|
+
clearTimeout(escapeTimer);
|
|
706
|
+
escapeTimer = undefined;
|
|
707
|
+
};
|
|
708
|
+
const readEscapeSequence = () => {
|
|
709
|
+
if (pendingInput.length < 2) return undefined;
|
|
710
|
+
if (pendingInput[1] === '[') {
|
|
711
|
+
for (let index = 2; index < pendingInput.length; index += 1) {
|
|
712
|
+
const code = pendingInput.codePointAt(index);
|
|
713
|
+
if (code >= 0x40 && code <= 0x7e) return pendingInput.slice(0, index + 1);
|
|
714
|
+
const isParameter = code >= 0x30 && code <= 0x3f;
|
|
715
|
+
const isIntermediate = code >= 0x20 && code <= 0x2f;
|
|
716
|
+
if (!isParameter && !isIntermediate) return pendingInput.slice(0, index + 1);
|
|
717
|
+
}
|
|
718
|
+
return pendingInput.length > 64 ? pendingInput : undefined;
|
|
719
|
+
}
|
|
720
|
+
if (pendingInput[1] === 'O') {
|
|
721
|
+
return pendingInput.length >= 3 ? pendingInput.slice(0, 3) : undefined;
|
|
722
|
+
}
|
|
723
|
+
return pendingInput.slice(0, 2);
|
|
724
|
+
};
|
|
725
|
+
const processPendingInput = () => {
|
|
726
|
+
while (pendingInput.length > 0) {
|
|
727
|
+
const first = pendingInput[0];
|
|
728
|
+
if (first === '\u0003') {
|
|
729
|
+
skipOnce();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (first === '\r' || first === '\n') {
|
|
733
|
+
pendingInput = pendingInput.slice(1);
|
|
734
|
+
finish(actions[selected].choice);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (first !== '\u001B') {
|
|
738
|
+
const numericSelection = Number(first) - 1;
|
|
739
|
+
pendingInput = pendingInput.slice(1);
|
|
740
|
+
if (Number.isInteger(numericSelection)
|
|
741
|
+
&& numericSelection >= 0
|
|
742
|
+
&& numericSelection < actions.length) {
|
|
743
|
+
selected = numericSelection;
|
|
744
|
+
renderActions(true);
|
|
745
|
+
}
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (pendingInput.length > 1
|
|
749
|
+
&& (pendingInput[1] === '\r'
|
|
750
|
+
|| pendingInput[1] === '\n'
|
|
751
|
+
|| pendingInput[1] === '\u0003')) {
|
|
752
|
+
skipOnce();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const sequence = readEscapeSequence();
|
|
756
|
+
if (sequence === undefined) {
|
|
757
|
+
waitForEscapeSuffix();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
clearEscapeTimer();
|
|
761
|
+
if (sequence === '\u001B[A' || sequence === '\u001B[D') {
|
|
762
|
+
selected = (selected - 1 + actions.length) % actions.length;
|
|
763
|
+
pendingInput = pendingInput.slice(sequence.length);
|
|
764
|
+
renderActions(true);
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (sequence === '\u001B[B' || sequence === '\u001B[C') {
|
|
768
|
+
selected = (selected + 1) % actions.length;
|
|
769
|
+
pendingInput = pendingInput.slice(sequence.length);
|
|
770
|
+
renderActions(true);
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
pendingInput = pendingInput.slice(sequence.length);
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
const onData = (chunk) => {
|
|
777
|
+
try {
|
|
778
|
+
pendingInput += Buffer.isBuffer(chunk) ? decoder.write(chunk) : String(chunk);
|
|
779
|
+
processPendingInput();
|
|
780
|
+
} catch (error) {
|
|
781
|
+
fail(error);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
try {
|
|
786
|
+
const supportedSignals = processRef.platform === 'win32'
|
|
787
|
+
? ['SIGINT', 'SIGTERM']
|
|
788
|
+
: Object.keys(UPDATE_SIGNAL_EXIT_CODES);
|
|
789
|
+
for (const signal of supportedSignals) {
|
|
790
|
+
const handler = () => handleSignal(signal);
|
|
791
|
+
signalHandlers.set(signal, handler);
|
|
792
|
+
processRef.once(signal, handler);
|
|
793
|
+
}
|
|
794
|
+
output.write(`\n${updateText(locale, 'available', { currentVersion, targetVersion: release.version })}\n`);
|
|
795
|
+
if (release.notesUrl) {
|
|
796
|
+
output.write(`${dim}${updateText(locale, 'releaseNotes')}${reset} ${underline}${release.notesUrl}${reset}\n`);
|
|
797
|
+
}
|
|
798
|
+
output.write('\n');
|
|
799
|
+
cursorHidden = true;
|
|
800
|
+
output.write('\u001B[?25l');
|
|
801
|
+
renderActions(false);
|
|
802
|
+
input.on('data', onData);
|
|
803
|
+
input.once('end', skipOnce);
|
|
804
|
+
input.once('close', skipOnce);
|
|
805
|
+
input.once('error', skipOnce);
|
|
806
|
+
if (typeof input.setRawMode === 'function') input.setRawMode(true);
|
|
807
|
+
if (typeof input.resume === 'function') input.resume();
|
|
808
|
+
} catch (error) {
|
|
809
|
+
fail(error);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function resolveTrustedNpmCliPath(options = {}) {
|
|
815
|
+
const execPath = path.resolve(options.execPath || process.execPath);
|
|
816
|
+
const executableDirectory = path.dirname(execPath);
|
|
817
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
818
|
+
const candidates = options.npmCliPath ? [options.npmCliPath] : [
|
|
819
|
+
path.join(executableDirectory, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
820
|
+
path.resolve(executableDirectory, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
821
|
+
path.resolve(executableDirectory, '..', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
822
|
+
path.resolve(packageRoot, '..', 'npm', 'bin', 'npm-cli.js'),
|
|
823
|
+
];
|
|
824
|
+
for (const candidate of candidates) {
|
|
825
|
+
try {
|
|
826
|
+
if (!path.isAbsolute(candidate)) continue;
|
|
827
|
+
const resolved = fs.realpathSync(candidate);
|
|
828
|
+
const parts = resolved.replaceAll('\\', '/').split('/');
|
|
829
|
+
const npmIndex = parts.length - 3;
|
|
830
|
+
if (parts.at(-1) !== 'npm-cli.js'
|
|
831
|
+
|| parts.at(-2) !== 'bin'
|
|
832
|
+
|| parts[npmIndex]?.toLowerCase() !== 'npm'
|
|
833
|
+
|| !fs.statSync(resolved).isFile()) {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
return resolved;
|
|
837
|
+
} catch {}
|
|
838
|
+
}
|
|
839
|
+
throw new Error('TRUSTED_NPM_CLI_NOT_FOUND');
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function createInstallInvocation(version, options = {}) {
|
|
843
|
+
if (!parseSemver(version)) throw new Error('INVALID_UPDATE_VERSION');
|
|
844
|
+
const packageSpec = `${PACKAGE_NAME}@${version}`;
|
|
845
|
+
const execPath = fs.realpathSync(path.resolve(options.execPath || process.execPath));
|
|
846
|
+
if (!fs.statSync(execPath).isFile()) throw new Error('INVALID_NODE_EXECUTABLE');
|
|
847
|
+
const npmCliPath = resolveTrustedNpmCliPath(options);
|
|
848
|
+
const packageRoot = fs.realpathSync(path.resolve(options.packageRoot));
|
|
849
|
+
const modulesRoot = path.dirname(packageRoot);
|
|
850
|
+
if (path.basename(modulesRoot).toLowerCase() !== 'node_modules') {
|
|
851
|
+
throw new Error('INVALID_GLOBAL_PACKAGE_ROOT');
|
|
852
|
+
}
|
|
853
|
+
let installPrefix = path.dirname(modulesRoot);
|
|
854
|
+
const platform = options.platform || process.platform;
|
|
855
|
+
if (platform !== 'win32' && path.basename(installPrefix) === 'lib') {
|
|
856
|
+
installPrefix = path.dirname(installPrefix);
|
|
857
|
+
}
|
|
858
|
+
let npmUserConfigPath;
|
|
859
|
+
let npmGlobalConfigPath;
|
|
860
|
+
if (options.npmUserConfigPath || options.npmGlobalConfigPath) {
|
|
861
|
+
if (!options.npmUserConfigPath || !options.npmGlobalConfigPath) {
|
|
862
|
+
throw new Error('INVALID_NPM_CONFIG_PATHS');
|
|
863
|
+
}
|
|
864
|
+
npmUserConfigPath = fs.realpathSync(path.resolve(options.npmUserConfigPath));
|
|
865
|
+
npmGlobalConfigPath = fs.realpathSync(path.resolve(options.npmGlobalConfigPath));
|
|
866
|
+
if (!fs.statSync(npmUserConfigPath).isFile()
|
|
867
|
+
|| !fs.statSync(npmGlobalConfigPath).isFile()
|
|
868
|
+
|| npmUserConfigPath === npmGlobalConfigPath) {
|
|
869
|
+
throw new Error('INVALID_NPM_CONFIG_PATHS');
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
command: execPath,
|
|
874
|
+
args: [
|
|
875
|
+
npmCliPath,
|
|
876
|
+
'i',
|
|
877
|
+
'-g',
|
|
878
|
+
packageSpec,
|
|
879
|
+
`--registry=${INSTALL_REGISTRY_URL}`,
|
|
880
|
+
'--strict-ssl=true',
|
|
881
|
+
'--dry-run=false',
|
|
882
|
+
'--ignore-scripts=false',
|
|
883
|
+
'--package-lock-only=false',
|
|
884
|
+
`--prefix=${installPrefix}`,
|
|
885
|
+
...(npmUserConfigPath ? [
|
|
886
|
+
`--userconfig=${npmUserConfigPath}`,
|
|
887
|
+
`--globalconfig=${npmGlobalConfigPath}`,
|
|
888
|
+
] : []),
|
|
889
|
+
],
|
|
890
|
+
windowsVerbatimArguments: false,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function createInstallEnvironment(
|
|
895
|
+
sourceEnv,
|
|
896
|
+
installDirectory,
|
|
897
|
+
npmUserConfigPath,
|
|
898
|
+
npmGlobalConfigPath,
|
|
899
|
+
) {
|
|
900
|
+
const allowedKeys = new Set([
|
|
901
|
+
'appdata',
|
|
902
|
+
'comspec',
|
|
903
|
+
'home',
|
|
904
|
+
'homedrive',
|
|
905
|
+
'homepath',
|
|
906
|
+
'lang',
|
|
907
|
+
'lc_all',
|
|
908
|
+
'lc_ctype',
|
|
909
|
+
'localappdata',
|
|
910
|
+
'logname',
|
|
911
|
+
'path',
|
|
912
|
+
'pathext',
|
|
913
|
+
'programdata',
|
|
914
|
+
'programfiles',
|
|
915
|
+
'programfiles(x86)',
|
|
916
|
+
'systemroot',
|
|
917
|
+
'term',
|
|
918
|
+
'tz',
|
|
919
|
+
'user',
|
|
920
|
+
'userprofile',
|
|
921
|
+
'windir',
|
|
922
|
+
]);
|
|
923
|
+
const result = {};
|
|
924
|
+
for (const [key, value] of Object.entries(sourceEnv || {})) {
|
|
925
|
+
const normalized = key.toLowerCase();
|
|
926
|
+
if (!allowedKeys.has(normalized) && !key.startsWith('BLUN_TEST_')) continue;
|
|
927
|
+
result[key] = value;
|
|
928
|
+
}
|
|
929
|
+
result.TEMP = installDirectory;
|
|
930
|
+
result.TMP = installDirectory;
|
|
931
|
+
result.TMPDIR = installDirectory;
|
|
932
|
+
result.npm_config_registry = INSTALL_REGISTRY_URL;
|
|
933
|
+
result.npm_config_strict_ssl = 'true';
|
|
934
|
+
result.npm_config_dry_run = 'false';
|
|
935
|
+
result.npm_config_ignore_scripts = 'false';
|
|
936
|
+
result.npm_config_package_lock_only = 'false';
|
|
937
|
+
result.npm_config_userconfig = npmUserConfigPath;
|
|
938
|
+
result.npm_config_globalconfig = npmGlobalConfigPath;
|
|
939
|
+
return result;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function pathIsWithin(root, candidate) {
|
|
943
|
+
const relative = path.relative(root, candidate);
|
|
944
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`)
|
|
945
|
+
&& relative !== '..'
|
|
946
|
+
&& !path.isAbsolute(relative));
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function resolveInstallTempRoot(options) {
|
|
950
|
+
if (typeof options.blunDir !== 'string' || options.blunDir.trim().length === 0) {
|
|
951
|
+
throw new Error('INVALID_UPDATE_BLUN_HOME');
|
|
952
|
+
}
|
|
953
|
+
const updateRoot = path.join(path.resolve(options.blunDir), UPDATE_DIRECTORY);
|
|
954
|
+
if (options.tempRoot === undefined) return updateRoot;
|
|
955
|
+
const explicitRoot = path.resolve(options.tempRoot);
|
|
956
|
+
if (!pathIsWithin(updateRoot, explicitRoot)) {
|
|
957
|
+
throw new Error('INVALID_UPDATE_TEMP_ROOT');
|
|
958
|
+
}
|
|
959
|
+
return explicitRoot;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function createPrivateInstallDirectory(tempRoot, blunDir) {
|
|
963
|
+
const updateRoot = path.join(path.resolve(blunDir), UPDATE_DIRECTORY);
|
|
964
|
+
let current = updateRoot;
|
|
965
|
+
ensurePrivateDirectory(current);
|
|
966
|
+
const relative = path.relative(updateRoot, tempRoot);
|
|
967
|
+
if (relative !== '') {
|
|
968
|
+
for (const segment of relative.split(path.sep)) {
|
|
969
|
+
current = path.join(current, segment);
|
|
970
|
+
ensurePrivateDirectory(current);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
let directory;
|
|
974
|
+
try {
|
|
975
|
+
directory = fs.mkdtempSync(path.join(tempRoot, 'blun-update-'));
|
|
976
|
+
ensurePrivateDirectory(directory);
|
|
977
|
+
return directory;
|
|
978
|
+
} catch (error) {
|
|
979
|
+
if (directory) {
|
|
980
|
+
try {
|
|
981
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
982
|
+
} catch {}
|
|
983
|
+
}
|
|
984
|
+
throw error;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function verifyInstalledPackageVersion(packageRoot, version) {
|
|
989
|
+
try {
|
|
990
|
+
const manifestPath = path.join(packageRoot, 'package.json');
|
|
991
|
+
const stat = fs.lstatSync(manifestPath);
|
|
992
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024) return false;
|
|
993
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
994
|
+
return readDataProperty(manifest, 'name') === PACKAGE_NAME
|
|
995
|
+
&& readDataProperty(manifest, 'version') === version;
|
|
996
|
+
} catch {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function preparePinnedInstaller(version, options = {}) {
|
|
1002
|
+
const forkImpl = options.forkImpl || fork;
|
|
1003
|
+
const preparation = Promise.withResolvers();
|
|
1004
|
+
const resolvePreparation = preparation.resolve;
|
|
1005
|
+
let child;
|
|
1006
|
+
let invocation;
|
|
1007
|
+
let leaseBusy = false;
|
|
1008
|
+
let leaseUnavailable = false;
|
|
1009
|
+
let sendError;
|
|
1010
|
+
let childError;
|
|
1011
|
+
let preparationSettled = false;
|
|
1012
|
+
let closeSettled = false;
|
|
1013
|
+
let selectedAction;
|
|
1014
|
+
let installDirectory;
|
|
1015
|
+
let npmUserConfigPath;
|
|
1016
|
+
let npmGlobalConfigPath;
|
|
1017
|
+
let disconnectTimer;
|
|
1018
|
+
const close = Promise.withResolvers();
|
|
1019
|
+
const closePromise = close.promise;
|
|
1020
|
+
const resolveClose = close.resolve;
|
|
1021
|
+
const cleanupDirectory = () => {
|
|
1022
|
+
try {
|
|
1023
|
+
if (installDirectory) {
|
|
1024
|
+
(options.removeDirectory || fs.rmSync)(installDirectory, {
|
|
1025
|
+
recursive: true,
|
|
1026
|
+
force: true,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
} catch {
|
|
1030
|
+
// Cleanup cannot change a completed npm result.
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
const settlePreparation = (result) => {
|
|
1034
|
+
if (preparationSettled) return;
|
|
1035
|
+
preparationSettled = true;
|
|
1036
|
+
clearTimeout(prepareTimeout);
|
|
1037
|
+
resolvePreparation(result);
|
|
1038
|
+
};
|
|
1039
|
+
const finishClose = (result) => {
|
|
1040
|
+
if (closeSettled) return;
|
|
1041
|
+
closeSettled = true;
|
|
1042
|
+
clearTimeout(disconnectTimer);
|
|
1043
|
+
cleanupDirectory();
|
|
1044
|
+
resolveClose(result);
|
|
1045
|
+
if (!preparationSettled) {
|
|
1046
|
+
settlePreparation({
|
|
1047
|
+
ready: false,
|
|
1048
|
+
reason: leaseBusy
|
|
1049
|
+
? 'busy'
|
|
1050
|
+
: leaseUnavailable
|
|
1051
|
+
? 'unavailable'
|
|
1052
|
+
: result.code === 75 || result.unknownState === true
|
|
1053
|
+
? 'unknown'
|
|
1054
|
+
: 'failed',
|
|
1055
|
+
result,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
const finishDisconnected = () => {
|
|
1060
|
+
if (closeSettled) return;
|
|
1061
|
+
closeSettled = true;
|
|
1062
|
+
const result = { code: undefined, error: childError, unknownState: true };
|
|
1063
|
+
resolveClose(result);
|
|
1064
|
+
if (!preparationSettled) {
|
|
1065
|
+
settlePreparation({ ready: false, reason: 'unknown', result });
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
const scheduleUnknownState = (error) => {
|
|
1069
|
+
if (error && childError === undefined) childError = error;
|
|
1070
|
+
if (disconnectTimer !== undefined) return;
|
|
1071
|
+
disconnectTimer = setTimeout(
|
|
1072
|
+
finishDisconnected,
|
|
1073
|
+
options.disconnectTimeoutMs || 1_000,
|
|
1074
|
+
);
|
|
1075
|
+
};
|
|
1076
|
+
const sendControl = (type) => {
|
|
1077
|
+
if (selectedAction) return closePromise;
|
|
1078
|
+
selectedAction = type;
|
|
1079
|
+
try {
|
|
1080
|
+
child.send({ type }, (error) => {
|
|
1081
|
+
if (!error) return;
|
|
1082
|
+
sendError = error;
|
|
1083
|
+
try {
|
|
1084
|
+
child.kill();
|
|
1085
|
+
} catch {}
|
|
1086
|
+
});
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
sendError = error;
|
|
1089
|
+
try {
|
|
1090
|
+
child.kill();
|
|
1091
|
+
} catch {}
|
|
1092
|
+
}
|
|
1093
|
+
return closePromise;
|
|
1094
|
+
};
|
|
1095
|
+
let prepareTimeout;
|
|
1096
|
+
try {
|
|
1097
|
+
installDirectory = createPrivateInstallDirectory(
|
|
1098
|
+
resolveInstallTempRoot(options),
|
|
1099
|
+
options.blunDir,
|
|
1100
|
+
);
|
|
1101
|
+
npmUserConfigPath = path.join(installDirectory, 'user-npmrc');
|
|
1102
|
+
npmGlobalConfigPath = path.join(installDirectory, 'global-npmrc');
|
|
1103
|
+
writePrivateFile(npmUserConfigPath, '');
|
|
1104
|
+
writePrivateFile(npmGlobalConfigPath, '');
|
|
1105
|
+
invocation = createInstallInvocation(version, {
|
|
1106
|
+
...options,
|
|
1107
|
+
npmUserConfigPath,
|
|
1108
|
+
npmGlobalConfigPath,
|
|
1109
|
+
});
|
|
1110
|
+
child = forkImpl(path.join(__dirname, 'update-lease.js'), ['--installer-worker'], {
|
|
1111
|
+
detached: true,
|
|
1112
|
+
execPath: invocation.command,
|
|
1113
|
+
cwd: installDirectory,
|
|
1114
|
+
env: createInstallEnvironment(
|
|
1115
|
+
options.env || process.env,
|
|
1116
|
+
installDirectory,
|
|
1117
|
+
npmUserConfigPath,
|
|
1118
|
+
npmGlobalConfigPath,
|
|
1119
|
+
),
|
|
1120
|
+
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
1121
|
+
windowsHide: true,
|
|
1122
|
+
});
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
cleanupDirectory();
|
|
1125
|
+
settlePreparation({ ready: false, reason: 'failed', result: { error } });
|
|
1126
|
+
return preparation.promise;
|
|
1127
|
+
}
|
|
1128
|
+
prepareTimeout = setTimeout(() => {
|
|
1129
|
+
sendError = new Error('UPDATE_INSTALLER_PREPARE_TIMEOUT');
|
|
1130
|
+
settlePreparation({ ready: false, reason: 'unknown', result: { error: sendError } });
|
|
1131
|
+
try {
|
|
1132
|
+
child.kill();
|
|
1133
|
+
} catch {}
|
|
1134
|
+
}, options.prepareTimeoutMs || 5_000);
|
|
1135
|
+
prepareTimeout.unref?.();
|
|
1136
|
+
child.once('error', (error) => {
|
|
1137
|
+
if (!preparationSettled && child.pid === undefined) {
|
|
1138
|
+
finishClose({ code: undefined, error });
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
scheduleUnknownState(error);
|
|
1142
|
+
});
|
|
1143
|
+
child.on('message', (message) => {
|
|
1144
|
+
if (message?.type === 'blun-update-lease-busy') leaseBusy = true;
|
|
1145
|
+
if (message?.type === 'blun-update-lease-unavailable') leaseUnavailable = true;
|
|
1146
|
+
if (message?.type === 'blun-update-lease-adopted') {
|
|
1147
|
+
settlePreparation(Object.freeze({
|
|
1148
|
+
ready: true,
|
|
1149
|
+
commit: () => sendControl('blun-update-lease-commit'),
|
|
1150
|
+
cancel: () => sendControl('blun-update-lease-cancel'),
|
|
1151
|
+
}));
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
const finishInstallerProcess = (code, signal) => {
|
|
1155
|
+
if (sendError || childError) {
|
|
1156
|
+
finishClose({
|
|
1157
|
+
code: undefined,
|
|
1158
|
+
error: sendError || childError,
|
|
1159
|
+
unknownState: true,
|
|
1160
|
+
});
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
finishClose({
|
|
1164
|
+
code: Number.isInteger(code) ? code : undefined,
|
|
1165
|
+
signal: typeof signal === 'string' ? signal : undefined,
|
|
1166
|
+
leaseBusy,
|
|
1167
|
+
leaseUnavailable,
|
|
1168
|
+
unknownState: !Number.isInteger(code),
|
|
1169
|
+
verified: !leaseBusy
|
|
1170
|
+
&& !leaseUnavailable
|
|
1171
|
+
&& code === 0
|
|
1172
|
+
&& verifyInstalledPackageVersion(options.packageRoot, version),
|
|
1173
|
+
});
|
|
1174
|
+
};
|
|
1175
|
+
child.once('exit', finishInstallerProcess);
|
|
1176
|
+
child.once('close', finishInstallerProcess);
|
|
1177
|
+
child.once('disconnect', () => scheduleUnknownState());
|
|
1178
|
+
try {
|
|
1179
|
+
child.send({
|
|
1180
|
+
type: 'blun-update-lease-handoff',
|
|
1181
|
+
packageRoot: path.resolve(options.packageRoot),
|
|
1182
|
+
installer: {
|
|
1183
|
+
npmCliPath: invocation.args[0],
|
|
1184
|
+
npmArgs: invocation.args.slice(1),
|
|
1185
|
+
installCwd: installDirectory,
|
|
1186
|
+
},
|
|
1187
|
+
}, (error) => {
|
|
1188
|
+
if (!error) return;
|
|
1189
|
+
sendError = error;
|
|
1190
|
+
try {
|
|
1191
|
+
child.kill();
|
|
1192
|
+
} catch {
|
|
1193
|
+
finishClose({ code: undefined, error, unknownState: true });
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
sendError = error;
|
|
1198
|
+
try {
|
|
1199
|
+
child.kill();
|
|
1200
|
+
} catch {
|
|
1201
|
+
finishClose({ code: undefined, error, unknownState: true });
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return preparation.promise;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
async function installPinnedVersion(version, options = {}) {
|
|
1208
|
+
const prepared = await preparePinnedInstaller(version, options);
|
|
1209
|
+
if (!prepared.ready) {
|
|
1210
|
+
return {
|
|
1211
|
+
...prepared.result,
|
|
1212
|
+
leaseBusy: prepared.reason === 'busy',
|
|
1213
|
+
leaseUnavailable: prepared.reason === 'unavailable',
|
|
1214
|
+
unknownState: prepared.reason === 'unknown' || prepared.result?.unknownState === true,
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
return prepared.commit();
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
async function probeInstallLease(options, packageRoot) {
|
|
1221
|
+
let result;
|
|
1222
|
+
try {
|
|
1223
|
+
result = await (options.acquireInstallLease || tryAcquireUpdateLease)({
|
|
1224
|
+
packageRoot,
|
|
1225
|
+
role: 'install',
|
|
1226
|
+
});
|
|
1227
|
+
} catch {
|
|
1228
|
+
return 'unavailable';
|
|
1229
|
+
}
|
|
1230
|
+
if (!result?.acquired) return result?.reason === 'busy' ? 'busy' : 'unavailable';
|
|
1231
|
+
try {
|
|
1232
|
+
result.lease.assertOwned();
|
|
1233
|
+
} catch {
|
|
1234
|
+
return 'busy';
|
|
1235
|
+
} finally {
|
|
1236
|
+
try {
|
|
1237
|
+
await result.lease.release();
|
|
1238
|
+
} catch {}
|
|
1239
|
+
}
|
|
1240
|
+
return 'clear';
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function noReleaseReason(currentVersion, registryTags, fallbackManifest) {
|
|
1244
|
+
const latest = readDataProperty(registryTags, 'latest');
|
|
1245
|
+
if (parseSemver(latest) && compareSemver(currentVersion, latest) > 0) {
|
|
1246
|
+
const next = readDataProperty(registryTags, 'next');
|
|
1247
|
+
return parseSemver(next) && compareSemver(next, currentVersion) === 0
|
|
1248
|
+
? 'current'
|
|
1249
|
+
: 'unavailable';
|
|
1250
|
+
}
|
|
1251
|
+
return parseSemver(latest) || fallbackManifest ? 'current' : 'unavailable';
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function explicitNoTarget(stdout, reason, currentVersion, locale) {
|
|
1255
|
+
stdout.write(`${updateText(locale, reason === 'current' ? 'noUpdate' : 'noTarget', { currentVersion })}\n`);
|
|
1256
|
+
return { kind: 'current', reason };
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function explicitFailure(stderr, reason, exitCode, locale) {
|
|
1260
|
+
const hasExitCode = Number.isInteger(exitCode) && exitCode !== 0;
|
|
1261
|
+
if (hasExitCode) {
|
|
1262
|
+
stderr.write(`${updateText(locale, 'failedCode', { exitCode })}\n`);
|
|
1263
|
+
} else {
|
|
1264
|
+
stderr.write(`${updateText(locale, 'verifyFailed')}\n`);
|
|
1265
|
+
}
|
|
1266
|
+
return {
|
|
1267
|
+
kind: 'failed',
|
|
1268
|
+
reason,
|
|
1269
|
+
exitCode: hasExitCode ? exitCode : 1,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function queueExplicitUpdate(blunDir, release, stdout, stderr, locale) {
|
|
1274
|
+
try {
|
|
1275
|
+
ensurePrivateDirectory(blunDir);
|
|
1276
|
+
writePendingUpdate(blunDir, release.version);
|
|
1277
|
+
} catch {
|
|
1278
|
+
return explicitFailure(stderr, 'private_home_unavailable', undefined, locale);
|
|
1279
|
+
}
|
|
1280
|
+
stdout.write(
|
|
1281
|
+
`${updateText(locale, 'queued', { targetVersion: release.version })}\n`,
|
|
1282
|
+
);
|
|
1283
|
+
return { kind: 'queued', version: release.version };
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
async function runUpdateFlow(options, explicitUpdate) {
|
|
1287
|
+
const {
|
|
1288
|
+
currentVersion,
|
|
1289
|
+
argv,
|
|
1290
|
+
env,
|
|
1291
|
+
stdin,
|
|
1292
|
+
stdout,
|
|
1293
|
+
stderr,
|
|
1294
|
+
blunDir,
|
|
1295
|
+
} = options;
|
|
1296
|
+
const locale = resolveUpdateLocale(options);
|
|
1297
|
+
if (!explicitUpdate && !isInteractiveUpdateStart({ argv, env, stdin, stdout })) {
|
|
1298
|
+
return { kind: 'continue', reason: 'not_interactive' };
|
|
1299
|
+
}
|
|
1300
|
+
if (!parseSemver(currentVersion)) {
|
|
1301
|
+
return explicitUpdate
|
|
1302
|
+
? explicitFailure(stderr, 'invalid_current_version', undefined, locale)
|
|
1303
|
+
: { kind: 'continue', reason: 'invalid_current_version' };
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
let pendingVersion = readPendingUpdate(blunDir);
|
|
1307
|
+
if (pendingVersion !== undefined && compareSemver(currentVersion, pendingVersion) >= 0) {
|
|
1308
|
+
clearPendingUpdate(blunDir);
|
|
1309
|
+
pendingVersion = undefined;
|
|
1310
|
+
}
|
|
1311
|
+
const now = (options.now || Date.now)();
|
|
1312
|
+
if (!explicitUpdate && pendingVersion === undefined && readSnoozeState(blunDir, now)) {
|
|
1313
|
+
return { kind: 'continue', reason: 'snoozed' };
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const packageRoot = path.resolve(options.packageRoot || path.resolve(__dirname, '..'));
|
|
1317
|
+
let leaseResult;
|
|
1318
|
+
const ownsNoticeLease = options.noticeLease === undefined;
|
|
1319
|
+
if (ownsNoticeLease) {
|
|
1320
|
+
try {
|
|
1321
|
+
leaseResult = await (options.acquireLease || tryAcquireUpdateLease)({ packageRoot });
|
|
1322
|
+
} catch {
|
|
1323
|
+
return explicitUpdate
|
|
1324
|
+
? explicitFailure(stderr, 'lease_unavailable', undefined, locale)
|
|
1325
|
+
: { kind: 'continue', reason: 'lease_unavailable' };
|
|
1326
|
+
}
|
|
1327
|
+
} else {
|
|
1328
|
+
leaseResult = { acquired: true, lease: options.noticeLease };
|
|
1329
|
+
}
|
|
1330
|
+
if (!leaseResult?.acquired) {
|
|
1331
|
+
if (explicitUpdate) {
|
|
1332
|
+
return explicitFailure(
|
|
1333
|
+
stderr,
|
|
1334
|
+
leaseResult?.reason === 'busy' ? 'update_in_progress' : 'lease_unavailable',
|
|
1335
|
+
undefined,
|
|
1336
|
+
locale,
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
return leaseResult?.reason === 'busy'
|
|
1340
|
+
? { kind: 'update_in_progress' }
|
|
1341
|
+
: { kind: 'continue', reason: 'lease_unavailable' };
|
|
1342
|
+
}
|
|
1343
|
+
const { lease } = leaseResult;
|
|
1344
|
+
let installerSession;
|
|
1345
|
+
|
|
1346
|
+
try {
|
|
1347
|
+
if (!explicitUpdate
|
|
1348
|
+
&& pendingVersion === undefined
|
|
1349
|
+
&& readSnoozeState(blunDir, (options.now || Date.now)())) {
|
|
1350
|
+
return { kind: 'continue', reason: 'snoozed' };
|
|
1351
|
+
}
|
|
1352
|
+
let sources;
|
|
1353
|
+
try {
|
|
1354
|
+
sources = await (options.loadReleaseSources || loadReleaseSources)();
|
|
1355
|
+
} catch {
|
|
1356
|
+
return explicitUpdate
|
|
1357
|
+
? explicitNoTarget(stdout, 'unavailable', currentVersion, locale)
|
|
1358
|
+
: { kind: 'continue', reason: 'unavailable' };
|
|
1359
|
+
}
|
|
1360
|
+
const registryTags = parseRegistryDocument(sources?.registryDocument);
|
|
1361
|
+
const fallbackManifest = parseFallbackManifest(sources?.fallbackDocument);
|
|
1362
|
+
const release = resolveEffectiveRelease(currentVersion, registryTags, fallbackManifest);
|
|
1363
|
+
if (!release) {
|
|
1364
|
+
const reason = noReleaseReason(currentVersion, registryTags, fallbackManifest);
|
|
1365
|
+
return explicitUpdate
|
|
1366
|
+
? explicitNoTarget(stdout, reason, currentVersion, locale)
|
|
1367
|
+
: { kind: 'continue', reason };
|
|
1368
|
+
}
|
|
1369
|
+
const queuedForRelease = pendingVersion === release.version;
|
|
1370
|
+
const initialInstallLease = await probeInstallLease(options, packageRoot);
|
|
1371
|
+
if (initialInstallLease === 'busy') {
|
|
1372
|
+
return explicitUpdate
|
|
1373
|
+
? queueExplicitUpdate(blunDir, release, stdout, stderr, locale)
|
|
1374
|
+
: { kind: 'update_in_progress' };
|
|
1375
|
+
}
|
|
1376
|
+
if (initialInstallLease !== 'clear') {
|
|
1377
|
+
return explicitUpdate
|
|
1378
|
+
? explicitFailure(stderr, 'lease_unavailable', undefined, locale)
|
|
1379
|
+
: { kind: 'continue', reason: 'lease_unavailable' };
|
|
1380
|
+
}
|
|
1381
|
+
if (!explicitUpdate && !queuedForRelease && readSkippedVersion(blunDir) === release.version) {
|
|
1382
|
+
return { kind: 'continue', reason: 'skipped_version' };
|
|
1383
|
+
}
|
|
1384
|
+
if (explicitUpdate || queuedForRelease) {
|
|
1385
|
+
try {
|
|
1386
|
+
ensurePrivateDirectory(blunDir);
|
|
1387
|
+
} catch {
|
|
1388
|
+
return explicitFailure(stderr, 'private_home_unavailable', undefined, locale);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
try {
|
|
1392
|
+
lease.assertOwned();
|
|
1393
|
+
} catch {
|
|
1394
|
+
return explicitUpdate
|
|
1395
|
+
? explicitFailure(stderr, 'update_in_progress', undefined, locale)
|
|
1396
|
+
: { kind: 'update_in_progress' };
|
|
1397
|
+
}
|
|
1398
|
+
const prePromptInstallLease = await probeInstallLease(options, packageRoot);
|
|
1399
|
+
if (prePromptInstallLease === 'busy') {
|
|
1400
|
+
return explicitUpdate
|
|
1401
|
+
? queueExplicitUpdate(blunDir, release, stdout, stderr, locale)
|
|
1402
|
+
: { kind: 'update_in_progress' };
|
|
1403
|
+
}
|
|
1404
|
+
if (prePromptInstallLease !== 'clear') {
|
|
1405
|
+
return explicitUpdate
|
|
1406
|
+
? explicitFailure(stderr, 'lease_unavailable', undefined, locale)
|
|
1407
|
+
: { kind: 'continue', reason: 'lease_unavailable' };
|
|
1408
|
+
}
|
|
1409
|
+
if (!options.installVersion) {
|
|
1410
|
+
installerSession = await (options.prepareInstaller || preparePinnedInstaller)(
|
|
1411
|
+
release.version,
|
|
1412
|
+
{ blunDir, packageRoot },
|
|
1413
|
+
);
|
|
1414
|
+
if (!installerSession?.ready) {
|
|
1415
|
+
if (explicitUpdate) {
|
|
1416
|
+
return explicitFailure(stderr, 'installer_unavailable', undefined, locale);
|
|
1417
|
+
}
|
|
1418
|
+
return installerSession?.reason === 'failed'
|
|
1419
|
+
? { kind: 'continue', reason: 'installer_unavailable' }
|
|
1420
|
+
: { kind: 'update_in_progress' };
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
let action;
|
|
1424
|
+
if (explicitUpdate || queuedForRelease) {
|
|
1425
|
+
action = 'install';
|
|
1426
|
+
} else {
|
|
1427
|
+
try {
|
|
1428
|
+
action = await (options.promptAction || promptUpdateAction)({
|
|
1429
|
+
currentVersion,
|
|
1430
|
+
release,
|
|
1431
|
+
input: stdin,
|
|
1432
|
+
output: stdout,
|
|
1433
|
+
locale,
|
|
1434
|
+
});
|
|
1435
|
+
} catch {
|
|
1436
|
+
return { kind: 'continue', reason: 'prompt_failed' };
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
if (action !== 'install') {
|
|
1440
|
+
if (installerSession?.ready) await installerSession.cancel();
|
|
1441
|
+
if (action === 'skip-once') {
|
|
1442
|
+
return { kind: 'continue', reason: 'skipped_once' };
|
|
1443
|
+
}
|
|
1444
|
+
if (action === 'skip-version') {
|
|
1445
|
+
try {
|
|
1446
|
+
writeSkippedVersion(blunDir, release.version);
|
|
1447
|
+
} catch {
|
|
1448
|
+
// A failed preference write must not block the existing console.
|
|
1449
|
+
}
|
|
1450
|
+
return { kind: 'continue', reason: 'skipped_version' };
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
writeSnoozeState(blunDir, (options.now || Date.now)());
|
|
1454
|
+
} catch {
|
|
1455
|
+
// A failed preference write must not block the existing console.
|
|
1456
|
+
}
|
|
1457
|
+
return { kind: 'continue', reason: 'postponed' };
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const updateProgress = createUpdateProgress(stdout);
|
|
1461
|
+
updateProgress(1, updateText(locale, 'progressChecked'));
|
|
1462
|
+
updateProgress(2, updateText(locale, 'installerReady'));
|
|
1463
|
+
|
|
1464
|
+
try {
|
|
1465
|
+
lease.assertOwned();
|
|
1466
|
+
} catch {
|
|
1467
|
+
return explicitUpdate
|
|
1468
|
+
? explicitFailure(stderr, 'update_in_progress', undefined, locale)
|
|
1469
|
+
: { kind: 'update_in_progress' };
|
|
1470
|
+
}
|
|
1471
|
+
let installResult;
|
|
1472
|
+
updateProgress(3, updateText(locale, 'installing'));
|
|
1473
|
+
try {
|
|
1474
|
+
installResult = options.installVersion
|
|
1475
|
+
? await options.installVersion(release.version)
|
|
1476
|
+
: await installerSession.commit();
|
|
1477
|
+
} catch {
|
|
1478
|
+
installResult = { code: undefined };
|
|
1479
|
+
}
|
|
1480
|
+
if (installResult?.leaseBusy === true
|
|
1481
|
+
|| installResult?.leaseUnavailable === true
|
|
1482
|
+
|| installResult?.unknownState === true) {
|
|
1483
|
+
updateProgress.finish();
|
|
1484
|
+
return explicitUpdate
|
|
1485
|
+
? explicitFailure(stderr, 'update_in_progress', undefined, locale)
|
|
1486
|
+
: { kind: 'update_in_progress' };
|
|
1487
|
+
}
|
|
1488
|
+
if (installResult?.code === 0 && installResult?.verified === true) {
|
|
1489
|
+
updateProgress(4, updateText(locale, 'installVerified'));
|
|
1490
|
+
clearPendingUpdate(blunDir);
|
|
1491
|
+
try {
|
|
1492
|
+
writeCoreInstallSuccess(
|
|
1493
|
+
blunDir,
|
|
1494
|
+
currentVersion,
|
|
1495
|
+
release.version,
|
|
1496
|
+
(options.now || Date.now)(),
|
|
1497
|
+
);
|
|
1498
|
+
} catch {
|
|
1499
|
+
stderr.write(
|
|
1500
|
+
`${updateText(locale, 'stateWarning', { targetVersion: release.version })}\n`,
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
stdout.write(
|
|
1504
|
+
`${updateText(locale, 'success', { currentVersion, targetVersion: release.version })}\n`,
|
|
1505
|
+
);
|
|
1506
|
+
if (release.notesUrl) stdout.write(`${updateText(locale, 'releaseNotes')} ${release.notesUrl}\n`);
|
|
1507
|
+
return { kind: 'installed', version: release.version };
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
if (Number.isInteger(installResult?.code) && installResult.code !== 0) {
|
|
1511
|
+
updateProgress.finish();
|
|
1512
|
+
if (explicitUpdate) {
|
|
1513
|
+
return explicitFailure(stderr, 'install_failed', installResult.code, locale);
|
|
1514
|
+
}
|
|
1515
|
+
stderr.write(`${updateText(locale, 'failedContinue', { exitCode: installResult.code })}\n`);
|
|
1516
|
+
return { kind: 'continue', reason: 'install_failed', exitCode: installResult.code };
|
|
1517
|
+
}
|
|
1518
|
+
updateProgress.finish();
|
|
1519
|
+
if (explicitUpdate) return explicitFailure(stderr, 'verification_failed', undefined, locale);
|
|
1520
|
+
stderr.write(`${updateText(locale, 'verifyRestart')}\n`);
|
|
1521
|
+
return { kind: 'update_in_progress' };
|
|
1522
|
+
} finally {
|
|
1523
|
+
if (installerSession?.ready) {
|
|
1524
|
+
try {
|
|
1525
|
+
await installerSession.cancel();
|
|
1526
|
+
} catch {}
|
|
1527
|
+
}
|
|
1528
|
+
if (ownsNoticeLease) {
|
|
1529
|
+
try {
|
|
1530
|
+
await lease.release();
|
|
1531
|
+
} catch {}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function runExplicitUpdate(options) {
|
|
1537
|
+
return runUpdateFlow(options, true);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function runUpdateNotice(options) {
|
|
1541
|
+
return runUpdateFlow(options, false);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
module.exports = {
|
|
1545
|
+
FALLBACK_MANIFEST_URL,
|
|
1546
|
+
PACKAGE_NAME,
|
|
1547
|
+
REGISTRY_URL,
|
|
1548
|
+
SNOOZE_MS,
|
|
1549
|
+
compareSemver,
|
|
1550
|
+
createInstallInvocation,
|
|
1551
|
+
installPinnedVersion,
|
|
1552
|
+
preparePinnedInstaller,
|
|
1553
|
+
isInteractiveUpdateStart,
|
|
1554
|
+
loadReleaseSources,
|
|
1555
|
+
parseFallbackManifest,
|
|
1556
|
+
parseRegistryDocument,
|
|
1557
|
+
promptUpdateAction,
|
|
1558
|
+
readSkippedVersion,
|
|
1559
|
+
readSnoozeState,
|
|
1560
|
+
requestTrustedJson,
|
|
1561
|
+
resolveEffectiveRelease,
|
|
1562
|
+
resolveUpdateLocale,
|
|
1563
|
+
runExplicitUpdate,
|
|
1564
|
+
runUpdateNotice,
|
|
1565
|
+
updateText,
|
|
1566
|
+
writeSkippedVersion,
|
|
1567
|
+
writeSnoozeState,
|
|
1568
|
+
};
|