quoxcli 0.2.0 → 0.4.0

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 (442) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/bin/quox.js +99 -126
  4. package/lib/api/endpoints/actions.js +15 -0
  5. package/lib/api/endpoints/admin.js +46 -0
  6. package/lib/api/endpoints/agent-schedules.js +26 -0
  7. package/lib/api/endpoints/agents.js +7 -0
  8. package/lib/api/endpoints/assistants.js +12 -0
  9. package/lib/api/endpoints/away.js +15 -0
  10. package/lib/api/endpoints/blog.js +16 -0
  11. package/lib/api/endpoints/chat.js +14 -0
  12. package/lib/api/endpoints/contactsq.js +6 -0
  13. package/lib/api/endpoints/conversations.js +14 -0
  14. package/lib/api/endpoints/devServerRegistry.js +25 -0
  15. package/lib/api/endpoints/engagement.js +26 -0
  16. package/lib/api/endpoints/evidence.js +20 -0
  17. package/lib/api/endpoints/executions.js +13 -0
  18. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  19. package/lib/api/endpoints/files.js +14 -0
  20. package/lib/api/endpoints/hat.js +44 -0
  21. package/lib/api/endpoints/inbox.js +20 -0
  22. package/lib/api/endpoints/index.js +26 -0
  23. package/lib/api/endpoints/loops.js +26 -0
  24. package/lib/api/endpoints/memory.js +19 -0
  25. package/lib/api/endpoints/mind.js +13 -0
  26. package/lib/api/endpoints/mon.js +179 -0
  27. package/lib/api/endpoints/objectives.js +12 -0
  28. package/lib/api/endpoints/plugins.js +18 -0
  29. package/lib/api/endpoints/presence.js +33 -0
  30. package/lib/api/endpoints/quoxplan.js +7 -0
  31. package/lib/api/endpoints/quoxseo.js +12 -0
  32. package/lib/api/endpoints/router.js +17 -0
  33. package/lib/api/endpoints/safety.js +7 -0
  34. package/lib/api/endpoints/server.js +21 -0
  35. package/lib/api/endpoints/setup.js +6 -0
  36. package/lib/api/endpoints/skills.js +20 -0
  37. package/lib/api/endpoints/stepup.js +20 -0
  38. package/lib/api/endpoints/teams.js +2 -2
  39. package/lib/api/endpoints/triggers.js +26 -0
  40. package/lib/api/endpoints/v1.js +120 -0
  41. package/lib/api/endpoints/ward.js +7 -0
  42. package/lib/api/endpoints/workflows.js +7 -0
  43. package/lib/api/endpoints/x.js +8 -0
  44. package/lib/commands/actions.js +131 -0
  45. package/lib/commands/admin.js +16 -0
  46. package/lib/commands/agent.js +170 -27
  47. package/lib/commands/agentAttach.js +140 -13
  48. package/lib/commands/approvals.js +78 -1
  49. package/lib/commands/aside.js +24 -0
  50. package/lib/commands/ask.js +146 -0
  51. package/lib/commands/assistant.js +59 -4
  52. package/lib/commands/auth.js +8 -2
  53. package/lib/commands/backup.js +8 -4
  54. package/lib/commands/blog.js +110 -0
  55. package/lib/commands/brain.js +105 -14
  56. package/lib/commands/build.js +19 -5
  57. package/lib/commands/chat.js +180 -26
  58. package/lib/commands/chatCompanion.js +27 -0
  59. package/lib/commands/chatState.js +45 -0
  60. package/lib/commands/claude.js +35 -38
  61. package/lib/commands/contacts.js +134 -4
  62. package/lib/commands/contactsSendAt.js +21 -0
  63. package/lib/commands/conversation.js +35 -4
  64. package/lib/commands/dashboard.js +43 -0
  65. package/lib/commands/department.js +28 -7
  66. package/lib/commands/devResident.js +15 -3
  67. package/lib/commands/devSupervisor.js +13 -1
  68. package/lib/commands/discord.js +75 -39
  69. package/lib/commands/done.js +98 -28
  70. package/lib/commands/engagement.js +107 -6
  71. package/lib/commands/evidence.js +154 -0
  72. package/lib/commands/ext.js +23 -0
  73. package/lib/commands/fetchAllowlist.js +71 -0
  74. package/lib/commands/file.js +38 -1
  75. package/lib/commands/fleet.js +336 -4
  76. package/lib/commands/governance.js +12 -3
  77. package/lib/commands/gripe.js +44 -13
  78. package/lib/commands/hat.js +254 -9
  79. package/lib/commands/helpers.js +18 -1
  80. package/lib/commands/hyperion.js +58 -0
  81. package/lib/commands/image.js +68 -0
  82. package/lib/commands/inbox.js +74 -23
  83. package/lib/commands/kuma.js +1050 -0
  84. package/lib/commands/kumaSla.js +174 -0
  85. package/lib/commands/licence.js +52 -155
  86. package/lib/commands/loop.js +291 -0
  87. package/lib/commands/lucid.js +10 -4
  88. package/lib/commands/memory.js +85 -8
  89. package/lib/commands/mind.js +71 -0
  90. package/lib/commands/mon.js +69 -2
  91. package/lib/commands/monitoring.js +1 -1
  92. package/lib/commands/motion.js +2 -2
  93. package/lib/commands/nucleus.js +48 -17
  94. package/lib/commands/objective.js +59 -1
  95. package/lib/commands/offload.js +89 -0
  96. package/lib/commands/onboard.js +2 -1
  97. package/lib/commands/org.js +30 -3
  98. package/lib/commands/peek.js +28 -0
  99. package/lib/commands/plan.js +38 -0
  100. package/lib/commands/plugin.js +110 -0
  101. package/lib/commands/presence.js +89 -0
  102. package/lib/commands/provider.js +217 -0
  103. package/lib/commands/qlarity.js +19 -3
  104. package/lib/commands/quoxchat.js +13 -4
  105. package/lib/commands/quoxseo.js +69 -0
  106. package/lib/commands/reach.js +2 -1
  107. package/lib/commands/research.js +79 -0
  108. package/lib/commands/router.js +77 -0
  109. package/lib/commands/rows.js +367 -0
  110. package/lib/commands/run.js +82 -10
  111. package/lib/commands/scope.js +2 -2
  112. package/lib/commands/server.js +405 -0
  113. package/lib/commands/setup.js +33 -7
  114. package/lib/commands/shortcut.js +41 -0
  115. package/lib/commands/skills.js +68 -9
  116. package/lib/commands/slot.js +260 -7
  117. package/lib/commands/status.js +1 -1
  118. package/lib/commands/team.js +3 -1
  119. package/lib/commands/tips.js +23 -0
  120. package/lib/commands/tool.js +133 -45
  121. package/lib/commands/trace.js +12 -7
  122. package/lib/commands/trigger.js +95 -0
  123. package/lib/commands/update.js +46 -0
  124. package/lib/commands/vault.js +31 -8
  125. package/lib/commands/vm.js +237 -0
  126. package/lib/commands/voice.js +34 -3
  127. package/lib/commands/volt.js +45 -3
  128. package/lib/commands/walkthrough.js +32 -0
  129. package/lib/commands/watch.js +73 -21
  130. package/lib/commands/welcome.js +14 -0
  131. package/lib/commands/workflow.js +189 -32
  132. package/lib/commands/x.js +13 -4
  133. package/lib/commands/youtube.js +172 -0
  134. package/lib/core/actingAs.js +2 -2
  135. package/lib/core/actionItems.js +115 -0
  136. package/lib/core/activeSummary.js +186 -0
  137. package/lib/core/agentAlias.js +2 -0
  138. package/lib/core/agentFamilies.js +7 -0
  139. package/lib/core/aside.js +54 -0
  140. package/lib/core/askDecision.js +60 -0
  141. package/lib/core/auth.js +164 -13
  142. package/lib/core/callerSource.js +1 -1
  143. package/lib/core/cliBuild.js +62 -0
  144. package/lib/core/client.js +152 -35
  145. package/lib/core/commandRegistrars.js +130 -0
  146. package/lib/core/config.js +143 -25
  147. package/lib/core/customHats.js +168 -0
  148. package/lib/core/doneEngineFetch.js +8 -0
  149. package/lib/core/errors.js +329 -20
  150. package/lib/core/findingReconcile.js +90 -0
  151. package/lib/core/global-flags.js +105 -5
  152. package/lib/core/governedSession.js +1 -3
  153. package/lib/core/hyperion.js +102 -0
  154. package/lib/core/inboxResolution.js +65 -0
  155. package/lib/core/lazyCommands.js +4 -1
  156. package/lib/core/liveSessionsReport.js +1 -1
  157. package/lib/core/loopGlyph.js +97 -0
  158. package/lib/core/offload.js +533 -0
  159. package/lib/core/org-override.js +1 -1
  160. package/lib/core/orgResolve.js +70 -0
  161. package/lib/core/output.js +11 -11
  162. package/lib/core/peek.js +126 -0
  163. package/lib/core/profileUsers.js +51 -0
  164. package/lib/core/promptCancel.js +18 -0
  165. package/lib/core/repoPath.js +11 -0
  166. package/lib/core/residentSeed.js +3 -1
  167. package/lib/core/rootOptions.js +39 -0
  168. package/lib/core/router.js +168 -0
  169. package/lib/core/selfUpdate.js +188 -0
  170. package/lib/core/sessionHeartbeat.js +12 -8
  171. package/lib/core/sessionsOverview.js +64 -0
  172. package/lib/core/shortcut.js +83 -0
  173. package/lib/core/slotKeeper.js +248 -3
  174. package/lib/core/slotWake.js +120 -0
  175. package/lib/core/supervisorEngine.js +71 -2
  176. package/lib/core/version.js +1 -1
  177. package/lib/core/vm.js +366 -0
  178. package/lib/core/vmTui.js +86 -0
  179. package/lib/core/walkthrough.js +35 -0
  180. package/lib/dragon/companionView.js +34 -0
  181. package/lib/dragon/frames.js +118 -0
  182. package/lib/dragon/jobs.js +123 -0
  183. package/lib/dragon/renderWorker.js +15 -0
  184. package/lib/dragon/select.js +72 -0
  185. package/lib/evidence/bundleExport.js +47 -0
  186. package/lib/evidence/bundleVerify.js +77 -0
  187. package/lib/hyperion/deck.js +29 -0
  188. package/lib/lucid/LucidCard.jsx +68 -0
  189. package/lib/lucid/LucidIntro.jsx +45 -58
  190. package/lib/lucid/cards.js +86 -0
  191. package/lib/lucid/markdown.js +81 -0
  192. package/lib/lucid/motion.js +56 -0
  193. package/lib/quoxon/EXTENSIONS.md +23 -0
  194. package/lib/quoxon/activity.js +14 -2
  195. package/lib/quoxon/aocl.js +117 -0
  196. package/lib/quoxon/api.js +690 -25
  197. package/lib/quoxon/app.js +3586 -250
  198. package/lib/quoxon/approvalFacts.js +96 -0
  199. package/lib/quoxon/approvalSentence.js +44 -0
  200. package/lib/quoxon/approvalsView.js +384 -0
  201. package/lib/quoxon/asideView.js +40 -0
  202. package/lib/quoxon/ask.js +121 -0
  203. package/lib/quoxon/away.js +52 -0
  204. package/lib/quoxon/bang.js +224 -0
  205. package/lib/quoxon/bulkRisk.js +17 -0
  206. package/lib/quoxon/capabilities.js +13 -6
  207. package/lib/quoxon/cells.js +53 -0
  208. package/lib/quoxon/commandShort.js +124 -0
  209. package/lib/quoxon/commands.js +429 -39
  210. package/lib/quoxon/connect.js +210 -23
  211. package/lib/quoxon/connectCard.js +19 -2
  212. package/lib/quoxon/consent.js +86 -0
  213. package/lib/quoxon/copy.js +4 -3
  214. package/lib/quoxon/countdownLine.js +20 -0
  215. package/lib/quoxon/deck.js +30 -0
  216. package/lib/quoxon/demo.autoplay.js +29 -0
  217. package/lib/quoxon/demo.fixtures.js +1 -1
  218. package/lib/quoxon/demo.js +46 -3
  219. package/lib/quoxon/devObserver.js +20 -0
  220. package/lib/quoxon/dividerAocl.js +192 -0
  221. package/lib/quoxon/dividerRule.js +57 -0
  222. package/lib/quoxon/dragonCompanion.js +255 -0
  223. package/lib/quoxon/dragonModes.js +151 -0
  224. package/lib/quoxon/dragonMoments.js +82 -0
  225. package/lib/quoxon/dragonReads.js +68 -0
  226. package/lib/quoxon/dragonView.js +41 -0
  227. package/lib/quoxon/earlyInput.js +40 -0
  228. package/lib/quoxon/echo.js +15 -0
  229. package/lib/quoxon/egg.js +2 -2
  230. package/lib/quoxon/events.js +266 -26
  231. package/lib/quoxon/extensions.js +276 -0
  232. package/lib/quoxon/feedback.js +50 -0
  233. package/lib/quoxon/gallery.js +21 -7
  234. package/lib/quoxon/handoffOffer.js +78 -0
  235. package/lib/quoxon/hatSwitch.js +37 -0
  236. package/lib/quoxon/headless.js +161 -0
  237. package/lib/quoxon/hostUrls.js +41 -0
  238. package/lib/quoxon/inboxView.js +79 -0
  239. package/lib/quoxon/indicator.js +6 -2
  240. package/lib/quoxon/installCli.js +79 -0
  241. package/lib/quoxon/intent.js +12 -1
  242. package/lib/quoxon/keys.js +51 -8
  243. package/lib/quoxon/launcher.js +103 -0
  244. package/lib/quoxon/loopCard.js +456 -0
  245. package/lib/quoxon/loopCards.js +180 -0
  246. package/lib/quoxon/loopStream.js +87 -0
  247. package/lib/quoxon/main.js +101 -160
  248. package/lib/quoxon/markdown.js +45 -21
  249. package/lib/quoxon/markdownSettle.js +19 -0
  250. package/lib/quoxon/matrixDomains.json +46 -0
  251. package/lib/quoxon/mentions.js +90 -0
  252. package/lib/quoxon/mind.js +103 -0
  253. package/lib/quoxon/mode.js +46 -0
  254. package/lib/quoxon/model.js +132 -26
  255. package/lib/quoxon/modelPrefs.js +345 -0
  256. package/lib/quoxon/motion/clock.js +3 -2
  257. package/lib/quoxon/motion/level.js +91 -0
  258. package/lib/quoxon/mouse.js +222 -0
  259. package/lib/quoxon/noai.js +57 -0
  260. package/lib/quoxon/objectiveOffer.js +82 -0
  261. package/lib/quoxon/objects/away.js +72 -0
  262. package/lib/quoxon/objects/fetch.js +17 -5
  263. package/lib/quoxon/objects/fleet.js +149 -0
  264. package/lib/quoxon/objects/health.js +186 -0
  265. package/lib/quoxon/objects/index.js +46 -7
  266. package/lib/quoxon/objects/mirror.js +137 -0
  267. package/lib/quoxon/objects/model.js +33 -0
  268. package/lib/quoxon/objects/peek.js +17 -0
  269. package/lib/quoxon/objects/proof.js +12 -4
  270. package/lib/quoxon/objects/prove.js +130 -0
  271. package/lib/quoxon/objects/replay.js +125 -0
  272. package/lib/quoxon/objects/sitrep.js +58 -0
  273. package/lib/quoxon/objects/view.js +11 -0
  274. package/lib/quoxon/onboarding/copy.js +32 -4
  275. package/lib/quoxon/onboarding/credential.js +17 -4
  276. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  277. package/lib/quoxon/onboarding/failureCause.js +33 -0
  278. package/lib/quoxon/onboarding/finish.js +15 -0
  279. package/lib/quoxon/onboarding/index.js +34 -7
  280. package/lib/quoxon/onboarding/installRunner.js +64 -5
  281. package/lib/quoxon/onboarding/preflight.js +17 -3
  282. package/lib/quoxon/onboarding/probe.js +65 -0
  283. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  284. package/lib/quoxon/onboarding/view.js +76 -19
  285. package/lib/quoxon/orgLabel.js +10 -0
  286. package/lib/quoxon/orgSwitch.js +59 -0
  287. package/lib/quoxon/ownBroadcast.js +22 -0
  288. package/lib/quoxon/palette.js +43 -0
  289. package/lib/quoxon/planSteps.js +51 -0
  290. package/lib/quoxon/print.js +12 -5
  291. package/lib/quoxon/profileSwitch.js +53 -0
  292. package/lib/quoxon/promptHistory.js +41 -0
  293. package/lib/quoxon/providers.js +48 -0
  294. package/lib/quoxon/qart.js +20 -51
  295. package/lib/quoxon/qdragon.js +410 -0
  296. package/lib/quoxon/qdragonFly.js +62 -0
  297. package/lib/quoxon/qdragonScenes.js +282 -0
  298. package/lib/quoxon/qeyes.js +213 -0
  299. package/lib/quoxon/qmotion.js +75 -37
  300. package/lib/quoxon/render.js +740 -124
  301. package/lib/quoxon/resend.js +72 -0
  302. package/lib/quoxon/roster.js +117 -17
  303. package/lib/quoxon/route.js +6 -5
  304. package/lib/quoxon/rows.js +954 -0
  305. package/lib/quoxon/rowsStore.js +80 -0
  306. package/lib/quoxon/screenContext.js +150 -0
  307. package/lib/quoxon/screenFrames.js +254 -0
  308. package/lib/quoxon/serverChip.js +42 -0
  309. package/lib/quoxon/session.js +182 -2
  310. package/lib/quoxon/sessionDead.js +17 -0
  311. package/lib/quoxon/signin.js +125 -0
  312. package/lib/quoxon/slash/agents.js +112 -0
  313. package/lib/quoxon/slash/argSources.js +65 -0
  314. package/lib/quoxon/slash/args.js +65 -0
  315. package/lib/quoxon/slash/automate.js +89 -0
  316. package/lib/quoxon/slash/broadcast.js +140 -0
  317. package/lib/quoxon/slash/broadcastRun.js +108 -0
  318. package/lib/quoxon/slash/card.js +30 -0
  319. package/lib/quoxon/slash/challenge.js +114 -0
  320. package/lib/quoxon/slash/cliCommand.js +94 -0
  321. package/lib/quoxon/slash/cliLeaves.json +1023 -0
  322. package/lib/quoxon/slash/compact.js +1 -1
  323. package/lib/quoxon/slash/confirm.js +30 -0
  324. package/lib/quoxon/slash/crystallize.js +69 -0
  325. package/lib/quoxon/slash/dashboard.js +136 -0
  326. package/lib/quoxon/slash/draft.js +54 -0
  327. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  328. package/lib/quoxon/slash/hatWalk.js +206 -0
  329. package/lib/quoxon/slash/inverses.js +77 -0
  330. package/lib/quoxon/slash/killer.js +24 -0
  331. package/lib/quoxon/slash/killerApi.js +16 -0
  332. package/lib/quoxon/slash/ledger.js +13 -8
  333. package/lib/quoxon/slash/listView.js +3 -1
  334. package/lib/quoxon/slash/modelCmd.js +261 -0
  335. package/lib/quoxon/slash/palette.js +22 -8
  336. package/lib/quoxon/slash/paletteController.js +117 -7
  337. package/lib/quoxon/slash/panels.js +21 -6
  338. package/lib/quoxon/slash/picker.js +20 -2
  339. package/lib/quoxon/slash/qpeCmd.js +35 -0
  340. package/lib/quoxon/slash/restart.js +95 -13
  341. package/lib/quoxon/slash/resume.js +18 -1
  342. package/lib/quoxon/slash/rowCmd.js +157 -0
  343. package/lib/quoxon/slash/rowWalk.js +175 -0
  344. package/lib/quoxon/slash/settings.js +16 -6
  345. package/lib/quoxon/slash/spawn.js +177 -0
  346. package/lib/quoxon/slash/stepUp.js +274 -0
  347. package/lib/quoxon/slash/stepUpCard.js +60 -0
  348. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  349. package/lib/quoxon/slash/todoCmd.js +122 -0
  350. package/lib/quoxon/slash/undoApi.js +38 -0
  351. package/lib/quoxon/slash/undoCmd.js +49 -0
  352. package/lib/quoxon/slash/undoWish.js +15 -0
  353. package/lib/quoxon/slash/watch.js +164 -0
  354. package/lib/quoxon/slash/wish.js +23 -0
  355. package/lib/quoxon/slash/wishCmd.js +32 -0
  356. package/lib/quoxon/slotChat.js +300 -0
  357. package/lib/quoxon/slotFormat.js +195 -0
  358. package/lib/quoxon/slotRun.js +131 -0
  359. package/lib/quoxon/sound/mapping.js +2 -1
  360. package/lib/quoxon/stray.js +22 -0
  361. package/lib/quoxon/suggestCommand.js +90 -0
  362. package/lib/quoxon/term.js +46 -11
  363. package/lib/quoxon/textBuffer.js +176 -2
  364. package/lib/quoxon/theme.js +58 -25
  365. package/lib/quoxon/tips.js +433 -0
  366. package/lib/quoxon/toolCards.js +30 -0
  367. package/lib/quoxon/toolError.js +41 -0
  368. package/lib/quoxon/transcript.js +42 -4
  369. package/lib/quoxon/web/qface.js +18 -10
  370. package/lib/quoxon/welcome.js +1 -1
  371. package/lib/quoxon/whatsnew.js +136 -0
  372. package/lib/quoxon/whatsnew.jsonl +134 -0
  373. package/lib/quoxon/windowOrg.js +123 -0
  374. package/lib/server/devServers.js +137 -0
  375. package/lib/server/diag.js +129 -0
  376. package/lib/server/doctor.js +71 -0
  377. package/lib/server/install.js +149 -0
  378. package/lib/server/pluginEnable.js +161 -0
  379. package/lib/server/skew.js +56 -0
  380. package/lib/server/snapshot.js +131 -0
  381. package/lib/server/status.js +128 -0
  382. package/lib/server/sys.js +65 -0
  383. package/lib/server/uninstall.js +63 -0
  384. package/lib/server/update.js +234 -0
  385. package/lib/toolOutputShape.js +315 -0
  386. package/lib/tui/views/HyperionDeck.jsx +79 -0
  387. package/lib/tui/views/LucidDeck.jsx +209 -217
  388. package/package.json +5 -1
  389. package/vendor/dragonlab/SOURCE +4 -0
  390. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  391. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  392. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  393. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  394. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  395. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  396. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  397. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  398. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  399. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  400. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  401. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  402. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  403. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  404. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  405. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  406. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  407. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  408. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  409. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  410. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  411. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  412. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  413. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  414. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  415. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  416. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  417. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  418. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  419. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  420. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  421. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  422. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  423. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  424. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  425. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  426. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  427. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  428. package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
  429. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  430. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  431. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  432. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  433. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  434. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  435. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  436. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  437. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  438. package/vendor/dragonlab/lib/power-effects.js +294 -0
  439. package/vendor/dragonlab/lib/power-registry.js +74 -0
  440. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  441. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
  442. package/lib/core/websiteSession.js +0 -148
@@ -0,0 +1,161 @@
1
+ /**
2
+ * PLUGIN-IMG #1012: `quox plugin enable <id>` starts a paid plugin from its
3
+ * prebuilt PRIVATE image on a self-hosted install.
4
+ *
5
+ * Order, and why:
6
+ * 1. Licence check FIRST. Not entitled: refuse with a clear message and
7
+ * run nothing on the box (no login, no pull, no compose). The entitlement
8
+ * read is the same resolved set auth's has-plugin uses (verified,
9
+ * instance-bound, unexpired leases), via the user-facing
10
+ * /api/setup/licensing/entitlements read.
11
+ * 2. Pull credential (TODO hook below): the licence server will issue a
12
+ * short-lived, package-scoped GHCR credential to an instance whose lease
13
+ * covers the plugin. Until that endpoint ships, the hook returns a clear
14
+ * "pending" error and enable stops there, unless the image is already
15
+ * present locally (for example a proving VM where it was loaded by hand),
16
+ * in which case it starts with --pull never.
17
+ * 3. docker compose -f docker-compose.yml -f docker-compose.images.yml
18
+ * -f docker-compose.plugins.yml --profile <profile> up -d <service>
19
+ * 4. Health: poll the container's healthcheck until healthy (or time out).
20
+ *
21
+ * Everything that touches the host goes through an injected `sys`
22
+ * (lib/server/sys.js), so tests prove what would run without running it.
23
+ */
24
+
25
+ /** Plugins that ship as prebuilt images. QuoxPlan only for now. */
26
+ export const PLUGIN_IMAGES = Object.freeze({
27
+ quoxplan: Object.freeze({
28
+ service: 'quoxplan',
29
+ profile: 'with-quoxplan',
30
+ container: 'quox-quoxplan',
31
+ image: 'ghcr.io/quoxai/quoxplan',
32
+ registry: 'ghcr.io',
33
+ }),
34
+ });
35
+
36
+ export const COMPOSE_FILES = Object.freeze(['docker-compose.yml', 'docker-compose.images.yml', 'docker-compose.plugins.yml']);
37
+
38
+ /** The image tag: the install's release tag (QUOX_IMAGE_TAG in .env), else latest. */
39
+ export function imageRef(spec, tag) {
40
+ return `${spec.image}:${tag || 'latest'}`;
41
+ }
42
+
43
+ /** The docker compose argv `enable` runs (after `docker`). */
44
+ export function composeUpArgs({ project }, spec, { pullNever = false } = {}) {
45
+ return [
46
+ 'compose',
47
+ ...(project ? ['-p', project] : []),
48
+ ...COMPOSE_FILES.flatMap((f) => ['-f', f]),
49
+ '--profile', spec.profile,
50
+ 'up', '-d',
51
+ ...(pullNever ? ['--pull', 'never'] : []),
52
+ spec.service,
53
+ ];
54
+ }
55
+
56
+ /**
57
+ * TODO(PLUGIN-IMG #1012, owner-gated): fetch a short-lived, package-scoped
58
+ * pull credential from the licence server for this instance's lease. The
59
+ * endpoint does not exist yet (licence-server change, owner deploy word).
60
+ * Contract when it lands: -> { ok: true, registry, username, token, expires_at }
61
+ * and enable logs in with `docker login <registry> -u <username> --password-stdin`.
62
+ */
63
+ export async function getPluginPullCredential(_pluginId, _ctx = {}) {
64
+ return {
65
+ ok: false,
66
+ pending: true,
67
+ error: 'pull credential pending licence-server endpoint: the licence server does not issue plugin image pull credentials yet (#1012)',
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Same rule as `quox plugin licence`: a governed plugin is entitled when the
73
+ * resolved entitlements carry it (bare or banded) and, when it is banded,
74
+ * this org holds a seat.
75
+ */
76
+ export function entitlementFrom(data, pluginId) {
77
+ const entitlements = Array.isArray(data?.entitlements) ? data.entitlements : [];
78
+ const governed = (Array.isArray(data?.governed) ? data.governed : []).includes(pluginId);
79
+ const match = entitlements.find((e) => e === pluginId || String(e).startsWith(`${pluginId}@`)) || null;
80
+ const band = (data?.org_bands && data.org_bands[pluginId]) || null;
81
+ if (!governed) return { entitled: true, governed: false };
82
+ if (!match) return { entitled: false, governed: true, reason: 'no_entitlement' };
83
+ if (band && band.org_licensed === false) return { entitled: false, governed: true, reason: 'org_not_in_band' };
84
+ return { entitled: true, governed: true };
85
+ }
86
+
87
+ /**
88
+ * @param {object} o
89
+ * @param {string} o.pluginId
90
+ * @param {(pluginId: string) => Promise<{ entitled: boolean, reason?: string }>} o.checkEntitlement
91
+ * @param {{ repoDir: string, project?: string }} o.install
92
+ * @param {{ run: Function }} o.sys
93
+ * @param {string} [o.tag] image tag (QUOX_IMAGE_TAG)
94
+ * @param {Function} [o.getPullCredential]
95
+ * @param {(ms: number) => Promise<void>} [o.sleep]
96
+ * @param {number} [o.healthTries]
97
+ * @param {(line: string) => void} [o.log]
98
+ * @returns {Promise<{ ok: boolean, stage: string, message: string, ran: string[][] }>}
99
+ */
100
+ export async function enablePlugin({
101
+ pluginId, checkEntitlement, install, sys, tag,
102
+ getPullCredential = getPluginPullCredential,
103
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
104
+ healthTries = 30, healthIntervalMs = 2000,
105
+ log = () => {},
106
+ }) {
107
+ const ran = [];
108
+ const run = async (cmd, args, opts) => { ran.push([cmd, ...args]); return sys.run(cmd, args, opts); };
109
+ const spec = PLUGIN_IMAGES[pluginId];
110
+ if (!spec) {
111
+ const known = Object.keys(PLUGIN_IMAGES).join(', ');
112
+ return { ok: false, stage: 'unknown', message: `${pluginId} does not ship as a prebuilt plugin image yet (available: ${known}).`, ran };
113
+ }
114
+
115
+ let ent;
116
+ try {
117
+ ent = await checkEntitlement(pluginId);
118
+ } catch (err) {
119
+ return { ok: false, stage: 'licence', message: `Could not check the ${pluginId} licence (${err.message}). Nothing was pulled or started.`, ran };
120
+ }
121
+ if (!ent || !ent.entitled) {
122
+ const why = ent?.reason === 'org_not_in_band' ? ' This organisation does not hold one of its seats.' : '';
123
+ return {
124
+ ok: false,
125
+ stage: 'licence',
126
+ message: `${pluginId} is not licensed on this install.${why} Nothing was pulled or started. Run \`quox plugin licence ${pluginId}\` to see the entitlement, then renew or buy the licence.`,
127
+ ran,
128
+ };
129
+ }
130
+
131
+ const ref = imageRef(spec, tag);
132
+ let pullNever = false;
133
+ const cred = await getPullCredential(pluginId, { install });
134
+ if (cred && cred.ok) {
135
+ const login = await run('docker', ['login', cred.registry || spec.registry, '-u', cred.username, '--password-stdin'], { stdinText: cred.token });
136
+ if (login.code !== 0) return { ok: false, stage: 'login', message: `docker login to ${cred.registry || spec.registry} failed: ${(login.stderr || '').trim().slice(0, 300)}`, ran };
137
+ } else {
138
+ const local = await run('docker', ['image', 'inspect', ref, '--format', '{{.Id}}']);
139
+ if (local.code !== 0) {
140
+ return { ok: false, stage: 'pull-credential', message: `${pluginId} is licensed, but its image ${ref} is private and ${cred?.error || 'no pull credential is available'}. Nothing was started.`, ran };
141
+ }
142
+ pullNever = true;
143
+ log(`${ref} is already present locally; starting it without a pull.`);
144
+ }
145
+
146
+ const up = await run('docker', composeUpArgs(install, spec, { pullNever }), { cwd: install.repoDir });
147
+ if (up.code !== 0) return { ok: false, stage: 'compose', message: `docker compose up for ${spec.service} failed: ${(up.stderr || '').trim().slice(-400)}`, ran };
148
+
149
+ for (let i = 0; i < healthTries; i += 1) {
150
+ const h = await run('docker', ['inspect', '--format', '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}', spec.container]);
151
+ const status = (h.stdout || '').trim();
152
+ if (h.code === 0 && status === 'healthy') {
153
+ return { ok: true, stage: 'healthy', message: `${pluginId} is running (${ref}) and healthy.`, ran };
154
+ }
155
+ if (h.code === 0 && status === 'unhealthy') {
156
+ return { ok: false, stage: 'health', message: `${spec.container} started but reports unhealthy. Check \`docker logs ${spec.container}\`.`, ran };
157
+ }
158
+ await sleep(healthIntervalMs);
159
+ }
160
+ return { ok: false, stage: 'health', message: `${spec.container} did not report healthy in time. Check \`docker logs ${spec.container}\`.`, ran };
161
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * C4: version skew between this CLI and the server it talks to. Warns at most once per day, and only when
3
+ * major or minor differ (a patch difference does not matter). Silent whenever either side is unknown.
4
+ *
5
+ * CLI side: the platform release the CLI was built from (the repo VERSION file beside cli/).
6
+ * Server side: the collector's public /version `release` (the image's /app/VERSION).
7
+ */
8
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { join, dirname, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { parseSemver, cmpSemver } from './install.js';
13
+
14
+ const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
15
+ export const DAY_MS = 86400000;
16
+
17
+ export function cliRelease(root = CLI_ROOT) {
18
+ try { const v = readFileSync(join(root, '..', 'VERSION'), 'utf8').trim(); return parseSemver(v) ? v : null; } catch { return null; }
19
+ }
20
+
21
+ export function skewCachePath(env = process.env) {
22
+ return join(env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'quox', 'version-skew.json');
23
+ }
24
+
25
+ /** -> null | { cli, server, newer: 'cli'|'server', message } */
26
+ export function skewOf(cli, server) {
27
+ const a = parseSemver(cli); const b = parseSemver(server);
28
+ if (!a || !b) return null;
29
+ if (a[0] === b[0] && a[1] === b[1]) return null;
30
+ const newer = cmpSemver(cli, server) > 0 ? 'cli' : 'server';
31
+ const message = newer === 'cli'
32
+ ? `this quox CLI is ${cli} but the server runs ${server}. Update the server: quox server update (on the server; quox server update --dry-run shows the plan first).`
33
+ : `this quox CLI is ${cli} but the server runs ${server}. Update the CLI to ${server} so its commands match the server.`;
34
+ return { cli, server, newer, message };
35
+ }
36
+
37
+ /**
38
+ * Once per day: read the server release, compare, return the warning (or null). Never throws.
39
+ * The cache is stamped BEFORE the network call, so a slow or failing server costs one try per day.
40
+ */
41
+ export async function checkSkewOncePerDay({ collectorUrl, fetchImpl = globalThis.fetch, now = () => Date.now(), cachePath = skewCachePath(), cli = cliRelease(), timeoutMs = 1500 } = {}) {
42
+ try {
43
+ if (!cli || !collectorUrl) return null;
44
+ let cache = {};
45
+ try { cache = JSON.parse(readFileSync(cachePath, 'utf8')) || {}; } catch { /* first run */ }
46
+ if (cache.checkedAt && now() - cache.checkedAt < DAY_MS) return null;
47
+ mkdirSync(dirname(cachePath), { recursive: true });
48
+ writeFileSync(cachePath, JSON.stringify({ ...cache, checkedAt: now() }) + '\n');
49
+ const r = await fetchImpl(`${collectorUrl.replace(/\/+$/, '')}/version`, { signal: AbortSignal.timeout(timeoutMs) });
50
+ if (!r.ok) return null;
51
+ const body = await r.json();
52
+ const sk = skewOf(cli, body?.release);
53
+ if (sk) writeFileSync(cachePath, JSON.stringify({ checkedAt: now(), warnedAt: now(), cli, server: body.release }) + '\n');
54
+ return sk;
55
+ } catch { return null; }
56
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * C4: snapshots = DB + config, taken before every update, restored by rollback.
3
+ *
4
+ * <backups>/<stamp>-<release>-<commit>/
5
+ * manifest.json written LAST with complete:true; a dir without it is not a snapshot
6
+ * postgres.sql pg_dumpall from the running postgres service
7
+ * volumes/<v>.tgz every other compose volume (the SQLite stores), tarred with the stack stopped
8
+ * config/.env, config/ssl/, config/docker-compose.override.yml (mode 0600 / as found)
9
+ */
10
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, cpSync, statSync } from 'node:fs';
11
+ import { join, basename } from 'node:path';
12
+ import { readEnvKeys } from './install.js';
13
+ import { compose } from './status.js';
14
+
15
+ export const HELPER_IMAGE = 'alpine:3.20';
16
+ const CONFIG_ITEMS = ['.env', 'ssl', 'docker-compose.override.yml'];
17
+
18
+ export function listSnapshots(backups) {
19
+ let names = [];
20
+ try { names = readdirSync(backups); } catch { return []; }
21
+ return names.map((n) => {
22
+ const dir = join(backups, n);
23
+ try { const m = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8')); return m?.complete ? { ...m, dir } : null; } catch { return null; }
24
+ }).filter(Boolean).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
25
+ }
26
+ export const latestSnapshot = (backups) => listSnapshots(backups)[0] || null;
27
+
28
+ const stampOf = (d) => d.toISOString().replace(/[:.]/g, '-').replace(/-\d{3}Z$/, 'Z');
29
+
30
+ /**
31
+ * The stack's volumes, from the containers' own mounts (compose volumes carry fixed `name:`s whose project
32
+ * label depends on who created them first, so a label filter misses some, postgres included, on real boxes).
33
+ * -> { volumes: [names], pgVolume: name|null }
34
+ */
35
+ export async function stackVolumes(install, run) {
36
+ const ids = await run('docker', compose(install, ['ps', '-a', '-q']), { cwd: install.repoDir });
37
+ const list = ids.code === 0 ? ids.stdout.split('\n').map((s) => s.trim()).filter(Boolean) : [];
38
+ if (!list.length) return { volumes: [], pgVolume: null };
39
+ const r = await run('docker', ['inspect', '--format', '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}>{{.Destination}};{{end}}{{end}}', ...list]);
40
+ if (r.code !== 0) return { volumes: [], pgVolume: null };
41
+ const seen = new Map();
42
+ for (const part of r.stdout.split(/[;\n]/).map((s) => s.trim()).filter(Boolean)) {
43
+ const [name, dest] = part.split('>');
44
+ if (name && !seen.has(name)) seen.set(name, dest || '');
45
+ }
46
+ const volumes = [...seen.keys()].sort();
47
+ return { volumes, pgVolume: volumes.find((v) => seen.get(v) === '/var/lib/postgresql/data') || null };
48
+ }
49
+
50
+ /**
51
+ * -> { ok, dir, manifest } | { ok:false, error }. g = gate(sys, { dryRun }).
52
+ * Order: dump postgres live, stop the stack, tar the other volumes, start the stack again.
53
+ */
54
+ export async function createSnapshot({ install, installed, backups, g, reason = 'manual', now = () => new Date(), log = () => {} }) {
55
+ const createdAt = now();
56
+ const dir = join(backups, `${stampOf(createdAt)}-${installed.release || 'unknown'}-${installed.commit || 'nocommit'}`);
57
+ const pgUser = readEnvKeys(install.repoDir, ['POSTGRES_USER']).POSTGRES_USER || 'quoxflow';
58
+ const { volumes, pgVolume } = await stackVolumes(install, g.ro);
59
+ const tarVolumes = volumes.filter((v) => v !== pgVolume);
60
+ const cfg = CONFIG_ITEMS.filter((f) => existsSync(join(install.repoDir, f)));
61
+
62
+ await g.fx(`create ${dir}`, () => mkdirSync(join(dir, 'volumes'), { recursive: true, mode: 0o700 }));
63
+ await g.fx(`copy config (${cfg.join(', ') || 'nothing found'}) to ${join(dir, 'config')}`, () => {
64
+ mkdirSync(join(dir, 'config'), { recursive: true, mode: 0o700 });
65
+ for (const f of cfg) cpSync(join(install.repoDir, f), join(dir, 'config', f), { recursive: true, preserveTimestamps: true });
66
+ });
67
+ let postgres = null;
68
+ if (pgVolume) {
69
+ const r = await g.mut('docker', compose(install, ['exec', '-T', 'postgres', 'pg_dumpall', '-U', pgUser]), { cwd: install.repoDir, stdoutFile: join(dir, 'postgres.sql') });
70
+ if (r.code !== 0) return { ok: false, error: `pg_dumpall failed (exit ${r.code}): ${(r.stderr || '').trim().split('\n').pop()}` };
71
+ postgres = 'postgres.sql';
72
+ }
73
+ if (tarVolumes.length) {
74
+ const s = await g.mut('docker', compose(install, ['stop']), { cwd: install.repoDir });
75
+ if (s.code !== 0) return { ok: false, error: `docker compose stop failed (exit ${s.code})` };
76
+ try {
77
+ for (const v of tarVolumes) {
78
+ const r = await g.mut('docker', ['run', '--rm', '-v', `${v}:/v:ro`, '-v', `${join(dir, 'volumes')}:/b`, HELPER_IMAGE, 'tar', 'czf', `/b/${v}.tgz`, '-C', '/v', '.']);
79
+ if (r.code !== 0) return { ok: false, error: `could not archive volume ${v} (exit ${r.code})` };
80
+ }
81
+ } finally {
82
+ await g.mut('docker', compose(install, ['start']), { cwd: install.repoDir });
83
+ }
84
+ }
85
+ const manifest = {
86
+ version: 1, complete: true, createdAt: createdAt.toISOString(), reason,
87
+ release: installed.release, commit: installed.commit, tag: installed.tag || null, siblings: installed.siblings || {},
88
+ project: install.project, repoDir: install.repoDir, root: install.root,
89
+ config: cfg, postgres, postgresUser: pgUser, pgVolume, volumes: tarVolumes
90
+ };
91
+ await g.fx(`write ${join(dir, 'manifest.json')} (marks the snapshot complete)`, () => writeFileSync(join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', { mode: 0o600 }));
92
+ return { ok: true, dir, manifest };
93
+ }
94
+
95
+ /** Restore data + config from a snapshot into the install. Caller has already checked out the code and built. */
96
+ export async function restoreSnapshot({ install, snap, g, pgReadyTries = 30, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
97
+ const dc = (args, opts = {}) => g.mut('docker', compose(install, args), { cwd: install.repoDir, ...opts });
98
+ await g.fx(`restore config (${(snap.config || []).join(', ') || 'none'}) from ${snap.dir}`, () => {
99
+ for (const f of snap.config || []) cpSync(join(snap.dir, 'config', f), join(install.repoDir, f), { recursive: true, force: true, preserveTimestamps: true });
100
+ });
101
+ let r = await dc(['down', '--remove-orphans']);
102
+ if (r.code !== 0) return { ok: false, error: `docker compose down failed (exit ${r.code})` };
103
+ for (const v of snap.volumes || []) {
104
+ r = await g.mut('docker', ['run', '--rm', '-v', `${v}:/v`, '-v', `${join(snap.dir, 'volumes')}:/b:ro`, HELPER_IMAGE, 'sh', '-c', `find /v -mindepth 1 -delete && tar xzf /b/${basename(v)}.tgz -C /v`]);
105
+ if (r.code !== 0) return { ok: false, error: `could not restore volume ${v} (exit ${r.code})` };
106
+ }
107
+ if (snap.postgres && snap.pgVolume) {
108
+ r = await g.mut('docker', ['volume', 'rm', snap.pgVolume]);
109
+ if (r.code !== 0) return { ok: false, error: `could not reset ${snap.pgVolume} (exit ${r.code})` };
110
+ r = await dc(['up', '-d', 'postgres']);
111
+ if (r.code !== 0) return { ok: false, error: `could not start postgres (exit ${r.code})` };
112
+ if (!g.dryRun) {
113
+ let ready = false;
114
+ for (let i = 0; i < pgReadyTries && !ready; i++) {
115
+ const p = await g.ro('docker', compose(install, ['exec', '-T', 'postgres', 'pg_isready', '-U', snap.postgresUser || 'quoxflow']), { cwd: install.repoDir });
116
+ ready = p.code === 0; if (!ready) await sleep(2000);
117
+ }
118
+ if (!ready) return { ok: false, error: 'postgres did not become ready to restore into' };
119
+ }
120
+ r = await dc(['exec', '-T', 'postgres', 'psql', '-q', '-U', snap.postgresUser || 'quoxflow', '-d', 'postgres'], { stdinFile: join(snap.dir, snap.postgres) });
121
+ if (r.code !== 0) return { ok: false, error: `psql restore failed (exit ${r.code})` };
122
+ }
123
+ return { ok: true };
124
+ }
125
+
126
+ export function snapshotSize(dir) {
127
+ let n = 0;
128
+ const walk = (d) => { for (const f of readdirSync(d)) { const p = join(d, f); const s = statSync(p); if (s.isDirectory()) walk(p); else n += s.size; } };
129
+ try { walk(dir); } catch { /* best effort */ }
130
+ return n;
131
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * C4: `quox server status` and /server. Read-only: version, health per service, disk, ports, last update.
3
+ * Anything that could not be read is said as such, never filled in.
4
+ */
5
+ import { readFileSync, writeFileSync, mkdirSync, appendFileSync, renameSync } from 'node:fs';
6
+ import { join, dirname } from 'node:path';
7
+ import { readInstalled } from './install.js';
8
+
9
+ const STATE_FILE = 'lifecycle-state.json';
10
+ const HISTORY_FILE = 'history.jsonl';
11
+
12
+ export function readState(root) { try { return JSON.parse(readFileSync(join(root, STATE_FILE), 'utf8')); } catch { return null; } }
13
+ export function writeState(root, state) {
14
+ mkdirSync(root, { recursive: true });
15
+ const p = join(root, STATE_FILE); const tmp = `${p}.${process.pid}.tmp`;
16
+ writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
17
+ renameSync(tmp, p);
18
+ }
19
+ export function appendHistory(root, event) {
20
+ mkdirSync(root, { recursive: true });
21
+ appendFileSync(join(root, HISTORY_FILE), JSON.stringify(event) + '\n', { mode: 0o600 });
22
+ }
23
+ export function readHistory(root) {
24
+ try { return readFileSync(join(root, HISTORY_FILE), 'utf8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); } catch { return []; }
25
+ }
26
+
27
+ /** `docker compose ps --format json` is a JSON array on some versions and NDJSON on others. */
28
+ export function parseComposePs(text) {
29
+ const t = String(text || '').trim();
30
+ if (!t) return [];
31
+ try { const j = JSON.parse(t); return Array.isArray(j) ? j : [j]; } catch { /* NDJSON */ }
32
+ return t.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
33
+ }
34
+
35
+ export const compose = (install, args) => ['compose', '-p', install.project, ...args];
36
+
37
+ export async function composeServices(install, sys) {
38
+ const r = await sys.run('docker', compose(install, ['ps', '-a', '--format', 'json']), { cwd: install.repoDir });
39
+ if (r.code !== 0) return { ok: false, error: (r.stderr || '').trim().split('\n').pop() || `docker compose ps exited ${r.code}` };
40
+ return {
41
+ ok: true,
42
+ services: parseComposePs(r.stdout).map((s) => ({
43
+ service: s.Service || s.Name,
44
+ name: s.Name,
45
+ state: s.State || '?',
46
+ health: s.Health || null,
47
+ ports: [...new Set((s.Publishers || []).filter((p) => p.PublishedPort).map((p) => `${p.URL && p.URL !== '0.0.0.0' && p.URL !== '::' ? p.URL + ':' : ''}${p.PublishedPort}->${p.TargetPort}`))]
48
+ })).sort((a, b) => String(a.service).localeCompare(String(b.service)))
49
+ };
50
+ }
51
+
52
+ export async function diskFree(path, sys) {
53
+ const r = await sys.run('df', ['-Pk', path]);
54
+ if (r.code !== 0) return { ok: false, error: `df exited ${r.code}` };
55
+ const line = r.stdout.trim().split('\n').pop().split(/\s+/);
56
+ const availKb = Number(line[3]); const sizeKb = Number(line[1]);
57
+ if (!Number.isFinite(availKb)) return { ok: false, error: 'could not read df output' };
58
+ return { ok: true, path, freeGB: +(availKb / 1048576).toFixed(1), sizeGB: +(sizeKb / 1048576).toFixed(1), usedPct: line[4] };
59
+ }
60
+
61
+ async function getJson(url, fetchImpl, timeout = 3000) {
62
+ try {
63
+ const r = await fetchImpl(url, { signal: AbortSignal.timeout(timeout) });
64
+ if (!r.ok) return { ok: false, status: r.status, error: `HTTP ${r.status}` };
65
+ let body = null; try { body = await r.json(); } catch { /* health may be text */ }
66
+ return { ok: true, status: r.status, body };
67
+ } catch (e) { return { ok: false, error: e?.cause?.code || e?.message || 'no answer' }; }
68
+ }
69
+
70
+ /** The running build, from the collector's public /version. */
71
+ export async function runningVersion(collectorUrl, fetchImpl = globalThis.fetch) {
72
+ const r = await getJson(`${collectorUrl.replace(/\/+$/, '')}/version`, fetchImpl);
73
+ if (!r.ok) return { ok: false, error: r.error };
74
+ return { ok: true, commit: r.body?.version || null, release: r.body?.release || null, stamped: r.body?.stamped ?? null };
75
+ }
76
+
77
+ export async function probeHttp(urls, fetchImpl = globalThis.fetch) {
78
+ return Promise.all(Object.entries(urls).map(async ([name, base]) => {
79
+ const t0 = Date.now();
80
+ const r = await getJson(`${base.replace(/\/+$/, '')}/health`, fetchImpl);
81
+ return { name, url: base, ok: r.ok, detail: r.ok ? `HTTP ${r.status}` : r.error, ms: Date.now() - t0 };
82
+ }));
83
+ }
84
+
85
+ export function defaultUrls(config) {
86
+ const get = (s, fb) => { try { return config?.getServiceUrl?.(s) || fb; } catch { return fb; } };
87
+ return { auth: get('auth', 'http://127.0.0.1:3101'), collector: get('collector', 'http://127.0.0.1:9848') };
88
+ }
89
+
90
+ /** -> the full status object. Never throws for a missing piece. */
91
+ export async function gatherStatus({ install, sys, fetchImpl = globalThis.fetch, urls, backups }) {
92
+ const [installed, running, svc, disk, http] = await Promise.all([
93
+ readInstalled(install, sys),
94
+ runningVersion(urls.collector, fetchImpl),
95
+ composeServices(install, sys),
96
+ diskFree(install.root, sys),
97
+ probeHttp(urls, fetchImpl)
98
+ ]);
99
+ const hist = backups ? readHistory(backups) : [];
100
+ const lastUpdate = [...hist].reverse().find((e) => e.op === 'update' || e.op === 'rollback') || null;
101
+ const state = backups ? readState(backups) : null;
102
+ return { install, installed, running, services: svc, disk, http, lastUpdate, inFlight: state && state.status === 'running' ? state : null };
103
+ }
104
+
105
+ const svcIcon = (s) => (s.state === 'running' ? (s.health && s.health !== 'healthy' ? '⚠' : '✔') : '✖');
106
+
107
+ /** Rows for the terminal and for /server (kind/key/text like the other panels). */
108
+ export function statusRows(st) {
109
+ const row = (key, text) => ({ kind: 'row', key, text: String(text) });
110
+ const out = [{ kind: 'head', text: 'Server' }];
111
+ out.push(row('Install', `${st.install.repoDir} (found via ${st.install.source}, project ${st.install.project})`));
112
+ const i = st.installed;
113
+ out.push(row('Version', `${i.release || '? no VERSION file'}${i.tag ? ` (${i.tag})` : ''} · checkout ${i.commit || '?'}${i.dirty ? ' · local changes' : ''}`));
114
+ const rv = st.running;
115
+ out.push(row('Running', rv.ok ? `${rv.commit || '?'}${rv.stamped === false ? ' (unstamped build)' : ''}${rv.commit && i.commit && !String(rv.commit).startsWith(i.commit) && !String(i.commit).startsWith(rv.commit) ? ' · differs from the checkout' : ''}` : `? ${rv.error}`));
116
+ for (const h of st.http) out.push(row(`${h.ok ? '✔' : '✖'} ${h.name}`, `${h.url} · ${h.detail} · ${h.ms}ms`));
117
+ if (!st.services.ok) out.push(row('✖ Services', `? ${st.services.error}`));
118
+ else {
119
+ const up = st.services.services.filter((s) => s.state === 'running').length;
120
+ out.push(row('Services', `${up} of ${st.services.services.length} running`));
121
+ for (const s of st.services.services) out.push({ kind: s.state === 'running' && (!s.health || s.health === 'healthy') ? 'dim' : 'row', key: ` ${svcIcon(s)} ${s.service}`, text: [s.state + (s.health ? ` (${s.health})` : ''), s.ports.join(' ')].filter(Boolean).join(' · ') });
122
+ }
123
+ out.push(row('Disk', st.disk.ok ? `${st.disk.freeGB} GB free of ${st.disk.sizeGB} GB (${st.disk.usedPct} used) at ${st.disk.path}` : `? ${st.disk.error}`));
124
+ const lu = st.lastUpdate;
125
+ out.push(row('Last update', lu ? `${lu.op} ${lu.status} ${lu.at}${lu.from ? ` · ${lu.from} -> ${lu.to || '?'}` : ''}` : 'none recorded by quox server update'));
126
+ if (st.inFlight) out.push(row('⚠ In flight', `an update to ${st.inFlight.target} stopped after ${st.inFlight.done?.slice(-1)[0] || 'preflight'}; quox server update resumes it`));
127
+ return out;
128
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * C4 lifecycle: the one place that runs host commands (docker, docker compose, git, df, tar, openssl).
3
+ *
4
+ * Every lifecycle module takes a `sys` and never spawns on its own, so tests pass a stub and a dry run
5
+ * can prove it never reached a mutating command. A call marked { ro: true } is read-only and still runs
6
+ * in a dry run (it is how the plan learns what is there); anything else is recorded, never run, when
7
+ * dry. That rule lives here, once, in `gate()`.
8
+ */
9
+ import { spawn as defaultSpawn } from 'node:child_process';
10
+ import { createWriteStream, createReadStream } from 'node:fs';
11
+
12
+ /** -> { run(cmd, args, opts) -> Promise<{ code, stdout, stderr }> } */
13
+ export function createSys({ spawnImpl = defaultSpawn } = {}) {
14
+ return {
15
+ kind: 'real',
16
+ run(cmd, args = [], { cwd, env, stdoutFile, stdinFile, stdinText, timeoutMs = 0 } = {}) {
17
+ return new Promise((resolve) => {
18
+ let child;
19
+ try {
20
+ child = spawnImpl(cmd, args, { cwd, env: env ? { ...process.env, ...env } : process.env, stdio: [stdinFile || typeof stdinText === 'string' ? 'pipe' : 'ignore', 'pipe', 'pipe'] });
21
+ } catch (e) { resolve({ code: 127, stdout: '', stderr: e.message }); return; }
22
+ let stdout = ''; let stderr = '';
23
+ const sink = stdoutFile ? createWriteStream(stdoutFile, { mode: 0o600 }) : null;
24
+ if (sink) child.stdout.pipe(sink); else child.stdout.on('data', (d) => { if (stdout.length < 4e6) stdout += d; });
25
+ child.stderr.on('data', (d) => { if (stderr.length < 1e6) stderr += d; });
26
+ if (stdinFile) createReadStream(stdinFile).pipe(child.stdin);
27
+ else if (typeof stdinText === 'string') child.stdin.end(stdinText); // secrets via stdin, never argv
28
+ const timer = timeoutMs ? setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* gone */ } }, timeoutMs) : null;
29
+ child.on('error', (e) => { clearTimeout(timer); resolve({ code: 127, stdout, stderr: stderr || e.message }); });
30
+ child.on('close', (code, sig) => {
31
+ clearTimeout(timer);
32
+ const done = () => resolve({ code: sig ? 128 : code, stdout, stderr });
33
+ if (sink) sink.end(done); else done();
34
+ });
35
+ });
36
+ }
37
+ };
38
+ }
39
+
40
+ export const show = (cmd, args = []) => [cmd, ...args].map((a) => (/^[\w./:=@%+,-]+$/.test(a) ? a : `'${String(a).replace(/'/g, `'\\''`)}'`)).join(' ');
41
+
42
+ /**
43
+ * A step runner over sys: ro calls always run; mutating calls run only when !dryRun, else they are
44
+ * recorded in `planned` and answered { code: 0, dry: true }. log(line) receives every command shown.
45
+ */
46
+ export function gate(sys, { dryRun = false, log = () => {} } = {}) {
47
+ const planned = [];
48
+ return {
49
+ dryRun,
50
+ planned,
51
+ async ro(cmd, args, opts) { return sys.run(cmd, args, opts); },
52
+ async mut(cmd, args, opts = {}) {
53
+ const line = show(cmd, args) + (opts.stdoutFile ? ` > ${opts.stdoutFile}` : '') + (opts.stdinFile ? ` < ${opts.stdinFile}` : '');
54
+ if (dryRun) { planned.push(line); log(`[dry run] ${line}`); return { code: 0, stdout: '', stderr: '', dry: true }; }
55
+ log(`$ ${line}`);
56
+ return sys.run(cmd, args, opts);
57
+ },
58
+ /** A filesystem change (or any non-command effect): described, and only performed when !dryRun. */
59
+ async fx(desc, fn) {
60
+ if (dryRun) { planned.push(desc); log(`[dry run] ${desc}`); return undefined; }
61
+ log(desc);
62
+ return fn();
63
+ }
64
+ };
65
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * C4: `quox server uninstall`. Lists exactly what goes first; two-step confirmation (the command layer);
3
+ * keeps backups unless --purge, and takes a final snapshot first unless --no-backup or --purge.
4
+ */
5
+ import { existsSync, readdirSync, rmSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+ import { join, resolve } from 'node:path';
8
+ import { SIBLINGS } from './install.js';
9
+ import { compose, composeServices } from './status.js';
10
+ import { createSnapshot, listSnapshots, stackVolumes } from './snapshot.js';
11
+ import { readInstalled } from './install.js';
12
+ import { gate } from './sys.js';
13
+
14
+ const DANGEROUS = (p, home) => {
15
+ const r = resolve(p);
16
+ return r === '/' || r === resolve(home) || r.split('/').filter(Boolean).length < 2;
17
+ };
18
+
19
+ /** Everything that will be removed and kept. Read-only. */
20
+ export async function planUninstall({ install, sys, backups, purge = false, journeyFile = join(homedir(), '.config', 'quox', 'onboarding.json'), home = homedir() }) {
21
+ const svc = await composeServices(install, sys);
22
+ const vols = await stackVolumes(install, (c, a, o) => sys.run(c, a, o));
23
+ const imgs = await sys.run('docker', compose(install, ['config', '--images']), { cwd: install.repoDir });
24
+ const dirs = [install.repoDir, ...SIBLINGS.map((s) => join(install.root, s)).filter((d) => existsSync(d))];
25
+ const refused = dirs.filter((d) => DANGEROUS(d, home));
26
+ let rootToo = false;
27
+ try { rootToo = !DANGEROUS(install.root, home) && readdirSync(install.root).every((f) => dirs.includes(join(install.root, f))); } catch { /* unreadable */ }
28
+ const snaps = listSnapshots(backups);
29
+ const backupsInside = dirs.some((d) => resolve(backups).startsWith(resolve(d) + '/')) || (rootToo && resolve(backups).startsWith(resolve(install.root) + '/'));
30
+ return {
31
+ backupsInside,
32
+ containers: svc.ok ? svc.services.map((s) => s.name) : [],
33
+ containersError: svc.ok ? null : svc.error,
34
+ volumes: vols.volumes,
35
+ images: imgs.code === 0 ? [...new Set(imgs.stdout.split('\n').map((s) => s.trim()).filter(Boolean))] : [],
36
+ networks: `the ${install.project} compose networks`,
37
+ dirs: dirs.filter((d) => !refused.includes(d)),
38
+ root: rootToo ? install.root : null,
39
+ refused,
40
+ journeyFile: existsSync(journeyFile) ? journeyFile : null,
41
+ backups: { dir: backups, count: snaps.length, removed: purge }
42
+ };
43
+ }
44
+
45
+ /** Executes the plan. Confirmation is the caller's job and must have happened already. */
46
+ export async function runUninstall({ install, sys, backups, purge = false, noBackup = false, dryRun = false, log = () => {}, plan, now }) {
47
+ const g = gate(sys, { dryRun, log });
48
+ if (plan.backupsInside && !purge) return { ok: false, step: 'plan', error: `the backups directory ${backups} sits inside the install, so removing the install would delete them. Set QUOX_SERVER_BACKUPS to a path outside it, or pass --purge.` };
49
+ let finalSnapshot = null;
50
+ if (!noBackup && !purge) {
51
+ const installed = await readInstalled(install, sys);
52
+ const snap = await createSnapshot({ install, installed, backups, g, reason: 'before uninstall', now, log });
53
+ if (!snap.ok) return { ok: false, step: 'backup', error: `${snap.error}. Nothing was removed; pass --no-backup to uninstall without a final snapshot.` };
54
+ finalSnapshot = snap.dir;
55
+ }
56
+ const r = await g.mut('docker', compose(install, ['down', '--volumes', '--rmi', 'local', '--remove-orphans']), { cwd: install.repoDir });
57
+ if (r.code !== 0) return { ok: false, step: 'down', error: `docker compose down failed (exit ${r.code}); the install directory was left in place` };
58
+ for (const d of plan.dirs) await g.fx(`remove ${d}`, () => rmSync(d, { recursive: true, force: true }));
59
+ if (plan.root) await g.fx(`remove ${plan.root} (now empty)`, () => rmSync(plan.root, { recursive: true, force: true }));
60
+ if (plan.journeyFile) await g.fx(`remove ${plan.journeyFile}`, () => rmSync(plan.journeyFile, { force: true }));
61
+ if (purge) await g.fx(`remove every backup in ${backups}`, () => rmSync(backups, { recursive: true, force: true }));
62
+ return { ok: true, dryRun, finalSnapshot, planned: g.planned };
63
+ }