@skrr-ai/cli 0.1.94 → 0.1.96

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 (298) hide show
  1. package/README.md +175 -165
  2. package/bin/run.js +32 -0
  3. package/bin/update-npm-runner.cjs +33 -0
  4. package/bin/update-session.cjs +194 -0
  5. package/bin/update-worker.cjs +208 -0
  6. package/dist/base-command.js +0 -15
  7. package/dist/commands/agent/message/send.d.ts +2 -0
  8. package/dist/commands/agent/message/send.js +75 -5
  9. package/dist/commands/agents/chat.js +1 -1
  10. package/dist/commands/agents/create.d.ts +0 -3
  11. package/dist/commands/agents/create.js +0 -26
  12. package/dist/commands/agents/egress.d.ts +17 -0
  13. package/dist/commands/agents/egress.js +84 -0
  14. package/dist/commands/agents/mailbox/messages.d.ts +14 -0
  15. package/dist/commands/agents/mailbox/messages.js +52 -0
  16. package/dist/commands/agents/mailbox/show.d.ts +13 -0
  17. package/dist/commands/agents/mailbox/show.js +52 -0
  18. package/dist/commands/agents/operating-plan.js +8 -0
  19. package/dist/commands/agents/roles/capture.d.ts +16 -0
  20. package/dist/commands/agents/roles/capture.js +53 -0
  21. package/dist/commands/agents/roles/install.d.ts +1 -0
  22. package/dist/commands/agents/roles/install.js +8 -1
  23. package/dist/commands/agents/roles/preview.d.ts +2 -0
  24. package/dist/commands/agents/roles/preview.js +27 -3
  25. package/dist/commands/agents/update.d.ts +0 -3
  26. package/dist/commands/agents/update.js +1 -31
  27. package/dist/commands/agents/usage-cap.d.ts +4 -1
  28. package/dist/commands/agents/usage-cap.js +49 -10
  29. package/dist/commands/code/doctor.d.ts +1 -0
  30. package/dist/commands/code/doctor.js +6 -0
  31. package/dist/commands/code/index.js +4 -0
  32. package/dist/commands/code/install.d.ts +5 -62
  33. package/dist/commands/code/install.js +86 -279
  34. package/dist/commands/code/jobs/show.js +12 -0
  35. package/dist/commands/code/rollback.d.ts +8 -0
  36. package/dist/commands/code/rollback.js +20 -0
  37. package/dist/commands/code/update.d.ts +8 -37
  38. package/dist/commands/code/update.js +134 -213
  39. package/dist/commands/commitments/action-proposals/decide.d.ts +2 -0
  40. package/dist/commands/commitments/action-proposals/decide.js +12 -0
  41. package/dist/commands/commitments/action-proposals.js +7 -0
  42. package/dist/commands/commitments/create.d.ts +1 -0
  43. package/dist/commands/commitments/create.js +17 -1
  44. package/dist/commands/commitments/delivery.d.ts +1 -0
  45. package/dist/commands/commitments/delivery.js +11 -1
  46. package/dist/commands/commitments/doctor.js +4 -0
  47. package/dist/commands/commitments/effective-policy.js +26 -0
  48. package/dist/commands/commitments/endpoints/create.js +5 -2
  49. package/dist/commands/commitments/observations.d.ts +10 -0
  50. package/dist/commands/commitments/observations.js +88 -0
  51. package/dist/commands/commitments/pause.d.ts +1 -1
  52. package/dist/commands/commitments/pause.js +4 -3
  53. package/dist/commands/commitments/preflight.js +2 -1
  54. package/dist/commands/commitments/reports.d.ts +2 -0
  55. package/dist/commands/commitments/reports.js +22 -4
  56. package/dist/commands/commitments/update.d.ts +1 -0
  57. package/dist/commands/commitments/update.js +35 -5
  58. package/dist/commands/daemon/install.js +4 -0
  59. package/dist/commands/generate-recovery-code.d.ts +13 -1
  60. package/dist/commands/generate-recovery-code.js +146 -13
  61. package/dist/commands/grants/create.d.ts +1 -0
  62. package/dist/commands/grants/create.js +8 -0
  63. package/dist/commands/grants/rotate.d.ts +5 -0
  64. package/dist/commands/grants/rotate.js +13 -1
  65. package/dist/commands/grants/update.d.ts +12 -0
  66. package/dist/commands/grants/update.js +27 -0
  67. package/dist/commands/instructions/guide.d.ts +20 -0
  68. package/dist/commands/instructions/guide.js +84 -0
  69. package/dist/commands/instructions/install.js +57 -33
  70. package/dist/commands/instructions/setup.d.ts +14 -0
  71. package/dist/commands/instructions/setup.js +72 -0
  72. package/dist/commands/integrations/execute.js +1 -1
  73. package/dist/commands/login.d.ts +5 -6
  74. package/dist/commands/login.js +27 -11
  75. package/dist/commands/logout.js +7 -7
  76. package/dist/commands/machines/hosted/start.js +7 -1
  77. package/dist/commands/machines/list.js +4 -4
  78. package/dist/commands/measures/confirm.d.ts +1 -7
  79. package/dist/commands/measures/confirm.js +15 -14
  80. package/dist/commands/measures/list.d.ts +1 -7
  81. package/dist/commands/measures/list.js +1 -7
  82. package/dist/commands/measures/record.js +4 -4
  83. package/dist/commands/measures/show.d.ts +11 -0
  84. package/dist/commands/measures/show.js +26 -0
  85. package/dist/commands/northstars/governance/configure.d.ts +1 -0
  86. package/dist/commands/northstars/governance/configure.js +8 -1
  87. package/dist/commands/proposals/discard.d.ts +5 -0
  88. package/dist/commands/proposals/discard.js +8 -0
  89. package/dist/commands/proposals/keep.d.ts +5 -0
  90. package/dist/commands/proposals/keep.js +8 -0
  91. package/dist/commands/proposals/list.d.ts +15 -0
  92. package/dist/commands/proposals/list.js +74 -0
  93. package/dist/commands/proposals/show.d.ts +12 -0
  94. package/dist/commands/proposals/show.js +27 -0
  95. package/dist/commands/proposals/snooze.d.ts +5 -0
  96. package/dist/commands/proposals/snooze.js +8 -0
  97. package/dist/commands/security/overview.d.ts +11 -0
  98. package/dist/commands/security/overview.js +158 -0
  99. package/dist/commands/signals/monitors/create.d.ts +9 -0
  100. package/dist/commands/signals/monitors/create.js +29 -0
  101. package/dist/commands/signals/monitors/list.d.ts +8 -0
  102. package/dist/commands/signals/monitors/list.js +16 -0
  103. package/dist/commands/signals/monitors/pause.d.ts +11 -0
  104. package/dist/commands/signals/monitors/pause.js +17 -0
  105. package/dist/commands/signals/monitors/resume.d.ts +11 -0
  106. package/dist/commands/signals/monitors/resume.js +17 -0
  107. package/dist/commands/signals/monitors/revise.d.ts +12 -0
  108. package/dist/commands/signals/monitors/revise.js +25 -0
  109. package/dist/commands/signals/monitors/show.d.ts +11 -0
  110. package/dist/commands/signals/monitors/show.js +17 -0
  111. package/dist/commands/signals/monitors/test.d.ts +12 -0
  112. package/dist/commands/signals/monitors/test.js +25 -0
  113. package/dist/commands/signals/monitors/transitions.d.ts +11 -0
  114. package/dist/commands/signals/monitors/transitions.js +17 -0
  115. package/dist/commands/signals/monitors/validate.d.ts +9 -0
  116. package/dist/commands/signals/monitors/validate.js +24 -0
  117. package/dist/commands/signals/schemas/list.d.ts +8 -0
  118. package/dist/commands/signals/schemas/list.js +16 -0
  119. package/dist/commands/signals/schemas/show.d.ts +11 -0
  120. package/dist/commands/signals/schemas/show.js +17 -0
  121. package/dist/commands/signals/sources/list.d.ts +8 -0
  122. package/dist/commands/signals/sources/list.js +16 -0
  123. package/dist/commands/signals/sources/receipts.d.ts +11 -0
  124. package/dist/commands/signals/sources/receipts.js +17 -0
  125. package/dist/commands/signals/sources/show.d.ts +11 -0
  126. package/dist/commands/signals/sources/show.js +17 -0
  127. package/dist/commands/spaces/claim.d.ts +12 -0
  128. package/dist/commands/spaces/claim.js +42 -0
  129. package/dist/commands/spaces/ownership.d.ts +12 -0
  130. package/dist/commands/spaces/ownership.js +46 -0
  131. package/dist/commands/spaces/usage.d.ts +12 -0
  132. package/dist/commands/spaces/usage.js +28 -0
  133. package/dist/commands/spaces/workflows/run.js +3 -0
  134. package/dist/commands/spaces/workflows/runs.js +2 -1
  135. package/dist/commands/spaces/workflows/update.d.ts +6 -1
  136. package/dist/commands/spaces/workflows/update.js +34 -2
  137. package/dist/commands/tasks/cancel-cascade.js +1 -1
  138. package/dist/commands/tasks/cascade-status.d.ts +2 -0
  139. package/dist/commands/tasks/cascade-status.js +27 -1
  140. package/dist/commands/tasks/create-tree.d.ts +5 -1
  141. package/dist/commands/tasks/create-tree.js +35 -7
  142. package/dist/commands/tasks/create.d.ts +4 -0
  143. package/dist/commands/tasks/create.js +16 -0
  144. package/dist/commands/tasks/deliverable/add.js +12 -3
  145. package/dist/commands/tasks/economics-compare.d.ts +15 -0
  146. package/dist/commands/tasks/economics-compare.js +60 -0
  147. package/dist/commands/tasks/expectations/evaluate.d.ts +13 -0
  148. package/dist/commands/tasks/expectations/evaluate.js +43 -0
  149. package/dist/commands/tasks/interrupt.js +2 -2
  150. package/dist/commands/tasks/list.d.ts +1 -0
  151. package/dist/commands/tasks/list.js +44 -1
  152. package/dist/commands/tasks/pause-schedule.js +1 -1
  153. package/dist/commands/tasks/show.d.ts +5 -0
  154. package/dist/commands/tasks/show.js +17 -0
  155. package/dist/commands/tasks/update.d.ts +4 -0
  156. package/dist/commands/tasks/update.js +9 -0
  157. package/dist/commands/tasks/workflow-findings.js +2 -2
  158. package/dist/commands/token/list.d.ts +3 -5
  159. package/dist/commands/token/list.js +99 -57
  160. package/dist/commands/triggers/create.d.ts +2 -0
  161. package/dist/commands/triggers/create.js +15 -0
  162. package/dist/commands/update.d.ts +14 -0
  163. package/dist/commands/update.js +61 -0
  164. package/dist/commands/workspaces/grants.d.ts +12 -0
  165. package/dist/commands/workspaces/grants.js +31 -0
  166. package/dist/commands/workspaces/stewardship.d.ts +13 -0
  167. package/dist/commands/workspaces/stewardship.js +34 -0
  168. package/dist/generated/managed-instruction-guides.json +9 -0
  169. package/dist/hooks/init.d.ts +5 -0
  170. package/dist/hooks/init.js +27 -0
  171. package/dist/lib/agent-config.d.ts +0 -12
  172. package/dist/lib/agent-config.js +1 -27
  173. package/dist/lib/agent-mailbox.d.ts +35 -0
  174. package/dist/lib/agent-mailbox.js +55 -0
  175. package/dist/lib/commitment-endpoints.d.ts +2 -1
  176. package/dist/lib/commitment-endpoints.js +2 -5
  177. package/dist/lib/commitment-product.d.ts +3 -1
  178. package/dist/lib/commitment-product.js +10 -0
  179. package/dist/lib/commitment-terms.js +1 -1
  180. package/dist/lib/commitments.d.ts +17 -15
  181. package/dist/lib/commitments.js +173 -137
  182. package/dist/lib/compasses.d.ts +14 -0
  183. package/dist/lib/compasses.js +25 -11
  184. package/dist/lib/daemon-installer.d.ts +2 -0
  185. package/dist/lib/daemon-installer.js +100 -24
  186. package/dist/lib/first-party-harness-doctor.js +31 -1
  187. package/dist/lib/first-party-harness-remote-install.d.ts +71 -0
  188. package/dist/lib/first-party-harness-remote-install.js +310 -0
  189. package/dist/lib/first-party-harness-remote-update.d.ts +46 -0
  190. package/dist/lib/first-party-harness-remote-update.js +242 -0
  191. package/dist/lib/first-party-harness-startup-update.d.ts +26 -0
  192. package/dist/lib/first-party-harness-startup-update.js +166 -0
  193. package/dist/lib/first-party-harness-update.d.ts +46 -0
  194. package/dist/lib/first-party-harness-update.js +289 -0
  195. package/dist/lib/first-party-harness.js +7 -4
  196. package/dist/lib/grants.d.ts +5 -1
  197. package/dist/lib/grants.js +9 -1
  198. package/dist/lib/hosted-machines.d.ts +7 -0
  199. package/dist/lib/mailbox-human-command.d.ts +11 -0
  200. package/dist/lib/mailbox-human-command.js +41 -0
  201. package/dist/lib/managed-instruction-local.d.ts +12 -0
  202. package/dist/lib/managed-instruction-local.js +58 -0
  203. package/dist/lib/managed-instruction-setup.d.ts +35 -0
  204. package/dist/lib/managed-instruction-setup.js +306 -0
  205. package/dist/lib/measures.d.ts +28 -2
  206. package/dist/lib/measures.js +38 -14
  207. package/dist/lib/memory-sync.d.ts +1 -0
  208. package/dist/lib/memory-sync.js +279 -68
  209. package/dist/lib/message-intent.d.ts +6 -2
  210. package/dist/lib/message-intent.js +1 -0
  211. package/dist/lib/northstars.js +2 -1
  212. package/dist/lib/platform-update-local.d.ts +2 -0
  213. package/dist/lib/platform-update-local.js +113 -0
  214. package/dist/lib/platform-update.d.ts +39 -0
  215. package/dist/lib/platform-update.js +84 -0
  216. package/dist/lib/profile-machine-identity-guard.d.ts +2 -0
  217. package/dist/lib/profile-machine-identity-guard.js +13 -0
  218. package/dist/lib/prompt.d.ts +1 -0
  219. package/dist/lib/prompt.js +4 -3
  220. package/dist/lib/proposal-decision-command.d.ts +19 -0
  221. package/dist/lib/proposal-decision-command.js +64 -0
  222. package/dist/lib/proposals.d.ts +8 -0
  223. package/dist/lib/proposals.js +22 -0
  224. package/dist/lib/roles.d.ts +141 -6
  225. package/dist/lib/roles.js +8 -2
  226. package/dist/lib/signals.d.ts +20 -0
  227. package/dist/lib/signals.js +24 -0
  228. package/dist/lib/space-usage.d.ts +3 -0
  229. package/dist/lib/space-usage.js +17 -0
  230. package/dist/lib/stewardship-output.d.ts +4 -0
  231. package/dist/lib/stewardship-output.js +82 -0
  232. package/dist/lib/task-asset-upload.d.ts +3 -1
  233. package/dist/lib/task-asset-upload.js +1 -0
  234. package/dist/lib/task-batch.d.ts +9 -0
  235. package/dist/lib/task-batch.js +94 -17
  236. package/dist/lib/task-economics.d.ts +16 -0
  237. package/dist/lib/task-economics.js +22 -0
  238. package/dist/lib/task-instruction-offer.d.ts +1 -4
  239. package/dist/lib/task-instruction-offer.js +3 -129
  240. package/dist/lib/triggers.d.ts +11 -3
  241. package/dist/lib/triggers.js +58 -1
  242. package/dist/lib/update-check.d.ts +61 -53
  243. package/dist/lib/update-check.js +453 -314
  244. package/dist/lib/usage-discovery.d.ts +10 -0
  245. package/dist/lib/usage-discovery.js +24 -3
  246. package/dist/lib/workflow-run-budget.d.ts +12 -0
  247. package/dist/lib/workflow-run-budget.js +37 -0
  248. package/dist/lib/workspaces.d.ts +48 -0
  249. package/dist/lib/workspaces.js +8 -0
  250. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +4 -0
  251. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +14 -1
  252. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +3 -4
  253. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +3 -4
  254. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessReleaseKeys.d.ts +82 -0
  255. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessReleaseKeys.js +105 -0
  256. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessUpdater.d.ts +110 -0
  257. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessUpdater.js +945 -0
  258. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/githubPushRefusal.d.ts +9 -0
  259. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/githubPushRefusal.js +49 -0
  260. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessPermissionControls.d.ts +32 -0
  261. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessPermissionControls.js +62 -0
  262. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -0
  263. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -1
  264. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +35 -5
  265. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releasePolicy.d.ts +63 -0
  266. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releasePolicy.js +169 -0
  267. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseVersions.d.ts +2 -0
  268. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseVersions.js +44 -0
  269. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +4 -0
  270. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +12 -0
  271. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +3 -4
  272. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +3 -4
  273. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessReleaseKeys.d.ts +82 -0
  274. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessReleaseKeys.js +102 -0
  275. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessUpdater.d.ts +110 -0
  276. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessUpdater.js +923 -0
  277. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/githubPushRefusal.d.ts +9 -0
  278. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/githubPushRefusal.js +44 -0
  279. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessPermissionControls.d.ts +32 -0
  280. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessPermissionControls.js +59 -0
  281. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -0
  282. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -0
  283. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +35 -5
  284. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releasePolicy.d.ts +63 -0
  285. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releasePolicy.js +163 -0
  286. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseVersions.d.ts +2 -0
  287. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseVersions.js +41 -0
  288. package/dist/node_modules/@skrr-ai/auth-core/package.json +41 -1
  289. package/dist/node_modules/@skrr-ai/data-provider/index.js +8069 -7121
  290. package/docs/v1/README.md +28 -0
  291. package/docs/v1/api.md +55 -0
  292. package/docs/v1/changelog.md +16 -0
  293. package/docs/v1/check-examples.mjs +178 -0
  294. package/docs/v1/examples/api-readonly.mjs +144 -0
  295. package/docs/v1/operations.md +87 -0
  296. package/docs/v1/quickstart.md +71 -0
  297. package/oclif.manifest.json +49424 -46174
  298. package/package.json +34 -11
@@ -37,91 +37,46 @@ exports.normalizeBaseUrl = normalizeBaseUrl;
37
37
  exports.isNudgeSuppressed = isNudgeSuppressed;
38
38
  exports.isAutoUpdateSuppressed = isAutoUpdateSuppressed;
39
39
  exports.isOlderVersion = isOlderVersion;
40
+ exports.isPrivateCliPrefix = isPrivateCliPrefix;
40
41
  exports.resolveCliPackageDir = resolveCliPackageDir;
41
42
  exports.classifyCliInstall = classifyCliInstall;
43
+ exports.cliUpdateCommand = cliUpdateCommand;
42
44
  exports.buildNudgeLine = buildNudgeLine;
43
45
  exports.decideCliUpdateAction = decideCliUpdateAction;
46
+ exports.readCliUpdateStatus = readCliUpdateStatus;
47
+ exports.checkCliUpdate = checkCliUpdate;
48
+ exports.performCliUpdate = performCliUpdate;
44
49
  exports.maybeWarnOutdatedCli = maybeWarnOutdatedCli;
50
+ exports.shouldCheckCliUpdateAtStartup = shouldCheckCliUpdateAtStartup;
45
51
  const node_child_process_1 = require("node:child_process");
46
52
  const fs = __importStar(require("node:fs"));
47
53
  const os = __importStar(require("node:os"));
48
54
  const path = __importStar(require("node:path"));
55
+ const node_crypto_1 = require("node:crypto");
49
56
  const node_util_1 = require("node:util");
50
- /**
51
- * update-check.ts — non-blocking "a newer `skrr` is available" advisory AND
52
- * background self-update for installs we own.
53
- *
54
- * The standalone `skrr` CLI (npm `@skrr-ai/cli`) has no in-place update path
55
- * of its own — the bytes belong to whatever installed them. On every command
56
- * this module decides — from a ~4h on-disk cache, with ZERO added latency —
57
- * what to do about version state, and refreshes that cache in the background:
58
- *
59
- * - npm-global, writable install + outdated → AUTO-UPDATE: schedule a
60
- * detached `npm install -g @skrr-ai/cli@<latest>` that runs after this
61
- * command exits (so a mid-flight command never reads half-swapped files),
62
- * once per TTL window, and print one stderr line saying so. If a later
63
- * run still sees the old version the attempt failed and we fall back to
64
- * the manual nudge.
65
- * - every other install shape → NUDGE: a single stderr line telling the
66
- * user the right update command for THEIR install (a project-local dep
67
- * is updated inside its project; a dev checkout is rebuilt, not npm'd).
68
- * - installs we must not touch → silence: the app-bundled CLI updates with
69
- * the desktop app; image-baked installs (dedicated-runtime guest) are
70
- * owned by the image.
71
- *
72
- * Hard guarantees (a version check must never make the CLI worse):
73
- * - Writes ONLY to stderr, never stdout, so `--json` output stays clean.
74
- * - Prints at most ONE line, and only on an interactive TTY — scripts, CI
75
- * and pipes neither see the nudge nor trigger an auto-update.
76
- * - Never blocks the command: the decision is synchronous off the cache;
77
- * the network refresh and install classification are fire-and-forget
78
- * and time-bounded; the actual `npm install` is deferred to process
79
- * exit and detached.
80
- * - Never throws: every path is wrapped so a malformed cache, offline
81
- * API, missing npm, or read-only home directory can't break a command.
82
- * - Never escalates: a global prefix we cannot write stays a nudge; the
83
- * CLI will not reach for sudo.
84
- * - Suppressible: `OVERSKY_NO_UPDATE_NUDGE` / `OVERSKY_SKIP_UPDATE_CHECK`
85
- * silence everything; `OVERSKY_CLI_AUTO_UPDATE=0` keeps the nudge but
86
- * skips the background install; `OVERSKY_CLI_BUNDLED=1` (set by the
87
- * desktop app's `skrr` wrapper) suppresses it entirely.
88
- */
89
- /** How long a cached advisory stays fresh before a background refresh. */
90
- const CHECK_TTL_MS = 4 * 60 * 60 * 1000; // 4h
91
- /** Hard ceiling on the background refresh request. */
57
+ const auth_core_1 = require("@skrr-ai/auth-core");
58
+ /** One update owner and one startup advisory. npm continues to own npm bytes;
59
+ * the durable worker waits for cooperating CLI sessions before replacing them.
60
+ * All update decisions are observable, and checking never schedules a change. */
61
+ const CHECK_TTL_MS = 4 * 60 * 60 * 1000;
92
62
  const FETCH_TIMEOUT_MS = 1500;
93
- /** Hard ceiling on the `npm root -g` classification probe. */
94
63
  const NPM_ROOT_TIMEOUT_MS = 1500;
95
- /** Marker written by the dedicated-runtime image bake — a guest's skrr is
96
- * image-pinned and must never be told to `npm update -g`. */
64
+ const CLI_PACKAGE = '@skrr-ai/cli';
97
65
  const DEDICATED_RUNTIME_ENV_FILE = '/etc/skrr-dedicated-runtime.env';
98
- /** Daemon-source values for image-pinned (hosted) runtime contexts. */
99
- const HOSTED_RUNTIME_SOURCES = new Set([
100
- 'dedicated-runtime',
101
- 'hosted-machine',
102
- 'cloud-coding',
103
- ]);
66
+ const HOSTED_RUNTIME_SOURCES = new Set(['dedicated-runtime', 'hosted-machine', 'cloud-coding']);
104
67
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
105
- function cacheFilePath() {
106
- return path.join(os.homedir(), '.skrr', 'cli-update-check.json');
68
+ function cacheFilePath(env = process.env) {
69
+ return path.join((0, auth_core_1.resolveConfigRoot)(env), 'cli-update-check.json');
107
70
  }
108
- /** Trailing-slash- and case-insensitive normalization for baseURL compares. */
109
71
  function normalizeBaseUrl(url) {
110
72
  return url.trim().replace(/\/+$/, '').toLowerCase();
111
73
  }
112
74
  function isTruthyEnv(value) {
113
- if (!value)
114
- return false;
115
- return ['1', 'true', 'on', 'yes'].includes(value.trim().toLowerCase());
75
+ return !!value && ['1', 'true', 'on', 'yes'].includes(value.trim().toLowerCase());
116
76
  }
117
77
  function isDisabledEnv(value) {
118
- if (!value)
119
- return false;
120
- return ['0', 'false', 'off', 'no', 'disabled'].includes(value.trim().toLowerCase());
78
+ return !!value && ['0', 'false', 'off', 'no', 'disabled'].includes(value.trim().toLowerCase());
121
79
  }
122
- /** True when this process runs on an image-pinned hosted-runtime guest. The
123
- * env marker covers the daemon unit's context; the env FILE is the reliable
124
- * signal for interactive tenant shells that never inherit it. */
125
80
  function isHostedRuntimeGuest(env) {
126
81
  if (HOSTED_RUNTIME_SOURCES.has((env.OVERSKY_DAEMON_SOURCE ?? '').trim()))
127
82
  return true;
@@ -132,67 +87,44 @@ function isHostedRuntimeGuest(env) {
132
87
  return false;
133
88
  }
134
89
  }
135
- /**
136
- * True when the advisory must stay silent regardless of version state:
137
- * explicit suppression, a desktop-app-bundled CLI (which auto-updates with
138
- * the app and must not be told to `npm update -g`), or an image-baked install
139
- * the control plane owns.
140
- */
141
90
  function isNudgeSuppressed(env = process.env) {
142
91
  return (isTruthyEnv(env.OVERSKY_NO_UPDATE_NUDGE) ||
143
92
  isTruthyEnv(env.OVERSKY_SKIP_UPDATE_CHECK) ||
93
+ isTruthyEnv(env.OCLIF_SKIP_NEW_VERSION_CHECK) ||
144
94
  env.OVERSKY_CLI_BUNDLED === '1' ||
145
95
  isHostedRuntimeGuest(env));
146
96
  }
147
- /** True when the background self-update is allowed to run at all. Suppression
148
- * implies no auto-update — a user who asked for silence did not ask for a
149
- * silent mutation of their global npm. */
150
97
  function isAutoUpdateSuppressed(env = process.env) {
151
- return isNudgeSuppressed(env) || isDisabledEnv(env.OVERSKY_CLI_AUTO_UPDATE);
152
- }
153
- /**
154
- * Parse the numeric release core of a version (`1.2.3-rc.1` → `[1,2,3]`),
155
- * padded to three parts. Returns null when the core isn't cleanly numeric,
156
- * so an unparseable version can only ever DISABLE the nudge, never fire a
157
- * spurious one.
158
- */
159
- function parseVersionCore(version) {
160
- const core = version.trim().split(/[-+]/, 1)[0];
161
- const parts = core.split('.');
162
- if (parts.length === 0 || parts.length > 3)
163
- return null;
164
- const nums = [];
165
- for (const part of parts) {
166
- if (!/^\d+$/.test(part))
167
- return null;
168
- nums.push(Number(part));
169
- }
170
- while (nums.length < 3)
171
- nums.push(0);
172
- return [nums[0], nums[1], nums[2]];
98
+ return (isNudgeSuppressed(env) ||
99
+ isDisabledEnv(env.OVERSKY_CLI_AUTO_UPDATE) ||
100
+ env.SKRR_CLI_UPDATE_SESSION_UNAVAILABLE === '1');
173
101
  }
174
- /**
175
- * True iff `current` is strictly older than `latest` by numeric release
176
- * core. Conservative: any unparseable input returns false (no nudge).
177
- * Prerelease suffixes are ignored, so `0.2.0-rc.1` is treated as `0.2.0` —
178
- * we never nag someone testing a prerelease of the latest line.
179
- */
180
102
  function isOlderVersion(current, latest) {
181
- const a = parseVersionCore(current);
182
- const b = parseVersionCore(latest);
183
- if (!a || !b)
103
+ return (0, auth_core_1.compareReleaseVersions)(current, latest) === -1;
104
+ }
105
+ /** Per-user leases cannot coordinate a global prefix shared with another OS
106
+ * user. Only private user-owned prefixes participate in managed replacement. */
107
+ function isPrivateCliPrefix(root) {
108
+ if (process.platform === 'win32' || typeof process.getuid !== 'function')
184
109
  return false;
185
- for (let i = 0; i < 3; i++) {
186
- if (a[i] < b[i])
187
- return true;
188
- if (a[i] > b[i])
110
+ try {
111
+ const home = fs.realpathSync(os.homedir());
112
+ let directory = fs.realpathSync(root);
113
+ if (!directory.startsWith(`${home}${path.sep}`))
189
114
  return false;
115
+ while (directory !== home) {
116
+ const stat = fs.statSync(directory);
117
+ if (stat.uid !== process.getuid() || (stat.mode & 0o022) !== 0)
118
+ return false;
119
+ directory = path.dirname(directory);
120
+ }
121
+ const homeStat = fs.statSync(home);
122
+ return homeStat.uid === process.getuid() && (homeStat.mode & 0o022) === 0;
123
+ }
124
+ catch {
125
+ return false;
190
126
  }
191
- return false;
192
127
  }
193
- // ---------------------------------------------------------------------------
194
- // Install-shape classification
195
- // ---------------------------------------------------------------------------
196
128
  const CLI_PACKAGE_DIR_RE = /^(.*[/\\]node_modules[/\\]@skrr-ai[/\\]cli)[/\\]/;
197
129
  /**
198
130
  * Resolve the package directory this process was launched from when it lives
@@ -240,11 +172,22 @@ function dirWritable(dir) {
240
172
  * dir beside `dist` exists only in a checkout — published packages ship
241
173
  * `dist` and no `src`.
242
174
  */
243
- function classifyCliInstall(env, argv, npmGlobalRoot) {
244
- if (env.OVERSKY_CLI_BUNDLED === '1' || isHostedRuntimeGuest(env)) {
175
+ function classifyCliInstall(env, argv, npmGlobalRoot, probeWritable = true) {
176
+ if (isHostedRuntimeGuest(env))
177
+ return { kind: 'image' };
178
+ if (env.OVERSKY_CLI_BUNDLED === '1')
245
179
  return { kind: 'bundled' };
180
+ const entry = argv[1];
181
+ try {
182
+ if (entry && /[/\\]Cellar[/\\]/.test(fs.realpathSync(entry)))
183
+ return { kind: 'homebrew' };
184
+ }
185
+ catch {
186
+ /* an unavailable entry remains conservatively classified */
246
187
  }
247
188
  const packageDir = resolveCliPackageDir(argv);
189
+ if (packageDir && fs.existsSync(path.join(packageDir, 'src', 'commands')))
190
+ return { kind: 'dev' };
248
191
  if (!packageDir) {
249
192
  // A dev checkout launches `cli/bin/run.js` directly — a `src/commands`
250
193
  // sibling of `dist` exists only in a checkout (published packages ship
@@ -290,7 +233,19 @@ function classifyCliInstall(env, argv, npmGlobalRoot) {
290
233
  // proof we must not treat it as ours to auto-update.
291
234
  return { kind: 'npm-local' };
292
235
  }
293
- return { kind: 'npm-global', writable: dirWritable(nodeModulesDir) };
236
+ let writable = false;
237
+ if (probeWritable)
238
+ writable = dirWritable(nodeModulesDir);
239
+ else {
240
+ try {
241
+ fs.accessSync(nodeModulesDir, fs.constants.W_OK);
242
+ writable = true;
243
+ }
244
+ catch {
245
+ /* read-only */
246
+ }
247
+ }
248
+ return { kind: 'npm-global', writable };
294
249
  }
295
250
  /**
296
251
  * Resolve `npm root -g` for install classification. Runs ONLY inside the
@@ -298,11 +253,12 @@ function classifyCliInstall(env, argv, npmGlobalRoot) {
298
253
  * no npm, a timeout, or an odd answer just leaves classification to the
299
254
  * path shape alone.
300
255
  */
301
- async function resolveNpmGlobalRoot() {
256
+ async function resolveNpmGlobalRoot(env = process.env) {
302
257
  try {
303
258
  const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
304
259
  const { stdout } = await execFileAsync(npmBin, ['root', '-g'], {
305
260
  timeout: NPM_ROOT_TIMEOUT_MS,
261
+ env,
306
262
  maxBuffer: 64 * 1024,
307
263
  shell: process.platform === 'win32',
308
264
  });
@@ -313,253 +269,436 @@ async function resolveNpmGlobalRoot() {
313
269
  return null;
314
270
  }
315
271
  }
316
- // ---------------------------------------------------------------------------
317
- // Advisory copy — one line, per install shape
318
- // ---------------------------------------------------------------------------
319
- /**
320
- * Build the one-line stderr advisory, or null when no nudge is warranted.
321
- * `installKind` tailors the command to the bytes' owner: a project-local dep
322
- * is updated inside its project, a dev checkout is rebuilt — telling either
323
- * to `npm update -g` would fix the wrong install.
324
- */
325
- function buildNudgeLine(currentVersion, latest, npmPackage, installKind) {
326
- if (!latest || !isOlderVersion(currentVersion, latest))
327
- return null;
328
- const pkg = npmPackage || '@skrr-ai/cli';
329
- const prefix = `[skrr] Update available: ${currentVersion} → ${latest}. `;
330
- const suffix = ` Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`;
331
- switch (installKind) {
272
+ function cliUpdateCommand(owner) {
273
+ switch (owner) {
274
+ case 'npm-global':
275
+ return `npm install -g ${CLI_PACKAGE}@latest`;
332
276
  case 'npm-local':
333
- return `${prefix}Run \`npm update ${pkg}\` in the owning project.${suffix}`;
277
+ return `npm update ${CLI_PACKAGE}`;
334
278
  case 'dev':
335
- return `${prefix}Rebuild your checkout (\`npm --prefix cli run build\`).${suffix}`;
336
- case 'other':
337
- return `${prefix}Update via your install method — see https://skrr.ai.${suffix}`;
338
- case 'npm-global':
279
+ return 'npm --prefix cli run build';
339
280
  case 'bundled':
281
+ return 'Update the skrr desktop app';
282
+ case 'image':
283
+ return 'Update the managed runtime image';
284
+ case 'homebrew':
285
+ return 'brew upgrade skrr';
340
286
  default:
341
- return `${prefix}Run \`npm update -g ${pkg}\` (or update the skrr app).${suffix}`;
287
+ return 'Update via your install method — see https://skrr.ai';
342
288
  }
343
289
  }
344
- /**
345
- * Decide, synchronously and from cache alone, what this command should do
346
- * about its version state. Pure — exported for direct unit testing.
347
- *
348
- * AUTO-UPDATE fires at most once per TTL window per target version, and only
349
- * for a provably writable npm-global install on an interactive TTY. An attempt
350
- * that didn't take (still outdated next run) falls back to the manual nudge
351
- * rather than retrying immediately.
352
- */
290
+ function buildNudgeLine(currentVersion, latest, _npmPackage, installKind = 'npm-global') {
291
+ if (!latest || !isOlderVersion(currentVersion, latest))
292
+ return null;
293
+ const command = installKind === 'npm-global' ? `npm update -g ${CLI_PACKAGE}` : cliUpdateCommand(installKind);
294
+ const context = installKind === 'npm-local'
295
+ ? ' in the owning project'
296
+ : installKind === 'dev'
297
+ ? ' to rebuild your checkout'
298
+ : '';
299
+ return `[skrr] Update available: ${currentVersion} → ${latest}. Run \`${command}\`${context}. Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`;
300
+ }
353
301
  function decideCliUpdateAction(args) {
354
302
  const { currentVersion, cache, env, isTTY } = args;
355
- const now = args.now ?? Date.now();
356
- if (!isTTY || isNudgeSuppressed(env))
357
- return null;
358
- if (!cache || !cache.latest || !isOlderVersion(currentVersion, cache.latest))
303
+ if (!isTTY ||
304
+ isNudgeSuppressed(env) ||
305
+ !cache?.latest ||
306
+ !isOlderVersion(currentVersion, cache.latest))
359
307
  return null;
360
308
  const install = cache.install ?? { kind: 'other' };
361
- const pkg = cache.npmPackage || '@skrr-ai/cli';
362
309
  const attempt = cache.autoUpdate;
363
- const attemptFresh = attempt != null && attempt.version === cache.latest && now - attempt.attemptedAt < CHECK_TTL_MS;
364
- if (install.kind === 'npm-global' && install.writable === true && !isAutoUpdateSuppressed(env)) {
365
- if (!attemptFresh) {
310
+ const sameAttempt = attempt?.version === cache.latest;
311
+ const fresh = sameAttempt && (args.now ?? Date.now()) - attempt.attemptedAt < CHECK_TTL_MS;
312
+ if (sameAttempt &&
313
+ ((!attempt.status && fresh) || attempt.status === 'queued' || attempt.status === 'running')) {
314
+ return {
315
+ type: 'nudge',
316
+ line: `[skrr] CLI update to ${cache.latest} ${attempt.status === 'running' ? 'is running' : 'is queued until active CLI sessions exit'}. Inspect \`skrr update --status\`.\n`,
317
+ };
318
+ }
319
+ if (install.kind === 'npm-global' && install.writable && !isAutoUpdateSuppressed(env)) {
320
+ if (!fresh)
366
321
  return {
367
322
  type: 'auto-update',
368
323
  version: cache.latest,
369
- pkg,
370
- line: `[skrr] Updating ${currentVersion} → ${cache.latest} in the background ` +
371
- `(\`npm install -g ${pkg}@${cache.latest}\`). Disable with OVERSKY_CLI_AUTO_UPDATE=0.\n`,
324
+ pkg: CLI_PACKAGE,
325
+ line: `[skrr] CLI update ${currentVersion} → ${cache.latest} queued in the background until active CLI sessions exit. Disable with OVERSKY_CLI_AUTO_UPDATE=0.\n`,
372
326
  };
373
- }
374
- // We already tried this version within the window and the update did not
375
- // take — tell the user the manual command instead of retrying every run.
327
+ if (attempt?.status === 'failed')
328
+ return {
329
+ type: 'nudge',
330
+ line: `[skrr] CLI update to ${cache.latest} failed. Inspect \`skrr update --status\` or run \`${cliUpdateCommand(install.kind)}\`.\n`,
331
+ };
332
+ }
333
+ const line = buildNudgeLine(currentVersion, cache.latest, CLI_PACKAGE, install.kind);
334
+ return line ? { type: 'nudge', line } : null;
335
+ }
336
+ function readCache(env = process.env) {
337
+ try {
338
+ const parsed = JSON.parse(fs.readFileSync(cacheFilePath(env), 'utf8'));
339
+ if (typeof parsed.lastCheckAt !== 'number' || typeof parsed.baseURL !== 'string')
340
+ return null;
376
341
  return {
377
- type: 'nudge',
378
- line: `[skrr] Update available: ${currentVersion} → ${cache.latest}. ` +
379
- `The background update did not take — run \`npm update -g ${pkg}\` manually.` +
380
- ` Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`,
342
+ ...parsed,
343
+ latest: typeof parsed.latest === 'string' ? parsed.latest : null,
344
+ npmPackage: CLI_PACKAGE,
381
345
  };
382
346
  }
383
- const line = buildNudgeLine(currentVersion, cache.latest, cache.npmPackage, install.kind);
384
- return line ? { type: 'nudge', line } : null;
347
+ catch {
348
+ return null;
349
+ }
385
350
  }
386
- // ---------------------------------------------------------------------------
387
- // Cache I/O
388
- // ---------------------------------------------------------------------------
389
- function readCache() {
351
+ function writeCache(cache, env) {
352
+ const file = cacheFilePath(env);
353
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
354
+ const temporary = `${file}.${process.pid}.tmp`;
390
355
  try {
391
- const raw = fs.readFileSync(cacheFilePath(), 'utf8');
392
- const parsed = JSON.parse(raw);
393
- if (parsed && typeof parsed.lastCheckAt === 'number' && typeof parsed.baseURL === 'string') {
394
- return {
395
- lastCheckAt: parsed.lastCheckAt,
396
- latest: typeof parsed.latest === 'string' ? parsed.latest : null,
397
- npmPackage: typeof parsed.npmPackage === 'string' ? parsed.npmPackage : null,
398
- baseURL: parsed.baseURL,
399
- install: parsed.install && typeof parsed.install.kind === 'string'
400
- ? {
401
- kind: parsed.install.kind,
402
- writable: typeof parsed.install.writable === 'boolean'
403
- ? parsed.install.writable
404
- : undefined,
405
- }
406
- : undefined,
407
- autoUpdate: parsed.autoUpdate &&
408
- typeof parsed.autoUpdate.attemptedAt === 'number' &&
409
- typeof parsed.autoUpdate.version === 'string'
410
- ? { attemptedAt: parsed.autoUpdate.attemptedAt, version: parsed.autoUpdate.version }
411
- : undefined,
412
- };
413
- }
356
+ fs.writeFileSync(temporary, JSON.stringify(cache), { mode: 0o600 });
357
+ fs.renameSync(temporary, file);
358
+ }
359
+ finally {
360
+ fs.rmSync(temporary, { force: true });
361
+ }
362
+ }
363
+ function packageRoot(argv) {
364
+ const installed = resolveCliPackageDir(argv);
365
+ if (installed)
366
+ return installed;
367
+ try {
368
+ if (argv[1])
369
+ return path.resolve(path.dirname(fs.realpathSync(argv[1])), '..');
414
370
  }
415
371
  catch {
416
- // Missing / corrupt cache → treat as absent; a refresh rebuilds it.
372
+ /* tests and virtual invocations */
417
373
  }
418
- return null;
374
+ return path.resolve(__dirname, '..', '..');
375
+ }
376
+ function runtime() {
377
+ // The standalone CommonJS bootstrap is also copied outside npm's package tree.
378
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
379
+ return require(path.resolve(__dirname, '..', '..', 'bin', 'update-session.cjs'));
419
380
  }
420
- function writeCache(cache) {
381
+ function attemptFor(argv) {
421
382
  try {
422
- const file = cacheFilePath();
423
- fs.mkdirSync(path.dirname(file), { recursive: true });
424
- const tmp = `${file}.${process.pid}.tmp`;
425
- fs.writeFileSync(tmp, JSON.stringify(cache), { mode: 0o600 });
426
- fs.renameSync(tmp, file);
383
+ return runtime().readJson(path.join(runtime().runtimeDir(packageRoot(argv)), 'result.json'));
427
384
  }
428
385
  catch {
429
- // Best-effort — a read-only home just means we re-check next run.
386
+ return null;
430
387
  }
431
388
  }
432
- /** Persist the auto-update attempt marker without losing the rest of the cache. */
433
- function markAutoUpdateAttempt(version) {
434
- const cache = readCache();
435
- if (!cache)
436
- return;
437
- writeCache({ ...cache, autoUpdate: { attemptedAt: Date.now(), version } });
389
+ function installedCliVersion(argv, fallback) {
390
+ try {
391
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot(argv), 'package.json'), 'utf8'));
392
+ return pkg.name === CLI_PACKAGE && typeof pkg.version === 'string' ? pkg.version : fallback;
393
+ }
394
+ catch {
395
+ return resolveCliPackageDir(argv) ? null : fallback;
396
+ }
438
397
  }
439
- async function refreshCache(baseURL) {
398
+ /** Cached, read-only status: no network, child process, lease, or mutation. */
399
+ function readCliUpdateStatus(opts) {
400
+ const env = opts.env ?? process.env;
401
+ const argv = opts.argv ?? process.argv;
402
+ const cache = readCache(env);
403
+ const matchingCache = cache?.baseURL === normalizeBaseUrl(opts.baseURL) ? cache : null;
404
+ // Classify the actual executing installation each time; a persisted owner
405
+ // from another installation/profile must never authorize npm mutation.
406
+ const install = classifyCliInstall(env, argv, matchingCache?.packageRoot === packageRoot(argv) ? (matchingCache.npmGlobalRoot ?? null) : null, false);
407
+ const installedVersion = installedCliVersion(argv, opts.currentVersion);
408
+ const offeredVersion = matchingCache?.latest ?? null;
409
+ let lastAttempt = attemptFor(argv);
410
+ if (lastAttempt?.status === 'running' && lastAttempt.workerPid) {
411
+ const api = runtime();
412
+ const mutation = api.readJson(path.join(api.runtimeDir(packageRoot(argv)), 'mutation.json'));
413
+ // Only kill(pid, 0): cached status remains free of subprocess/network work.
414
+ const activeWorker = api.isAlive({ pid: lastAttempt.workerPid });
415
+ const activeMutation = mutation && api.isAlive({ pid: mutation.pid, group: mutation.group });
416
+ if (!activeWorker && !activeMutation)
417
+ lastAttempt = {
418
+ ...lastAttempt,
419
+ interrupted: true,
420
+ message: 'The updater was interrupted. The next interactive invocation retries this request safely.',
421
+ };
422
+ }
423
+ const newer = !!offeredVersion && !!installedVersion && isOlderVersion(installedVersion, offeredVersion);
424
+ let state = offeredVersion
425
+ ? newer
426
+ ? 'update-available'
427
+ : 'up-to-date'
428
+ : 'unknown';
429
+ if (install.kind === 'bundled' || install.kind === 'image')
430
+ state = 'managed';
431
+ else if ((lastAttempt?.status === 'queued' || lastAttempt?.status === 'running') &&
432
+ installedVersion !== lastAttempt.version)
433
+ state = 'pending';
434
+ else if (lastAttempt?.status === 'failed' && newer)
435
+ state = 'failed';
436
+ else if (installedVersion && isOlderVersion(opts.currentVersion, installedVersion))
437
+ state = 'restart-pending';
438
+ let directory;
440
439
  try {
441
- const url = `${baseURL.replace(/\/+$/, '')}/api/config/cli-version`;
442
- const res = await fetch(url, {
443
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
444
- headers: { accept: 'application/json' },
445
- });
446
- if (!res.ok)
447
- return;
448
- const body = (await res.json());
449
- // Classify the install shape while we're already off the hot path — the
450
- // `npm root -g` probe is the only piece that can cost real time and it is
451
- // bounded by NPM_ROOT_TIMEOUT_MS. It only runs when the entry point even
452
- // looks like an npm package dir.
453
- let install;
454
- if (resolveCliPackageDir()) {
455
- install = classifyCliInstall(process.env, process.argv, await resolveNpmGlobalRoot());
456
- }
457
- else {
458
- install = classifyCliInstall(process.env, process.argv, null);
459
- }
460
- // Preserve a pending auto-update marker across refreshes so the
461
- // once-per-window gate survives the cache being rewritten.
462
- const prior = readCache();
463
- writeCache({
464
- lastCheckAt: Date.now(),
465
- latest: typeof body.latest === 'string' ? body.latest : null,
466
- npmPackage: typeof body.npmPackage === 'string' ? body.npmPackage : null,
467
- baseURL: normalizeBaseUrl(baseURL),
468
- install,
469
- autoUpdate: prior?.autoUpdate,
470
- });
440
+ directory = runtime().runtimeDir(packageRoot(argv));
471
441
  }
472
442
  catch {
473
- // Offline / timeout / bad payload — leave the old cache, retry later.
443
+ directory = path.join(os.homedir(), '.skrr', 'cli-update-runtime');
474
444
  }
445
+ let autoUpdateReason = null;
446
+ if (install.kind !== 'npm-global')
447
+ autoUpdateReason = `The ${install.kind} installation is owned by its installer.`;
448
+ else if (!install.writable)
449
+ autoUpdateReason = 'The npm global prefix is read-only.';
450
+ else if (process.platform === 'win32')
451
+ autoUpdateReason =
452
+ 'Use npm directly on Windows; managed replacement requires POSIX process-group coordination.';
453
+ else if (!isPrivateCliPrefix(packageRoot(argv)))
454
+ autoUpdateReason = 'Shared or external npm prefixes require the owning package manager.';
455
+ else if (env.SKRR_CLI_UPDATE_SESSION_UNAVAILABLE === '1')
456
+ autoUpdateReason = 'A safe CLI session lease could not be recorded.';
457
+ else if (isAutoUpdateSuppressed(env))
458
+ autoUpdateReason = 'Automatic CLI updates are disabled by environment policy.';
459
+ return {
460
+ component: 'cli',
461
+ runningVersion: opts.currentVersion,
462
+ installedVersion,
463
+ offeredVersion,
464
+ owner: install.kind,
465
+ state,
466
+ autoUpdateAllowed: autoUpdateReason === null,
467
+ autoUpdateReason,
468
+ command: cliUpdateCommand(install.kind),
469
+ lastCheckAt: matchingCache?.lastCheckAt ?? null,
470
+ lastAttempt,
471
+ logPath: path.join(directory, 'update.log'),
472
+ message: lastAttempt?.message,
473
+ };
475
474
  }
476
- // ---------------------------------------------------------------------------
477
- // Deferred self-update
478
- // ---------------------------------------------------------------------------
479
- let autoUpdateScheduled = false;
480
- /**
481
- * Schedule `npm install -g <pkg>@<version>` to run AFTER this process exits.
482
- *
483
- * Why deferred: oclif lazily loads command modules from disk, so swapping the
484
- * package while a command runs could hand it a mid-replace `dist/`. The child
485
- * is detached + stdio-ignored so it outlives this process and holds no pipes.
486
- * `spawn` inside an 'exit' handler works because the fork itself is
487
- * synchronous; only its events would need the (already drained) loop.
488
- *
489
- * The version comes from our own release feed's `latest`, validated to a
490
- * semver-ish token so the win32 shell path can't smuggle syntax.
491
- */
492
- function schedulePostExitSelfUpdate(pkg, version) {
493
- if (autoUpdateScheduled)
494
- return;
495
- // Strict npm-name + semver-ish token validation — the win32 path spawns
496
- // through a shell, so nothing with shell syntax may reach the spec.
497
- const validPkg = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/i.test(pkg) ||
498
- /^[a-z0-9][a-z0-9._-]*$/i.test(pkg);
499
- if (!validPkg || !/^[A-Za-z0-9._-]+$/.test(version))
500
- return;
501
- autoUpdateScheduled = true;
502
- process.once('exit', () => {
475
+ /** Bounded discovery, also usable without a daemon, account, or TTY. */
476
+ async function checkCliUpdate(opts) {
477
+ const env = opts.env ?? process.env;
478
+ const argv = opts.argv ?? process.argv;
479
+ const cache = readCache(env);
480
+ const stale = !cache ||
481
+ cache.baseURL !== normalizeBaseUrl(opts.baseURL) ||
482
+ Date.now() - cache.lastCheckAt >= CHECK_TTL_MS;
483
+ let checkFailed = false;
484
+ if ((opts.force || stale) && (opts.force || !isTruthyEnv(env.OVERSKY_SKIP_UPDATE_CHECK))) {
503
485
  try {
504
- const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
505
- const child = (0, node_child_process_1.spawn)(npmBin, ['install', '-g', `${pkg}@${version}`], {
506
- detached: true,
507
- stdio: 'ignore',
508
- shell: process.platform === 'win32',
509
- });
510
- child.unref();
486
+ // Independent probes share the same 1.5-second bound rather than adding
487
+ // an npm probe after the network timeout on every first invocation.
488
+ const [response, npmRoot] = await Promise.all([
489
+ fetch(`${opts.baseURL.replace(/\/+$/, '')}/api/config/cli-version`, {
490
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
491
+ headers: { accept: 'application/json' },
492
+ }),
493
+ resolveCliPackageDir(argv) ? resolveNpmGlobalRoot(env) : Promise.resolve(null),
494
+ ]);
495
+ if (!response.ok)
496
+ throw new Error(`Version service returned HTTP ${response.status}`);
497
+ const body = (await response.json());
498
+ if (typeof body.latest !== 'string' ||
499
+ (0, auth_core_1.compareReleaseVersions)(body.latest, body.latest) === null)
500
+ throw new Error('Version service returned an invalid version');
501
+ writeCache({
502
+ lastCheckAt: Date.now(),
503
+ latest: body.latest,
504
+ npmPackage: CLI_PACKAGE,
505
+ baseURL: normalizeBaseUrl(opts.baseURL),
506
+ install: classifyCliInstall(env, argv, npmRoot),
507
+ packageRoot: packageRoot(argv),
508
+ npmGlobalRoot: npmRoot,
509
+ autoUpdate: cache?.autoUpdate,
510
+ }, env);
511
+ }
512
+ catch {
513
+ checkFailed = true;
514
+ }
515
+ }
516
+ const status = readCliUpdateStatus(opts);
517
+ if (checkFailed)
518
+ return {
519
+ ...status,
520
+ state: 'check-failed',
521
+ message: 'Could not check for a newer CLI version. Any offered version is from the last successful check; retry `skrr update --check`.',
522
+ };
523
+ return status;
524
+ }
525
+ function npmExecutable(env) {
526
+ const name = process.platform === 'win32' ? 'npm.cmd' : 'npm';
527
+ for (const directory of (env.PATH ?? '').split(path.delimiter)) {
528
+ if (!directory || !path.isAbsolute(directory))
529
+ continue;
530
+ try {
531
+ const candidate = path.join(directory, name);
532
+ fs.accessSync(candidate, fs.constants.X_OK);
533
+ return fs.realpathSync(candidate);
511
534
  }
512
535
  catch {
513
- /* a failed self-update just means the nudge returns next run */
536
+ /* next PATH entry */
514
537
  }
538
+ }
539
+ return null;
540
+ }
541
+ function updaterEnvironment(env) {
542
+ // The public npm package update does not require application credentials.
543
+ return Object.fromEntries(Object.entries(env).filter(([key]) => !/(?:TOKEN|SECRET|PASSWORD|CREDENTIAL|API_KEY|PRIVATE_KEY|AUTH_FD|AUTH_SOCK)/i.test(key) &&
544
+ !/^(?:SKRR_|OVERSKY_|OPENAI_|ANTHROPIC_|CODEX_|CLAUDE_|NODE_OPTIONS$)/i.test(key)));
545
+ }
546
+ function startWorker(directory, env) {
547
+ const sourceDir = path.resolve(__dirname, '..', '..', 'bin');
548
+ const workerBytes = fs.readFileSync(path.join(sourceDir, 'update-worker.cjs'));
549
+ const helperBytes = fs.readFileSync(path.join(sourceDir, 'update-session.cjs'));
550
+ const runnerBytes = fs.readFileSync(path.join(sourceDir, 'update-npm-runner.cjs'));
551
+ const digest = (0, node_crypto_1.createHash)('sha256')
552
+ .update(workerBytes)
553
+ .update(helperBytes)
554
+ .update(runnerBytes)
555
+ .digest('hex')
556
+ .slice(0, 24);
557
+ const immutableDir = path.join(directory, 'workers', digest);
558
+ fs.mkdirSync(immutableDir, { recursive: true, mode: 0o700 });
559
+ for (const [file, bytes] of [
560
+ ['update-worker.cjs', workerBytes],
561
+ ['update-session.cjs', helperBytes],
562
+ ['update-npm-runner.cjs', runnerBytes],
563
+ ]) {
564
+ const destination = path.join(immutableDir, file);
565
+ if (!fs.existsSync(destination))
566
+ fs.writeFileSync(destination, bytes, { mode: 0o600, flag: 'wx' });
567
+ }
568
+ const child = (0, node_child_process_1.spawn)(process.execPath, [path.join(immutableDir, 'update-worker.cjs'), directory], {
569
+ detached: true,
570
+ stdio: 'ignore',
571
+ env: updaterEnvironment(env),
572
+ });
573
+ child.once('error', () => {
574
+ /* persisted queue remains retryable */
515
575
  });
576
+ child.unref();
516
577
  }
517
- /**
518
- * Entry point, called once per command from `BaseCommand.init()`.
519
- *
520
- * Synchronous by design: it decides from the cache (no network on the hot
521
- * path) and only kicks off a background refresh when the cache is stale,
522
- * missing, or from a different server. Returns immediately.
523
- */
524
- function maybeWarnOutdatedCli(opts) {
578
+ /** Queue an exact version through the owning global npm prefix. The current
579
+ * invocation stays leased, so replacement starts after it and peers exit. */
580
+ async function performCliUpdate(opts) {
581
+ const env = opts.env ?? process.env;
582
+ const argv = opts.argv ?? process.argv;
583
+ const status = await checkCliUpdate({ ...opts, force: opts.force ?? true });
584
+ if (status.state === 'check-failed')
585
+ return status;
586
+ const checkedCache = readCache(env);
587
+ const install = classifyCliInstall(env, argv, checkedCache?.packageRoot === packageRoot(argv) ? (checkedCache.npmGlobalRoot ?? null) : null);
588
+ if (install.kind !== 'npm-global' ||
589
+ install.writable !== true ||
590
+ !isPrivateCliPrefix(packageRoot(argv)) ||
591
+ env.SKRR_CLI_UPDATE_SESSION_UNAVAILABLE === '1') {
592
+ return {
593
+ ...status,
594
+ message: `This CLI is owned by ${install.kind}${install.kind === 'npm-global' ? ' in a shared or external prefix' : ''}. ${cliUpdateCommand(install.kind)}.`,
595
+ };
596
+ }
597
+ const recoverableAttempt = status.lastAttempt && ['queued', 'running'].includes(status.lastAttempt.status);
598
+ if (!status.offeredVersion ||
599
+ !status.installedVersion ||
600
+ (!isOlderVersion(status.installedVersion, status.offeredVersion) && !recoverableAttempt))
601
+ return status;
602
+ const npmPath = npmExecutable(env);
603
+ if (!npmPath)
604
+ return {
605
+ ...status,
606
+ state: 'failed',
607
+ message: 'npm is unavailable; use the package manager that installed this CLI.',
608
+ };
609
+ const api = runtime();
610
+ if (env.SKRR_CLI_UPDATE_SESSION_REGISTERED !== String(process.pid))
611
+ api.registerSession(packageRoot(argv));
612
+ const directory = api.runtimeDir(packageRoot(argv));
613
+ const release = api.acquireLock(path.join(directory, 'request.lock'), 1000);
525
614
  try {
526
- if (isNudgeSuppressed())
615
+ const old = attemptFor(argv);
616
+ if (!old || !['queued', 'running'].includes(old.status)) {
617
+ api.writeJson(path.join(directory, 'result.json'), {
618
+ status: 'queued',
619
+ version: status.offeredVersion,
620
+ attemptedAt: Date.now(),
621
+ packageRoot: packageRoot(argv),
622
+ npmPath,
623
+ coordinationVersion: api.COORDINATION_VERSION,
624
+ message: 'Waiting for active CLI sessions to exit; the next invocation uses the new version.',
625
+ });
626
+ }
627
+ startWorker(directory, env);
628
+ }
629
+ finally {
630
+ release();
631
+ }
632
+ return readCliUpdateStatus(opts);
633
+ }
634
+ /** One startup advisory across oclif command families. Awaiting bounded
635
+ * discovery makes the first invocation useful rather than losing its fetch
636
+ * when a short command exits. No update/check errors can break a command. */
637
+ async function maybeWarnOutdatedCli(opts) {
638
+ try {
639
+ const env = opts.env ?? process.env;
640
+ if (!process.stderr.isTTY || isNudgeSuppressed(env))
527
641
  return;
528
- // Scripts, pipes, and CI never get the advisory — matches npm/oclif
529
- // convention and keeps `sky ... | jq` and captured stderr clean.
530
- if (!process.stderr.isTTY)
642
+ const status = await checkCliUpdate(opts);
643
+ if (status.state === 'check-failed')
531
644
  return;
532
- const normalized = normalizeBaseUrl(opts.baseURL);
533
- const cache = readCache();
534
- // Decide from whatever we last saw. A STALE cache is still a valid
535
- // signal — CLI versions only move forward — so we act on it and refresh
536
- // in the background for next time. We only trust a cache that was fetched
537
- // against the same server the CLI is currently pointed at.
538
- if (cache && cache.baseURL === normalized) {
539
- const action = decideCliUpdateAction({
540
- currentVersion: opts.currentVersion,
541
- cache,
542
- env: process.env,
543
- isTTY: true,
544
- });
545
- if (action?.type === 'auto-update') {
546
- // Record BEFORE scheduling so a same-second second invocation sees the
547
- // attempt and doesn't double-spawn.
548
- markAutoUpdateAttempt(action.version);
549
- schedulePostExitSelfUpdate(action.pkg, action.version);
550
- }
645
+ const cache = readCache(env);
646
+ if (!cache || cache.baseURL !== normalizeBaseUrl(opts.baseURL))
647
+ return;
648
+ const action = decideCliUpdateAction({
649
+ currentVersion: opts.currentVersion,
650
+ cache: {
651
+ ...cache,
652
+ install: { kind: status.owner, writable: status.autoUpdateAllowed },
653
+ autoUpdate: status.lastAttempt
654
+ ? {
655
+ attemptedAt: status.lastAttempt.attemptedAt,
656
+ version: status.lastAttempt.version,
657
+ status: status.lastAttempt.status,
658
+ }
659
+ : cache.autoUpdate,
660
+ },
661
+ env,
662
+ isTTY: true,
663
+ });
664
+ if (action?.type === 'auto-update') {
665
+ const queued = await performCliUpdate({ ...opts, force: false });
666
+ if (queued.state === 'pending')
667
+ process.stderr.write(`[skrr] CLI update to ${queued.lastAttempt?.version ?? action.version} queued in the background until active CLI sessions exit. Disable with OVERSKY_CLI_AUTO_UPDATE=0.\n`);
668
+ else if (queued.message)
669
+ process.stderr.write(`[skrr] ${queued.message}\n`);
670
+ }
671
+ else {
672
+ // Resume a queue whose worker exhausted its bounded wait, without
673
+ // classifying queued/running work as a failed npm install.
674
+ if (status.lastAttempt &&
675
+ ['queued', 'running'].includes(status.lastAttempt.status) &&
676
+ status.autoUpdateAllowed)
677
+ await performCliUpdate({ ...opts, force: false });
551
678
  if (action)
552
679
  process.stderr.write(action.line);
553
680
  }
554
- const stale = !cache || cache.baseURL !== normalized || Date.now() - cache.lastCheckAt > CHECK_TTL_MS;
555
- if (stale) {
556
- // Fire-and-forget. Not awaited, so it adds no latency; if the command
557
- // exits before it resolves the refresh is simply dropped and retried
558
- // on the next invocation.
559
- void refreshCache(opts.baseURL);
560
- }
561
681
  }
562
682
  catch {
563
- // A version advisory must never break a command.
683
+ /* version discovery must never prevent the user's command */
564
684
  }
565
685
  }
686
+ function shouldCheckCliUpdateAtStartup(argv, isTTY, env = process.env) {
687
+ if (!isTTY || isNudgeSuppressed(env) || isTruthyEnv(env.CI))
688
+ return false;
689
+ if (argv.some((arg) => /^(?:--json(?:=|$)|--help$|-h$|--version$|-v$|--headless$|--print$|-p$|--non-interactive$|--no-interactive$)/.test(arg)))
690
+ return false;
691
+ const command = argv
692
+ .filter((arg) => !arg.startsWith('-'))
693
+ .slice(0, 2)
694
+ .join(':');
695
+ return ![
696
+ 'update',
697
+ 'code:run',
698
+ 'code:acp',
699
+ 'code:update',
700
+ 'code:install',
701
+ 'daemon:update',
702
+ 'daemons:update',
703
+ ].includes(command);
704
+ }