agl 1.0.17 → 21.0.2

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 (277) hide show
  1. package/.smartconfig.json +55 -0
  2. package/changelog.md +1026 -0
  3. package/cli.js +14 -0
  4. package/dist_serve/bundle.js +44778 -0
  5. package/dist_serve/index.html +13 -0
  6. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  7. package/dist_ts/00_commitinfo_data.js +9 -0
  8. package/dist_ts/classes.authmodels.d.ts +126 -0
  9. package/dist_ts/classes.authmodels.js +1422 -0
  10. package/dist_ts/classes.authstore.d.ts +160 -0
  11. package/dist_ts/classes.authstore.js +2483 -0
  12. package/dist_ts/classes.browserresourcehost.d.ts +85 -0
  13. package/dist_ts/classes.browserresourcehost.js +912 -0
  14. package/dist_ts/classes.cli.d.ts +21 -0
  15. package/dist_ts/classes.cli.js +1082 -0
  16. package/dist_ts/classes.config.d.ts +23 -0
  17. package/dist_ts/classes.config.js +267 -0
  18. package/dist_ts/classes.controller.d.ts +523 -0
  19. package/dist_ts/classes.controller.js +10611 -0
  20. package/dist_ts/classes.controllermanagement.d.ts +32 -0
  21. package/dist_ts/classes.controllermanagement.js +222 -0
  22. package/dist_ts/classes.draftmanager.d.ts +30 -0
  23. package/dist_ts/classes.draftmanager.js +179 -0
  24. package/dist_ts/classes.embeddeddb.d.ts +30 -0
  25. package/dist_ts/classes.embeddeddb.js +159 -0
  26. package/dist_ts/classes.flexframedtransport.d.ts +56 -0
  27. package/dist_ts/classes.flexframedtransport.js +275 -0
  28. package/dist_ts/classes.flexmodels.private.d.ts +231 -0
  29. package/dist_ts/classes.flexmodels.private.js +985 -0
  30. package/dist_ts/classes.flexmodels.public.d.ts +207 -0
  31. package/dist_ts/classes.flexmodels.public.js +885 -0
  32. package/dist_ts/classes.flexprojectionreader.d.ts +89 -0
  33. package/dist_ts/classes.flexprojectionreader.js +580 -0
  34. package/dist_ts/classes.flexservice.d.ts +196 -0
  35. package/dist_ts/classes.flexservice.js +3704 -0
  36. package/dist_ts/classes.flexstore.d.ts +174 -0
  37. package/dist_ts/classes.flexstore.js +3400 -0
  38. package/dist_ts/classes.flexsupervisor.d.ts +105 -0
  39. package/dist_ts/classes.flexsupervisor.js +1191 -0
  40. package/dist_ts/classes.gitreversion.d.ts +431 -0
  41. package/dist_ts/classes.gitreversion.js +10592 -0
  42. package/dist_ts/classes.harnessoperationmanager.d.ts +113 -0
  43. package/dist_ts/classes.harnessoperationmanager.js +602 -0
  44. package/dist_ts/classes.opencodeclient.d.ts +146 -0
  45. package/dist_ts/classes.opencodeclient.js +3003 -0
  46. package/dist_ts/classes.opencodesupervisor.d.ts +60 -0
  47. package/dist_ts/classes.opencodesupervisor.js +1003 -0
  48. package/dist_ts/classes.passkeymanager.d.ts +34 -0
  49. package/dist_ts/classes.passkeymanager.js +456 -0
  50. package/dist_ts/classes.processinspection.d.ts +50 -0
  51. package/dist_ts/classes.processinspection.js +484 -0
  52. package/dist_ts/classes.resourcecoordinator.d.ts +64 -0
  53. package/dist_ts/classes.resourcecoordinator.js +294 -0
  54. package/dist_ts/classes.sessionintelligencereader.d.ts +13 -0
  55. package/dist_ts/classes.sessionintelligencereader.js +40 -0
  56. package/dist_ts/classes.systemmetricssampler.d.ts +53 -0
  57. package/dist_ts/classes.systemmetricssampler.js +168 -0
  58. package/dist_ts/classes.terminalmanager.d.ts +95 -0
  59. package/dist_ts/classes.terminalmanager.js +981 -0
  60. package/dist_ts/classes.terminalresourcehost.d.ts +20 -0
  61. package/dist_ts/classes.terminalresourcehost.js +84 -0
  62. package/dist_ts/classes.upgradecoordinator.d.ts +244 -0
  63. package/dist_ts/classes.upgradecoordinator.js +2155 -0
  64. package/dist_ts/classes.upgradetransaction.d.ts +92 -0
  65. package/dist_ts/classes.upgradetransaction.js +2125 -0
  66. package/dist_ts/classes.uploadmanager.d.ts +24 -0
  67. package/dist_ts/classes.uploadmanager.js +210 -0
  68. package/dist_ts/classes.validation.d.ts +91 -0
  69. package/dist_ts/classes.validation.js +1087 -0
  70. package/dist_ts/flexharness.child.d.ts +1 -0
  71. package/dist_ts/flexharness.child.js +28 -0
  72. package/dist_ts/functions.canonicaldirectory.d.ts +21 -0
  73. package/dist_ts/functions.canonicaldirectory.js +144 -0
  74. package/dist_ts/functions.controllerdataroot.d.ts +15 -0
  75. package/dist_ts/functions.controllerdataroot.js +76 -0
  76. package/dist_ts/functions.embeddeddb.d.ts +2 -0
  77. package/dist_ts/functions.embeddeddb.js +21 -0
  78. package/dist_ts/functions.flexcredentialstore.d.ts +16 -0
  79. package/dist_ts/functions.flexcredentialstore.js +93 -0
  80. package/dist_ts/functions.opencodeauth.d.ts +3 -0
  81. package/dist_ts/functions.opencodeauth.js +31 -0
  82. package/dist_ts/functions.runtimeenvironment.d.ts +2 -0
  83. package/dist_ts/functions.runtimeenvironment.js +79 -0
  84. package/dist_ts/functions.upgradefollower.d.ts +3 -0
  85. package/dist_ts/functions.upgradefollower.js +95 -0
  86. package/dist_ts/index.d.ts +24 -0
  87. package/dist_ts/index.js +25 -0
  88. package/dist_ts/interfaces.auth.d.ts +181 -0
  89. package/dist_ts/interfaces.auth.js +82 -0
  90. package/dist_ts/interfaces.config.d.ts +34 -0
  91. package/dist_ts/interfaces.config.js +2 -0
  92. package/dist_ts/interfaces.flexipc.d.ts +939 -0
  93. package/dist_ts/interfaces.flexipc.js +1492 -0
  94. package/dist_ts/interfaces.gitreversion.d.ts +246 -0
  95. package/dist_ts/interfaces.gitreversion.js +2 -0
  96. package/dist_ts/interfaces.lifecycle.d.ts +37 -0
  97. package/dist_ts/interfaces.lifecycle.js +2 -0
  98. package/dist_ts/interfaces.opencode.d.ts +101 -0
  99. package/dist_ts/interfaces.opencode.js +3 -0
  100. package/dist_ts/interfaces.projects.d.ts +156 -0
  101. package/dist_ts/interfaces.projects.js +5 -0
  102. package/dist_ts/plugins.d.ts +36 -0
  103. package/dist_ts/plugins.js +40 -0
  104. package/dist_ts_interfaces/index.d.ts +2 -0
  105. package/dist_ts_interfaces/index.js +3 -0
  106. package/dist_ts_interfaces/interfaces.d.ts +747 -0
  107. package/dist_ts_interfaces/interfaces.js +47 -0
  108. package/dist_ts_interfaces/plugins.d.ts +4 -0
  109. package/dist_ts_interfaces/plugins.js +3 -0
  110. package/dist_ts_interfaces/requests.d.ts +806 -0
  111. package/dist_ts_interfaces/requests.js +2 -0
  112. package/dist_ts_migration/classes.documentmigrationrunner.d.ts +8 -0
  113. package/dist_ts_migration/classes.documentmigrationrunner.js +24 -0
  114. package/dist_ts_migration/classes.flexmigrationrunner.d.ts +11 -0
  115. package/dist_ts_migration/classes.flexmigrationrunner.js +25 -0
  116. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.d.ts +10 -0
  117. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.js +15 -0
  118. package/dist_ts_migration/classes.migrationrunner.d.ts +8 -0
  119. package/dist_ts_migration/classes.migrationrunner.js +34 -0
  120. package/dist_ts_migration/index.d.ts +12 -0
  121. package/dist_ts_migration/index.js +13 -0
  122. package/dist_ts_migration/v10_flexharnessv3migration.d.ts +11 -0
  123. package/dist_ts_migration/v10_flexharnessv3migration.js +23 -0
  124. package/dist_ts_migration/v13_flexprojectionorder.d.ts +11 -0
  125. package/dist_ts_migration/v13_flexprojectionorder.js +23 -0
  126. package/dist_ts_migration/v16_flexprovidercredentials.d.ts +22 -0
  127. package/dist_ts_migration/v16_flexprovidercredentials.js +149 -0
  128. package/dist_ts_migration/v18_resourcelayout.d.ts +22 -0
  129. package/dist_ts_migration/v18_resourcelayout.js +23 -0
  130. package/dist_ts_migration/v18_resourcelayoutmigration.d.ts +6 -0
  131. package/dist_ts_migration/v18_resourcelayoutmigration.js +61 -0
  132. package/dist_ts_migration/v19_flexprojectmanagement.d.ts +11 -0
  133. package/dist_ts_migration/v19_flexprojectmanagement.js +21 -0
  134. package/dist_ts_migration/v1_embeddeddatabaselocation.d.ts +28 -0
  135. package/dist_ts_migration/v1_embeddeddatabaselocation.js +415 -0
  136. package/dist_ts_migration/v22_projectfilesystemidentitymigration.d.ts +7 -0
  137. package/dist_ts_migration/v22_projectfilesystemidentitymigration.js +105 -0
  138. package/dist_ts_migration/v2_controllerdataroot.d.ts +130 -0
  139. package/dist_ts_migration/v2_controllerdataroot.js +1887 -0
  140. package/dist_ts_migration/v6_protocoldocumentmigration.d.ts +7 -0
  141. package/dist_ts_migration/v6_protocoldocumentmigration.js +100 -0
  142. package/dist_ts_migration/v6_protocoldocuments.d.ts +7 -0
  143. package/dist_ts_migration/v6_protocoldocuments.js +138 -0
  144. package/html/index.html +13 -0
  145. package/{LICENSE → license.md} +2 -2
  146. package/package.json +86 -48
  147. package/readme.md +480 -0
  148. package/readme.plan.md +188 -0
  149. package/ts/00_commitinfo_data.ts +8 -0
  150. package/ts/classes.authmodels.ts +1245 -0
  151. package/ts/classes.authstore.ts +3274 -0
  152. package/ts/classes.browserresourcehost.ts +1205 -0
  153. package/ts/classes.cli.ts +1254 -0
  154. package/ts/classes.config.ts +351 -0
  155. package/ts/classes.controller.ts +14426 -0
  156. package/ts/classes.controllermanagement.ts +330 -0
  157. package/ts/classes.draftmanager.ts +232 -0
  158. package/ts/classes.embeddeddb.ts +166 -0
  159. package/ts/classes.flexframedtransport.ts +283 -0
  160. package/ts/classes.flexmodels.private.ts +823 -0
  161. package/ts/classes.flexmodels.public.ts +732 -0
  162. package/ts/classes.flexprojectionreader.ts +816 -0
  163. package/ts/classes.flexservice.ts +4302 -0
  164. package/ts/classes.flexstore.ts +4565 -0
  165. package/ts/classes.flexsupervisor.ts +1453 -0
  166. package/ts/classes.gitreversion.ts +13353 -0
  167. package/ts/classes.harnessoperationmanager.ts +870 -0
  168. package/ts/classes.opencodeclient.ts +4110 -0
  169. package/ts/classes.opencodesupervisor.ts +1280 -0
  170. package/ts/classes.passkeymanager.ts +570 -0
  171. package/ts/classes.processinspection.ts +623 -0
  172. package/ts/classes.resourcecoordinator.ts +436 -0
  173. package/ts/classes.sessionintelligencereader.ts +52 -0
  174. package/ts/classes.systemmetricssampler.ts +215 -0
  175. package/ts/classes.terminalmanager.ts +1207 -0
  176. package/ts/classes.terminalresourcehost.ts +122 -0
  177. package/ts/classes.upgradecoordinator.ts +2675 -0
  178. package/ts/classes.upgradetransaction.ts +2900 -0
  179. package/ts/classes.uploadmanager.ts +240 -0
  180. package/ts/classes.validation.ts +1409 -0
  181. package/ts/flexharness.child.ts +35 -0
  182. package/ts/functions.canonicaldirectory.ts +213 -0
  183. package/ts/functions.controllerdataroot.ts +107 -0
  184. package/ts/functions.embeddeddb.ts +22 -0
  185. package/ts/functions.flexcredentialstore.ts +128 -0
  186. package/ts/functions.opencodeauth.ts +34 -0
  187. package/ts/functions.runtimeenvironment.ts +90 -0
  188. package/ts/functions.upgradefollower.ts +115 -0
  189. package/ts/index.ts +36 -6
  190. package/ts/interfaces.auth.ts +316 -0
  191. package/ts/interfaces.config.ts +37 -0
  192. package/ts/interfaces.flexipc.ts +2586 -0
  193. package/ts/interfaces.gitreversion.ts +336 -0
  194. package/ts/interfaces.lifecycle.ts +46 -0
  195. package/ts/interfaces.opencode.ts +153 -0
  196. package/ts/interfaces.projects.ts +182 -0
  197. package/ts/plugins.ts +64 -0
  198. package/ts_interfaces/index.ts +2 -0
  199. package/ts_interfaces/interfaces.ts +827 -0
  200. package/ts_interfaces/plugins.ts +15 -0
  201. package/ts_interfaces/requests.ts +1186 -0
  202. package/ts_migration/classes.documentmigrationrunner.ts +32 -0
  203. package/ts_migration/classes.flexmigrationrunner.ts +33 -0
  204. package/ts_migration/classes.flexpostharnessmigrationrunner.ts +22 -0
  205. package/ts_migration/classes.migrationrunner.ts +40 -0
  206. package/ts_migration/index.ts +12 -0
  207. package/ts_migration/v10_flexharnessv3migration.ts +35 -0
  208. package/ts_migration/v13_flexprojectionorder.ts +33 -0
  209. package/ts_migration/v16_flexprovidercredentials.ts +196 -0
  210. package/ts_migration/v18_resourcelayout.ts +55 -0
  211. package/ts_migration/v18_resourcelayoutmigration.ts +68 -0
  212. package/ts_migration/v19_flexprojectmanagement.ts +28 -0
  213. package/ts_migration/v1_embeddeddatabaselocation.ts +560 -0
  214. package/ts_migration/v22_projectfilesystemidentitymigration.ts +121 -0
  215. package/ts_migration/v2_controllerdataroot.ts +2349 -0
  216. package/ts_migration/v6_protocoldocumentmigration.ts +144 -0
  217. package/ts_migration/v6_protocoldocuments.ts +198 -0
  218. package/ts_web/00_commitinfo_data.ts +8 -0
  219. package/ts_web/classes.controllersocketclient.ts +517 -0
  220. package/ts_web/classes.sessiondraftsync.ts +390 -0
  221. package/ts_web/elements.harnesscontrollerapp.ts +11494 -0
  222. package/ts_web/index.ts +2 -0
  223. package/ts_web/plugins.ts +31 -0
  224. package/.gitlab-ci.yml +0 -71
  225. package/.npmignore +0 -5
  226. package/README.md +0 -40
  227. package/dist/cli.js +0 -2
  228. package/dist/index.d.ts +0 -0
  229. package/dist/index.js +0 -9
  230. package/dist/mod_bundle/index.d.ts +0 -1
  231. package/dist/mod_bundle/index.js +0 -18
  232. package/dist/mod_bundle/mod.plugins.d.ts +0 -4
  233. package/dist/mod_bundle/mod.plugins.js +0 -11
  234. package/dist/mod_install/index.d.ts +0 -4
  235. package/dist/mod_install/index.js +0 -21
  236. package/dist/mod_install/mod.plugins.d.ts +0 -1
  237. package/dist/mod_install/mod.plugins.js +0 -7
  238. package/dist/mod_standard/index.d.ts +0 -4
  239. package/dist/mod_standard/index.js +0 -21
  240. package/dist/mod_standard/mod.browser.d.ts +0 -6
  241. package/dist/mod_standard/mod.browser.js +0 -14
  242. package/dist/mod_standard/mod.clean.d.ts +0 -4
  243. package/dist/mod_standard/mod.clean.js +0 -12
  244. package/dist/mod_standard/mod.compile.d.ts +0 -4
  245. package/dist/mod_standard/mod.compile.js +0 -21
  246. package/dist/mod_standard/mod.plugins.d.ts +0 -8
  247. package/dist/mod_standard/mod.plugins.js +0 -19
  248. package/dist/mod_standard/mod.test.d.ts +0 -4
  249. package/dist/mod_standard/mod.test.js +0 -12
  250. package/dist/plr.cli.d.ts +0 -3
  251. package/dist/plr.cli.js +0 -57
  252. package/dist/plr.config.d.ts +0 -11
  253. package/dist/plr.config.js +0 -26
  254. package/dist/plr.mods.d.ts +0 -17
  255. package/dist/plr.mods.js +0 -17
  256. package/dist/plr.paths.d.ts +0 -19
  257. package/dist/plr.paths.js +0 -26
  258. package/dist/plr.plugins.d.ts +0 -8
  259. package/dist/plr.plugins.js +0 -16
  260. package/npmextra.json +0 -10
  261. package/ts/mod_bundle/index.ts +0 -20
  262. package/ts/mod_bundle/mod.plugins.ts +0 -9
  263. package/ts/mod_install/index.ts +0 -20
  264. package/ts/mod_install/mod.plugins.ts +0 -1
  265. package/ts/mod_standard/index.ts +0 -23
  266. package/ts/mod_standard/mod.browser.ts +0 -17
  267. package/ts/mod_standard/mod.clean.ts +0 -13
  268. package/ts/mod_standard/mod.compile.ts +0 -23
  269. package/ts/mod_standard/mod.plugins.ts +0 -17
  270. package/ts/mod_standard/mod.test.ts +0 -13
  271. package/ts/plr.cli.ts +0 -60
  272. package/ts/plr.config.ts +0 -36
  273. package/ts/plr.mods.ts +0 -25
  274. package/ts/plr.paths.ts +0 -29
  275. package/ts/plr.plugins.ts +0 -17
  276. package/tslint.json +0 -3
  277. package/yarn.lock +0 -3464
@@ -0,0 +1,2900 @@
1
+ import * as plugins from './plugins.js';
2
+ import { commitinfo } from './00_commitinfo_data.js';
3
+ import {
4
+ controllerPackageName,
5
+ controllerProtocolVersion,
6
+ controllerUpgradeManagementVersion,
7
+ type IControllerStatus,
8
+ } from '../ts_interfaces/index.js';
9
+ import { controllerDataDirectory } from './classes.config.js';
10
+ import {
11
+ isLoopbackPortListening,
12
+ queryControllerStatus,
13
+ requestControllerUpgradeFinalize,
14
+ requestControllerUpgradePrepareBegin,
15
+ stopVerifiedController,
16
+ } from './classes.controllermanagement.js';
17
+ import {
18
+ inspectControllerProcess,
19
+ listControllerDataWriterProcessesForCli,
20
+ listControllerProcessesForCli,
21
+ processIdentityHasCliCommand,
22
+ readControllerProcessIdentity,
23
+ readProcessGroupMemberPids,
24
+ } from './classes.processinspection.js';
25
+ import {
26
+ createUpgradeToken,
27
+ type TUpgradeTransaction,
28
+ type IUpgradeTransactionV3,
29
+ type IUpgradeExpectedController,
30
+ type IUpgradeWorkerPayload,
31
+ UpgradeCoordinator,
32
+ upgradeCoordinationVersion,
33
+ upgradePackageTransitionTransactionVersion,
34
+ upgradePackageTransitionSource,
35
+ upgradePackageTransitionTarget,
36
+ upgradeTokenEnvironmentVariable,
37
+ serializeUpgradeWorkerPayload,
38
+ } from './classes.upgradecoordinator.js';
39
+
40
+ const npmRegistryUrl = 'https://registry.npmjs.org/';
41
+ const commandOutputByteLimit = 1024 * 1024;
42
+ const loggedCommandOutputByteLimit = 16 * 1024 * 1024;
43
+ const packageManagerTimeoutMs = 10 * 60 * 1000;
44
+ const controllerRestartTimeoutMs = 90_000;
45
+ const commandGroupDrainTimeoutMs = 5_000;
46
+ const candidateCleanupRetryIntervalMs = 250;
47
+ const retainedUpgradeLogCount = 10;
48
+ const minimumPrunableUpgradeLogAgeMs = 30 * 60 * 1000;
49
+ const upgradePreparationPollMs = 250;
50
+ const upgradePreparationSourceCheckMs = 500;
51
+ const upgradePreparationAcceptanceReconciliationMs = 5_000;
52
+ const upgradePreparationCleanupAllowanceMs = 60_000;
53
+ const upgradeLogPattern = /^upgrade-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{6}\.[0-9]{3}Z-[A-Za-z0-9_-]{10}\.log$/;
54
+ const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
55
+ let upgradeWorkerCliPath: string | undefined;
56
+ const currentPackageIdentity = String(controllerPackageName)
57
+ === upgradePackageTransitionSource.packageName
58
+ ? upgradePackageTransitionSource
59
+ : upgradePackageTransitionTarget;
60
+ const currentCliName = currentPackageIdentity.cliName;
61
+
62
+ const packageManagerEnvironmentAllowlist = new Set([
63
+ 'CI',
64
+ 'COREPACK_HOME',
65
+ 'FORCE_COLOR',
66
+ 'HOME',
67
+ 'HTTP_PROXY',
68
+ 'HTTPS_PROXY',
69
+ 'LANG',
70
+ 'LOGNAME',
71
+ 'NODE_EXTRA_CA_CERTS',
72
+ 'NO_PROXY',
73
+ 'NPM_CONFIG_REGISTRY',
74
+ 'NPM_CONFIG_USERCONFIG',
75
+ 'PATH',
76
+ 'PNPM_HOME',
77
+ 'SHELL',
78
+ 'SSL_CERT_DIR',
79
+ 'SSL_CERT_FILE',
80
+ 'TEMP',
81
+ 'TERM',
82
+ 'TMP',
83
+ 'TMPDIR',
84
+ 'TZ',
85
+ 'USER',
86
+ 'XDG_CACHE_HOME',
87
+ 'XDG_CONFIG_HOME',
88
+ 'XDG_DATA_HOME',
89
+ 'XDG_RUNTIME_DIR',
90
+ 'XDG_STATE_HOME',
91
+ ]);
92
+
93
+ interface IParsedSemver {
94
+ major: string;
95
+ minor: string;
96
+ patch: string;
97
+ prerelease: string[];
98
+ }
99
+
100
+ class UpgradeCommandCleanupError extends Error {}
101
+
102
+ interface IGlobalListDependency {
103
+ version?: unknown;
104
+ path?: unknown;
105
+ }
106
+
107
+ interface IGlobalListResult {
108
+ path?: unknown;
109
+ dependencies?: Record<string, IGlobalListDependency>;
110
+ }
111
+
112
+ export interface IPnpmGlobalInstallation {
113
+ packageManagerPath: string;
114
+ globalRoot: string;
115
+ globalBinDirectory: string;
116
+ packageDirectory: string;
117
+ cliPath: string;
118
+ packageVersion: string;
119
+ }
120
+
121
+ export interface IDetachedUpgradeWorker {
122
+ pid: number;
123
+ processGroupId: number;
124
+ fingerprint: string;
125
+ cliPath: string;
126
+ logFilePath: string;
127
+ }
128
+
129
+ const proxyEnvironmentValueIsSafe = (valueArg: string): boolean => {
130
+ try {
131
+ const parsed = new URL(valueArg);
132
+ return parsed.username.length === 0 && parsed.password.length === 0;
133
+ } catch {
134
+ return false;
135
+ }
136
+ };
137
+
138
+ export const createPackageManagerEnvironment = (
139
+ sourceArg: NodeJS.ProcessEnv = process.env,
140
+ ): NodeJS.ProcessEnv => {
141
+ const environment: NodeJS.ProcessEnv = {};
142
+ for (const [name, value] of Object.entries(sourceArg)) {
143
+ if (value === undefined) continue;
144
+ const normalizedName = name.toUpperCase();
145
+ if (
146
+ !packageManagerEnvironmentAllowlist.has(normalizedName)
147
+ && !normalizedName.startsWith('LC_')
148
+ ) continue;
149
+ if (
150
+ (normalizedName === 'HTTP_PROXY' || normalizedName === 'HTTPS_PROXY')
151
+ && !proxyEnvironmentValueIsSafe(value)
152
+ ) continue;
153
+ environment[name] = value;
154
+ }
155
+ return environment;
156
+ };
157
+
158
+ const findExecutable = async (nameArg: string, environmentArg: NodeJS.ProcessEnv): Promise<string> => {
159
+ const pathValue = environmentArg.PATH;
160
+ if (!pathValue) throw new Error(`Unable to locate ${nameArg}: PATH is empty.`);
161
+ for (const directory of pathValue.split(plugins.path.delimiter)) {
162
+ if (!directory) continue;
163
+ const candidate = plugins.path.join(directory, nameArg);
164
+ try {
165
+ await plugins.fs.promises.access(candidate, plugins.fs.constants.X_OK);
166
+ return await plugins.fs.promises.realpath(candidate);
167
+ } catch {
168
+ continue;
169
+ }
170
+ }
171
+ throw new Error(`Unable to locate ${nameArg} on PATH.`);
172
+ };
173
+
174
+ interface IManagedCommandOptions {
175
+ executable: string;
176
+ arguments: string[];
177
+ environment: NodeJS.ProcessEnv;
178
+ timeoutMs: number;
179
+ captureOutput: boolean;
180
+ outputByteLimit?: number;
181
+ }
182
+
183
+ interface IPreparedManagedCommand extends IManagedCommandOptions {
184
+ cwd: string;
185
+ workerIdentity: NonNullable<Awaited<ReturnType<typeof readControllerProcessIdentity>>>;
186
+ ownsWorkerProcessGroup: boolean;
187
+ }
188
+
189
+ const prepareManagedCommand = async (
190
+ optionsArg: IManagedCommandOptions,
191
+ ): Promise<IPreparedManagedCommand> => {
192
+ const workerIdentity = await readControllerProcessIdentity(process.pid);
193
+ if (!workerIdentity) throw new Error('Unable to establish upgrade command ownership.');
194
+ const ownsWorkerProcessGroup = workerIdentity.processGroupLeader
195
+ && await processIdentityHasCliCommand(
196
+ workerIdentity,
197
+ upgradeWorkerCliPath ?? await resolveCurrentCliPath(),
198
+ '__upgrade-worker',
199
+ { allowMissingAbsolutePath: true },
200
+ );
201
+ return {
202
+ ...optionsArg,
203
+ cwd: plugins.os.homedir(),
204
+ workerIdentity,
205
+ ownsWorkerProcessGroup,
206
+ };
207
+ };
208
+
209
+ const runPreparedManagedCommand = async (
210
+ optionsArg: IPreparedManagedCommand,
211
+ ): Promise<{ stdout: string; stderr: string }> => {
212
+ const child = plugins.childProcess.spawn(optionsArg.executable, optionsArg.arguments, {
213
+ cwd: optionsArg.cwd,
214
+ detached: !optionsArg.ownsWorkerProcessGroup,
215
+ env: optionsArg.environment,
216
+ shell: false,
217
+ stdio: ['ignore', 'pipe', 'pipe'],
218
+ });
219
+ const outcomePromise = new Promise<{
220
+ code: number | null;
221
+ signal: NodeJS.Signals | null;
222
+ error?: Error;
223
+ }>((resolve) => {
224
+ child.once('error', (errorArg) => resolve({ code: null, signal: null, error: errorArg }));
225
+ child.once('close', (codeArg, signalArg) => resolve({ code: codeArg, signal: signalArg }));
226
+ });
227
+ const commandProcessGroupId = optionsArg.ownsWorkerProcessGroup
228
+ ? optionsArg.workerIdentity.processGroupId
229
+ : child.pid;
230
+ let stdout = '';
231
+ let stderr = '';
232
+ let outputBytes = 0;
233
+ let outputError: Error | undefined;
234
+ const recordOutput = (
235
+ streamArg: 'stdout' | 'stderr',
236
+ chunkArg: Buffer,
237
+ sourceArg?: NodeJS.ReadableStream,
238
+ ) => {
239
+ outputBytes += chunkArg.byteLength;
240
+ if (outputBytes > (optionsArg.outputByteLimit ?? commandOutputByteLimit)) {
241
+ outputError ??= new Error('Upgrade command output exceeded its size limit.');
242
+ return;
243
+ }
244
+ if (optionsArg.captureOutput) {
245
+ if (streamArg === 'stdout') stdout += chunkArg.toString('utf8');
246
+ else stderr += chunkArg.toString('utf8');
247
+ return;
248
+ }
249
+ const destination = streamArg === 'stdout' ? process.stdout : process.stderr;
250
+ if (!destination.write(chunkArg) && sourceArg && 'pause' in sourceArg && 'resume' in sourceArg) {
251
+ sourceArg.pause();
252
+ destination.once('drain', () => sourceArg.resume());
253
+ }
254
+ };
255
+ child.stdout?.on('data', (chunkArg: Buffer) => recordOutput('stdout', chunkArg, child.stdout!));
256
+ child.stderr?.on('data', (chunkArg: Buffer) => recordOutput('stderr', chunkArg, child.stderr!));
257
+
258
+ const listCommandPids = async (): Promise<number[]> => {
259
+ if (!commandProcessGroupId) {
260
+ return child.pid && child.exitCode === null && child.signalCode === null ? [child.pid] : [];
261
+ }
262
+ const members = await readProcessGroupMemberPids(commandProcessGroupId);
263
+ return optionsArg.ownsWorkerProcessGroup
264
+ ? members.filter((processIdArg) => processIdArg !== process.pid)
265
+ : members;
266
+ };
267
+ const signalDirectChild = (signalArg: NodeJS.Signals): void => {
268
+ if (!child.pid || child.exitCode !== null || child.signalCode !== null) return;
269
+ try {
270
+ process.kill(child.pid, signalArg);
271
+ } catch (errorArg) {
272
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ESRCH') throw errorArg;
273
+ }
274
+ };
275
+ const signalCommand = async (signalArg: NodeJS.Signals): Promise<void> => {
276
+ let processIds: number[];
277
+ try {
278
+ processIds = await listCommandPids();
279
+ } catch (errorArg) {
280
+ signalDirectChild(signalArg);
281
+ throw errorArg;
282
+ }
283
+ for (const processId of processIds) {
284
+ try {
285
+ process.kill(processId, signalArg);
286
+ } catch (errorArg) {
287
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ESRCH') throw errorArg;
288
+ }
289
+ }
290
+ };
291
+ const waitForCommandDrain = async (timeoutMsArg: number): Promise<boolean> => {
292
+ const deadline = Date.now() + timeoutMsArg;
293
+ while (Date.now() < deadline) {
294
+ if ((await listCommandPids()).length === 0) return true;
295
+ await new Promise((resolve) => setTimeout(resolve, 50));
296
+ }
297
+ return (await listCommandPids()).length === 0;
298
+ };
299
+ let interruption: string | undefined;
300
+ let terminating: Promise<void> | undefined;
301
+ let terminationError: unknown;
302
+ const terminate = (): Promise<void> => {
303
+ if (terminating) return terminating;
304
+ terminating = (async () => {
305
+ try {
306
+ await signalCommand('SIGTERM');
307
+ if (await waitForCommandDrain(commandGroupDrainTimeoutMs)) return;
308
+ await signalCommand('SIGKILL');
309
+ if (!await waitForCommandDrain(commandGroupDrainTimeoutMs)) {
310
+ throw new Error('The upgrade command process group did not terminate.');
311
+ }
312
+ } catch (errorArg) {
313
+ terminationError = errorArg;
314
+ signalDirectChild('SIGKILL');
315
+ throw errorArg;
316
+ }
317
+ })();
318
+ return terminating;
319
+ };
320
+ const beginTermination = (reasonArg: string): void => {
321
+ interruption ??= reasonArg;
322
+ void terminate().catch(() => undefined);
323
+ };
324
+ const onSigint = () => beginTermination('SIGINT');
325
+ const onSigterm = () => beginTermination('SIGTERM');
326
+ process.on('SIGINT', onSigint);
327
+ process.on('SIGTERM', onSigterm);
328
+ const timeout = setTimeout(() => beginTermination('timeout'), optionsArg.timeoutMs);
329
+ const outputLimitPoll = setInterval(() => {
330
+ if (outputError) beginTermination('output limit');
331
+ }, 25);
332
+ try {
333
+ const outcome = await outcomePromise;
334
+ try {
335
+ if (!await waitForCommandDrain(2_000)) {
336
+ beginTermination('surviving child processes');
337
+ }
338
+ } catch (errorArg) {
339
+ terminationError = errorArg;
340
+ signalDirectChild('SIGKILL');
341
+ }
342
+ if (terminating) await terminating.catch(() => undefined);
343
+ if (terminationError) {
344
+ throw new UpgradeCommandCleanupError('Upgrade command cleanup failed.', {
345
+ cause: terminationError,
346
+ });
347
+ }
348
+ if (interruption) {
349
+ throw new Error(`${plugins.path.basename(optionsArg.executable)} was interrupted by ${interruption}.`);
350
+ }
351
+ if (outputError) throw outputError;
352
+ if (outcome.error) throw outcome.error;
353
+ if (outcome.code !== 0) {
354
+ throw new Error(
355
+ `${plugins.path.basename(optionsArg.executable)} exited with ${outcome.signal ?? outcome.code ?? 'an unknown result'}${stderr.trim() ? `: ${stderr.trim()}` : '.'}`,
356
+ );
357
+ }
358
+ return { stdout, stderr };
359
+ } finally {
360
+ clearTimeout(timeout);
361
+ clearInterval(outputLimitPoll);
362
+ process.removeListener('SIGINT', onSigint);
363
+ process.removeListener('SIGTERM', onSigterm);
364
+ }
365
+ };
366
+
367
+ const runManagedCommand = async (
368
+ optionsArg: IManagedCommandOptions,
369
+ ): Promise<{ stdout: string; stderr: string }> => await runPreparedManagedCommand(
370
+ await prepareManagedCommand(optionsArg),
371
+ );
372
+
373
+ const runCapturedCommand = async (
374
+ executableArg: string,
375
+ argumentsArg: string[],
376
+ environmentArg: NodeJS.ProcessEnv,
377
+ timeoutMsArg = 30_000,
378
+ outputByteLimitArg = commandOutputByteLimit,
379
+ ): Promise<string> => {
380
+ const result = await runManagedCommand({
381
+ executable: executableArg,
382
+ arguments: argumentsArg,
383
+ environment: environmentArg,
384
+ timeoutMs: timeoutMsArg,
385
+ captureOutput: true,
386
+ outputByteLimit: outputByteLimitArg,
387
+ });
388
+ return result.stdout;
389
+ };
390
+
391
+ const assertSingleLineAbsolutePath = async (valueArg: string, nameArg: string): Promise<string> => {
392
+ const value = valueArg.trim();
393
+ if (!value || value.includes('\n') || !plugins.path.isAbsolute(value)) {
394
+ throw new Error(`${nameArg} did not return one absolute path.`);
395
+ }
396
+ return await plugins.fs.promises.realpath(value);
397
+ };
398
+
399
+ const readGlobalListDependency = (
400
+ outputArg: string,
401
+ packageNameArg: string,
402
+ ): { version: string; path: string } => {
403
+ let value: unknown;
404
+ try {
405
+ value = JSON.parse(outputArg) as unknown;
406
+ } catch {
407
+ throw new Error('pnpm list --global returned malformed JSON.');
408
+ }
409
+ if (!Array.isArray(value)) {
410
+ throw new Error('pnpm list --global returned an unexpected result.');
411
+ }
412
+ const dependencies = value.flatMap((entryArg) => {
413
+ if (!entryArg || typeof entryArg !== 'object' || Array.isArray(entryArg)) return [];
414
+ const entry = entryArg as IGlobalListResult;
415
+ const dependency = entry.dependencies?.[packageNameArg];
416
+ return dependency ? [dependency] : [];
417
+ });
418
+ if (dependencies.length !== 1) {
419
+ throw new Error(`Expected exactly one pnpm-global installation of ${packageNameArg}.`);
420
+ }
421
+ const dependency = dependencies[0];
422
+ if (
423
+ typeof dependency.version !== 'string'
424
+ || typeof dependency.path !== 'string'
425
+ || !plugins.path.isAbsolute(dependency.path)
426
+ ) {
427
+ throw new Error('pnpm reported invalid global package metadata.');
428
+ }
429
+ return { version: dependency.version, path: dependency.path };
430
+ };
431
+
432
+ export const resolvePnpmGlobalInstallation = async (): Promise<IPnpmGlobalInstallation> => {
433
+ if (process.platform !== 'linux' && process.platform !== 'darwin') {
434
+ throw new Error(`${currentCliName} upgrade is currently supported on Linux and macOS only.`);
435
+ }
436
+ const environment = createPackageManagerEnvironment();
437
+ const packageManagerPath = await findExecutable('pnpm', environment);
438
+ const globalRootOutput = await runCapturedCommand(
439
+ packageManagerPath,
440
+ ['root', '--global'],
441
+ environment,
442
+ );
443
+ const globalBinOutput = await runCapturedCommand(
444
+ packageManagerPath,
445
+ ['bin', '--global'],
446
+ environment,
447
+ );
448
+ const listOutput = await runCapturedCommand(
449
+ packageManagerPath,
450
+ ['list', '--global', controllerPackageName, '--depth=0', '--json'],
451
+ environment,
452
+ );
453
+ const globalRoot = await assertSingleLineAbsolutePath(globalRootOutput, 'pnpm root --global');
454
+ const globalBinDirectory = await assertSingleLineAbsolutePath(
455
+ globalBinOutput,
456
+ 'pnpm bin --global',
457
+ );
458
+ const dependency = readGlobalListDependency(listOutput, controllerPackageName);
459
+ const dependencyRelativePath = plugins.path.relative(globalRoot, dependency.path);
460
+ if (
461
+ dependencyRelativePath.startsWith(`..${plugins.path.sep}`)
462
+ || dependencyRelativePath === '..'
463
+ || plugins.path.isAbsolute(dependencyRelativePath)
464
+ ) {
465
+ throw new Error('pnpm reported a global package path outside its global root.');
466
+ }
467
+ const packageDirectory = await plugins.fs.promises.realpath(dependency.path);
468
+ const packageJsonPath = plugins.path.join(packageDirectory, 'package.json');
469
+ const packageJsonStats = await plugins.fs.promises.stat(packageJsonPath);
470
+ if (!packageJsonStats.isFile() || packageJsonStats.size < 2 || packageJsonStats.size > 64 * 1024) {
471
+ throw new Error(`The pnpm-global ${controllerPackageName} package metadata is not a bounded file.`);
472
+ }
473
+ const packageJson = JSON.parse(await plugins.fs.promises.readFile(packageJsonPath, 'utf8')) as {
474
+ name?: unknown;
475
+ version?: unknown;
476
+ bin?: unknown;
477
+ controllerUpgradeManagementVersion?: unknown;
478
+ };
479
+ const currentIdentity: { cliName: string; managementVersion: number } = currentPackageIdentity;
480
+ if (
481
+ packageJson.name !== controllerPackageName
482
+ || packageJson.version !== dependency.version
483
+ || packageJson.controllerUpgradeManagementVersion !== currentIdentity.managementVersion
484
+ || Number(controllerUpgradeManagementVersion) !== currentIdentity.managementVersion
485
+ || !packageJson.bin
486
+ || typeof packageJson.bin !== 'object'
487
+ || Array.isArray(packageJson.bin)
488
+ || !Object.keys(packageJson.bin).length
489
+ || Object.keys(packageJson.bin).length !== 1
490
+ || (packageJson.bin as Record<string, unknown>)[currentIdentity.cliName] !== './cli.js'
491
+ ) {
492
+ throw new Error(`The pnpm-global ${controllerPackageName} package metadata is incompatible.`);
493
+ }
494
+ const cliPath = await plugins.fs.promises.realpath(plugins.path.join(packageDirectory, 'cli.js'));
495
+ if (!(await plugins.fs.promises.stat(cliPath)).isFile()) {
496
+ throw new Error('The pnpm-global controller direct CLI is not a file.');
497
+ }
498
+ return {
499
+ packageManagerPath,
500
+ globalRoot,
501
+ globalBinDirectory,
502
+ packageDirectory,
503
+ cliPath,
504
+ packageVersion: dependency.version,
505
+ };
506
+ };
507
+
508
+ interface IPackageTransitionContext {
509
+ packageManagerPath: string;
510
+ globalRoot: string;
511
+ globalBinDirectory: string;
512
+ }
513
+
514
+ interface IExactGlobalPackageInstallation extends IPnpmGlobalInstallation {
515
+ packageName: string;
516
+ cliName: string;
517
+ cliRelativePath: string;
518
+ listedPackagePath: string;
519
+ groupDirectory: string;
520
+ }
521
+
522
+ interface IPackageTransitionTopology {
523
+ source?: IExactGlobalPackageInstallation;
524
+ target?: IExactGlobalPackageInstallation;
525
+ }
526
+
527
+ type TPackageTransitionTopology = 'source-only' | 'grouped' | 'target-only';
528
+
529
+ const parseGlobalListDependencies = (
530
+ outputArg: string,
531
+ ): { path: string; dependencies: Record<string, { version: string; path: string }> } => {
532
+ let value: unknown;
533
+ try {
534
+ value = JSON.parse(outputArg) as unknown;
535
+ } catch {
536
+ throw new Error('pnpm list --global returned malformed JSON.');
537
+ }
538
+ if (!Array.isArray(value) || value.length !== 1) {
539
+ throw new Error('pnpm list --global returned an unexpected package-transition result.');
540
+ }
541
+ const entry = value[0];
542
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
543
+ throw new Error('pnpm list --global returned invalid package-transition metadata.');
544
+ }
545
+ const raw = entry as IGlobalListResult;
546
+ if (typeof raw.path !== 'string' || !plugins.path.isAbsolute(raw.path)) {
547
+ throw new Error('pnpm list --global omitted its global package root.');
548
+ }
549
+ const dependencies: Record<string, { version: string; path: string }> = {};
550
+ for (const packageName of [
551
+ upgradePackageTransitionSource.packageName,
552
+ upgradePackageTransitionTarget.packageName,
553
+ ]) {
554
+ const dependency = raw.dependencies?.[packageName];
555
+ if (!dependency) continue;
556
+ if (
557
+ typeof dependency.version !== 'string'
558
+ || typeof dependency.path !== 'string'
559
+ || !plugins.path.isAbsolute(dependency.path)
560
+ ) throw new Error(`pnpm reported invalid global metadata for ${packageName}.`);
561
+ dependencies[packageName] = {
562
+ version: dependency.version,
563
+ path: dependency.path,
564
+ };
565
+ }
566
+ return { path: raw.path, dependencies };
567
+ };
568
+
569
+ const packageGroupDirectory = (
570
+ globalRootArg: string,
571
+ packagePathArg: string,
572
+ packageNameArg: string,
573
+ ): string => {
574
+ const relative = plugins.path.relative(globalRootArg, packagePathArg);
575
+ if (
576
+ !relative
577
+ || relative === '..'
578
+ || relative.startsWith(`..${plugins.path.sep}`)
579
+ || plugins.path.isAbsolute(relative)
580
+ ) throw new Error(`pnpm reported ${packageNameArg} outside its global root.`);
581
+ const expectedSuffix = plugins.path.join('node_modules', ...packageNameArg.split('/'));
582
+ const suffix = `${plugins.path.sep}${expectedSuffix}`;
583
+ if (!relative.endsWith(suffix)) {
584
+ throw new Error(`pnpm reported an unexpected global path for ${packageNameArg}.`);
585
+ }
586
+ const groupRelativePath = relative.slice(0, -suffix.length);
587
+ if (!groupRelativePath || groupRelativePath.includes(plugins.path.sep)) {
588
+ throw new Error(`pnpm reported an invalid isolated group for ${packageNameArg}.`);
589
+ }
590
+ return plugins.path.join(globalRootArg, groupRelativePath);
591
+ };
592
+
593
+ const readExactGlobalPackage = async (
594
+ contextArg: IPackageTransitionContext,
595
+ dependencyArg: { version: string; path: string },
596
+ identityArg: {
597
+ packageName: string;
598
+ version: string;
599
+ managementVersion: number;
600
+ cliName: string;
601
+ cliRelativePath: string;
602
+ },
603
+ ): Promise<IExactGlobalPackageInstallation> => {
604
+ if (dependencyArg.version !== identityArg.version) {
605
+ throw new Error(
606
+ `The pnpm-global ${identityArg.packageName} version is ${dependencyArg.version}, not ${identityArg.version}.`,
607
+ );
608
+ }
609
+ const groupDirectory = packageGroupDirectory(
610
+ contextArg.globalRoot,
611
+ dependencyArg.path,
612
+ identityArg.packageName,
613
+ );
614
+ const packageDirectory = await plugins.fs.promises.realpath(dependencyArg.path);
615
+ const packageJsonPath = plugins.path.join(packageDirectory, 'package.json');
616
+ const packageJsonStats = await plugins.fs.promises.stat(packageJsonPath);
617
+ if (!packageJsonStats.isFile() || packageJsonStats.size < 2 || packageJsonStats.size > 64 * 1024) {
618
+ throw new Error(`The pnpm-global ${identityArg.packageName} metadata is not a bounded file.`);
619
+ }
620
+ let packageJson: unknown;
621
+ try {
622
+ packageJson = JSON.parse(await plugins.fs.promises.readFile(packageJsonPath, 'utf8')) as unknown;
623
+ } catch (errorArg) {
624
+ throw new Error(`The pnpm-global ${identityArg.packageName} metadata is malformed.`, {
625
+ cause: errorArg,
626
+ });
627
+ }
628
+ if (!packageJson || typeof packageJson !== 'object' || Array.isArray(packageJson)) {
629
+ throw new Error(`The pnpm-global ${identityArg.packageName} metadata is invalid.`);
630
+ }
631
+ const manifest = packageJson as Record<string, unknown>;
632
+ const bin = manifest.bin;
633
+ if (
634
+ manifest.name !== identityArg.packageName
635
+ || manifest.version !== identityArg.version
636
+ || manifest.controllerUpgradeManagementVersion !== identityArg.managementVersion
637
+ || !bin
638
+ || typeof bin !== 'object'
639
+ || Array.isArray(bin)
640
+ || Object.keys(bin).length !== 1
641
+ || (bin as Record<string, unknown>)[identityArg.cliName] !== identityArg.cliRelativePath
642
+ ) throw new Error(`The pnpm-global ${identityArg.packageName} identity is incompatible.`);
643
+ const cliPath = await plugins.fs.promises.realpath(
644
+ plugins.path.join(packageDirectory, identityArg.cliRelativePath),
645
+ );
646
+ if (!(await plugins.fs.promises.stat(cliPath)).isFile()) {
647
+ throw new Error(`The pnpm-global ${identityArg.packageName} direct CLI is not a file.`);
648
+ }
649
+ return {
650
+ packageManagerPath: contextArg.packageManagerPath,
651
+ globalRoot: contextArg.globalRoot,
652
+ globalBinDirectory: contextArg.globalBinDirectory,
653
+ packageDirectory,
654
+ cliPath,
655
+ packageVersion: identityArg.version,
656
+ packageName: identityArg.packageName,
657
+ cliName: identityArg.cliName,
658
+ cliRelativePath: identityArg.cliRelativePath,
659
+ listedPackagePath: dependencyArg.path,
660
+ groupDirectory,
661
+ };
662
+ };
663
+
664
+ const readGlobalShimTarget = async (
665
+ binDirectoryArg: string,
666
+ cliNameArg: string,
667
+ ): Promise<string | undefined> => {
668
+ const shimPath = plugins.path.join(binDirectoryArg, cliNameArg);
669
+ let stats: plugins.fs.Stats;
670
+ try {
671
+ stats = await plugins.fs.promises.lstat(shimPath);
672
+ } catch (errorArg) {
673
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
674
+ throw errorArg;
675
+ }
676
+ if (
677
+ !stats.isFile()
678
+ || stats.isSymbolicLink()
679
+ || stats.size < 2
680
+ || stats.size > 1024 * 1024
681
+ || (stats.mode & 0o111) === 0
682
+ ) throw new Error(`The pnpm-global ${cliNameArg} shim is not a bounded executable file.`);
683
+ const contents = await plugins.fs.promises.readFile(shimPath, 'utf8');
684
+ const markers = [...contents.matchAll(/^# cmd-shim-target=(.+)$/gm)];
685
+ if (markers.length !== 1 || !plugins.path.isAbsolute(markers[0][1])) {
686
+ throw new Error(`The pnpm-global ${cliNameArg} shim has no exact ownership marker.`);
687
+ }
688
+ try {
689
+ return await plugins.fs.promises.realpath(markers[0][1]);
690
+ } catch (errorArg) {
691
+ throw new Error(`The pnpm-global ${cliNameArg} shim target is unavailable.`, {
692
+ cause: errorArg,
693
+ });
694
+ }
695
+ };
696
+
697
+ const assertGlobalShimOwnership = async (
698
+ binDirectoryArg: string,
699
+ cliNameArg: string,
700
+ cliPathArg?: string,
701
+ ): Promise<void> => {
702
+ const markerTarget = await readGlobalShimTarget(binDirectoryArg, cliNameArg);
703
+ if (markerTarget === undefined) {
704
+ if (cliPathArg === undefined) return;
705
+ throw new Error(`The pnpm-global ${cliNameArg} shim is missing.`);
706
+ }
707
+ if (cliPathArg === undefined) {
708
+ throw new Error(`The pnpm-global ${cliNameArg} shim survived without its owning package.`);
709
+ }
710
+ if (markerTarget !== cliPathArg) {
711
+ throw new Error(`The pnpm-global ${cliNameArg} shim is owned by a different package.`);
712
+ }
713
+ };
714
+
715
+ const maximumPnpmGlobalGroups = 1_024;
716
+
717
+ const findExactGlobalPackageLink = async (
718
+ globalRootArg: string,
719
+ packageNameArg: string,
720
+ packageDirectoryArg: string,
721
+ ): Promise<{ listedPackagePath: string; groupDirectory: string }> => {
722
+ const entries = await plugins.fs.promises.readdir(globalRootArg, { withFileTypes: true });
723
+ if (entries.length > maximumPnpmGlobalGroups) {
724
+ throw new Error('The pnpm global root exceeds the recovery scan bound.');
725
+ }
726
+ const matches: string[] = [];
727
+ for (const entry of entries) {
728
+ if (!entry.isDirectory()) continue;
729
+ const candidate = plugins.path.join(
730
+ globalRootArg,
731
+ entry.name,
732
+ 'node_modules',
733
+ ...packageNameArg.split('/'),
734
+ );
735
+ try {
736
+ if (await plugins.fs.promises.realpath(candidate) === packageDirectoryArg) {
737
+ matches.push(candidate);
738
+ }
739
+ } catch (errorArg) {
740
+ const code = (errorArg as NodeJS.ErrnoException).code;
741
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') throw errorArg;
742
+ }
743
+ }
744
+ if (matches.length !== 1) {
745
+ throw new Error(`Expected exactly one filesystem-owned installation of ${packageNameArg}.`);
746
+ }
747
+ return {
748
+ listedPackagePath: matches[0],
749
+ groupDirectory: packageGroupDirectory(globalRootArg, matches[0], packageNameArg),
750
+ };
751
+ };
752
+
753
+ const resolveAuthoritativeRecoveryInstallation = async (
754
+ transactionArg: TUpgradeTransaction,
755
+ capturedInstallationArg: IPnpmGlobalInstallation,
756
+ ): Promise<IPnpmGlobalInstallation> => {
757
+ const [globalRoot, globalBinDirectory, packageManagerPath] = await Promise.all([
758
+ plugins.fs.promises.realpath(capturedInstallationArg.globalRoot),
759
+ plugins.fs.promises.realpath(capturedInstallationArg.globalBinDirectory),
760
+ plugins.fs.promises.realpath(capturedInstallationArg.packageManagerPath),
761
+ ]);
762
+ if (
763
+ globalRoot !== capturedInstallationArg.globalRoot
764
+ || globalBinDirectory !== capturedInstallationArg.globalBinDirectory
765
+ || packageManagerPath !== capturedInstallationArg.packageManagerPath
766
+ ) throw new Error('The captured pnpm recovery paths changed unexpectedly.');
767
+ await plugins.fs.promises.access(packageManagerPath, plugins.fs.constants.X_OK);
768
+
769
+ const currentIdentity: {
770
+ packageName: string;
771
+ versions: string[];
772
+ managementVersion: number;
773
+ cliName: string;
774
+ cliRelativePath: string;
775
+ } = transactionArg.version === upgradePackageTransitionTransactionVersion
776
+ ? (() => {
777
+ const identity = transactionArg.targetPackageCommitStarted === true
778
+ ? upgradePackageTransitionTarget
779
+ : upgradePackageTransitionSource;
780
+ return {
781
+ packageName: identity.packageName,
782
+ versions: [identity.version],
783
+ managementVersion: identity.managementVersion,
784
+ cliName: identity.cliName,
785
+ cliRelativePath: identity.cliRelativePath,
786
+ };
787
+ })()
788
+ : (() => {
789
+ const identity = String(controllerPackageName) === upgradePackageTransitionTarget.packageName
790
+ ? upgradePackageTransitionTarget
791
+ : upgradePackageTransitionSource;
792
+ return {
793
+ packageName: String(controllerPackageName),
794
+ versions: [
795
+ transactionArg.sourceVersion,
796
+ ...(transactionArg.targetVersion ? [transactionArg.targetVersion] : []),
797
+ ],
798
+ managementVersion: Number(controllerUpgradeManagementVersion),
799
+ cliName: identity.cliName,
800
+ cliRelativePath: identity.cliRelativePath,
801
+ };
802
+ })();
803
+ const cliPath = await readGlobalShimTarget(globalBinDirectory, currentIdentity.cliName);
804
+ if (!cliPath) {
805
+ throw new Error(`The authoritative pnpm-global ${currentIdentity.cliName} shim is missing.`);
806
+ }
807
+ const packageDirectory = plugins.path.dirname(cliPath);
808
+ if (
809
+ await plugins.fs.promises.realpath(
810
+ plugins.path.join(packageDirectory, currentIdentity.cliRelativePath),
811
+ ) !== cliPath
812
+ ) throw new Error('The authoritative recovery CLI path is incompatible.');
813
+ const packageJsonPath = plugins.path.join(packageDirectory, 'package.json');
814
+ const packageJsonStats = await plugins.fs.promises.stat(packageJsonPath);
815
+ if (!packageJsonStats.isFile() || packageJsonStats.size < 2 || packageJsonStats.size > 64 * 1024) {
816
+ throw new Error('The authoritative recovery package metadata is not a bounded file.');
817
+ }
818
+ let packageJson: unknown;
819
+ try {
820
+ packageJson = JSON.parse(await plugins.fs.promises.readFile(packageJsonPath, 'utf8')) as unknown;
821
+ } catch (errorArg) {
822
+ throw new Error('The authoritative recovery package metadata is malformed.', {
823
+ cause: errorArg,
824
+ });
825
+ }
826
+ if (!packageJson || typeof packageJson !== 'object' || Array.isArray(packageJson)) {
827
+ throw new Error('The authoritative recovery package metadata is invalid.');
828
+ }
829
+ const manifest = packageJson as Record<string, unknown>;
830
+ const bin = manifest.bin;
831
+ if (
832
+ manifest.name !== currentIdentity.packageName
833
+ || typeof manifest.version !== 'string'
834
+ || !currentIdentity.versions.includes(manifest.version)
835
+ || manifest.controllerUpgradeManagementVersion !== currentIdentity.managementVersion
836
+ || !bin
837
+ || typeof bin !== 'object'
838
+ || Array.isArray(bin)
839
+ || Object.keys(bin).length !== 1
840
+ || (bin as Record<string, unknown>)[currentIdentity.cliName]
841
+ !== currentIdentity.cliRelativePath
842
+ ) throw new Error('The authoritative recovery package identity is incompatible.');
843
+ if (!(await plugins.fs.promises.stat(cliPath)).isFile()) {
844
+ throw new Error('The authoritative recovery CLI is not a file.');
845
+ }
846
+ await findExactGlobalPackageLink(globalRoot, currentIdentity.packageName, packageDirectory);
847
+ return {
848
+ packageManagerPath,
849
+ globalRoot,
850
+ globalBinDirectory,
851
+ packageDirectory,
852
+ cliPath,
853
+ packageVersion: manifest.version,
854
+ };
855
+ };
856
+
857
+ const verifyPackageTransitionTopology = async (
858
+ contextArg: IPackageTransitionContext,
859
+ expectedArg: TPackageTransitionTopology,
860
+ ): Promise<IPackageTransitionTopology> => {
861
+ const output = await runCapturedCommand(
862
+ contextArg.packageManagerPath,
863
+ [
864
+ 'list',
865
+ '--global',
866
+ upgradePackageTransitionSource.packageName,
867
+ upgradePackageTransitionTarget.packageName,
868
+ '--depth=0',
869
+ '--json',
870
+ ],
871
+ createPackageManagerEnvironment(),
872
+ );
873
+ const listed = parseGlobalListDependencies(output);
874
+ if (await plugins.fs.promises.realpath(listed.path) !== contextArg.globalRoot) {
875
+ throw new Error('The pnpm global root changed during the package transition.');
876
+ }
877
+ const sourceDependency = listed.dependencies[upgradePackageTransitionSource.packageName];
878
+ const targetDependency = listed.dependencies[upgradePackageTransitionTarget.packageName];
879
+ const sourceExpected = expectedArg !== 'target-only';
880
+ const targetExpected = expectedArg !== 'source-only';
881
+ if (Boolean(sourceDependency) !== sourceExpected || Boolean(targetDependency) !== targetExpected) {
882
+ throw new Error(`The pnpm-global package topology is not ${expectedArg}.`);
883
+ }
884
+ const [source, target] = await Promise.all([
885
+ sourceDependency
886
+ ? readExactGlobalPackage(contextArg, sourceDependency, upgradePackageTransitionSource)
887
+ : undefined,
888
+ targetDependency
889
+ ? readExactGlobalPackage(contextArg, targetDependency, upgradePackageTransitionTarget)
890
+ : undefined,
891
+ ]);
892
+ if (expectedArg === 'grouped' && source?.groupDirectory !== target?.groupDirectory) {
893
+ throw new Error('The source and target packages are not installed in one pnpm group.');
894
+ }
895
+ await Promise.all([
896
+ assertGlobalShimOwnership(
897
+ contextArg.globalBinDirectory,
898
+ upgradePackageTransitionSource.cliName,
899
+ source?.cliPath,
900
+ ),
901
+ assertGlobalShimOwnership(
902
+ contextArg.globalBinDirectory,
903
+ upgradePackageTransitionTarget.cliName,
904
+ target?.cliPath,
905
+ ),
906
+ ]);
907
+ return {
908
+ ...(source ? { source } : {}),
909
+ ...(target ? { target } : {}),
910
+ };
911
+ };
912
+
913
+ const resolvePackageManagerContext = async (): Promise<IPackageTransitionContext> => {
914
+ if (process.platform !== 'linux' && process.platform !== 'darwin') {
915
+ throw new Error(`${currentCliName} upgrade is currently supported on Linux and macOS only.`);
916
+ }
917
+ const environment = createPackageManagerEnvironment();
918
+ const packageManagerPath = await findExecutable('pnpm', environment);
919
+ const globalRoot = await assertSingleLineAbsolutePath(
920
+ await runCapturedCommand(packageManagerPath, ['root', '--global'], environment),
921
+ 'pnpm root --global',
922
+ );
923
+ const globalBinDirectory = await assertSingleLineAbsolutePath(
924
+ await runCapturedCommand(packageManagerPath, ['bin', '--global'], environment),
925
+ 'pnpm bin --global',
926
+ );
927
+ return { packageManagerPath, globalRoot, globalBinDirectory };
928
+ };
929
+
930
+ export const resolveInheritedUpgradeTargetInstallation = async (
931
+ transactionArg: TUpgradeTransaction,
932
+ ): Promise<IPnpmGlobalInstallation> => {
933
+ if (transactionArg.version === upgradeCoordinationVersion) {
934
+ return await resolvePnpmGlobalInstallation();
935
+ }
936
+ const topology = await verifyPackageTransitionTopology(
937
+ await resolvePackageManagerContext(),
938
+ 'target-only',
939
+ );
940
+ if (!topology.target) throw new Error('The committed target package is unavailable.');
941
+ return topology.target;
942
+ };
943
+
944
+ export const resolveCurrentCliPath = async (): Promise<string> => {
945
+ return await plugins.fs.promises.realpath(
946
+ plugins.url.fileURLToPath(new URL('../cli.js', import.meta.url)),
947
+ );
948
+ };
949
+
950
+ const currentPackageIsDevelopmentCheckout = async (): Promise<boolean> => {
951
+ const packageRoot = plugins.path.dirname(await resolveCurrentCliPath());
952
+ try {
953
+ const stats = await plugins.fs.promises.lstat(plugins.path.join(packageRoot, '.git'));
954
+ return stats.isDirectory() || stats.isFile();
955
+ } catch (errorArg) {
956
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return false;
957
+ throw errorArg;
958
+ }
959
+ };
960
+
961
+ export const resolveCurrentPnpmGlobalInstallation = async (): Promise<IPnpmGlobalInstallation> => {
962
+ const [installation, cliPath] = await Promise.all([
963
+ resolvePnpmGlobalInstallation(),
964
+ resolveCurrentCliPath(),
965
+ ]);
966
+ if (installation.cliPath !== cliPath || installation.packageVersion !== commitinfo.version) {
967
+ throw new Error(`${currentCliName} upgrade must be run from the active pnpm-global installation.`);
968
+ }
969
+ return installation;
970
+ };
971
+
972
+ export const tryResolveCurrentPnpmGlobalInstallation = async (
973
+ ): Promise<IPnpmGlobalInstallation | undefined> => {
974
+ if (await currentPackageIsDevelopmentCheckout()) return undefined;
975
+ return await resolveCurrentPnpmGlobalInstallation();
976
+ };
977
+
978
+ const parseSemver = (valueArg: string): IParsedSemver => {
979
+ const match = semverPattern.exec(valueArg);
980
+ if (!match) throw new Error(`Invalid semantic version: ${valueArg}`);
981
+ const prerelease = match[4]?.split('.') ?? [];
982
+ if (prerelease.some((identifierArg) => /^\d+$/.test(identifierArg)
983
+ && identifierArg.length > 1
984
+ && identifierArg.startsWith('0'))) {
985
+ throw new Error(`Invalid semantic version: ${valueArg}`);
986
+ }
987
+ return {
988
+ major: match[1],
989
+ minor: match[2],
990
+ patch: match[3],
991
+ prerelease,
992
+ };
993
+ };
994
+
995
+ const compareNumericStrings = (leftArg: string, rightArg: string): number => {
996
+ if (leftArg.length !== rightArg.length) return leftArg.length < rightArg.length ? -1 : 1;
997
+ return leftArg === rightArg ? 0 : leftArg < rightArg ? -1 : 1;
998
+ };
999
+
1000
+ export const compareSemver = (leftArg: string, rightArg: string): number => {
1001
+ const left = parseSemver(leftArg);
1002
+ const right = parseSemver(rightArg);
1003
+ for (const key of ['major', 'minor', 'patch'] as const) {
1004
+ const comparison = compareNumericStrings(left[key], right[key]);
1005
+ if (comparison !== 0) return comparison;
1006
+ }
1007
+ if (left.prerelease.length === 0 || right.prerelease.length === 0) {
1008
+ if (left.prerelease.length === right.prerelease.length) return 0;
1009
+ return left.prerelease.length === 0 ? 1 : -1;
1010
+ }
1011
+ const identifierCount = Math.max(left.prerelease.length, right.prerelease.length);
1012
+ for (let index = 0; index < identifierCount; index++) {
1013
+ const leftIdentifier = left.prerelease[index];
1014
+ const rightIdentifier = right.prerelease[index];
1015
+ if (leftIdentifier === undefined || rightIdentifier === undefined) {
1016
+ return leftIdentifier === rightIdentifier ? 0 : leftIdentifier === undefined ? -1 : 1;
1017
+ }
1018
+ if (leftIdentifier === rightIdentifier) continue;
1019
+ const leftNumeric = /^\d+$/.test(leftIdentifier);
1020
+ const rightNumeric = /^\d+$/.test(rightIdentifier);
1021
+ if (leftNumeric && rightNumeric) {
1022
+ return compareNumericStrings(leftIdentifier, rightIdentifier);
1023
+ }
1024
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
1025
+ return leftIdentifier < rightIdentifier ? -1 : 1;
1026
+ }
1027
+ return 0;
1028
+ };
1029
+
1030
+ const readLatestVersion = async (installationArg: IPnpmGlobalInstallation): Promise<string> => {
1031
+ const output = await runCapturedCommand(
1032
+ installationArg.packageManagerPath,
1033
+ [
1034
+ 'view',
1035
+ controllerPackageName,
1036
+ 'dist-tags.latest',
1037
+ `--registry=${npmRegistryUrl}`,
1038
+ '--json',
1039
+ ],
1040
+ createPackageManagerEnvironment(),
1041
+ );
1042
+ let version: unknown;
1043
+ try {
1044
+ version = JSON.parse(output) as unknown;
1045
+ } catch {
1046
+ throw new Error('pnpm view returned malformed JSON for the npm latest tag.');
1047
+ }
1048
+ if (typeof version !== 'string') {
1049
+ throw new Error('The npm latest tag did not resolve to one package version.');
1050
+ }
1051
+ parseSemver(version);
1052
+ return version;
1053
+ };
1054
+
1055
+ const assertInternalRegistryUrl = (valueArg: string): string => {
1056
+ let parsed: URL;
1057
+ try {
1058
+ parsed = new URL(valueArg);
1059
+ } catch (errorArg) {
1060
+ throw new Error('The internal upgrade registry URL is invalid.', { cause: errorArg });
1061
+ }
1062
+ if (
1063
+ (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
1064
+ || parsed.username.length > 0
1065
+ || parsed.password.length > 0
1066
+ ) throw new Error('The internal upgrade registry URL is unsafe.');
1067
+ return parsed.toString();
1068
+ };
1069
+
1070
+ const resolveExactPackageTransitionTarget = async (
1071
+ contextArg: IPackageTransitionContext,
1072
+ registryUrlArg: string,
1073
+ ): Promise<void> => {
1074
+ const output = await runCapturedCommand(
1075
+ contextArg.packageManagerPath,
1076
+ [
1077
+ 'view',
1078
+ `${upgradePackageTransitionTarget.packageName}@${upgradePackageTransitionTarget.version}`,
1079
+ `--registry=${registryUrlArg}`,
1080
+ '--json',
1081
+ ],
1082
+ createPackageManagerEnvironment(),
1083
+ );
1084
+ let metadata: unknown;
1085
+ try {
1086
+ metadata = JSON.parse(output) as unknown;
1087
+ } catch (errorArg) {
1088
+ throw new Error('pnpm view returned malformed target package metadata.', { cause: errorArg });
1089
+ }
1090
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
1091
+ throw new Error('pnpm view returned invalid target package metadata.');
1092
+ }
1093
+ const manifest = metadata as Record<string, unknown>;
1094
+ const bin = manifest.bin;
1095
+ if (
1096
+ manifest.name !== upgradePackageTransitionTarget.packageName
1097
+ || manifest.version !== upgradePackageTransitionTarget.version
1098
+ || manifest.controllerUpgradeManagementVersion
1099
+ !== upgradePackageTransitionTarget.managementVersion
1100
+ || !bin
1101
+ || typeof bin !== 'object'
1102
+ || Array.isArray(bin)
1103
+ || Object.keys(bin).length !== 1
1104
+ || (bin as Record<string, unknown>)[upgradePackageTransitionTarget.cliName]
1105
+ !== upgradePackageTransitionTarget.cliRelativePath
1106
+ ) throw new Error('The exact target package metadata is incompatible.');
1107
+ };
1108
+
1109
+ const openUpgradeLog = async (tokenArg: string): Promise<{
1110
+ handle: plugins.fs.promises.FileHandle;
1111
+ path: string;
1112
+ }> => {
1113
+ const directory = controllerDataDirectory();
1114
+ await plugins.fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
1115
+ const directoryStats = await plugins.fs.promises.lstat(directory);
1116
+ if (
1117
+ !directoryStats.isDirectory()
1118
+ || directoryStats.isSymbolicLink()
1119
+ || (typeof process.getuid === 'function' && directoryStats.uid !== process.getuid())
1120
+ || (directoryStats.mode & 0o077) !== 0
1121
+ ) {
1122
+ throw new Error(`The ${currentCliName} upgrade log directory is unsafe: ${directory}`);
1123
+ }
1124
+ const existingLogs: Array<{ path: string; mtimeMs: number }> = [];
1125
+ for (const entry of await plugins.fs.promises.readdir(directory, { withFileTypes: true })) {
1126
+ if (!entry.isFile() || !upgradeLogPattern.test(entry.name)) continue;
1127
+ const existingPath = plugins.path.join(directory, entry.name);
1128
+ const stats = await plugins.fs.promises.lstat(existingPath);
1129
+ if (
1130
+ !stats.isFile()
1131
+ || stats.isSymbolicLink()
1132
+ || (typeof process.getuid === 'function' && stats.uid !== process.getuid())
1133
+ || (stats.mode & 0o077) !== 0
1134
+ ) {
1135
+ throw new Error(`An existing ${currentCliName} upgrade log is unsafe: ${existingPath}`);
1136
+ }
1137
+ existingLogs.push({ path: existingPath, mtimeMs: stats.mtimeMs });
1138
+ }
1139
+ existingLogs.sort((leftArg, rightArg) => rightArg.mtimeMs - leftArg.mtimeMs);
1140
+ for (const staleLog of existingLogs.slice(retainedUpgradeLogCount - 1)) {
1141
+ if (Date.now() - staleLog.mtimeMs < minimumPrunableUpgradeLogAgeMs) continue;
1142
+ await plugins.fs.promises.unlink(staleLog.path).catch((errorArg) => {
1143
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
1144
+ });
1145
+ }
1146
+ const filePath = plugins.path.join(
1147
+ directory,
1148
+ `upgrade-${new Date().toISOString().replaceAll(':', '')}-${plugins.crypto
1149
+ .createHash('sha256')
1150
+ .update(tokenArg, 'utf8')
1151
+ .digest('hex')
1152
+ .slice(0, 10)}.log`,
1153
+ );
1154
+ const handle = await plugins.fs.promises.open(
1155
+ filePath,
1156
+ plugins.fs.constants.O_WRONLY
1157
+ | plugins.fs.constants.O_APPEND
1158
+ | plugins.fs.constants.O_CREAT
1159
+ | plugins.fs.constants.O_EXCL
1160
+ | plugins.fs.constants.O_NOFOLLOW,
1161
+ 0o600,
1162
+ );
1163
+ return { handle, path: filePath };
1164
+ };
1165
+
1166
+ interface IUpgradeRecoveryWorkerContext {
1167
+ version: 1;
1168
+ mode: 'recovery';
1169
+ canonicalGlobalRoot: string;
1170
+ packageManagerPath: string;
1171
+ globalBinDirectory: string;
1172
+ cliPath: string;
1173
+ }
1174
+
1175
+ const serializeUpgradeRecoveryWorkerContext = (
1176
+ contextArg: IUpgradeRecoveryWorkerContext,
1177
+ ): string => Buffer.from(JSON.stringify(contextArg), 'utf8').toString('base64url');
1178
+
1179
+ const parseUpgradeRecoveryWorkerContext = (
1180
+ valueArg: unknown,
1181
+ ): IUpgradeRecoveryWorkerContext => {
1182
+ if (typeof valueArg !== 'string' || valueArg.length < 16 || valueArg.length > 16 * 1024) {
1183
+ throw new Error('The internal upgrade recovery worker context is invalid.');
1184
+ }
1185
+ let parsed: unknown;
1186
+ try {
1187
+ parsed = JSON.parse(Buffer.from(valueArg, 'base64url').toString('utf8')) as unknown;
1188
+ } catch (errorArg) {
1189
+ throw new Error('The internal upgrade recovery worker context is malformed.', {
1190
+ cause: errorArg,
1191
+ });
1192
+ }
1193
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1194
+ throw new Error('The internal upgrade recovery worker context is malformed.');
1195
+ }
1196
+ const context = parsed as Record<string, unknown>;
1197
+ if (
1198
+ Object.keys(context).length !== 6
1199
+ || ![
1200
+ 'version',
1201
+ 'mode',
1202
+ 'canonicalGlobalRoot',
1203
+ 'packageManagerPath',
1204
+ 'globalBinDirectory',
1205
+ 'cliPath',
1206
+ ].every((keyArg) => Object.hasOwn(context, keyArg))
1207
+ || context.version !== 1
1208
+ || context.mode !== 'recovery'
1209
+ || typeof context.canonicalGlobalRoot !== 'string'
1210
+ || typeof context.packageManagerPath !== 'string'
1211
+ || typeof context.globalBinDirectory !== 'string'
1212
+ || typeof context.cliPath !== 'string'
1213
+ || !plugins.path.isAbsolute(context.canonicalGlobalRoot)
1214
+ || !plugins.path.isAbsolute(context.packageManagerPath)
1215
+ || !plugins.path.isAbsolute(context.globalBinDirectory)
1216
+ || !plugins.path.isAbsolute(context.cliPath)
1217
+ ) throw new Error('The internal upgrade recovery worker context is incompatible.');
1218
+ return context as unknown as IUpgradeRecoveryWorkerContext;
1219
+ };
1220
+
1221
+ const waitForProcessGroupDrain = async (
1222
+ processGroupIdArg: number,
1223
+ timeoutMsArg: number,
1224
+ ): Promise<boolean> => {
1225
+ const deadline = Date.now() + timeoutMsArg;
1226
+ while (Date.now() < deadline) {
1227
+ if ((await readProcessGroupMemberPids(processGroupIdArg)).length === 0) return true;
1228
+ await new Promise((resolve) => setTimeout(resolve, 50));
1229
+ }
1230
+ return (await readProcessGroupMemberPids(processGroupIdArg)).length === 0;
1231
+ };
1232
+
1233
+ const terminateSpawnedDetachedWorker = async (processIdArg: number): Promise<void> => {
1234
+ const signal = (signalArg: NodeJS.Signals): void => {
1235
+ try {
1236
+ process.kill(-processIdArg, signalArg);
1237
+ } catch (errorArg) {
1238
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ESRCH') throw errorArg;
1239
+ }
1240
+ };
1241
+ signal('SIGTERM');
1242
+ if (await waitForProcessGroupDrain(processIdArg, commandGroupDrainTimeoutMs)) return;
1243
+ signal('SIGKILL');
1244
+ if (!await waitForProcessGroupDrain(processIdArg, commandGroupDrainTimeoutMs)) {
1245
+ throw new Error('The detached upgrade worker candidate group did not terminate.');
1246
+ }
1247
+ };
1248
+
1249
+ interface ICandidateCleanupRetryDependencies {
1250
+ beforeAttempt?: (processIdArg?: number) => Promise<void>;
1251
+ delay?: (millisecondsArg: number) => Promise<void>;
1252
+ }
1253
+
1254
+ const retryCandidateCleanupUntilDrained = async (
1255
+ cleanupArg: () => Promise<void>,
1256
+ dependenciesArg: ICandidateCleanupRetryDependencies = {},
1257
+ processIdArg?: number,
1258
+ ): Promise<void> => {
1259
+ const delay = dependenciesArg.delay
1260
+ ?? (async (millisecondsArg: number) => new Promise((resolve) => (
1261
+ setTimeout(resolve, millisecondsArg)
1262
+ )));
1263
+ while (true) {
1264
+ try {
1265
+ await dependenciesArg.beforeAttempt?.(processIdArg);
1266
+ await cleanupArg();
1267
+ return;
1268
+ } catch {
1269
+ await delay(candidateCleanupRetryIntervalMs);
1270
+ }
1271
+ }
1272
+ };
1273
+
1274
+ interface IDetachedUpgradeWorkerProcessDependencies {
1275
+ readProcessIdentity?: typeof readControllerProcessIdentity;
1276
+ processHasCliCommand?: typeof processIdentityHasCliCommand;
1277
+ candidateCleanup?: ICandidateCleanupRetryDependencies;
1278
+ }
1279
+
1280
+ const launchDetachedUpgradeWorkerProcess = async (optionsArg: {
1281
+ installation: IPnpmGlobalInstallation;
1282
+ payload: IUpgradeWorkerPayload;
1283
+ environment?: NodeJS.ProcessEnv;
1284
+ internalArguments?: string[];
1285
+ }, dependenciesArg: IDetachedUpgradeWorkerProcessDependencies = {}): Promise<IDetachedUpgradeWorker> => {
1286
+ if (process.platform !== 'linux' && process.platform !== 'darwin') {
1287
+ throw new Error(`${currentCliName} upgrade is currently supported on Linux and macOS only.`);
1288
+ }
1289
+ const log = await openUpgradeLog(optionsArg.payload.token);
1290
+ let child: plugins.childProcess.ChildProcess | undefined;
1291
+ let logClosed = false;
1292
+ try {
1293
+ const launchedChild = plugins.childProcess.spawn(
1294
+ process.execPath,
1295
+ [
1296
+ optionsArg.installation.cliPath,
1297
+ '__upgrade-worker',
1298
+ serializeUpgradeWorkerPayload(optionsArg.payload),
1299
+ ...(optionsArg.internalArguments ?? []),
1300
+ ],
1301
+ {
1302
+ cwd: plugins.os.homedir(),
1303
+ detached: true,
1304
+ env: {
1305
+ ...(optionsArg.environment ?? process.env),
1306
+ [upgradeTokenEnvironmentVariable]: optionsArg.payload.token,
1307
+ },
1308
+ shell: false,
1309
+ stdio: ['ignore', log.handle.fd, log.handle.fd],
1310
+ },
1311
+ );
1312
+ child = launchedChild;
1313
+ await new Promise<void>((resolve, reject) => {
1314
+ launchedChild.once('spawn', resolve);
1315
+ launchedChild.once('error', reject);
1316
+ });
1317
+ await log.handle.close();
1318
+ logClosed = true;
1319
+ if (!launchedChild.pid) {
1320
+ throw new Error(`The detached ${currentCliName} upgrade worker has no process ID.`);
1321
+ }
1322
+ const identity = await (dependenciesArg.readProcessIdentity
1323
+ ?? readControllerProcessIdentity)(launchedChild.pid);
1324
+ if (
1325
+ !identity
1326
+ || !identity.processGroupLeader
1327
+ || identity.processGroupId !== launchedChild.pid
1328
+ || !await (dependenciesArg.processHasCliCommand ?? processIdentityHasCliCommand)(
1329
+ identity,
1330
+ optionsArg.installation.cliPath,
1331
+ '__upgrade-worker',
1332
+ )
1333
+ ) throw new Error('The detached upgrade worker candidate identity is invalid.');
1334
+ launchedChild.unref();
1335
+ return {
1336
+ pid: identity.pid,
1337
+ processGroupId: identity.processGroupId,
1338
+ fingerprint: identity.fingerprint,
1339
+ cliPath: optionsArg.installation.cliPath,
1340
+ logFilePath: log.path,
1341
+ };
1342
+ } catch (errorArg) {
1343
+ const errors: unknown[] = [errorArg];
1344
+ if (!logClosed) {
1345
+ try {
1346
+ await log.handle.close();
1347
+ logClosed = true;
1348
+ } catch (logCleanupErrorArg) {
1349
+ errors.push(logCleanupErrorArg);
1350
+ }
1351
+ }
1352
+ if (child?.pid) {
1353
+ await retryCandidateCleanupUntilDrained(
1354
+ async () => terminateSpawnedDetachedWorker(child!.pid!),
1355
+ dependenciesArg.candidateCleanup,
1356
+ child.pid,
1357
+ );
1358
+ }
1359
+ if (errors.length > 1) {
1360
+ throw new AggregateError(
1361
+ errors,
1362
+ 'The detached upgrade worker launch and log cleanup both failed.',
1363
+ );
1364
+ }
1365
+ throw errorArg;
1366
+ }
1367
+ };
1368
+
1369
+ interface ILaunchDetachedUpgradeWorkerOptions {
1370
+ installation: IPnpmGlobalInstallation;
1371
+ payload: IUpgradeWorkerPayload;
1372
+ environment?: NodeJS.ProcessEnv;
1373
+ }
1374
+
1375
+ export function launchDetachedUpgradeWorker(
1376
+ optionsArg: ILaunchDetachedUpgradeWorkerOptions,
1377
+ ): Promise<IDetachedUpgradeWorker>;
1378
+ export async function launchDetachedUpgradeWorker(
1379
+ optionsArg: ILaunchDetachedUpgradeWorkerOptions,
1380
+ dependenciesArg: IDetachedUpgradeWorkerProcessDependencies = {},
1381
+ ): Promise<IDetachedUpgradeWorker> {
1382
+ return await launchDetachedUpgradeWorkerProcess(optionsArg, dependenciesArg);
1383
+ }
1384
+
1385
+ const launchDetachedRecoveryWorker = async (optionsArg: {
1386
+ installation: IPnpmGlobalInstallation;
1387
+ payload: IUpgradeWorkerPayload;
1388
+ environment?: NodeJS.ProcessEnv;
1389
+ }): Promise<IDetachedUpgradeWorker> => await launchDetachedUpgradeWorkerProcess({
1390
+ ...optionsArg,
1391
+ internalArguments: [serializeUpgradeRecoveryWorkerContext({
1392
+ version: 1,
1393
+ mode: 'recovery',
1394
+ canonicalGlobalRoot: optionsArg.installation.globalRoot,
1395
+ packageManagerPath: optionsArg.installation.packageManagerPath,
1396
+ globalBinDirectory: optionsArg.installation.globalBinDirectory,
1397
+ cliPath: optionsArg.installation.cliPath,
1398
+ })],
1399
+ });
1400
+
1401
+ const runLoggedCommand = async (optionsArg: {
1402
+ executable: string;
1403
+ arguments: string[];
1404
+ environment: NodeJS.ProcessEnv;
1405
+ timeoutMs: number;
1406
+ }): Promise<void> => {
1407
+ await runManagedCommand({
1408
+ ...optionsArg,
1409
+ captureOutput: false,
1410
+ outputByteLimit: loggedCommandOutputByteLimit,
1411
+ });
1412
+ };
1413
+
1414
+ const installVersion = async (
1415
+ installationArg: IPnpmGlobalInstallation,
1416
+ versionArg: string,
1417
+ ): Promise<void> => {
1418
+ await runLoggedCommand({
1419
+ executable: installationArg.packageManagerPath,
1420
+ arguments: [
1421
+ 'add',
1422
+ '--global',
1423
+ '--save-exact',
1424
+ '--yes',
1425
+ '--reporter=append-only',
1426
+ '--allow-build=node-pty',
1427
+ '--allow-build=fs-ext-extra-prebuilt',
1428
+ `--registry=${npmRegistryUrl}`,
1429
+ `${controllerPackageName}@${versionArg}`,
1430
+ ],
1431
+ environment: createPackageManagerEnvironment(),
1432
+ timeoutMs: packageManagerTimeoutMs,
1433
+ });
1434
+ };
1435
+
1436
+ const packageAddArguments = (packageSpecArg: string, registryUrlArg: string): string[] => [
1437
+ 'add',
1438
+ '--global',
1439
+ '--save-exact',
1440
+ '--yes',
1441
+ '--reporter=append-only',
1442
+ '--allow-build=node-pty',
1443
+ '--allow-build=fs-ext-extra-prebuilt',
1444
+ `--registry=${registryUrlArg}`,
1445
+ packageSpecArg,
1446
+ ];
1447
+
1448
+ const preparePackageAdd = async (
1449
+ contextArg: IPackageTransitionContext,
1450
+ packageSpecArg: string,
1451
+ registryUrlArg: string,
1452
+ ): Promise<IPreparedManagedCommand> => await prepareManagedCommand({
1453
+ executable: contextArg.packageManagerPath,
1454
+ arguments: packageAddArguments(packageSpecArg, registryUrlArg),
1455
+ environment: createPackageManagerEnvironment(),
1456
+ timeoutMs: packageManagerTimeoutMs,
1457
+ captureOutput: false,
1458
+ outputByteLimit: loggedCommandOutputByteLimit,
1459
+ });
1460
+
1461
+ const addExactPackage = async (
1462
+ contextArg: IPackageTransitionContext,
1463
+ packageSpecArg: string,
1464
+ registryUrlArg: string,
1465
+ ): Promise<void> => {
1466
+ await runPreparedManagedCommand(
1467
+ await preparePackageAdd(contextArg, packageSpecArg, registryUrlArg),
1468
+ );
1469
+ };
1470
+
1471
+ const assertExpectedController = (
1472
+ statusArg: IControllerStatus,
1473
+ expectedArg: IUpgradeExpectedController,
1474
+ ): void => {
1475
+ if (
1476
+ statusArg.controllerPid !== expectedArg.pid
1477
+ || statusArg.processGroupId !== expectedArg.processGroupId
1478
+ || statusArg.processFingerprint !== expectedArg.processFingerprint
1479
+ || statusArg.lifecycleState !== 'ready'
1480
+ ) {
1481
+ throw new Error('The running controller changed after the upgrade request was authorized.');
1482
+ }
1483
+ };
1484
+
1485
+ const prepareControllerStart = async (
1486
+ installationArg: IPnpmGlobalInstallation,
1487
+ portArg: number,
1488
+ tokenArg: string,
1489
+ ): Promise<IPreparedManagedCommand> => await prepareManagedCommand({
1490
+ executable: process.execPath,
1491
+ arguments: [installationArg.cliPath, 'start', '--port', String(portArg)],
1492
+ environment: {
1493
+ ...process.env,
1494
+ [upgradeTokenEnvironmentVariable]: tokenArg,
1495
+ },
1496
+ timeoutMs: controllerRestartTimeoutMs,
1497
+ captureOutput: false,
1498
+ outputByteLimit: loggedCommandOutputByteLimit,
1499
+ });
1500
+
1501
+ const startController = async (
1502
+ installationArg: IPnpmGlobalInstallation,
1503
+ portArg: number,
1504
+ tokenArg: string,
1505
+ ): Promise<void> => await runPreparedManagedCommand(
1506
+ await prepareControllerStart(installationArg, portArg, tokenArg),
1507
+ ).then(() => undefined);
1508
+
1509
+ const retainedTargetTransactionMatches = (
1510
+ transactionArg: TUpgradeTransaction,
1511
+ installationArg: IPnpmGlobalInstallation,
1512
+ ): transactionArg is TUpgradeTransaction & {
1513
+ targetVersion: string;
1514
+ targetStartupInvoked: true;
1515
+ controller: NonNullable<TUpgradeTransaction['controller']>;
1516
+ } => Boolean(
1517
+ !transactionArg.terminal
1518
+ && transactionArg.controllerWasRunning
1519
+ && transactionArg.targetStartupInvoked === true
1520
+ && (transactionArg.version === upgradeCoordinationVersion
1521
+ || transactionArg.targetPackageCommitted === true)
1522
+ && (transactionArg.phase === 'restarting' || transactionArg.phase === 'continuing')
1523
+ && transactionArg.targetVersion === installationArg.packageVersion
1524
+ && transactionArg.controller?.command === '__serve'
1525
+ && transactionArg.controller.cliPath === installationArg.cliPath
1526
+ && transactionArg.controller.pid === transactionArg.controller.processGroupId
1527
+ && /^linux:[1-9][0-9]*:[1-9][0-9]*$/.test(transactionArg.controller.fingerprint)
1528
+ );
1529
+
1530
+ export interface IRetainedUpgradeTargetAuthorityOptions {
1531
+ coordinator: UpgradeCoordinator;
1532
+ token: string;
1533
+ }
1534
+
1535
+ export interface IRetainedUpgradeTargetAuthorityDependencies {
1536
+ platform?: NodeJS.Platform;
1537
+ resolveInstallation?: (
1538
+ transactionArg: TUpgradeTransaction,
1539
+ ) => Promise<IPnpmGlobalInstallation>;
1540
+ inspectProcess?: typeof inspectControllerProcess;
1541
+ }
1542
+
1543
+ export const retainedUpgradeTargetIsAuthoritative = async (
1544
+ optionsArg: IRetainedUpgradeTargetAuthorityOptions,
1545
+ dependenciesArg: IRetainedUpgradeTargetAuthorityDependencies = {},
1546
+ ): Promise<boolean> => {
1547
+ if ((dependenciesArg.platform ?? process.platform) !== 'linux') return false;
1548
+ const resolveInstallation = dependenciesArg.resolveInstallation
1549
+ ?? resolveInheritedUpgradeTargetInstallation;
1550
+ const inspectProcess = dependenciesArg.inspectProcess ?? inspectControllerProcess;
1551
+ const transaction = await optionsArg.coordinator.readTransaction(optionsArg.token);
1552
+ const installation = await resolveInstallation(transaction);
1553
+ if (!retainedTargetTransactionMatches(transaction, installation)) return false;
1554
+ const controller = transaction.controller;
1555
+ const identity = await inspectProcess({
1556
+ pid: controller.pid,
1557
+ cliPath: controller.cliPath,
1558
+ port: transaction.port,
1559
+ expectedProcessGroupId: controller.processGroupId,
1560
+ expectedFingerprint: controller.fingerprint,
1561
+ command: controller.command,
1562
+ });
1563
+ if (!identity?.processGroupLeader || identity.fingerprint !== controller.fingerprint) return false;
1564
+
1565
+ const currentTransaction = await optionsArg.coordinator.readTransaction(optionsArg.token);
1566
+ const currentInstallation = await resolveInstallation(currentTransaction);
1567
+ if (
1568
+ !retainedTargetTransactionMatches(currentTransaction, currentInstallation)
1569
+ || currentTransaction.controller.pid !== controller.pid
1570
+ || currentTransaction.controller.processGroupId !== controller.processGroupId
1571
+ || currentTransaction.controller.fingerprint !== controller.fingerprint
1572
+ || currentTransaction.controller.cliPath !== controller.cliPath
1573
+ ) return false;
1574
+ const currentIdentity = await inspectProcess({
1575
+ pid: currentTransaction.controller.pid,
1576
+ cliPath: currentTransaction.controller.cliPath,
1577
+ port: currentTransaction.port,
1578
+ expectedProcessGroupId: currentTransaction.controller.processGroupId,
1579
+ expectedFingerprint: currentTransaction.controller.fingerprint,
1580
+ command: currentTransaction.controller.command,
1581
+ });
1582
+ return Boolean(
1583
+ currentIdentity?.processGroupLeader
1584
+ && currentIdentity.fingerprint === currentTransaction.controller.fingerprint
1585
+ );
1586
+ };
1587
+
1588
+ const updateWorkerProgress = async (
1589
+ coordinatorArg: UpgradeCoordinator,
1590
+ tokenArg: string,
1591
+ phaseArg: TUpgradeTransaction['phase'],
1592
+ messageArg: string,
1593
+ ): Promise<TUpgradeTransaction> => coordinatorArg.mutateTransaction(tokenArg, (current) => ({
1594
+ ...current,
1595
+ phase: phaseArg,
1596
+ message: messageArg,
1597
+ }));
1598
+
1599
+ const grantControllerAction = async (
1600
+ coordinatorArg: UpgradeCoordinator,
1601
+ tokenArg: string,
1602
+ statusArg: IControllerStatus,
1603
+ actionArg: 'prepare' | 'finalize',
1604
+ modeArg?: 'continue' | 'compensate' | 'reopen',
1605
+ ): Promise<void> => coordinatorArg.createControllerActionGrant({
1606
+ token: tokenArg,
1607
+ action: actionArg,
1608
+ ...(modeArg === undefined ? {} : { mode: modeArg }),
1609
+ packageVersion: statusArg.packageVersion,
1610
+ controller: {
1611
+ pid: statusArg.controllerPid,
1612
+ processGroupId: statusArg.processGroupId,
1613
+ processFingerprint: statusArg.processFingerprint,
1614
+ },
1615
+ });
1616
+
1617
+ const queryUpgradeManagedController = async (
1618
+ portArg: number,
1619
+ identityArg: {
1620
+ packageName: string;
1621
+ packageVersion: string;
1622
+ managementVersion: number;
1623
+ },
1624
+ ): Promise<IControllerStatus> => queryControllerStatus(portArg, {
1625
+ packageName: identityArg.packageName,
1626
+ packageVersion: identityArg.packageVersion,
1627
+ upgradeManagementVersion: identityArg.managementVersion,
1628
+ });
1629
+
1630
+ const finalizeControllerUpgrade = async (
1631
+ coordinatorArg: UpgradeCoordinator,
1632
+ tokenArg: string,
1633
+ statusArg: IControllerStatus,
1634
+ modeArg: 'continue' | 'compensate' | 'reopen',
1635
+ ): Promise<void> => {
1636
+ await grantControllerAction(coordinatorArg, tokenArg, statusArg, 'finalize', modeArg);
1637
+ let response: Awaited<ReturnType<typeof requestControllerUpgradeFinalize>>;
1638
+ try {
1639
+ response = await requestControllerUpgradeFinalize(statusArg.controllerPid > 0
1640
+ ? (await coordinatorArg.readTransaction(tokenArg)).port
1641
+ : 0, tokenArg, modeArg);
1642
+ } finally {
1643
+ await coordinatorArg.removeControllerActionGrant('finalize', tokenArg);
1644
+ }
1645
+ if (response.failed > 0) {
1646
+ throw new Error(`${response.failed} paused session${response.failed === 1 ? '' : 's'} could not be continued.`);
1647
+ }
1648
+ };
1649
+
1650
+ export interface IWaitForControllerUpgradePreparationOptions {
1651
+ coordinator: UpgradeCoordinator;
1652
+ token: string;
1653
+ targetVersion: string;
1654
+ gracePeriodMs: number;
1655
+ controllerIsLive: () => Promise<boolean>;
1656
+ now?: () => number;
1657
+ delay?: (millisecondsArg: number) => Promise<void>;
1658
+ }
1659
+
1660
+ export const waitForControllerUpgradePreparation = async (
1661
+ optionsArg: IWaitForControllerUpgradePreparationOptions,
1662
+ ): Promise<TUpgradeTransaction> => {
1663
+ const now = optionsArg.now ?? Date.now;
1664
+ const delay = optionsArg.delay
1665
+ ?? (async (millisecondsArg: number) => new Promise((resolve) => (
1666
+ setTimeout(resolve, millisecondsArg)
1667
+ )));
1668
+ const acceptanceReconciliationDeadline = now()
1669
+ + upgradePreparationAcceptanceReconciliationMs;
1670
+ let nextSourceCheckAt = 0;
1671
+ while (true) {
1672
+ const currentTime = now();
1673
+ if (currentTime >= nextSourceCheckAt) {
1674
+ if (!await optionsArg.controllerIsLive()) {
1675
+ throw new Error('The source controller exited before upgrade preparation completed.');
1676
+ }
1677
+ nextSourceCheckAt = currentTime + upgradePreparationSourceCheckMs;
1678
+ }
1679
+ const transaction = await optionsArg.coordinator.readTransaction(optionsArg.token);
1680
+ if (transaction.terminal) {
1681
+ throw new Error(
1682
+ transaction.terminal.error
1683
+ ?? transaction.message
1684
+ ?? 'The source controller rejected upgrade preparation.',
1685
+ );
1686
+ }
1687
+ if (
1688
+ transaction.preparationAcceptedAt === undefined
1689
+ || transaction.preparationDeadlineAt === undefined
1690
+ ) {
1691
+ if (currentTime >= acceptanceReconciliationDeadline) {
1692
+ throw new Error('The source controller did not durably accept upgrade preparation.');
1693
+ }
1694
+ await delay(upgradePreparationPollMs);
1695
+ continue;
1696
+ }
1697
+ if (
1698
+ transaction.targetVersion !== optionsArg.targetVersion
1699
+ || transaction.gracePeriodMs !== optionsArg.gracePeriodMs
1700
+ || transaction.preparationDeadlineAt
1701
+ !== transaction.preparationAcceptedAt + optionsArg.gracePeriodMs
1702
+ ) throw new Error('The durable upgrade preparation does not match the worker request.');
1703
+ if (transaction.phase === 'stopping') {
1704
+ if (
1705
+ transaction.preparationCompletedAt === undefined
1706
+ || transaction.preparationCompletedAt < transaction.preparationAcceptedAt
1707
+ || transaction.preparationCompletedAt > transaction.preparationDeadlineAt
1708
+ ) throw new Error('The durable upgrade preparation completion is invalid.');
1709
+ return transaction;
1710
+ }
1711
+ if (currentTime > transaction.preparationDeadlineAt + upgradePreparationCleanupAllowanceMs) {
1712
+ throw new Error('Timed out waiting for upgrade preparation cleanup to settle.');
1713
+ }
1714
+ await delay(upgradePreparationPollMs);
1715
+ }
1716
+ };
1717
+
1718
+ export interface IPrepareAndStopRunningControllerOptions {
1719
+ coordinator: UpgradeCoordinator;
1720
+ token: string;
1721
+ port: number;
1722
+ targetVersion: string;
1723
+ gracePeriodMs: number;
1724
+ currentInstallation: IPnpmGlobalInstallation;
1725
+ runningStatus: IControllerStatus;
1726
+ }
1727
+
1728
+ export interface IPrepareAndStopRunningControllerDependencies {
1729
+ requestPrepareBegin?: typeof requestControllerUpgradePrepareBegin;
1730
+ stopController?: typeof stopVerifiedController;
1731
+ controllerIsLive?: () => Promise<boolean>;
1732
+ now?: () => number;
1733
+ delay?: (millisecondsArg: number) => Promise<void>;
1734
+ }
1735
+
1736
+ export const prepareAndStopRunningController = async (
1737
+ optionsArg: IPrepareAndStopRunningControllerOptions,
1738
+ dependenciesArg: IPrepareAndStopRunningControllerDependencies = {},
1739
+ ): Promise<void> => {
1740
+ await updateWorkerProgress(
1741
+ optionsArg.coordinator,
1742
+ optionsArg.token,
1743
+ 'preparing',
1744
+ `Preparing sessions for ${optionsArg.currentInstallation.packageVersion} to ${optionsArg.targetVersion}.`,
1745
+ );
1746
+ await grantControllerAction(
1747
+ optionsArg.coordinator,
1748
+ optionsArg.token,
1749
+ optionsArg.runningStatus,
1750
+ 'prepare',
1751
+ );
1752
+ let beginError: unknown;
1753
+ try {
1754
+ await (dependenciesArg.requestPrepareBegin ?? requestControllerUpgradePrepareBegin)(
1755
+ optionsArg.port,
1756
+ optionsArg.token,
1757
+ optionsArg.targetVersion,
1758
+ optionsArg.gracePeriodMs,
1759
+ );
1760
+ } catch (errorArg) {
1761
+ beginError = errorArg;
1762
+ } finally {
1763
+ await optionsArg.coordinator.removeControllerActionGrant('prepare', optionsArg.token);
1764
+ }
1765
+ try {
1766
+ await waitForControllerUpgradePreparation({
1767
+ coordinator: optionsArg.coordinator,
1768
+ token: optionsArg.token,
1769
+ targetVersion: optionsArg.targetVersion,
1770
+ gracePeriodMs: optionsArg.gracePeriodMs,
1771
+ controllerIsLive: dependenciesArg.controllerIsLive ?? (async () => Boolean(
1772
+ await inspectControllerProcess({
1773
+ pid: optionsArg.runningStatus.controllerPid,
1774
+ cliPath: optionsArg.currentInstallation.cliPath,
1775
+ port: optionsArg.port,
1776
+ expectedProcessGroupId: optionsArg.runningStatus.processGroupId,
1777
+ expectedFingerprint: optionsArg.runningStatus.processFingerprint,
1778
+ command: optionsArg.runningStatus.processMode === 'detached' ? '__serve' : 'foreground',
1779
+ }),
1780
+ )),
1781
+ ...(dependenciesArg.now ? { now: dependenciesArg.now } : {}),
1782
+ ...(dependenciesArg.delay ? { delay: dependenciesArg.delay } : {}),
1783
+ });
1784
+ } catch (errorArg) {
1785
+ if (beginError === undefined) throw errorArg;
1786
+ throw new AggregateError(
1787
+ [beginError, errorArg],
1788
+ 'The source controller did not complete asynchronous upgrade preparation.',
1789
+ );
1790
+ }
1791
+ await (dependenciesArg.stopController ?? stopVerifiedController)({
1792
+ status: optionsArg.runningStatus,
1793
+ cliPath: optionsArg.currentInstallation.cliPath,
1794
+ port: optionsArg.port,
1795
+ });
1796
+ };
1797
+
1798
+ const restorePreviousInstallation = async (
1799
+ installationArg: IPnpmGlobalInstallation,
1800
+ ): Promise<IPnpmGlobalInstallation> => {
1801
+ try {
1802
+ const current = await resolvePnpmGlobalInstallation();
1803
+ if (current.packageVersion === installationArg.packageVersion) return current;
1804
+ } catch (errorArg) {
1805
+ if (errorContainsCommandCleanupFailure(errorArg)) throw errorArg;
1806
+ // Reinstalling the exact prior version is the authoritative rollback.
1807
+ }
1808
+ await installVersion(installationArg, installationArg.packageVersion);
1809
+ const restored = await resolvePnpmGlobalInstallation();
1810
+ if (restored.packageVersion !== installationArg.packageVersion) {
1811
+ throw new Error('The previous pnpm-global package version was not restored.');
1812
+ }
1813
+ return restored;
1814
+ };
1815
+
1816
+ const errorContainsCommandCleanupFailure = (errorArg: unknown): boolean => {
1817
+ if (errorArg instanceof UpgradeCommandCleanupError) return true;
1818
+ if (errorArg instanceof AggregateError) {
1819
+ return errorArg.errors.some((nestedErrorArg) => (
1820
+ errorContainsCommandCleanupFailure(nestedErrorArg)
1821
+ ));
1822
+ }
1823
+ return errorArg instanceof Error
1824
+ && errorArg.cause !== undefined
1825
+ && errorContainsCommandCleanupFailure(errorArg.cause);
1826
+ };
1827
+
1828
+ const sourceControllerIdentity = (transactionArg: TUpgradeTransaction): {
1829
+ packageName: string;
1830
+ packageVersion: string;
1831
+ managementVersion: number;
1832
+ } => transactionArg.version === upgradeCoordinationVersion
1833
+ ? {
1834
+ packageName: controllerPackageName,
1835
+ packageVersion: transactionArg.sourceVersion,
1836
+ managementVersion: controllerUpgradeManagementVersion,
1837
+ }
1838
+ : {
1839
+ packageName: transactionArg.sourcePackageName,
1840
+ packageVersion: transactionArg.sourceVersion,
1841
+ managementVersion: transactionArg.sourceManagementVersion,
1842
+ };
1843
+
1844
+ const requirePackageTransitionTransaction = (
1845
+ transactionArg: TUpgradeTransaction,
1846
+ ): IUpgradeTransactionV3 => {
1847
+ if (transactionArg.version !== upgradePackageTransitionTransactionVersion) {
1848
+ throw new Error('The package-transition transaction changed format.');
1849
+ }
1850
+ return transactionArg;
1851
+ };
1852
+
1853
+ const targetControllerIdentity = (transactionArg: TUpgradeTransaction): {
1854
+ packageName: string;
1855
+ packageVersion: string;
1856
+ managementVersion: number;
1857
+ } => transactionArg.version === upgradeCoordinationVersion
1858
+ ? {
1859
+ packageName: controllerPackageName,
1860
+ packageVersion: transactionArg.targetVersion
1861
+ ?? (() => { throw new Error('The upgrade target version is unavailable.'); })(),
1862
+ managementVersion: controllerUpgradeManagementVersion,
1863
+ }
1864
+ : {
1865
+ packageName: transactionArg.targetPackageName,
1866
+ packageVersion: transactionArg.targetVersion,
1867
+ managementVersion: transactionArg.targetManagementVersion,
1868
+ };
1869
+
1870
+ export const assertInheritedUpgradeTransaction = (
1871
+ transactionArg: TUpgradeTransaction,
1872
+ currentIdentityArg: {
1873
+ packageName: string;
1874
+ packageVersion: string;
1875
+ managementVersion: number;
1876
+ cliName: string;
1877
+ cliRelativePath: string;
1878
+ },
1879
+ ): void => {
1880
+ if (
1881
+ !transactionArg.controllerWasRunning
1882
+ || transactionArg.targetStartupInvoked !== true
1883
+ || transactionArg.targetVersion !== currentIdentityArg.packageVersion
1884
+ ) throw new Error('The inherited upgrade transaction did not authorize target startup.');
1885
+ if (transactionArg.version === upgradeCoordinationVersion) {
1886
+ if (
1887
+ currentIdentityArg.packageName !== String(controllerPackageName)
1888
+ || currentIdentityArg.managementVersion !== controllerUpgradeManagementVersion
1889
+ ) throw new Error('The inherited v2 upgrade target identity is incompatible.');
1890
+ return;
1891
+ }
1892
+ if (
1893
+ transactionArg.targetPackageCommitted !== true
1894
+ || transactionArg.targetPackageName !== currentIdentityArg.packageName
1895
+ || transactionArg.targetManagementVersion !== currentIdentityArg.managementVersion
1896
+ || transactionArg.targetCliName !== currentIdentityArg.cliName
1897
+ || transactionArg.targetCliRelativePath !== currentIdentityArg.cliRelativePath
1898
+ ) throw new Error('The inherited v3 upgrade target identity is incompatible.');
1899
+ };
1900
+
1901
+ const runPackageTransitionUpgradeWorker = async (optionsArg: {
1902
+ coordinator: UpgradeCoordinator;
1903
+ payload: IUpgradeWorkerPayload;
1904
+ transaction: IUpgradeTransactionV3;
1905
+ currentInstallation: IPnpmGlobalInstallation;
1906
+ registryUrl: string;
1907
+ }): Promise<void> => {
1908
+ const {
1909
+ coordinator,
1910
+ payload,
1911
+ transaction: initialTransaction,
1912
+ currentInstallation,
1913
+ registryUrl,
1914
+ } = optionsArg;
1915
+ if (
1916
+ commitinfo.name !== initialTransaction.sourcePackageName
1917
+ || commitinfo.version !== initialTransaction.sourceVersion
1918
+ || currentInstallation.packageVersion !== initialTransaction.sourceVersion
1919
+ ) throw new Error('The package-transition worker is not running from the exact bridge package.');
1920
+ const context: IPackageTransitionContext = {
1921
+ packageManagerPath: currentInstallation.packageManagerPath,
1922
+ globalRoot: currentInstallation.globalRoot,
1923
+ globalBinDirectory: currentInstallation.globalBinDirectory,
1924
+ };
1925
+
1926
+ // Registry validation is deliberately complete before controller preparation or package mutation.
1927
+ await resolveExactPackageTransitionTarget(context, registryUrl);
1928
+ await coordinator.mutateTransaction(payload.token, (current) => {
1929
+ if (current.version !== upgradePackageTransitionTransactionVersion) {
1930
+ throw new Error('The package-transition transaction changed format.');
1931
+ }
1932
+ return {
1933
+ ...current,
1934
+ phase: 'checking',
1935
+ message: `Resolved exact target ${current.targetPackageName}@${current.targetVersion}.`,
1936
+ };
1937
+ });
1938
+ process.stdout.write(
1939
+ `${initialTransaction.sourceCliName} upgrade: resolved ${initialTransaction.targetPackageName}@${initialTransaction.targetVersion}.\n`,
1940
+ );
1941
+
1942
+ const controllerProcesses = await listControllerProcessesForCli(currentInstallation.cliPath);
1943
+ let runningStatus: IControllerStatus | undefined;
1944
+ if (payload.expectedController) {
1945
+ runningStatus = await queryControllerStatus(payload.port, {
1946
+ packageName: initialTransaction.sourcePackageName,
1947
+ packageVersion: initialTransaction.sourceVersion,
1948
+ protocolVersion: controllerProtocolVersion,
1949
+ upgradeManagementVersion: initialTransaction.sourceManagementVersion,
1950
+ });
1951
+ assertExpectedController(runningStatus, payload.expectedController);
1952
+ if (
1953
+ controllerProcesses.length !== 1
1954
+ || controllerProcesses[0].identity.pid !== runningStatus.controllerPid
1955
+ || controllerProcesses[0].port !== payload.port
1956
+ ) throw new Error('Additional or mismatched pnpm-global controller instances prevent upgrade.');
1957
+ } else if (controllerProcesses.length > 0) {
1958
+ throw new Error('A pnpm-global controller started after the upgrade request; upgrade aborted.');
1959
+ } else if (await isLoopbackPortListening(payload.port)) {
1960
+ throw new Error(
1961
+ `The requested controller port became occupied while ${initialTransaction.sourceCliName} upgrade was starting.`,
1962
+ );
1963
+ }
1964
+
1965
+ if (runningStatus) {
1966
+ await prepareAndStopRunningController({
1967
+ coordinator,
1968
+ token: payload.token,
1969
+ port: payload.port,
1970
+ targetVersion: initialTransaction.targetVersion,
1971
+ gracePeriodMs: payload.gracePeriodMs,
1972
+ currentInstallation,
1973
+ runningStatus,
1974
+ });
1975
+ process.stdout.write(`${initialTransaction.sourceCliName} upgrade: bridge controller stopped cleanly.\n`);
1976
+ }
1977
+
1978
+ let targetInstallation: IPnpmGlobalInstallation;
1979
+ try {
1980
+ await updateWorkerProgress(
1981
+ coordinator,
1982
+ payload.token,
1983
+ 'installing',
1984
+ `Transitioning ${initialTransaction.sourcePackageName} to ${initialTransaction.targetPackageName}.`,
1985
+ );
1986
+ if (await isLoopbackPortListening(payload.port)) {
1987
+ throw new Error('The controller port became occupied before package replacement.');
1988
+ }
1989
+ const sourceDataWriters = await listControllerDataWriterProcessesForCli(
1990
+ currentInstallation.cliPath,
1991
+ );
1992
+ if (sourceDataWriters.length > 0) {
1993
+ const writer = sourceDataWriters[0];
1994
+ throw new Error(
1995
+ `Package replacement is blocked by ${writer.kind} source writer PID ${writer.identity.pid}.`,
1996
+ );
1997
+ }
1998
+
1999
+ const sourceSpec = `${initialTransaction.sourcePackageName}@${initialTransaction.sourceVersion}`;
2000
+ const targetSpec = `${initialTransaction.targetPackageName}@${initialTransaction.targetVersion}`;
2001
+ const groupedSpec = `${sourceSpec},${targetSpec}`;
2002
+ let current = requirePackageTransitionTransaction(
2003
+ await coordinator.readTransaction(payload.token),
2004
+ );
2005
+
2006
+ if (current.targetPackageCommitStarted !== true) {
2007
+ if (current.groupedTargetVerified !== true) {
2008
+ if (current.packageTransitionStarted === true) {
2009
+ await addExactPackage(context, sourceSpec, registryUrl);
2010
+ await verifyPackageTransitionTopology(context, 'source-only');
2011
+ }
2012
+ const groupedAdd = await preparePackageAdd(context, groupedSpec, registryUrl);
2013
+ if (current.packageTransitionStarted !== true) {
2014
+ current = requirePackageTransitionTransaction(await coordinator.mutateTransaction(payload.token, (transaction) => {
2015
+ if (transaction.version !== upgradePackageTransitionTransactionVersion) {
2016
+ throw new Error('The package-transition transaction changed format.');
2017
+ }
2018
+ return {
2019
+ ...transaction,
2020
+ packageTransitionStarted: true as const,
2021
+ message: `Installing grouped ${sourceSpec},${targetSpec}.`,
2022
+ };
2023
+ }));
2024
+ }
2025
+ await runPreparedManagedCommand(groupedAdd);
2026
+ await verifyPackageTransitionTopology(context, 'grouped');
2027
+ current = requirePackageTransitionTransaction(await coordinator.mutateTransaction(payload.token, (transaction) => {
2028
+ if (transaction.version !== upgradePackageTransitionTransactionVersion) {
2029
+ throw new Error('The package-transition transaction changed format.');
2030
+ }
2031
+ return {
2032
+ ...transaction,
2033
+ groupedTargetVerified: true as const,
2034
+ message: 'Verified the grouped bridge and target package topology.',
2035
+ };
2036
+ }));
2037
+ } else {
2038
+ await verifyPackageTransitionTopology(context, 'grouped');
2039
+ }
2040
+
2041
+ const targetAdd = await preparePackageAdd(context, targetSpec, registryUrl);
2042
+ current = requirePackageTransitionTransaction(await coordinator.mutateTransaction(payload.token, (transaction) => {
2043
+ if (transaction.version !== upgradePackageTransitionTransactionVersion) {
2044
+ throw new Error('The package-transition transaction changed format.');
2045
+ }
2046
+ return {
2047
+ ...transaction,
2048
+ targetPackageCommitStarted: true as const,
2049
+ message: `Committing exact target ${targetSpec}.`,
2050
+ };
2051
+ }));
2052
+ await runPreparedManagedCommand(targetAdd);
2053
+ } else if (current.targetPackageCommitted !== true) {
2054
+ await addExactPackage(context, targetSpec, registryUrl);
2055
+ }
2056
+
2057
+ if (current.targetPackageCommitted !== true) {
2058
+ const topology = await verifyPackageTransitionTopology(context, 'target-only');
2059
+ if (!topology.target) throw new Error('The exact target package installation is unavailable.');
2060
+ targetInstallation = topology.target;
2061
+ await coordinator.mutateTransaction(payload.token, (transaction) => {
2062
+ if (transaction.version !== upgradePackageTransitionTransactionVersion) {
2063
+ throw new Error('The package-transition transaction changed format.');
2064
+ }
2065
+ return {
2066
+ ...transaction,
2067
+ targetPackageCommitted: true as const,
2068
+ message: `Committed exact target ${targetSpec}.`,
2069
+ };
2070
+ });
2071
+ } else {
2072
+ const topology = await verifyPackageTransitionTopology(context, 'target-only');
2073
+ if (!topology.target) throw new Error('The exact target package installation is unavailable.');
2074
+ targetInstallation = topology.target;
2075
+ }
2076
+ } catch (upgradeErrorArg) {
2077
+ if (errorContainsCommandCleanupFailure(upgradeErrorArg)) throw upgradeErrorArg;
2078
+ const current = await coordinator.readTransaction(payload.token);
2079
+ if (
2080
+ current.version === upgradePackageTransitionTransactionVersion
2081
+ && current.targetPackageCommitStarted === true
2082
+ ) throw upgradeErrorArg;
2083
+ const recoveryErrors: unknown[] = [upgradeErrorArg];
2084
+ try {
2085
+ const sourceSpec = `${initialTransaction.sourcePackageName}@${initialTransaction.sourceVersion}`;
2086
+ if (
2087
+ current.version === upgradePackageTransitionTransactionVersion
2088
+ && current.packageTransitionStarted === true
2089
+ ) await addExactPackage(context, sourceSpec, registryUrl);
2090
+ const sourceTopology = await verifyPackageTransitionTopology(context, 'source-only');
2091
+ if (!sourceTopology.source) throw new Error('The exact bridge package was not restored.');
2092
+ if (runningStatus) {
2093
+ await startController(sourceTopology.source, payload.port, payload.token);
2094
+ const restoredStatus = await queryUpgradeManagedController(
2095
+ payload.port,
2096
+ sourceControllerIdentity(initialTransaction),
2097
+ );
2098
+ await finalizeControllerUpgrade(
2099
+ coordinator,
2100
+ payload.token,
2101
+ restoredStatus,
2102
+ 'compensate',
2103
+ );
2104
+ process.stdout.write(
2105
+ `${initialTransaction.sourceCliName} upgrade: bridge package restored and restarted.\n`,
2106
+ );
2107
+ }
2108
+ } catch (recoveryErrorArg) {
2109
+ recoveryErrors.push(recoveryErrorArg);
2110
+ }
2111
+ throw new AggregateError(
2112
+ recoveryErrors,
2113
+ `${initialTransaction.sourceCliName} upgrade failed before target package commit began.`,
2114
+ );
2115
+ }
2116
+
2117
+ process.stdout.write(
2118
+ `${initialTransaction.sourceCliName} upgrade: committed ${initialTransaction.targetPackageName}@${initialTransaction.targetVersion}.\n`,
2119
+ );
2120
+ if (runningStatus) {
2121
+ await updateWorkerProgress(
2122
+ coordinator,
2123
+ payload.token,
2124
+ 'restarting',
2125
+ `Restarting ${initialTransaction.targetPackageName} ${initialTransaction.targetVersion}.`,
2126
+ );
2127
+ const preparedTargetStart = await prepareControllerStart(
2128
+ targetInstallation,
2129
+ payload.port,
2130
+ payload.token,
2131
+ );
2132
+ await coordinator.mutateTransaction(payload.token, (current) => {
2133
+ if (current.version !== upgradePackageTransitionTransactionVersion) {
2134
+ throw new Error('The package-transition transaction changed format.');
2135
+ }
2136
+ return {
2137
+ ...current,
2138
+ targetStartupInvoked: true as const,
2139
+ message: `Invoking ${current.targetPackageName} ${current.targetVersion}.`,
2140
+ };
2141
+ });
2142
+ await runPreparedManagedCommand(preparedTargetStart);
2143
+ const targetStatus = await queryUpgradeManagedController(
2144
+ payload.port,
2145
+ targetControllerIdentity(initialTransaction),
2146
+ );
2147
+ await updateWorkerProgress(
2148
+ coordinator,
2149
+ payload.token,
2150
+ payload.continueSessions ? 'continuing' : 'restarting',
2151
+ payload.continueSessions
2152
+ ? 'Continuing sessions paused for the upgrade.'
2153
+ : 'Reopening harness prompt admission.',
2154
+ );
2155
+ await finalizeControllerUpgrade(
2156
+ coordinator,
2157
+ payload.token,
2158
+ targetStatus,
2159
+ payload.continueSessions ? 'continue' : 'reopen',
2160
+ );
2161
+ process.stdout.write(
2162
+ `${initialTransaction.sourceCliName} upgrade: target controller restarted on port ${payload.port}.\n`,
2163
+ );
2164
+ }
2165
+ await coordinator.finishTransaction(
2166
+ payload.token,
2167
+ true,
2168
+ `Upgrade to ${initialTransaction.targetPackageName}@${initialTransaction.targetVersion} completed.`,
2169
+ );
2170
+ };
2171
+
2172
+ interface IUpgradeWorkerInternalOptions {
2173
+ registryUrl?: string;
2174
+ recoveryContext?: string;
2175
+ recoveryBody?: (optionsArg: {
2176
+ coordinator: UpgradeCoordinator;
2177
+ token: string;
2178
+ transaction: TUpgradeTransaction;
2179
+ installation: IPnpmGlobalInstallation;
2180
+ }) => Promise<boolean>;
2181
+ isCommandCleanupFailure?: (errorArg: unknown) => boolean;
2182
+ }
2183
+
2184
+ const runRecoveryUpgradeWorker = async (
2185
+ payloadArg: IUpgradeWorkerPayload,
2186
+ internalOptionsArg: IUpgradeWorkerInternalOptions,
2187
+ ): Promise<void> => {
2188
+ const context = parseUpgradeRecoveryWorkerContext(internalOptionsArg.recoveryContext);
2189
+ const coordinator = new UpgradeCoordinator(context.canonicalGlobalRoot);
2190
+ let lock: Awaited<ReturnType<UpgradeCoordinator['acquireUpgradeLock']>> | undefined;
2191
+ let releaseLock = true;
2192
+ try {
2193
+ await coordinator.waitForWorkerAcknowledgement(payloadArg.token, 10_000);
2194
+ lock = await coordinator.acquireUpgradeLock({
2195
+ token: payloadArg.token,
2196
+ cliPath: context.cliPath,
2197
+ });
2198
+ await coordinator.waitForStartLeasesToDrain(lock);
2199
+ const transaction = await coordinator.readTransaction(payloadArg.token);
2200
+ if (transaction.terminal) return;
2201
+ if (
2202
+ transaction.port !== payloadArg.port
2203
+ || transaction.gracePeriodMs !== payloadArg.gracePeriodMs
2204
+ || transaction.continueSessions !== payloadArg.continueSessions
2205
+ ) throw new Error('The recovery worker payload does not match its upgrade transaction.');
2206
+ const identity = await readControllerProcessIdentity(process.pid);
2207
+ if (
2208
+ !identity
2209
+ || !identity.processGroupLeader
2210
+ || identity.processGroupId !== process.pid
2211
+ || transaction.worker?.pid !== identity.pid
2212
+ || transaction.worker.processGroupId !== identity.processGroupId
2213
+ || transaction.worker.fingerprint !== identity.fingerprint
2214
+ || transaction.worker.cliPath !== context.cliPath
2215
+ ) throw new Error('The recovery worker transaction identity is incompatible.');
2216
+ const installation = await resolveAuthoritativeRecoveryInstallation(transaction, {
2217
+ packageManagerPath: context.packageManagerPath,
2218
+ globalRoot: context.canonicalGlobalRoot,
2219
+ globalBinDirectory: context.globalBinDirectory,
2220
+ packageDirectory: plugins.path.dirname(context.cliPath),
2221
+ cliPath: context.cliPath,
2222
+ packageVersion: '',
2223
+ });
2224
+ if (installation.cliPath !== context.cliPath) {
2225
+ throw new Error('The authoritative recovery CLI changed during handoff.');
2226
+ }
2227
+ upgradeWorkerCliPath = installation.cliPath;
2228
+ await (internalOptionsArg.recoveryBody ?? recoverStalledUpgradeUnderLock)({
2229
+ coordinator,
2230
+ token: payloadArg.token,
2231
+ transaction,
2232
+ installation,
2233
+ });
2234
+ } catch (errorArg) {
2235
+ const cleanupFailed = (internalOptionsArg.isCommandCleanupFailure
2236
+ ?? errorContainsCommandCleanupFailure)(errorArg);
2237
+ if (cleanupFailed) releaseLock = false;
2238
+ await coordinator.mutateTransaction(payloadArg.token, (current) => ({
2239
+ ...current,
2240
+ message: errorArg instanceof Error ? errorArg.message : String(errorArg),
2241
+ })).catch(() => undefined);
2242
+ throw errorArg;
2243
+ } finally {
2244
+ if (releaseLock) await lock?.release();
2245
+ }
2246
+ };
2247
+
2248
+ export function runUpgradeWorker(payloadArg: IUpgradeWorkerPayload): Promise<void>;
2249
+ export async function runUpgradeWorker(
2250
+ payloadArg: IUpgradeWorkerPayload,
2251
+ internalOptionsArg: IUpgradeWorkerInternalOptions = {},
2252
+ ): Promise<void> {
2253
+ const environmentToken = process.env[upgradeTokenEnvironmentVariable];
2254
+ if (environmentToken !== payloadArg.token) {
2255
+ throw new Error(`The detached ${currentCliName} upgrade worker token binding is invalid.`);
2256
+ }
2257
+ delete process.env[upgradeTokenEnvironmentVariable];
2258
+ if (internalOptionsArg.recoveryContext !== undefined) {
2259
+ await runRecoveryUpgradeWorker(payloadArg, internalOptionsArg);
2260
+ return;
2261
+ }
2262
+ const installation = await resolveCurrentPnpmGlobalInstallation();
2263
+ upgradeWorkerCliPath = installation.cliPath;
2264
+ const coordinator = new UpgradeCoordinator(installation.globalRoot);
2265
+ let lock: Awaited<ReturnType<UpgradeCoordinator['acquireUpgradeLock']>> | undefined;
2266
+ let releaseLock = true;
2267
+ try {
2268
+ await coordinator.registerWorker(payloadArg.token, installation.cliPath);
2269
+ await coordinator.waitForWorkerAcknowledgement(payloadArg.token, 10_000);
2270
+ lock = await coordinator.acquireUpgradeLock({
2271
+ token: payloadArg.token,
2272
+ cliPath: installation.cliPath,
2273
+ });
2274
+ await coordinator.waitForStartLeasesToDrain(lock);
2275
+ const currentInstallation = await resolveCurrentPnpmGlobalInstallation();
2276
+ if (currentInstallation.globalRoot !== installation.globalRoot) {
2277
+ throw new Error(`The pnpm global root changed while ${currentCliName} upgrade was starting.`);
2278
+ }
2279
+ const transaction = await coordinator.readTransaction(payloadArg.token);
2280
+ if (transaction.version === upgradePackageTransitionTransactionVersion) {
2281
+ await runPackageTransitionUpgradeWorker({
2282
+ coordinator,
2283
+ payload: payloadArg,
2284
+ transaction,
2285
+ currentInstallation,
2286
+ registryUrl: assertInternalRegistryUrl(internalOptionsArg.registryUrl ?? npmRegistryUrl),
2287
+ });
2288
+ return;
2289
+ }
2290
+ const latestVersion = await readLatestVersion(currentInstallation);
2291
+ await coordinator.mutateTransaction(payloadArg.token, (current) => {
2292
+ if (current.version !== upgradeCoordinationVersion) {
2293
+ throw new Error('The same-package upgrade transaction changed format.');
2294
+ }
2295
+ return {
2296
+ ...current,
2297
+ targetVersion: latestVersion,
2298
+ phase: 'checking',
2299
+ message: `Installed ${currentInstallation.packageVersion}; npm latest is ${latestVersion}.`,
2300
+ };
2301
+ });
2302
+ const versionComparison = compareSemver(latestVersion, currentInstallation.packageVersion);
2303
+ process.stdout.write(
2304
+ `${currentCliName} upgrade: installed ${currentInstallation.packageVersion}, npm latest ${latestVersion}.\n`,
2305
+ );
2306
+ if (versionComparison <= 0) {
2307
+ process.stdout.write(
2308
+ versionComparison === 0
2309
+ ? `${currentCliName} upgrade: already up to date.\n`
2310
+ : `${currentCliName} upgrade: installed version is newer than npm latest; refusing to downgrade.\n`,
2311
+ );
2312
+ await coordinator.finishTransaction(
2313
+ payloadArg.token,
2314
+ true,
2315
+ versionComparison === 0
2316
+ ? 'Already up to date.'
2317
+ : 'Installed version is newer than npm latest; no downgrade was performed.',
2318
+ );
2319
+ return;
2320
+ }
2321
+
2322
+ const controllerProcesses = await listControllerProcessesForCli(currentInstallation.cliPath);
2323
+ let runningStatus: IControllerStatus | undefined;
2324
+ if (payloadArg.expectedController) {
2325
+ runningStatus = await queryControllerStatus(payloadArg.port, {
2326
+ packageName: controllerPackageName,
2327
+ packageVersion: currentInstallation.packageVersion,
2328
+ protocolVersion: controllerProtocolVersion,
2329
+ upgradeManagementVersion: controllerUpgradeManagementVersion,
2330
+ });
2331
+ assertExpectedController(runningStatus, payloadArg.expectedController);
2332
+ if (
2333
+ controllerProcesses.length !== 1
2334
+ || controllerProcesses[0].identity.pid !== runningStatus.controllerPid
2335
+ || controllerProcesses[0].port !== payloadArg.port
2336
+ ) {
2337
+ throw new Error('Additional or mismatched pnpm-global controller instances prevent upgrade.');
2338
+ }
2339
+ } else if (controllerProcesses.length > 0) {
2340
+ throw new Error('A pnpm-global controller started after the upgrade request; upgrade aborted.');
2341
+ } else if (await isLoopbackPortListening(payloadArg.port)) {
2342
+ throw new Error(
2343
+ `The requested controller port became occupied while ${currentCliName} upgrade was starting.`,
2344
+ );
2345
+ }
2346
+
2347
+ if (runningStatus) {
2348
+ await prepareAndStopRunningController({
2349
+ coordinator,
2350
+ token: payloadArg.token,
2351
+ port: payloadArg.port,
2352
+ targetVersion: latestVersion,
2353
+ gracePeriodMs: payloadArg.gracePeriodMs,
2354
+ currentInstallation,
2355
+ runningStatus,
2356
+ });
2357
+ process.stdout.write(`${currentCliName} upgrade: previous controller stopped cleanly.\n`);
2358
+ }
2359
+
2360
+ let upgradedInstallation: IPnpmGlobalInstallation;
2361
+ try {
2362
+ await updateWorkerProgress(
2363
+ coordinator,
2364
+ payloadArg.token,
2365
+ 'installing',
2366
+ `Installing ${controllerPackageName} ${latestVersion}.`,
2367
+ );
2368
+ if (await isLoopbackPortListening(payloadArg.port)) {
2369
+ throw new Error('The controller port became occupied before package replacement.');
2370
+ }
2371
+ const sourceDataWriters = await listControllerDataWriterProcessesForCli(
2372
+ currentInstallation.cliPath,
2373
+ );
2374
+ if (sourceDataWriters.length > 0) {
2375
+ const writer = sourceDataWriters[0];
2376
+ throw new Error(
2377
+ `Package replacement is blocked by ${writer.kind} source writer PID ${writer.identity.pid}.`,
2378
+ );
2379
+ }
2380
+ await installVersion(currentInstallation, latestVersion);
2381
+ upgradedInstallation = await resolvePnpmGlobalInstallation();
2382
+ if (upgradedInstallation.packageVersion !== latestVersion) {
2383
+ throw new Error('pnpm completed without installing the requested npm latest version.');
2384
+ }
2385
+ } catch (upgradeErrorArg) {
2386
+ if (errorContainsCommandCleanupFailure(upgradeErrorArg)) throw upgradeErrorArg;
2387
+ const recoveryErrors: unknown[] = [upgradeErrorArg];
2388
+ try {
2389
+ const restored = await restorePreviousInstallation(currentInstallation);
2390
+ if (runningStatus) {
2391
+ await startController(restored, payloadArg.port, payloadArg.token);
2392
+ const restoredStatus = await queryUpgradeManagedController(
2393
+ payloadArg.port,
2394
+ {
2395
+ packageName: controllerPackageName,
2396
+ packageVersion: restored.packageVersion,
2397
+ managementVersion: controllerUpgradeManagementVersion,
2398
+ },
2399
+ );
2400
+ await finalizeControllerUpgrade(
2401
+ coordinator,
2402
+ payloadArg.token,
2403
+ restoredStatus,
2404
+ 'compensate',
2405
+ );
2406
+ process.stdout.write(
2407
+ `${currentCliName} upgrade: previous controller version restored and restarted.\n`,
2408
+ );
2409
+ }
2410
+ } catch (recoveryErrorArg) {
2411
+ recoveryErrors.push(recoveryErrorArg);
2412
+ }
2413
+ throw new AggregateError(
2414
+ recoveryErrors,
2415
+ `${currentCliName} upgrade failed before the new controller started.`,
2416
+ );
2417
+ }
2418
+
2419
+ process.stdout.write(`${currentCliName} upgrade: installed ${latestVersion}.\n`);
2420
+ if (runningStatus) {
2421
+ // From this point onward rollback is unsafe: startup may run data migrations.
2422
+ await updateWorkerProgress(
2423
+ coordinator,
2424
+ payloadArg.token,
2425
+ 'restarting',
2426
+ `Restarting ${controllerPackageName} ${latestVersion}.`,
2427
+ );
2428
+ await coordinator.mutateTransaction(payloadArg.token, (current) => ({
2429
+ ...current,
2430
+ targetStartupInvoked: true,
2431
+ message: `Invoking ${controllerPackageName} ${latestVersion}.`,
2432
+ }));
2433
+ await startController(upgradedInstallation, payloadArg.port, payloadArg.token);
2434
+ const upgradedStatus = await queryUpgradeManagedController(payloadArg.port, {
2435
+ packageName: controllerPackageName,
2436
+ packageVersion: latestVersion,
2437
+ managementVersion: controllerUpgradeManagementVersion,
2438
+ });
2439
+ await updateWorkerProgress(
2440
+ coordinator,
2441
+ payloadArg.token,
2442
+ payloadArg.continueSessions ? 'continuing' : 'restarting',
2443
+ payloadArg.continueSessions
2444
+ ? 'Continuing sessions paused for the upgrade.'
2445
+ : 'Reopening harness prompt admission.',
2446
+ );
2447
+ await finalizeControllerUpgrade(
2448
+ coordinator,
2449
+ payloadArg.token,
2450
+ upgradedStatus,
2451
+ payloadArg.continueSessions ? 'continue' : 'reopen',
2452
+ );
2453
+ process.stdout.write(
2454
+ `${currentCliName} upgrade: controller restarted on port ${payloadArg.port}.\n`,
2455
+ );
2456
+ }
2457
+ await coordinator.finishTransaction(
2458
+ payloadArg.token,
2459
+ true,
2460
+ `Upgrade to ${latestVersion} completed.`,
2461
+ );
2462
+ } catch (errorArg) {
2463
+ if (errorContainsCommandCleanupFailure(errorArg)) releaseLock = false;
2464
+ const failedTransaction = await coordinator.readTransaction(payloadArg.token).catch(() => undefined);
2465
+ if (
2466
+ (failedTransaction?.version === upgradePackageTransitionTransactionVersion
2467
+ ? failedTransaction.targetPackageCommitStarted === true
2468
+ : failedTransaction?.targetStartupInvoked === true)
2469
+ || errorContainsCommandCleanupFailure(errorArg)
2470
+ ) {
2471
+ await coordinator.mutateTransaction(payloadArg.token, (current) => ({
2472
+ ...current,
2473
+ message: errorArg instanceof Error ? errorArg.message : String(errorArg),
2474
+ })).catch(() => undefined);
2475
+ throw errorArg;
2476
+ }
2477
+ await coordinator.finishTransaction(
2478
+ payloadArg.token,
2479
+ false,
2480
+ 'Upgrade failed.',
2481
+ errorArg instanceof Error ? errorArg.message : String(errorArg),
2482
+ ).catch(() => undefined);
2483
+ throw errorArg;
2484
+ } finally {
2485
+ if (releaseLock) await lock?.release();
2486
+ }
2487
+ }
2488
+
2489
+ const recoverPackageTransition = async (optionsArg: {
2490
+ coordinator: UpgradeCoordinator;
2491
+ token: string;
2492
+ transaction: IUpgradeTransactionV3;
2493
+ installation: IPnpmGlobalInstallation;
2494
+ }): Promise<boolean> => {
2495
+ const context: IPackageTransitionContext = {
2496
+ packageManagerPath: optionsArg.installation.packageManagerPath,
2497
+ globalRoot: optionsArg.installation.globalRoot,
2498
+ globalBinDirectory: optionsArg.installation.globalBinDirectory,
2499
+ };
2500
+ const registryUrl = assertInternalRegistryUrl(npmRegistryUrl);
2501
+ const transaction = optionsArg.transaction;
2502
+ if (transaction.targetPackageCommitStarted === true) {
2503
+ const targetSpec = `${transaction.targetPackageName}@${transaction.targetVersion}`;
2504
+ let targetTopology: IPackageTransitionTopology;
2505
+ if (transaction.targetPackageCommitted === true) {
2506
+ try {
2507
+ targetTopology = await verifyPackageTransitionTopology(context, 'target-only');
2508
+ } catch (verificationErrorArg) {
2509
+ try {
2510
+ await addExactPackage(context, targetSpec, registryUrl);
2511
+ targetTopology = await verifyPackageTransitionTopology(context, 'target-only');
2512
+ } catch (normalizationErrorArg) {
2513
+ throw new AggregateError(
2514
+ [verificationErrorArg, normalizationErrorArg],
2515
+ 'The committed target package could not be normalized without restoring the bridge.',
2516
+ );
2517
+ }
2518
+ }
2519
+ } else {
2520
+ await addExactPackage(context, targetSpec, registryUrl);
2521
+ targetTopology = await verifyPackageTransitionTopology(context, 'target-only');
2522
+ await optionsArg.coordinator.mutateTransaction(optionsArg.token, (current) => {
2523
+ if (current.version !== upgradePackageTransitionTransactionVersion) {
2524
+ throw new Error('The package-transition transaction changed format.');
2525
+ }
2526
+ return {
2527
+ ...current,
2528
+ targetPackageCommitted: true as const,
2529
+ message: `Committed exact target ${targetSpec} during bounded recovery.`,
2530
+ };
2531
+ });
2532
+ }
2533
+ const targetInstallation = targetTopology.target;
2534
+ if (!targetInstallation) throw new Error('The exact target package is unavailable.');
2535
+ let current = requirePackageTransitionTransaction(
2536
+ await optionsArg.coordinator.readTransaction(optionsArg.token),
2537
+ );
2538
+ if (current.controllerWasRunning) {
2539
+ if (current.targetStartupInvoked !== true) {
2540
+ const candidates = (await listControllerProcessesForCli(targetInstallation.cliPath))
2541
+ .filter((candidateArg) => candidateArg.port === current.port);
2542
+ if (candidates.length > 0 || await isLoopbackPortListening(current.port)) {
2543
+ throw new Error(
2544
+ 'A target controller exists before its durable startup marker; refusing to adopt it.',
2545
+ );
2546
+ }
2547
+ const preparedTargetStart = await prepareControllerStart(
2548
+ targetInstallation,
2549
+ current.port,
2550
+ optionsArg.token,
2551
+ );
2552
+ current = requirePackageTransitionTransaction(await optionsArg.coordinator.mutateTransaction(optionsArg.token, (candidate) => {
2553
+ if (candidate.version !== upgradePackageTransitionTransactionVersion) {
2554
+ throw new Error('The package-transition transaction changed format.');
2555
+ }
2556
+ return {
2557
+ ...candidate,
2558
+ phase: 'restarting',
2559
+ targetStartupInvoked: true as const,
2560
+ message: `Invoking ${candidate.targetPackageName} during bounded recovery.`,
2561
+ };
2562
+ }));
2563
+ await runPreparedManagedCommand(preparedTargetStart);
2564
+ }
2565
+ let status = await queryControllerStatus(current.port, {
2566
+ packageName: current.targetPackageName,
2567
+ packageVersion: current.targetVersion,
2568
+ upgradeManagementVersion: current.targetManagementVersion,
2569
+ }).catch(() => undefined);
2570
+ if (!status) {
2571
+ if (await retainedUpgradeTargetIsAuthoritative({
2572
+ coordinator: optionsArg.coordinator,
2573
+ token: optionsArg.token,
2574
+ })) return false;
2575
+ const targetCandidates = (await listControllerProcessesForCli(targetInstallation.cliPath))
2576
+ .filter((candidateArg) => candidateArg.port === current.port);
2577
+ if (targetCandidates.length > 0 || await isLoopbackPortListening(current.port)) {
2578
+ throw new Error(
2579
+ 'A target controller is already starting or listening but cannot be adopted safely; refusing to launch a replacement.',
2580
+ );
2581
+ }
2582
+ await startController(targetInstallation, current.port, optionsArg.token);
2583
+ status = await queryUpgradeManagedController(
2584
+ current.port,
2585
+ targetControllerIdentity(current),
2586
+ );
2587
+ }
2588
+ await finalizeControllerUpgrade(
2589
+ optionsArg.coordinator,
2590
+ optionsArg.token,
2591
+ status,
2592
+ current.continueSessions ? 'continue' : 'reopen',
2593
+ );
2594
+ }
2595
+ await optionsArg.coordinator.finishTransaction(
2596
+ optionsArg.token,
2597
+ true,
2598
+ `Upgrade to ${current.targetPackageName}@${current.targetVersion} completed during bounded recovery.`,
2599
+ );
2600
+ return true;
2601
+ }
2602
+
2603
+ const sourceSpec = `${transaction.sourcePackageName}@${transaction.sourceVersion}`;
2604
+ let sourceTopology: IPackageTransitionTopology;
2605
+ if (transaction.packageTransitionStarted === true) {
2606
+ await addExactPackage(context, sourceSpec, registryUrl);
2607
+ sourceTopology = await verifyPackageTransitionTopology(context, 'source-only');
2608
+ } else {
2609
+ sourceTopology = await verifyPackageTransitionTopology(context, 'source-only');
2610
+ }
2611
+ const sourceInstallation = sourceTopology.source;
2612
+ if (!sourceInstallation) throw new Error('The exact bridge package is unavailable.');
2613
+ if (transaction.controllerWasRunning) {
2614
+ let status = await queryControllerStatus(transaction.port, {
2615
+ packageName: transaction.sourcePackageName,
2616
+ packageVersion: transaction.sourceVersion,
2617
+ upgradeManagementVersion: transaction.sourceManagementVersion,
2618
+ }).catch(() => undefined);
2619
+ if (!status) {
2620
+ await startController(sourceInstallation, transaction.port, optionsArg.token);
2621
+ status = await queryUpgradeManagedController(
2622
+ transaction.port,
2623
+ sourceControllerIdentity(transaction),
2624
+ );
2625
+ }
2626
+ await finalizeControllerUpgrade(
2627
+ optionsArg.coordinator,
2628
+ optionsArg.token,
2629
+ status,
2630
+ 'compensate',
2631
+ );
2632
+ }
2633
+ await optionsArg.coordinator.finishTransaction(
2634
+ optionsArg.token,
2635
+ false,
2636
+ 'The stalled package transition restored the exact bridge package.',
2637
+ `The upgrade stopped making progress during phase ${transaction.phase}.`,
2638
+ );
2639
+ return true;
2640
+ };
2641
+
2642
+ const recoverStalledUpgradeUnderLock = async (optionsArg: {
2643
+ coordinator: UpgradeCoordinator;
2644
+ token: string;
2645
+ transaction: TUpgradeTransaction;
2646
+ installation: IPnpmGlobalInstallation;
2647
+ }): Promise<boolean> => {
2648
+ const transaction = optionsArg.transaction;
2649
+ if (transaction.terminal) return true;
2650
+ if (transaction.targetStartupInvoked === true && await retainedUpgradeTargetIsAuthoritative({
2651
+ coordinator: optionsArg.coordinator,
2652
+ token: optionsArg.token,
2653
+ }, {
2654
+ resolveInstallation: async () => optionsArg.installation,
2655
+ })) return false;
2656
+ if (transaction.version === upgradePackageTransitionTransactionVersion) {
2657
+ return await recoverPackageTransition({
2658
+ coordinator: optionsArg.coordinator,
2659
+ token: optionsArg.token,
2660
+ transaction,
2661
+ installation: optionsArg.installation,
2662
+ });
2663
+ }
2664
+ const afterTargetStartupBoundary = transaction.targetStartupInvoked === true;
2665
+ if (afterTargetStartupBoundary) {
2666
+ if (!transaction.targetVersion) {
2667
+ throw new Error('The stalled upgrade has no retained target version.');
2668
+ }
2669
+ const targetInstallation = await resolvePnpmGlobalInstallation();
2670
+ if (targetInstallation.packageVersion !== transaction.targetVersion) {
2671
+ throw new Error(
2672
+ 'The package installation does not match the retained post-startup upgrade target.',
2673
+ );
2674
+ }
2675
+ if (transaction.controllerWasRunning) {
2676
+ let status = await queryControllerStatus(transaction.port, {
2677
+ packageName: controllerPackageName,
2678
+ packageVersion: targetInstallation.packageVersion,
2679
+ upgradeManagementVersion: controllerUpgradeManagementVersion,
2680
+ }).catch(() => undefined);
2681
+ if (!status) {
2682
+ if (await retainedUpgradeTargetIsAuthoritative({
2683
+ coordinator: optionsArg.coordinator,
2684
+ token: optionsArg.token,
2685
+ })) return false;
2686
+ const targetCandidates = (await listControllerProcessesForCli(targetInstallation.cliPath))
2687
+ .filter((candidateArg) => candidateArg.port === transaction.port);
2688
+ if (targetCandidates.length > 0 || await isLoopbackPortListening(transaction.port)) {
2689
+ throw new Error(
2690
+ 'A target controller is already starting or listening but cannot be adopted safely; refusing to launch a replacement.',
2691
+ );
2692
+ }
2693
+ await startController(targetInstallation, transaction.port, optionsArg.token);
2694
+ status = await queryUpgradeManagedController(
2695
+ transaction.port,
2696
+ {
2697
+ packageName: controllerPackageName,
2698
+ packageVersion: targetInstallation.packageVersion,
2699
+ managementVersion: controllerUpgradeManagementVersion,
2700
+ },
2701
+ );
2702
+ }
2703
+ await finalizeControllerUpgrade(
2704
+ optionsArg.coordinator,
2705
+ optionsArg.token,
2706
+ status,
2707
+ transaction.continueSessions ? 'continue' : 'reopen',
2708
+ );
2709
+ }
2710
+ await optionsArg.coordinator.finishTransaction(
2711
+ optionsArg.token,
2712
+ true,
2713
+ `Upgrade to ${transaction.targetVersion} completed during bounded recovery.`,
2714
+ );
2715
+ return true;
2716
+ }
2717
+
2718
+ const sourceInstallation = await restorePreviousInstallation({
2719
+ ...optionsArg.installation,
2720
+ packageVersion: transaction.sourceVersion,
2721
+ });
2722
+ if (transaction.controllerWasRunning) {
2723
+ let status = await queryControllerStatus(transaction.port, {
2724
+ packageName: controllerPackageName,
2725
+ packageVersion: sourceInstallation.packageVersion,
2726
+ upgradeManagementVersion: controllerUpgradeManagementVersion,
2727
+ }).catch(() => undefined);
2728
+ if (!status) {
2729
+ await startController(sourceInstallation, transaction.port, optionsArg.token);
2730
+ status = await queryUpgradeManagedController(
2731
+ transaction.port,
2732
+ {
2733
+ packageName: controllerPackageName,
2734
+ packageVersion: sourceInstallation.packageVersion,
2735
+ managementVersion: controllerUpgradeManagementVersion,
2736
+ },
2737
+ );
2738
+ }
2739
+ await finalizeControllerUpgrade(
2740
+ optionsArg.coordinator,
2741
+ optionsArg.token,
2742
+ status,
2743
+ 'compensate',
2744
+ );
2745
+ }
2746
+ await optionsArg.coordinator.finishTransaction(
2747
+ optionsArg.token,
2748
+ false,
2749
+ 'The stalled upgrade restored the source package.',
2750
+ `The upgrade stopped making progress during phase ${transaction.phase}.`,
2751
+ );
2752
+ return true;
2753
+ };
2754
+
2755
+ interface IRecoverStalledUpgradeOptions {
2756
+ coordinator: UpgradeCoordinator;
2757
+ token: string;
2758
+ transaction: TUpgradeTransaction;
2759
+ installation: IPnpmGlobalInstallation;
2760
+ command: '__serve' | 'foreground' | 'upgrade';
2761
+ }
2762
+
2763
+ interface IRecoverStalledUpgradeDependencies {
2764
+ launchRecoveryWorker?: typeof launchDetachedRecoveryWorker;
2765
+ terminateWorkerCandidate?: (
2766
+ candidateArg: IDetachedUpgradeWorker,
2767
+ ) => Promise<void>;
2768
+ candidateCleanup?: ICandidateCleanupRetryDependencies;
2769
+ }
2770
+
2771
+ export function recoverStalledUpgrade(
2772
+ optionsArg: IRecoverStalledUpgradeOptions,
2773
+ ): Promise<boolean>;
2774
+ export async function recoverStalledUpgrade(
2775
+ optionsArg: IRecoverStalledUpgradeOptions,
2776
+ dependenciesArg: IRecoverStalledUpgradeDependencies = {},
2777
+ ): Promise<boolean> {
2778
+ await optionsArg.coordinator.terminateTransactionWorker(optionsArg.transaction);
2779
+ let lock: Awaited<ReturnType<UpgradeCoordinator['acquireUpgradeLock']>>;
2780
+ try {
2781
+ lock = await optionsArg.coordinator.acquireUpgradeLock({
2782
+ token: optionsArg.token,
2783
+ cliPath: optionsArg.installation.cliPath,
2784
+ command: optionsArg.command,
2785
+ });
2786
+ } catch (errorArg) {
2787
+ if (
2788
+ errorArg instanceof Error
2789
+ && (errorArg.message.includes('already running')
2790
+ || errorArg.message.includes('initializing'))
2791
+ ) return false;
2792
+ throw errorArg;
2793
+ }
2794
+
2795
+ let candidate: IDetachedUpgradeWorker | undefined;
2796
+ let lockTransferred = false;
2797
+ try {
2798
+ const transaction = await optionsArg.coordinator.readTransaction(optionsArg.token);
2799
+ if (transaction.terminal) {
2800
+ await lock.release();
2801
+ return true;
2802
+ }
2803
+ const recoveryInstallation = await resolveAuthoritativeRecoveryInstallation(
2804
+ transaction,
2805
+ optionsArg.installation,
2806
+ );
2807
+ const payload: IUpgradeWorkerPayload = {
2808
+ version: upgradeCoordinationVersion,
2809
+ token: optionsArg.token,
2810
+ port: transaction.port,
2811
+ gracePeriodMs: transaction.gracePeriodMs,
2812
+ continueSessions: transaction.continueSessions,
2813
+ };
2814
+ candidate = await (dependenciesArg.launchRecoveryWorker ?? launchDetachedRecoveryWorker)({
2815
+ installation: recoveryInstallation,
2816
+ payload,
2817
+ });
2818
+ const launchedTransaction = await optionsArg.coordinator.recordWorkerLaunch(
2819
+ optionsArg.token,
2820
+ candidate.pid,
2821
+ candidate.cliPath,
2822
+ candidate.logFilePath,
2823
+ );
2824
+ const worker = launchedTransaction.worker;
2825
+ if (
2826
+ !worker
2827
+ || worker.pid !== candidate.pid
2828
+ || worker.processGroupId !== candidate.processGroupId
2829
+ || worker.fingerprint !== candidate.fingerprint
2830
+ || worker.cliPath !== candidate.cliPath
2831
+ || worker.logFilePath !== candidate.logFilePath
2832
+ ) throw new Error('The recovery worker launch identity was not retained exactly.');
2833
+ await optionsArg.coordinator.transferUpgradeLockToWorker({
2834
+ lock,
2835
+ token: optionsArg.token,
2836
+ worker,
2837
+ });
2838
+ lockTransferred = true;
2839
+ await optionsArg.coordinator.acknowledgeWorker(optionsArg.token);
2840
+ return true;
2841
+ } catch (errorArg) {
2842
+ const errors: unknown[] = [errorArg];
2843
+ if (candidate) {
2844
+ await retryCandidateCleanupUntilDrained(
2845
+ async () => await (dependenciesArg.terminateWorkerCandidate
2846
+ ?? ((candidateArg) => optionsArg.coordinator.terminateUpgradeWorkerCandidate(candidateArg)))(
2847
+ candidate!,
2848
+ ),
2849
+ dependenciesArg.candidateCleanup,
2850
+ candidate.pid,
2851
+ );
2852
+ }
2853
+ try {
2854
+ if (!lockTransferred) {
2855
+ try {
2856
+ await lock.release();
2857
+ } catch {
2858
+ const replacementLock = await optionsArg.coordinator.acquireUpgradeLock({
2859
+ token: optionsArg.token,
2860
+ cliPath: optionsArg.installation.cliPath,
2861
+ command: optionsArg.command,
2862
+ });
2863
+ await replacementLock.release();
2864
+ }
2865
+ } else {
2866
+ const replacementLock = await optionsArg.coordinator.acquireUpgradeLock({
2867
+ token: optionsArg.token,
2868
+ cliPath: optionsArg.installation.cliPath,
2869
+ command: optionsArg.command,
2870
+ });
2871
+ await replacementLock.release();
2872
+ }
2873
+ } catch (lockCleanupErrorArg) {
2874
+ errors.push(lockCleanupErrorArg);
2875
+ }
2876
+ if (errors.length > 1) {
2877
+ throw new AggregateError(
2878
+ errors,
2879
+ 'Stalled upgrade recovery handoff failed and cleanup was incomplete.',
2880
+ );
2881
+ }
2882
+ throw errorArg;
2883
+ }
2884
+ }
2885
+
2886
+ export const createUpgradeWorkerPayload = (optionsArg: {
2887
+ port: number;
2888
+ gracePeriodMs: number;
2889
+ continueSessions: boolean;
2890
+ expectedController?: IUpgradeExpectedController;
2891
+ }): IUpgradeWorkerPayload => ({
2892
+ version: upgradeCoordinationVersion,
2893
+ token: createUpgradeToken(),
2894
+ port: optionsArg.port,
2895
+ gracePeriodMs: optionsArg.gracePeriodMs,
2896
+ continueSessions: optionsArg.continueSessions,
2897
+ ...(optionsArg.expectedController
2898
+ ? { expectedController: optionsArg.expectedController }
2899
+ : {}),
2900
+ });