@withbuddi/buddi 0.1.0-pre.38 → 0.1.0-pre.39

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 (253) hide show
  1. package/extension/background.js +635 -21
  2. package/extension/manifest.json +2 -2
  3. package/node_modules/@buddi/cli/dist/.tsbuildinfo +1 -1
  4. package/node_modules/@buddi/cli/dist/access-cmd.d.ts +1 -1
  5. package/node_modules/@buddi/cli/dist/access-cmd.d.ts.map +1 -1
  6. package/node_modules/@buddi/cli/dist/access-cmd.js +23 -3
  7. package/node_modules/@buddi/cli/dist/access-cmd.js.map +1 -1
  8. package/node_modules/@buddi/cli/dist/args.d.ts +2 -2
  9. package/node_modules/@buddi/cli/dist/args.d.ts.map +1 -1
  10. package/node_modules/@buddi/cli/dist/args.js +7 -2
  11. package/node_modules/@buddi/cli/dist/args.js.map +1 -1
  12. package/node_modules/@buddi/cli/dist/commands.d.ts.map +1 -1
  13. package/node_modules/@buddi/cli/dist/commands.js +11 -0
  14. package/node_modules/@buddi/cli/dist/commands.js.map +1 -1
  15. package/node_modules/@buddi/cli/dist/main.d.ts.map +1 -1
  16. package/node_modules/@buddi/cli/dist/main.js +2 -1
  17. package/node_modules/@buddi/cli/dist/main.js.map +1 -1
  18. package/node_modules/@buddi/gateway/dist/.tsbuildinfo +1 -1
  19. package/node_modules/@buddi/gateway/dist/missions/ask-cap.d.ts +26 -0
  20. package/node_modules/@buddi/gateway/dist/missions/ask-cap.d.ts.map +1 -0
  21. package/node_modules/@buddi/gateway/dist/missions/ask-cap.js +83 -0
  22. package/node_modules/@buddi/gateway/dist/missions/ask-cap.js.map +1 -0
  23. package/node_modules/@buddi/gateway/dist/missions/execute.d.ts +12 -1
  24. package/node_modules/@buddi/gateway/dist/missions/execute.d.ts.map +1 -1
  25. package/node_modules/@buddi/gateway/dist/missions/execute.js +73 -11
  26. package/node_modules/@buddi/gateway/dist/missions/execute.js.map +1 -1
  27. package/node_modules/@buddi/gateway/dist/missions/parked.d.ts +6 -0
  28. package/node_modules/@buddi/gateway/dist/missions/parked.d.ts.map +1 -1
  29. package/node_modules/@buddi/gateway/dist/missions/parked.js +7 -1
  30. package/node_modules/@buddi/gateway/dist/missions/parked.js.map +1 -1
  31. package/node_modules/@buddi/gateway/dist/serve.js +1 -1
  32. package/node_modules/@buddi/gateway/dist/serve.js.map +1 -1
  33. package/node_modules/@buddi/gateway/dist/system-context.d.ts +7 -0
  34. package/node_modules/@buddi/gateway/dist/system-context.d.ts.map +1 -1
  35. package/node_modules/@buddi/gateway/dist/system-context.js +8 -0
  36. package/node_modules/@buddi/gateway/dist/system-context.js.map +1 -1
  37. package/node_modules/@buddi/gateway/dist/web/api-routes.d.ts.map +1 -1
  38. package/node_modules/@buddi/gateway/dist/web/api-routes.js +11 -3
  39. package/node_modules/@buddi/gateway/dist/web/api-routes.js.map +1 -1
  40. package/node_modules/@buddi/gateway/dist/web/extension.d.ts +13 -1
  41. package/node_modules/@buddi/gateway/dist/web/extension.d.ts.map +1 -1
  42. package/node_modules/@buddi/gateway/dist/web/extension.js +125 -5
  43. package/node_modules/@buddi/gateway/dist/web/extension.js.map +1 -1
  44. package/node_modules/@buddi/gateway/dist/web/remote-hand.d.ts.map +1 -1
  45. package/node_modules/@buddi/gateway/dist/web/remote-hand.js +45 -2
  46. package/node_modules/@buddi/gateway/dist/web/remote-hand.js.map +1 -1
  47. package/node_modules/@buddi/gateway/dist/web/secrets.d.ts +26 -1
  48. package/node_modules/@buddi/gateway/dist/web/secrets.d.ts.map +1 -1
  49. package/node_modules/@buddi/gateway/dist/web/secrets.js +66 -3
  50. package/node_modules/@buddi/gateway/dist/web/secrets.js.map +1 -1
  51. package/node_modules/@buddi/gateway/dist/web/server.d.ts.map +1 -1
  52. package/node_modules/@buddi/gateway/dist/web/server.js +63 -7
  53. package/node_modules/@buddi/gateway/dist/web/server.js.map +1 -1
  54. package/node_modules/@buddi/gateway/dist/web/version.d.ts +2 -0
  55. package/node_modules/@buddi/gateway/dist/web/version.d.ts.map +1 -1
  56. package/node_modules/@buddi/gateway/dist/web/version.js +5 -1
  57. package/node_modules/@buddi/gateway/dist/web/version.js.map +1 -1
  58. package/node_modules/@buddi/install/dist/.tsbuildinfo +1 -1
  59. package/node_modules/@buddi/install/dist/app-layout.d.ts +127 -0
  60. package/node_modules/@buddi/install/dist/app-layout.d.ts.map +1 -0
  61. package/node_modules/@buddi/install/dist/app-layout.js +323 -0
  62. package/node_modules/@buddi/install/dist/app-layout.js.map +1 -0
  63. package/node_modules/@buddi/install/dist/index.d.ts +1 -0
  64. package/node_modules/@buddi/install/dist/index.d.ts.map +1 -1
  65. package/node_modules/@buddi/install/dist/index.js +1 -0
  66. package/node_modules/@buddi/install/dist/index.js.map +1 -1
  67. package/node_modules/@buddi/install/dist/launcher.d.ts.map +1 -1
  68. package/node_modules/@buddi/install/dist/launcher.js +18 -4
  69. package/node_modules/@buddi/install/dist/launcher.js.map +1 -1
  70. package/node_modules/@buddi/install/dist/provenance.d.ts +34 -0
  71. package/node_modules/@buddi/install/dist/provenance.d.ts.map +1 -0
  72. package/node_modules/@buddi/install/dist/provenance.js +200 -0
  73. package/node_modules/@buddi/install/dist/provenance.js.map +1 -0
  74. package/node_modules/@buddi/install/dist/supervisor.d.ts.map +1 -1
  75. package/node_modules/@buddi/install/dist/supervisor.js +9 -4
  76. package/node_modules/@buddi/install/dist/supervisor.js.map +1 -1
  77. package/node_modules/@buddi/install/dist/upgrade.d.ts +77 -3
  78. package/node_modules/@buddi/install/dist/upgrade.d.ts.map +1 -1
  79. package/node_modules/@buddi/install/dist/upgrade.js +325 -43
  80. package/node_modules/@buddi/install/dist/upgrade.js.map +1 -1
  81. package/node_modules/@buddi/tool-browser/dist/.tsbuildinfo +1 -1
  82. package/node_modules/@buddi/tool-browser/dist/controller.d.ts +6 -0
  83. package/node_modules/@buddi/tool-browser/dist/controller.d.ts.map +1 -1
  84. package/node_modules/@buddi/tool-browser/dist/controller.js +80 -6
  85. package/node_modules/@buddi/tool-browser/dist/controller.js.map +1 -1
  86. package/node_modules/@buddi/tool-browser/dist/driver.d.ts +3 -1
  87. package/node_modules/@buddi/tool-browser/dist/driver.d.ts.map +1 -1
  88. package/node_modules/@buddi/tool-browser/dist/driver.js +82 -1
  89. package/node_modules/@buddi/tool-browser/dist/driver.js.map +1 -1
  90. package/node_modules/@buddi/tool-browser/dist/extension.d.ts +60 -7
  91. package/node_modules/@buddi/tool-browser/dist/extension.d.ts.map +1 -1
  92. package/node_modules/@buddi/tool-browser/dist/extension.js +90 -13
  93. package/node_modules/@buddi/tool-browser/dist/extension.js.map +1 -1
  94. package/node_modules/@buddi/tool-browser/dist/index.d.ts +6 -4
  95. package/node_modules/@buddi/tool-browser/dist/index.d.ts.map +1 -1
  96. package/node_modules/@buddi/tool-browser/dist/index.js +7 -4
  97. package/node_modules/@buddi/tool-browser/dist/index.js.map +1 -1
  98. package/node_modules/@buddi/tool-browser/dist/logins.d.ts +150 -0
  99. package/node_modules/@buddi/tool-browser/dist/logins.d.ts.map +1 -0
  100. package/node_modules/@buddi/tool-browser/dist/logins.js +476 -0
  101. package/node_modules/@buddi/tool-browser/dist/logins.js.map +1 -0
  102. package/node_modules/@buddi/tool-browser/dist/routes.d.ts +22 -1
  103. package/node_modules/@buddi/tool-browser/dist/routes.d.ts.map +1 -1
  104. package/node_modules/@buddi/tool-browser/dist/routes.js +51 -3
  105. package/node_modules/@buddi/tool-browser/dist/routes.js.map +1 -1
  106. package/node_modules/@buddi/tool-browser/dist/secrets.d.ts +3 -1
  107. package/node_modules/@buddi/tool-browser/dist/secrets.d.ts.map +1 -1
  108. package/node_modules/@buddi/tool-browser/dist/secrets.js +10 -6
  109. package/node_modules/@buddi/tool-browser/dist/secrets.js.map +1 -1
  110. package/node_modules/@buddi/tool-browser/dist/service.d.ts +26 -2
  111. package/node_modules/@buddi/tool-browser/dist/service.d.ts.map +1 -1
  112. package/node_modules/@buddi/tool-browser/dist/service.js +117 -3
  113. package/node_modules/@buddi/tool-browser/dist/service.js.map +1 -1
  114. package/node_modules/@buddi/tool-browser/dist/types.d.ts +88 -0
  115. package/node_modules/@buddi/tool-browser/dist/types.d.ts.map +1 -1
  116. package/node_modules/@buddi/tool-browser/dist/types.js +18 -0
  117. package/node_modules/@buddi/tool-browser/dist/types.js.map +1 -1
  118. package/node_modules/hono/dist/cjs/hono-base.js +2 -0
  119. package/node_modules/hono/dist/cjs/middleware/language/language.js +1 -1
  120. package/node_modules/hono/dist/cjs/middleware/mount/index.js +71 -0
  121. package/node_modules/hono/dist/cjs/request.js +4 -4
  122. package/node_modules/hono/dist/hono-base.js +2 -0
  123. package/node_modules/hono/dist/middleware/mount/index.js +70 -0
  124. package/node_modules/hono/dist/request.js +4 -4
  125. package/node_modules/hono/dist/types/hono-base.d.ts +2 -0
  126. package/node_modules/hono/dist/types/jsx/base.d.ts +1 -1
  127. package/node_modules/hono/dist/types/middleware/mount/index.d.ts +46 -0
  128. package/node_modules/hono/dist/types/request.d.ts +4 -4
  129. package/node_modules/hono/package.json +9 -1
  130. package/package.json +2 -2
  131. package/packages/cli/dist/.tsbuildinfo +1 -1
  132. package/packages/cli/dist/access-cmd.d.ts +1 -1
  133. package/packages/cli/dist/access-cmd.d.ts.map +1 -1
  134. package/packages/cli/dist/access-cmd.js +23 -3
  135. package/packages/cli/dist/access-cmd.js.map +1 -1
  136. package/packages/cli/dist/args.d.ts +2 -2
  137. package/packages/cli/dist/args.d.ts.map +1 -1
  138. package/packages/cli/dist/args.js +7 -2
  139. package/packages/cli/dist/args.js.map +1 -1
  140. package/packages/cli/dist/commands.d.ts.map +1 -1
  141. package/packages/cli/dist/commands.js +11 -0
  142. package/packages/cli/dist/commands.js.map +1 -1
  143. package/packages/cli/dist/main.d.ts.map +1 -1
  144. package/packages/cli/dist/main.js +2 -1
  145. package/packages/cli/dist/main.js.map +1 -1
  146. package/packages/gateway/dist/.tsbuildinfo +1 -1
  147. package/packages/gateway/dist/missions/ask-cap.d.ts +26 -0
  148. package/packages/gateway/dist/missions/ask-cap.d.ts.map +1 -0
  149. package/packages/gateway/dist/missions/ask-cap.js +83 -0
  150. package/packages/gateway/dist/missions/ask-cap.js.map +1 -0
  151. package/packages/gateway/dist/missions/execute.d.ts +12 -1
  152. package/packages/gateway/dist/missions/execute.d.ts.map +1 -1
  153. package/packages/gateway/dist/missions/execute.js +73 -11
  154. package/packages/gateway/dist/missions/execute.js.map +1 -1
  155. package/packages/gateway/dist/missions/parked.d.ts +6 -0
  156. package/packages/gateway/dist/missions/parked.d.ts.map +1 -1
  157. package/packages/gateway/dist/missions/parked.js +7 -1
  158. package/packages/gateway/dist/missions/parked.js.map +1 -1
  159. package/packages/gateway/dist/serve.js +1 -1
  160. package/packages/gateway/dist/serve.js.map +1 -1
  161. package/packages/gateway/dist/system-context.d.ts +7 -0
  162. package/packages/gateway/dist/system-context.d.ts.map +1 -1
  163. package/packages/gateway/dist/system-context.js +8 -0
  164. package/packages/gateway/dist/system-context.js.map +1 -1
  165. package/packages/gateway/dist/web/api-routes.d.ts.map +1 -1
  166. package/packages/gateway/dist/web/api-routes.js +11 -3
  167. package/packages/gateway/dist/web/api-routes.js.map +1 -1
  168. package/packages/gateway/dist/web/extension.d.ts +13 -1
  169. package/packages/gateway/dist/web/extension.d.ts.map +1 -1
  170. package/packages/gateway/dist/web/extension.js +125 -5
  171. package/packages/gateway/dist/web/extension.js.map +1 -1
  172. package/packages/gateway/dist/web/remote-hand.d.ts.map +1 -1
  173. package/packages/gateway/dist/web/remote-hand.js +45 -2
  174. package/packages/gateway/dist/web/remote-hand.js.map +1 -1
  175. package/packages/gateway/dist/web/secrets.d.ts +26 -1
  176. package/packages/gateway/dist/web/secrets.d.ts.map +1 -1
  177. package/packages/gateway/dist/web/secrets.js +66 -3
  178. package/packages/gateway/dist/web/secrets.js.map +1 -1
  179. package/packages/gateway/dist/web/server.d.ts.map +1 -1
  180. package/packages/gateway/dist/web/server.js +63 -7
  181. package/packages/gateway/dist/web/server.js.map +1 -1
  182. package/packages/gateway/dist/web/version.d.ts +2 -0
  183. package/packages/gateway/dist/web/version.d.ts.map +1 -1
  184. package/packages/gateway/dist/web/version.js +5 -1
  185. package/packages/gateway/dist/web/version.js.map +1 -1
  186. package/packages/install/dist/.tsbuildinfo +1 -1
  187. package/packages/install/dist/app-layout.d.ts +127 -0
  188. package/packages/install/dist/app-layout.d.ts.map +1 -0
  189. package/packages/install/dist/app-layout.js +323 -0
  190. package/packages/install/dist/app-layout.js.map +1 -0
  191. package/packages/install/dist/index.d.ts +1 -0
  192. package/packages/install/dist/index.d.ts.map +1 -1
  193. package/packages/install/dist/index.js +1 -0
  194. package/packages/install/dist/index.js.map +1 -1
  195. package/packages/install/dist/launcher.d.ts.map +1 -1
  196. package/packages/install/dist/launcher.js +18 -4
  197. package/packages/install/dist/launcher.js.map +1 -1
  198. package/packages/install/dist/provenance.d.ts +34 -0
  199. package/packages/install/dist/provenance.d.ts.map +1 -0
  200. package/packages/install/dist/provenance.js +200 -0
  201. package/packages/install/dist/provenance.js.map +1 -0
  202. package/packages/install/dist/supervisor.d.ts.map +1 -1
  203. package/packages/install/dist/supervisor.js +9 -4
  204. package/packages/install/dist/supervisor.js.map +1 -1
  205. package/packages/install/dist/upgrade.d.ts +77 -3
  206. package/packages/install/dist/upgrade.d.ts.map +1 -1
  207. package/packages/install/dist/upgrade.js +325 -43
  208. package/packages/install/dist/upgrade.js.map +1 -1
  209. package/packages/tools/browser/dist/.tsbuildinfo +1 -1
  210. package/packages/tools/browser/dist/controller.d.ts +6 -0
  211. package/packages/tools/browser/dist/controller.d.ts.map +1 -1
  212. package/packages/tools/browser/dist/controller.js +80 -6
  213. package/packages/tools/browser/dist/controller.js.map +1 -1
  214. package/packages/tools/browser/dist/driver.d.ts +3 -1
  215. package/packages/tools/browser/dist/driver.d.ts.map +1 -1
  216. package/packages/tools/browser/dist/driver.js +82 -1
  217. package/packages/tools/browser/dist/driver.js.map +1 -1
  218. package/packages/tools/browser/dist/extension.d.ts +60 -7
  219. package/packages/tools/browser/dist/extension.d.ts.map +1 -1
  220. package/packages/tools/browser/dist/extension.js +90 -13
  221. package/packages/tools/browser/dist/extension.js.map +1 -1
  222. package/packages/tools/browser/dist/index.d.ts +6 -4
  223. package/packages/tools/browser/dist/index.d.ts.map +1 -1
  224. package/packages/tools/browser/dist/index.js +7 -4
  225. package/packages/tools/browser/dist/index.js.map +1 -1
  226. package/packages/tools/browser/dist/logins.d.ts +150 -0
  227. package/packages/tools/browser/dist/logins.d.ts.map +1 -0
  228. package/packages/tools/browser/dist/logins.js +476 -0
  229. package/packages/tools/browser/dist/logins.js.map +1 -0
  230. package/packages/tools/browser/dist/routes.d.ts +22 -1
  231. package/packages/tools/browser/dist/routes.d.ts.map +1 -1
  232. package/packages/tools/browser/dist/routes.js +51 -3
  233. package/packages/tools/browser/dist/routes.js.map +1 -1
  234. package/packages/tools/browser/dist/secrets.d.ts +3 -1
  235. package/packages/tools/browser/dist/secrets.d.ts.map +1 -1
  236. package/packages/tools/browser/dist/secrets.js +10 -6
  237. package/packages/tools/browser/dist/secrets.js.map +1 -1
  238. package/packages/tools/browser/dist/service.d.ts +26 -2
  239. package/packages/tools/browser/dist/service.d.ts.map +1 -1
  240. package/packages/tools/browser/dist/service.js +117 -3
  241. package/packages/tools/browser/dist/service.js.map +1 -1
  242. package/packages/tools/browser/dist/types.d.ts +88 -0
  243. package/packages/tools/browser/dist/types.d.ts.map +1 -1
  244. package/packages/tools/browser/dist/types.js +18 -0
  245. package/packages/tools/browser/dist/types.js.map +1 -1
  246. package/packages/web/dist/assets/index-DYfeUW-0.css +1 -0
  247. package/packages/web/dist/assets/index-vpc904_b.js +146 -0
  248. package/packages/web/dist/assets/{lottie_light-CuiSMqnl.js → lottie_light-BFAMYgLA.js} +1 -1
  249. package/packages/web/dist/build.json +1 -1
  250. package/packages/web/dist/index.html +2 -2
  251. package/packages/web/dist/precache.json +4 -4
  252. package/packages/web/dist/assets/index-Bac-CdD-.css +0 -1
  253. package/packages/web/dist/assets/index-DI8bp_D7.js +0 -146
@@ -39,7 +39,7 @@
39
39
  import { execFile } from 'node:child_process';
40
40
  import { existsSync, readFileSync } from 'node:fs';
41
41
  import { request } from 'node:http';
42
- import { open, readFile } from 'node:fs/promises';
42
+ import { open, readFile, realpath, rm } from 'node:fs/promises';
43
43
  import path from 'node:path';
44
44
  import { promisify } from 'node:util';
45
45
  /*
@@ -53,6 +53,7 @@ import { compareSemver, parseSemver } from '@buddi/core/semver';
53
53
  import { atomicJson, launchAgentLabel, SERVICE_UNIT_VAR } from './environment.js';
54
54
  import { JobStore } from './backup.js';
55
55
  import { createPostgresCheck } from './postgres-check.js';
56
+ import { APP_BUNDLE, appLayout, insideAppBundle, placeRelease, pruneReleases, releaseDir, stageRelease, switchCurrent } from './app-layout.js';
56
57
  const run = promisify(execFile);
57
58
  /** Where `buddi` is published, and where the check and the install both look. */
58
59
  export const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
@@ -177,6 +178,7 @@ export async function readUpgradeState(data, current) {
177
178
  ...(typeof check.latest === 'string' ? { latest: check.latest } : {}),
178
179
  ...(typeof check.latest === 'string' && typeof check.latestNotes === 'string' ? { latestNotes: check.latestNotes } : {}),
179
180
  ...(typeof check.error === 'string' ? { error: check.error } : {}),
181
+ ...(typeof check.processing === 'string' && check.processing === check.latest ? { processing: check.processing } : {}),
180
182
  },
181
183
  current,
182
184
  ...(typeof parsed.registry === 'string' ? { registry: parsed.registry } : {}),
@@ -188,17 +190,177 @@ export async function writeUpgradeState(data, state) {
188
190
  await atomicJson(upgradeStateFile(data), { ...state, history: state.history.slice(-HISTORY_LIMIT) });
189
191
  }
190
192
  export function versionView(state) {
193
+ const newer = isNewer(state.check.latest, state.current);
194
+ const processing = newer && state.check.processing !== undefined && state.check.processing === state.check.latest ? state.check.processing : undefined;
191
195
  return {
192
196
  current: state.current,
193
197
  ...(state.check.latest === undefined ? {} : { latest: state.check.latest }),
194
198
  ...(state.check.latest === undefined || state.check.latestNotes === undefined ? {} : { latestNotes: state.check.latestNotes }),
195
199
  ...(state.check.lastAt === undefined ? {} : { checkedAt: state.check.lastAt }),
196
200
  checkEnabled: state.check.enabled,
197
- updateAvailable: isNewer(state.check.latest, state.current),
201
+ updateAvailable: newer && processing === undefined,
198
202
  ...(state.check.error === undefined ? {} : { error: state.check.error }),
203
+ ...(processing === undefined ? {} : { processing: { version: processing, message: processingSentence(processing) } }),
199
204
  history: state.history,
200
205
  };
201
206
  }
207
+ /** What the sheet and a failed install say while npm has not served a fresh version's tarball yet. */
208
+ export function processingSentence(version) {
209
+ return `npm is still processing ${version}; try again in a few minutes.`;
210
+ }
211
+ /**
212
+ * Where npm serves a version's tarball by convention: `<registry>/@withbuddi/buddi/-/buddi-<version>.tgz`.
213
+ * Only the fallback: the version document's `dist.tarball` is what npm itself downloads.
214
+ */
215
+ export function tarballUrl(registry, version) {
216
+ return `${registry.replace(/\/+$/, '')}/${PACKAGE_NAME}/-/${PACKAGE_NAME.split('/').pop()}-${version}.tgz`;
217
+ }
218
+ /**
219
+ * The credentials npm itself would send to this registry, read from the
220
+ * owner's npmrc (`NPM_CONFIG_USERCONFIG`, else `~/.npmrc`): `_authToken` as a
221
+ * bearer, `_auth` or `username` + `_password` as basic. The longest
222
+ * `//host/path/:` key the registry URL starts with wins, as in npm, and
223
+ * `${VAR}` is read from the environment. Nothing found is no headers: the
224
+ * public registry needs none.
225
+ *
226
+ * Read so the check and the install see the same registry: a mirror that wants
227
+ * a token answered the anonymous check 401, which once read as "still
228
+ * processing" for ever.
229
+ */
230
+ export function registryAuth(registry, env, read = file => readFileSync(file, 'utf8')) {
231
+ const file = env.NPM_CONFIG_USERCONFIG ?? env.npm_config_userconfig ?? (env.HOME ? path.join(env.HOME, '.npmrc') : undefined);
232
+ if (file === undefined)
233
+ return {};
234
+ let text;
235
+ try {
236
+ text = read(file);
237
+ }
238
+ catch {
239
+ return {};
240
+ }
241
+ let url;
242
+ try {
243
+ url = new URL(registry.replace(/\/+$/, '') + '/');
244
+ }
245
+ catch {
246
+ return {};
247
+ }
248
+ const nerf = `//${url.host}${url.pathname}`;
249
+ const entries = new Map();
250
+ for (const line of text.split(/\r?\n/)) {
251
+ const trimmed = line.trim();
252
+ if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith(';'))
253
+ continue;
254
+ const eq = trimmed.indexOf('=');
255
+ if (eq <= 0)
256
+ continue;
257
+ const value = trimmed.slice(eq + 1).trim().replace(/^"(.*)"$/, '$1').replace(/\$\{([^}]+)\}/g, (_, name) => env[name] ?? '');
258
+ entries.set(trimmed.slice(0, eq).trim(), value);
259
+ }
260
+ const scopes = [...new Set([...entries.keys()].filter(k => k.startsWith('//') && k.includes('/:')).map(k => k.slice(0, k.lastIndexOf('/:') + 1)))]
261
+ .filter(scope => nerf.startsWith(scope))
262
+ .sort((a, b) => b.length - a.length);
263
+ for (const scope of scopes) {
264
+ const token = entries.get(`${scope}:_authToken`);
265
+ if (token)
266
+ return { authorization: `Bearer ${token}` };
267
+ const auth = entries.get(`${scope}:_auth`);
268
+ if (auth)
269
+ return { authorization: `Basic ${auth}` };
270
+ const user = entries.get(`${scope}:username`), password = entries.get(`${scope}:_password`);
271
+ if (user && password)
272
+ return { authorization: `Basic ${Buffer.from(`${user}:${Buffer.from(password, 'base64').toString('utf8')}`).toString('base64')}` };
273
+ }
274
+ return {};
275
+ }
276
+ /** Credentials go to the registry's own origin only, never to a host a document names. */
277
+ export function authFor(url, registry, auth) {
278
+ if (auth === undefined || Object.keys(auth).length === 0)
279
+ return {};
280
+ try {
281
+ return new URL(url).origin === new URL(registry).origin ? auth : {};
282
+ }
283
+ catch {
284
+ return {};
285
+ }
286
+ }
287
+ /**
288
+ * Does npm serve this version's tarball yet?
289
+ *
290
+ * The tarball is the one the version document names (`dist.tarball`), asked
291
+ * with the registry's credentials. Only a 404 (of the document or the
292
+ * tarball) is "still processing": that is what npmjs answers in the minutes
293
+ * after a publish. A registry that refuses HEAD (405, 501) is asked for one
294
+ * byte instead. Anything else — a 401 or 403 from a mirror, a 5xx — is not a
295
+ * publish in progress and is not reported as one: the install that follows
296
+ * says what is actually wrong. A transport failure throws.
297
+ */
298
+ export async function tarballServed(registry, version, http, opts = {}) {
299
+ const base = registry.replace(/\/+$/, '');
300
+ let url = opts.tarball;
301
+ if (url === undefined || url === '') {
302
+ const doc = await http(`${base}/${PACKAGE_PATH}/${version}`, {
303
+ method: 'GET',
304
+ headers: { accept: 'application/json', ...authFor(base, base, opts.auth) },
305
+ signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
306
+ });
307
+ if (doc.status === 404)
308
+ return false;
309
+ if (doc.status === 200) {
310
+ const body = await doc.json().catch(() => undefined);
311
+ if (typeof body?.dist?.tarball === 'string' && body.dist.tarball !== '')
312
+ url = body.dist.tarball;
313
+ }
314
+ }
315
+ if (url === undefined || url === '')
316
+ url = tarballUrl(base, version);
317
+ const headers = authFor(url, base, opts.auth);
318
+ const head = await http(url, { method: 'HEAD', headers, signal: AbortSignal.timeout(CHECK_TIMEOUT_MS) });
319
+ if (head.status >= 200 && head.status < 300)
320
+ return true;
321
+ if (head.status === 404)
322
+ return false;
323
+ if (head.status === 405 || head.status === 501) {
324
+ try {
325
+ const ranged = await http(url, { method: 'GET', headers: { ...headers, range: 'bytes=0-0' }, signal: AbortSignal.timeout(CHECK_TIMEOUT_MS), maxBytes: 64 * 1024 });
326
+ return ranged.status !== 404;
327
+ }
328
+ catch {
329
+ // A server that ignored the range and started sending the whole tarball is serving it.
330
+ return true;
331
+ }
332
+ }
333
+ return true;
334
+ }
335
+ /**
336
+ * npm's own words for *this package's* tarball not being served (yet): `E404`
337
+ * and a 404 line naming `@withbuddi/buddi`. A 404 for anything else — a
338
+ * dependency, a mirror's missing path — is not a publish in progress, and
339
+ * keeps npm's own words.
340
+ */
341
+ export function isNotFound(error) {
342
+ // The "npm install <spec> failed:" this module puts in front names the package itself.
343
+ const said = error.replace(/npm (install|cache add) \S+ failed:/g, '');
344
+ if (!/\bE404\b/.test(said))
345
+ return false;
346
+ return said.split('\n').some(line => /\b404\b/.test(line) && /@withbuddi(\/|%2f)buddi\b/i.test(line));
347
+ }
348
+ /**
349
+ * What npm said, safe to keep in the history and show on the dashboard:
350
+ * credentials in URLs and npmrc-style tokens masked, terminal escapes and
351
+ * other control characters dropped, the last eight lines.
352
+ */
353
+ export function sanitizeNpmOutput(text) {
354
+ return text
355
+ // eslint-disable-next-line no-control-regex
356
+ .replace(/\u001b\[[0-9;]*[A-Za-z]/g, '')
357
+ // eslint-disable-next-line no-control-regex
358
+ .replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '')
359
+ .replace(/(\w+:\/\/)[^\s/@:]+(:[^\s/@]*)?@/g, '$1***@')
360
+ .replace(/(_authToken|_auth|_password)(\s*[=:]\s*)\S+/gi, '$1$2***')
361
+ .replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/g, '$1 ***')
362
+ .trim().split('\n').slice(-8).join('\n');
363
+ }
202
364
  /** Release notes longer than this are cut; `build.mjs` keeps them to 8 KB. */
203
365
  export const NOTES_LIMIT = 8 * 1024;
204
366
  /**
@@ -210,10 +372,11 @@ export const NOTES_LIMIT = 8 * 1024;
210
372
  * same document carries the release notes (`buddi.notes`, written by
211
373
  * `scripts/release/build.mjs`), so they cost no second request.
212
374
  */
213
- export async function fetchLatestRelease(registry, http) {
214
- const response = await http(`${registry.replace(/\/+$/, '')}/${PACKAGE_PATH}/latest`, {
375
+ export async function fetchLatestRelease(registry, http, auth) {
376
+ const base = registry.replace(/\/+$/, '');
377
+ const response = await http(`${base}/${PACKAGE_PATH}/latest`, {
215
378
  method: 'GET',
216
- headers: { accept: 'application/json' },
379
+ headers: { accept: 'application/json', ...authFor(base, base, auth) },
217
380
  signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
218
381
  });
219
382
  if (response.status !== 200)
@@ -225,10 +388,11 @@ export async function fetchLatestRelease(registry, http) {
225
388
  // and a number on the dashboard. It gets to name a version, and nothing else.
226
389
  if (!isVersion(body.version))
227
390
  throw new Error(`the registry named "${body.version}", which is not a version`);
391
+ const tarball = typeof body.dist?.tarball === 'string' && body.dist.tarball !== '' ? { tarball: body.dist.tarball } : {};
228
392
  const notes = typeof body.buddi === 'object' && body.buddi !== null ? body.buddi.notes : undefined;
229
393
  if (typeof notes !== 'string' || notes.trim() === '')
230
- return { version: body.version };
231
- return { version: body.version, notes: notes.trim().slice(0, NOTES_LIMIT) };
394
+ return { version: body.version, ...tarball };
395
+ return { version: body.version, notes: notes.trim().slice(0, NOTES_LIMIT), ...tarball };
232
396
  }
233
397
  /**
234
398
  * Read the layout back off the install root rather than trusting npm's config.
@@ -278,6 +442,10 @@ export const NOT_GLOBAL = 'This buddi was installed inside another project; upgr
278
442
  */
279
443
  export function upgradeTarget(root, opts = {}) {
280
444
  const platform = opts.platform ?? process.platform;
445
+ // The bundle is signed: npm must never write there. buddi.app upgrades it
446
+ // through its own layout (`app-layout.ts`) when it is the one running buddi.
447
+ if (insideAppBundle(root))
448
+ return { error: APP_BUNDLE };
281
449
  const target = installTarget(root, platform);
282
450
  if (target.global)
283
451
  return target;
@@ -354,7 +522,7 @@ export function createInstaller(opts = {}) {
354
522
  const failed = (verb, spec, err) => {
355
523
  const stderr = err?.stderr;
356
524
  const text = (stderr ?? (err instanceof Error ? err.message : String(err))).toString().trim();
357
- return new Error(`npm ${verb} ${spec} failed: ${text.split('\n').slice(-8).join('\n')}`);
525
+ return new Error(`npm ${verb} ${spec} failed: ${sanitizeNpmOutput(text)}`);
358
526
  };
359
527
  return async (spec, where) => {
360
528
  if (!isLocalTarball(spec)) {
@@ -402,6 +570,11 @@ export function createInstaller(opts = {}) {
402
570
  * into nothing, leaving the installation down until the next login.
403
571
  */
404
572
  export function restartPlan(facts) {
573
+ // buddi.app is this supervisor's parent: it starts the launcher `current`
574
+ // points at again as soon as this exits with APP_RESTART_EXIT (app-layout.ts).
575
+ if (facts.appLayout === true) {
576
+ return { mode: 'app', reason: 'buddi.app starts the release `current` points at again' };
577
+ }
405
578
  if (facts.platform === 'darwin' && facts.label !== '' && facts.xpcServiceName === facts.label) {
406
579
  return { mode: 'launchd', reason: 'launchd keeps this job alive and runs the launcher from the install root' };
407
580
  }
@@ -424,9 +597,9 @@ export function restartPlan(facts) {
424
597
  * owner actually asked for.
425
598
  */
426
599
  export async function handOver(opts) {
427
- const plan = opts.plan ?? restartPlan({ platform: process.platform, label: launchAgentLabel(opts.ctx.data), serviceUnit: process.env[SERVICE_UNIT_VAR] ?? opts.env[SERVICE_UNIT_VAR], xpcServiceName: process.env.XPC_SERVICE_NAME ?? opts.env.XPC_SERVICE_NAME });
600
+ const plan = opts.plan ?? restartPlan({ platform: process.platform, label: launchAgentLabel(opts.ctx.data), serviceUnit: process.env[SERVICE_UNIT_VAR] ?? opts.env[SERVICE_UNIT_VAR], xpcServiceName: process.env.XPC_SERVICE_NAME ?? opts.env.XPC_SERVICE_NAME, appLayout: appLayout(opts.env) !== undefined });
428
601
  const log = opts.log ?? ((line) => console.error(line));
429
- if (plan.mode === 'launchd' || plan.mode === 'systemd') {
602
+ if (plan.mode === 'launchd' || plan.mode === 'systemd' || plan.mode === 'app') {
430
603
  log(`supervisor: exiting for the upgrade; ${plan.reason}.`);
431
604
  return { plan, ok: true, attempts: 0 };
432
605
  }
@@ -505,11 +678,15 @@ export async function statusOnSocket(socket, timeoutMs = 2_000) {
505
678
  * dashboard is not available in that state — the gateway is deliberately not
506
679
  * started — so this has to be complete on its own.
507
680
  */
508
- export function recoverySentence(entry) {
681
+ export function recoverySentence(entry, opts = {}) {
509
682
  const where = entry.step === 'starting' ? 'while starting' : 'while migrating';
510
- return `Upgrade to ${entry.to} failed ${where}: ${entry.error ?? 'unknown error'}. ` +
511
- `The backup taken first is ${entry.backup ?? 'not available'}. ` +
512
- `Reinstall with \`npm install -g ${PACKAGE_NAME}@${entry.from}\` and run \`buddi backup restore ${entry.backup ?? '<backup>'}\`.`;
683
+ const head = `Upgrade to ${entry.to} failed ${where}: ${entry.error ?? 'unknown error'}. ` +
684
+ `The backup taken first is ${entry.backup ?? 'not available'}. `;
685
+ if (opts.app === true) {
686
+ // buddi.app keeps the release that ran before; its menu goes back to it.
687
+ return head + `In the buddi menu, choose Advanced → Restart with the Previous Version (${entry.from}), then restore ${entry.backup ?? 'that backup'} in Settings → Backups.`;
688
+ }
689
+ return head + `Reinstall with \`npm install -g ${PACKAGE_NAME}@${entry.from}\` and run \`buddi backup restore ${entry.backup ?? '<backup>'}\`.`;
513
690
  }
514
691
  function message(err) {
515
692
  return err instanceof Error ? err.message : String(err);
@@ -519,10 +696,16 @@ export function createUpgradeService(opts) {
519
696
  const log = opts.log ?? ((line) => console.error(line));
520
697
  const jobs = new JobStore();
521
698
  const registry = registryFor(ctx.env);
699
+ /** What npm would send this registry; the check asks with the same. */
700
+ const auth = registryAuth(registry, ctx.env);
522
701
  const checkInterval = opts.checkIntervalMs ?? (Number(ctx.env.BUDDI_UPGRADE_CHECK_INTERVAL_MS) || CHECK_INTERVAL_MS);
523
702
  const install = opts.install ?? createInstaller();
524
703
  const checkPostgres = opts.checkPostgres ?? createPostgresCheck({ env: ctx.env });
525
704
  const backupWait = opts.backupWaitMs ?? BACKUP_WAIT_MS;
705
+ /** buddi.app's releases folder, when the app runs this installation. */
706
+ const releases = appLayout(ctx.env);
707
+ const npm = opts.npm ?? (async (bin, argv, where) => await run(bin, argv, where));
708
+ const dressed = (view) => releases === undefined ? view : { ...view, app: true };
526
709
  let running;
527
710
  /** Has the code under this process already been replaced? See `perform`. */
528
711
  let replaced = false;
@@ -539,13 +722,17 @@ export function createUpgradeService(opts) {
539
722
  return await save({ ...state, check: { ...state.check, lastAt: at, error: 'this installation has no outbound transport' } });
540
723
  }
541
724
  try {
542
- const latest = await fetchLatestRelease(registry, opts.http);
725
+ const latest = await fetchLatestRelease(registry, opts.http, auth);
726
+ // npm names a fresh version minutes before it serves the tarball; until
727
+ // it does, the version is known but not offered (Upgrade would 404).
728
+ const served = !isNewer(latest.version, current) || await tarballServed(registry, latest.version, opts.http, { tarball: latest.tarball, auth });
543
729
  // A check that answered clears the error a check that did not left, and
544
730
  // the notes are always the ones this answer carried: notes kept from an
545
731
  // older `latest` would describe the wrong release.
546
732
  return await save({ ...state, check: {
547
733
  enabled: state.check.enabled, lastAt: at, latest: latest.version,
548
734
  ...(latest.notes === undefined ? {} : { latestNotes: latest.notes }),
735
+ ...(served ? {} : { processing: latest.version }),
549
736
  } });
550
737
  }
551
738
  catch (err) {
@@ -586,10 +773,12 @@ export function createUpgradeService(opts) {
586
773
  if (asked !== undefined) {
587
774
  return isVersion(asked) ? { version: asked } : { error: `"${asked}" is not a version this can install.` };
588
775
  }
589
- const known = (await read()).check.latest;
590
- if (isNewer(known, current))
591
- return { version: known };
776
+ const kept = (await read()).check;
777
+ if (isNewer(kept.latest, current) && kept.processing === undefined)
778
+ return { version: kept.latest };
592
779
  const state = await runCheck();
780
+ if (state.check.processing !== undefined)
781
+ return { error: processingSentence(state.check.processing) };
593
782
  if (isVersion(state.check.latest))
594
783
  return { version: state.check.latest };
595
784
  return { error: state.check.error ?? 'the registry did not name a version to upgrade to' };
@@ -615,6 +804,8 @@ export function createUpgradeService(opts) {
615
804
  else
616
805
  log(`upgrade: ${step} failed: ${error}`);
617
806
  };
807
+ if (releases !== undefined)
808
+ return await performInApp(job, version, releases, give, startedAt, (next) => { to = next; }, (taken) => { archive = taken; });
618
809
  // Where npm may write, decided before anything is stopped or archived.
619
810
  const target = upgradeTarget(ctx.root);
620
811
  if ('error' in target)
@@ -626,27 +817,18 @@ export function createUpgradeService(opts) {
626
817
  return await give('checking', resolved.error, false);
627
818
  to = resolved.version;
628
819
  spec = `${PACKAGE_NAME}@${resolved.version}`;
820
+ // Asked before the backup and the stop: a version npm is still
821
+ // processing would only reach npm's 404 after both.
822
+ if (opts.http !== undefined) {
823
+ const served = await tarballServed(registry, to, opts.http, { auth }).catch(() => undefined);
824
+ if (served === false)
825
+ return await give('checking', processingSentence(to), false);
826
+ }
629
827
  }
630
- /*
631
- * The backup is encrypted exactly as the schedule says, because that
632
- * setting is the owner's answer to "may an archive of everything sit on
633
- * this disk in the clear", and an upgrade is not an exception to it.
634
- */
635
- const schedule = await backup.schedule().catch(() => undefined);
636
- const encrypt = schedule?.encryptLocal !== false;
637
- if (encrypt && !backup.hasVault()) {
638
- return await give('backup', 'This installation has no vault, so the backup an upgrade takes first cannot be encrypted. Turn off "Encrypt local backups" in Settings, or set up a vault.', false);
639
- }
640
- jobs.phase(job, 'backup', 'taking a backup before anything changes');
641
- const started = backup.create(encrypt);
642
- if ('status' in started)
643
- return await give('backup', started.error, false);
644
- const done = await settled(started, backupWait);
645
- if (done === undefined)
646
- return await give('backup', 'the backup did not finish within twenty minutes', false);
647
- if (done.phase !== 'done')
648
- return await give('backup', done.error ?? 'the backup did not finish', false);
649
- archive = done.report?.archive;
828
+ const taken = await takeBackup(job);
829
+ if ('error' in taken)
830
+ return await give('backup', taken.error, false);
831
+ archive = taken.archive;
650
832
  jobs.phase(job, 'stopping', 'stopping the gateway; the database stays up');
651
833
  await opts.stopGateway();
652
834
  jobs.phase(job, 'installing', `installing ${spec}`);
@@ -654,7 +836,9 @@ export function createUpgradeService(opts) {
654
836
  await install(spec, { registry, root: ctx.root, target });
655
837
  }
656
838
  catch (err) {
657
- return await give('installing', message(err), true);
839
+ // npm's 404 wall for a version it has not served yet is one sentence here.
840
+ const said = message(err);
841
+ return await give('installing', tarball === undefined && isNotFound(said) ? processingSentence(to) : said, true);
658
842
  }
659
843
  /*
660
844
  * What is on disk now, rather than what was asked for. npm can be pointed
@@ -698,6 +882,104 @@ export function createUpgradeService(opts) {
698
882
  return await give('verifying', `${refused} It is still running on ${current}.`, true);
699
883
  }
700
884
  }
885
+ await handOverTo(job, to, startedAt, archive);
886
+ };
887
+ /**
888
+ * The backup every upgrade takes first. Encrypted exactly as the schedule
889
+ * says, because that setting is the owner's answer to "may an archive of
890
+ * everything sit on this disk in the clear", and an upgrade is not an
891
+ * exception to it.
892
+ */
893
+ const takeBackup = async (job) => {
894
+ const schedule = await backup.schedule().catch(() => undefined);
895
+ const encrypt = schedule?.encryptLocal !== false;
896
+ if (encrypt && !backup.hasVault()) {
897
+ return { error: 'This installation has no vault, so the backup an upgrade takes first cannot be encrypted. Turn off "Encrypt local backups" in Settings, or set up a vault.' };
898
+ }
899
+ jobs.phase(job, 'backup', 'taking a backup before anything changes');
900
+ const started = backup.create(encrypt);
901
+ if ('status' in started)
902
+ return { error: started.error };
903
+ const done = await settled(started, backupWait);
904
+ if (done === undefined)
905
+ return { error: 'the backup did not finish within twenty minutes' };
906
+ if (done.phase !== 'done')
907
+ return { error: done.error ?? 'the backup did not finish' };
908
+ return { archive: done.report?.archive };
909
+ };
910
+ /**
911
+ * buddi.app's upgrade: the new release goes into `<releases>/buddi-<v>`,
912
+ * verified and checked while the gateway still runs, and only then is there
913
+ * a backup, a stop and the switch of `current`. See `app-layout.ts`.
914
+ */
915
+ const performInApp = async (job, version, dir, give, startedAt, setTo, setArchive) => {
916
+ if (ctx.env.BUDDI_UPGRADE_SOURCE?.trim())
917
+ return await give('checking', 'buddi.app upgrades from the registry only; BUDDI_UPGRADE_SOURCE is for npm installations.', false);
918
+ const resolved = await resolveVersion(version);
919
+ if ('error' in resolved)
920
+ return await give('checking', resolved.error, false);
921
+ const to = resolved.version;
922
+ setTo(to);
923
+ if (opts.http === undefined)
924
+ return await give('checking', 'this installation has no outbound transport', false);
925
+ const served = await tarballServed(registry, to, opts.http, { auth }).catch(() => undefined);
926
+ if (served === false)
927
+ return await give('checking', processingSentence(to), false);
928
+ const base = await realpath(dir).catch(() => dir);
929
+ if ((path.resolve(ctx.root) + path.sep).startsWith(releaseDir(base, to) + path.sep)) {
930
+ return await give('checking', `${to} is the version running.`, false);
931
+ }
932
+ /*
933
+ * Everything is checked in a staging folder of its own, and only a release
934
+ * that passed every check is renamed into `buddi-<v>`: a failed retry never
935
+ * touches a `buddi-<v>` that `previous` (after a rollback) still points at.
936
+ */
937
+ jobs.phase(job, 'installing', `downloading and verifying ${to}`);
938
+ let staged;
939
+ try {
940
+ staged = await stageRelease({ releases: dir, version: to, registry, http: opts.http, exec: npm, log, auth, ...(opts.provenanceChain === undefined ? {} : { chain: opts.provenanceChain }) });
941
+ }
942
+ catch (err) {
943
+ const said = message(err);
944
+ return await give('installing', isNotFound(said) ? processingSentence(to) : said, false);
945
+ }
946
+ const dropStaging = async () => { await rm(staged.staging, { recursive: true, force: true }).catch(() => { }); };
947
+ if (ctx.state.database !== 'external') {
948
+ jobs.phase(job, 'verifying', `checking that ${to} can start its database`);
949
+ const verdict = await checkPostgres(staged.root);
950
+ if (!verdict.ok) {
951
+ await dropStaging();
952
+ return await give('verifying', `${verdict.error} buddi did not switch to ${to}. It is still running on ${current}.`, false);
953
+ }
954
+ }
955
+ let placed;
956
+ try {
957
+ placed = await placeRelease(dir, to, staged.staging, log);
958
+ }
959
+ catch (err) {
960
+ await dropStaging();
961
+ return await give('installing', `could not put ${to} in place: ${message(err)}`, false);
962
+ }
963
+ // Only what neither link uses goes: a kept `buddi-<v>` that `previous` names stays.
964
+ const discard = async () => { await pruneReleases(dir).catch(() => []); };
965
+ const taken = await takeBackup(job);
966
+ if ('error' in taken) {
967
+ await discard();
968
+ return await give('backup', taken.error, false);
969
+ }
970
+ setArchive(taken.archive);
971
+ jobs.phase(job, 'stopping', 'stopping the gateway; the database stays up');
972
+ await opts.stopGateway();
973
+ try {
974
+ await switchCurrent(dir, placed.root, ctx.root);
975
+ }
976
+ catch (err) {
977
+ return await give('installing', `could not switch to ${to}: ${message(err)}`, true);
978
+ }
979
+ await handOverTo(job, to, startedAt, taken.archive);
980
+ };
981
+ /** The point of no return, shared by both layouts. */
982
+ const handOverTo = async (job, to, startedAt, archive) => {
701
983
  replaced = true;
702
984
  jobs.phase(job, 'restarting', `handing over to ${to}`);
703
985
  /*
@@ -733,14 +1015,14 @@ export function createUpgradeService(opts) {
733
1015
  return {
734
1016
  current,
735
1017
  async view() {
736
- return versionView(await read());
1018
+ return dressed(versionView(await read()));
737
1019
  },
738
1020
  async check() {
739
- return versionView(await runCheck());
1021
+ return dressed(versionView(await runCheck()));
740
1022
  },
741
1023
  async setCheckEnabled(enabled) {
742
1024
  const state = await read();
743
- return versionView(await save({ ...state, check: { ...state.check, enabled } }));
1025
+ return dressed(versionView(await save({ ...state, check: { ...state.check, enabled } })));
744
1026
  },
745
1027
  start(version) {
746
1028
  if (version !== undefined && !isVersion(version)) {
@@ -852,7 +1134,7 @@ export function upgradeDoctorLines(view, phase) {
852
1134
  if (last !== undefined) {
853
1135
  lines.push(`Last upgrade: ${last.from} to ${last.to}, ${last.outcome}${last.step === undefined ? '' : ` at ${last.step}`}`);
854
1136
  if (phase === 'upgrade-failed' && last.outcome === 'failed')
855
- lines.push(recoverySentence(last));
1137
+ lines.push(recoverySentence(last, { app: view.app === true }));
856
1138
  }
857
1139
  return lines;
858
1140
  }