blun-king-cli 9.1.601 → 9.1.603

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +105 -94
  2. package/LIESMICH.txt +57 -57
  3. package/README.md +90 -90
  4. package/bin/agentspine-king-goal-inbox.mjs +127 -127
  5. package/bin/agentspine-king-goal-intake.mjs +106 -106
  6. package/bin/agentspine-king-host-runner.mjs +109 -109
  7. package/bin/agentspine-king-snapshot-policy.cjs +80 -80
  8. package/bin/agentspine-king-status-policy.mjs +226 -226
  9. package/bin/agentspine-king-worker-host.mjs +160 -160
  10. package/bin/core-bootstrap.js +77 -77
  11. package/bin/launcher-mode.js +93 -93
  12. package/bin/launcher-restart-policy.cjs +131 -131
  13. package/bin/launcher-runtime.js +491 -491
  14. package/bin/managed-node.js +186 -186
  15. package/bin/managed-plugin-selection.cjs +115 -115
  16. package/bin/native-module-repair.js +201 -201
  17. package/bin/node-runtime.js +34 -34
  18. package/bin/node-version.js +49 -49
  19. package/bin/plugin-bootstrap.js +162 -162
  20. package/bin/private-paths.js +1606 -1606
  21. package/bin/runtime-exit-ledger.cjs +144 -144
  22. package/bin/runtime-exit-ledger.d.cts +23 -23
  23. package/bin/standard-tools-bootstrap.js +272 -272
  24. package/bin/turn-thinking-policy.cjs +82 -82
  25. package/bin/update-copy.js +200 -200
  26. package/bin/update-lease.js +597 -597
  27. package/bin/update-notice.js +1568 -1568
  28. package/bin/verify-agent-behavior.cjs +122 -122
  29. package/bin/verify-agent-components.cjs +104 -104
  30. package/bin/verify-bundled-agent-sources.cjs +83 -83
  31. package/bin/windows-node-crash-dump.cjs +289 -289
  32. package/blun.mjs +1709 -1601
  33. package/codebase-index/codebase_index.py +470 -470
  34. package/dist-web/assets/{CodeBlockNode-CXnTLMNr.js → CodeBlockNode-TKKkJX8L.js} +1 -1
  35. 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
  36. package/dist-web/assets/DesignSystemView-DwK1aQMo.js +12 -0
  37. package/dist-web/assets/DesignSystemView-szjTj7ns.css +1 -0
  38. package/dist-web/assets/{Tooltip-CFQEzqC-.js → Tooltip-CTWv7WCU.js} +1 -1
  39. package/dist-web/assets/{abnfDiagram-N423BO3Z-Z1APZG5m.js → abnfDiagram-N423BO3Z-DA8V8Zjh.js} +1 -1
  40. package/dist-web/assets/{arc-C4VyWloT.js → arc-YtddKbdY.js} +1 -1
  41. package/dist-web/assets/{architectureDiagram-T3A2C74G-DpddbUcM.js → architectureDiagram-T3A2C74G-CGEiObmo.js} +1 -1
  42. package/dist-web/assets/{blockDiagram-VBNYF7ZC-BqccOSMr.js → blockDiagram-VBNYF7ZC-Bd5Vt50e.js} +1 -1
  43. package/dist-web/assets/{c4Diagram-5PPSVZJV-OHAcJrTy.js → c4Diagram-5PPSVZJV-D2ApPM54.js} +1 -1
  44. package/dist-web/assets/channel-BnluexuN.js +1 -0
  45. package/dist-web/assets/{chunk-2GRJ4B5K-DqJHs8Fw.js → chunk-2GRJ4B5K-BDcIEZSl.js} +1 -1
  46. package/dist-web/assets/{chunk-2Q5K7J3B-B1nCd91o.js → chunk-2Q5K7J3B-6aSabCX7.js} +1 -1
  47. package/dist-web/assets/{chunk-5RXB4S5H-neG70kZx.js → chunk-5RXB4S5H-BFRQvIJS.js} +1 -1
  48. package/dist-web/assets/{chunk-5VM5RSS4-l7biGqeH.js → chunk-5VM5RSS4-ChAa-DNY.js} +1 -1
  49. package/dist-web/assets/{chunk-6Q2QTUOP-BO-F11Gs.js → chunk-6Q2QTUOP-BIfrb2Jc.js} +1 -1
  50. package/dist-web/assets/{chunk-GF5L2VYU-BtC69fw0.js → chunk-GF5L2VYU-BR0gvtWI.js} +1 -1
  51. package/dist-web/assets/{chunk-JWPE2WC7-DIOubEqr.js → chunk-JWPE2WC7-BgXKsx5E.js} +1 -1
  52. package/dist-web/assets/{chunk-KBJHAD2P-DDUeH_Ba.js → chunk-KBJHAD2P-I8mrZhpz.js} +1 -1
  53. package/dist-web/assets/{chunk-RYQCIY6F-B9gFMVbS.js → chunk-RYQCIY6F-BMPaem92.js} +1 -1
  54. package/dist-web/assets/{chunk-XXDRQBXY-Bn1Xz6TQ.js → chunk-XXDRQBXY-COy0J-0j.js} +1 -1
  55. package/dist-web/assets/classDiagram-JCYQIIEL-BliFdSD9.js +1 -0
  56. package/dist-web/assets/classDiagram-v2-OCEON4UE-BliFdSD9.js +1 -0
  57. package/dist-web/assets/{cose-bilkent-JH36ORCC-DqigO3T4.js → cose-bilkent-JH36ORCC-MosTTRdo.js} +1 -1
  58. package/dist-web/assets/{cynefin-VYW2F7L2-rpD93tQ-.js → cynefin-VYW2F7L2-5tjPgrkP.js} +1 -1
  59. package/dist-web/assets/{cynefinDiagram-MW4NZA55-DbTqnkVJ.js → cynefinDiagram-MW4NZA55-CbDcdInR.js} +1 -1
  60. package/dist-web/assets/{dagre-VZM6K2ZE-DcM8aEVY.js → dagre-VZM6K2ZE-YaBom1ZH.js} +1 -1
  61. package/dist-web/assets/{diagram-7IWD3JNH-UymgsBNa.js → diagram-7IWD3JNH-Da4m-Hgz.js} +1 -1
  62. package/dist-web/assets/{diagram-B4RE2ZJO-BiYu5WPF.js → diagram-B4RE2ZJO-CAUUegxg.js} +1 -1
  63. package/dist-web/assets/{diagram-LBJQPF4R-BD3kkgOO.js → diagram-LBJQPF4R-o21mWKG4.js} +1 -1
  64. package/dist-web/assets/{diagram-Q27KOJAE-D1mYQ_cI.js → diagram-Q27KOJAE-BA3z3sXf.js} +1 -1
  65. package/dist-web/assets/{diagram-UB23O5K3-BtX8u6HG.js → diagram-UB23O5K3-CGRdSSDK.js} +1 -1
  66. package/dist-web/assets/{ebnfDiagram-BXEA7PRR-CBAnFkRF.js → ebnfDiagram-BXEA7PRR-Bw-iU9FP.js} +1 -1
  67. package/dist-web/assets/{erDiagram-JOGREHBK-3vKcWsxm.js → erDiagram-JOGREHBK-BwM1yiR0.js} +1 -1
  68. package/dist-web/assets/{flowDiagram-UKHOOZJN-D9wBbHm2.js → flowDiagram-UKHOOZJN-z05eOR1Q.js} +1 -1
  69. package/dist-web/assets/{ganttDiagram-PKOTCBZU-Cd_dViF-.js → ganttDiagram-PKOTCBZU-BMUubNTl.js} +1 -1
  70. package/dist-web/assets/{gitGraphDiagram-DS77QQ5N-BiEEtTo7.js → gitGraphDiagram-DS77QQ5N-Dccbi1wU.js} +1 -1
  71. package/dist-web/assets/{index-DdEakFJM.js → index-BBbC5LRF.js} +1 -1
  72. package/dist-web/assets/{index-BmKj7CB1.css → index-C9eDH3u-.css} +1 -1
  73. package/dist-web/assets/{index-Ba1OvLjf.js → index-D48p0M2S.js} +2 -2
  74. package/dist-web/assets/{index-CiPA8Kex.js → index-h0KEOmkU.js} +14 -14
  75. package/dist-web/assets/{index10-CNr4U-Nw.js → index10-Qbuw1N6u.js} +1 -1
  76. package/dist-web/assets/{index11-Dto_yZNz.js → index11-g8v025wN.js} +1 -1
  77. package/dist-web/assets/{index5-B2PldaO2.js → index5-BYNcV8BY.js} +1 -1
  78. package/dist-web/assets/{index6-CBdZmmyx.js → index6-BetANtnV.js} +1 -1
  79. package/dist-web/assets/{index7-Dz9oCsUF.js → index7-BcPbgCKN.js} +1 -1
  80. package/dist-web/assets/{index8-B6k25jtU.js → index8-B1lp-RvF.js} +2 -2
  81. package/dist-web/assets/{infoDiagram-6WML65LV-D4ph2sIH.js → infoDiagram-6WML65LV-BF_pbUnN.js} +1 -1
  82. package/dist-web/assets/{ishikawaDiagram-WSZJBQD7-DjKjbMm9.js → ishikawaDiagram-WSZJBQD7-Dedv4urS.js} +1 -1
  83. package/dist-web/assets/{journeyDiagram-NVQOT4AX-DttHDfPQ.js → journeyDiagram-NVQOT4AX-B1Ex3b6o.js} +1 -1
  84. package/dist-web/assets/{kanban-definition-27J2QSJJ-C5gB-W0d.js → kanban-definition-27J2QSJJ-BtCGliBH.js} +1 -1
  85. package/dist-web/assets/{linear-BRGe4-d6.js → linear-ByItc8Qr.js} +1 -1
  86. package/dist-web/assets/{mermaid.core-D-9Sx1No.js → mermaid.core-B1M2O7nN.js} +4 -4
  87. package/dist-web/assets/{mindmap-definition-FAOFIHXS-Cg_hoVIq.js → mindmap-definition-FAOFIHXS-D0UHKUyj.js} +1 -1
  88. package/dist-web/assets/{pegDiagram-VL7TDLO6-CSX9hitD.js → pegDiagram-VL7TDLO6-D7zYCVmG.js} +1 -1
  89. package/dist-web/assets/{pieDiagram-7S7Q4E2Y-0L0mxahZ.js → pieDiagram-7S7Q4E2Y-Bd0i9CHa.js} +1 -1
  90. package/dist-web/assets/{quadrantDiagram-CIZ2JOQS-U483z0er.js → quadrantDiagram-CIZ2JOQS-MG-Ug47d.js} +1 -1
  91. package/dist-web/assets/{railroadDiagram-AXF67PYL-D80a2_DJ.js → railroadDiagram-AXF67PYL-AZdl90Gj.js} +1 -1
  92. package/dist-web/assets/{requirementDiagram-LRYGKXZP-CeyHfq8t.js → requirementDiagram-LRYGKXZP-DjplF9kt.js} +1 -1
  93. package/dist-web/assets/{sankeyDiagram-W5VNT64P-CCISkkrR.js → sankeyDiagram-W5VNT64P-Bi9Zgfjs.js} +1 -1
  94. package/dist-web/assets/{sequenceDiagram-SI44F4Z6-jQfm2XS7.js → sequenceDiagram-SI44F4Z6-ChEpdCvb.js} +1 -1
  95. package/dist-web/assets/{sizeCapture-X5ZJPWSS-lRZsmCoS.js → sizeCapture-X5ZJPWSS-B5RFUdaa.js} +1 -1
  96. package/dist-web/assets/{stateDiagram-OKZ733FA-ClCgTdUm.js → stateDiagram-OKZ733FA-BkRblSKp.js} +1 -1
  97. package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fVkQcACW.js +1 -0
  98. package/dist-web/assets/{swimlanes-SLNWSIFB-BCdOUN05.js → swimlanes-SLNWSIFB-DLXH6Zhc.js} +2 -2
  99. package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-Bg-BiZJQ.js +8 -0
  100. package/dist-web/assets/{timeline-definition-Z64GVDOM-DpWutSKa.js → timeline-definition-Z64GVDOM-C_N88siO.js} +1 -1
  101. package/dist-web/assets/{vennDiagram-T6HMQDX7-DVC5JVsc.js → vennDiagram-T6HMQDX7-DMsnvDt0.js} +1 -1
  102. package/dist-web/assets/{vue.runtime.esm-bundler-BwwRtQwQ.js → vue.runtime.esm-bundler-B-3uIy85.js} +1 -1
  103. package/dist-web/assets/{wardleyDiagram-T6FBY63Y-BK2mrq7L.js → wardleyDiagram-T6FBY63Y-C9SJIPzq.js} +1 -1
  104. package/dist-web/assets/{xychartDiagram-ELKLHX3M-DUJu9F7-.js → xychartDiagram-ELKLHX3M-DZLWA6Go.js} +1 -1
  105. package/dist-web/index.html +32 -32
  106. package/dist-web/vis/index.html.gz +0 -0
  107. package/package.json +1 -1
  108. package/scripts/fix-node-pty-perms.js +53 -53
  109. package/standard-skills/blun-app-design-system/SKILL.md +30 -30
  110. package/standard-skills/blun-app-design-system/provenance.json +17 -17
  111. package/standard-skills/blun-app-design-system/references/tokens.md +48 -48
  112. package/standard-skills/blun-app-design-system/scripts/check-blun-design.mjs +49 -49
  113. package/telegram-plugin/blun.plugin.json +20 -20
  114. package/telegram-plugin/commands/access.md +37 -37
  115. package/telegram-plugin/dist/bridge.mjs +81 -81
  116. package/telegram-plugin/dist/mcp-server.mjs +158 -158
  117. package/worker-host.mjs +932 -932
  118. package/dist-web/assets/DesignSystemView-6HgAUEmC.css +0 -1
  119. package/dist-web/assets/DesignSystemView-BbMOu-XO.js +0 -12
  120. package/dist-web/assets/channel-B2VOQOI-.js +0 -1
  121. package/dist-web/assets/classDiagram-JCYQIIEL-BA1TjAxj.js +0 -1
  122. package/dist-web/assets/classDiagram-v2-OCEON4UE-BA1TjAxj.js +0 -1
  123. package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fi_lrTAx.js +0 -1
  124. package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-BUd_uYK3.js +0 -8
@@ -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
+ };