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,188 @@
1
+ /**
2
+ * The one CLI updater (owner 2026-10-16 "we need /update", "or quox --update"): shared by /update in the
3
+ * conversation client and `quox update` / `quox --update` in the shell.
4
+ *
5
+ * It updates THIS quox the way it was installed, and never builds a second path where one already exists:
6
+ * npm <root> is node_modules/quoxcli -> npm install -g quoxcli@<latest>
7
+ * shared-git the box's shared CLI tree -> quox-cli-refresh (it refuses a dirty tree itself)
8
+ * git any other git checkout -> git -C <root> pull --ff-only (clean and on main only)
9
+ * server a self-hosted install, at a v* tag -> points at `quox server update` (that is its updater)
10
+ * bundle a dev-server bundle (.bundle-sha) -> points at the server's refresh
11
+ * It never runs git stash, reset or checkout. Every external call is injectable (deps.run / spawn) for tests.
12
+ *
13
+ * Network: `npm view quoxcli ...` (the npm registry) and `git fetch origin main` (the tree's own remote), only
14
+ * when the user asks for an update. Nothing is sent but those requests.
15
+ */
16
+ import { spawnSync, execFile } from 'node:child_process';
17
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import os from 'node:os';
20
+ import { RUNNING_ROOT, findOnPath } from '../quoxon/slash/restart.js';
21
+ import { cmpSemver } from '../server/install.js';
22
+ import { askText } from '../quoxon/ask.js';
23
+
24
+ export const NPM_PACKAGE = 'quoxcli';
25
+ export const NOTES_MAX = 5;
26
+
27
+ function defaultRun(cmd, args, { timeout = 30000 } = {}) {
28
+ const r = spawnSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout });
29
+ if (r.error) return { code: null, stdout: '', stderr: `${r.error.code || 'failed'}: ${r.error.message}` };
30
+ return { code: r.status, stdout: String(r.stdout || '').trim(), stderr: String(r.stderr || '').trim() };
31
+ }
32
+
33
+ /** The same, without blocking: the check runs while the conversation client keeps painting. */
34
+ function asyncRun(cmd, args, { timeout = 30000 } = {}) {
35
+ return new Promise((done) => {
36
+ execFile(cmd, args, { encoding: 'utf8', timeout }, (err, stdout, stderr) => {
37
+ if (err && typeof err.code !== 'number') return done({ code: null, stdout: '', stderr: `${err.code || (err.killed ? 'timed out' : 'failed')}: ${String(stderr || err.message).trim()}` });
38
+ done({ code: err ? err.code : 0, stdout: String(stdout || '').trim(), stderr: String(stderr || '').trim() });
39
+ });
40
+ });
41
+ }
42
+
43
+ function withDeps(deps = {}, run = defaultRun) {
44
+ return {
45
+ run,
46
+ exists: existsSync,
47
+ readFile: (p) => readFileSync(p, 'utf8'),
48
+ realpath: (p) => { try { return realpathSync(p); } catch { return p; } },
49
+ findOnPath,
50
+ home: os.homedir(),
51
+ ...deps,
52
+ };
53
+ }
54
+
55
+ const gitOut = (d, dir, args, timeout = 5000) => {
56
+ const r = d.run('git', ['-C', dir, ...args], { timeout });
57
+ return r.code === 0 ? String(r.stdout || '').trim() : null;
58
+ };
59
+ const gitOutA = async (d, dir, args, timeout = 5000) => {
60
+ const r = await d.run('git', ['-C', dir, ...args], { timeout });
61
+ return r.code === 0 ? String(r.stdout || '').trim() : null;
62
+ };
63
+ const lastLine = (s) => String(s || '').trim().split('\n').filter(Boolean).pop() || '';
64
+
65
+ /** How this quox was installed. -> { kind, root, version, branch?, refresh?, tag? } */
66
+ export function detectInstall({ root = RUNNING_ROOT, env = process.env, deps = {} } = {}) {
67
+ const d = withDeps(deps);
68
+ const real = d.realpath(root);
69
+ let version = null;
70
+ try { version = JSON.parse(d.readFile(join(root, 'package.json'))).version || null; } catch { /* no package.json */ }
71
+ if (/[/\\]node_modules[/\\]quoxcli$/.test(real)) return { kind: 'npm', root: real, version };
72
+ if (d.exists(join(root, '.bundle-sha'))) return { kind: 'bundle', root: real, version };
73
+ const top = gitOut(d, real, ['rev-parse', '--show-toplevel']);
74
+ if (top) {
75
+ const branch = gitOut(d, top, ['rev-parse', '--abbrev-ref', 'HEAD']);
76
+ const refresh = d.findOnPath('quox-cli-refresh', env);
77
+ const shared = d.realpath(env.QUOX_CLI_DIR || join(d.home, 'quox-dashboard-cli'));
78
+ if (refresh && d.realpath(top) === shared) return { kind: 'shared-git', root: top, refresh, branch, version };
79
+ if (branch === 'HEAD') {
80
+ const tag = gitOut(d, top, ['describe', '--tags', '--exact-match']);
81
+ if (tag && /^v\d/.test(tag)) return { kind: 'server', root: top, tag, version };
82
+ }
83
+ return { kind: 'git', root: top, branch, version };
84
+ }
85
+ return { kind: 'unknown', root: real, version };
86
+ }
87
+
88
+ async function checkNpm(info, d) {
89
+ const cur = info.version || '0.0.0';
90
+ const v = await d.run('npm', ['view', NPM_PACKAGE, 'version'], { timeout: 20000 });
91
+ if (v.code !== 0 || !v.stdout) return { ok: false, kind: 'npm', current: cur, error: `npm view ${NPM_PACKAGE} version failed: ${lastLine(v.stderr) || `exit ${v.code}`}` };
92
+ const latest = lastLine(v.stdout);
93
+ const cmd = `npm install -g ${NPM_PACKAGE}@${latest}`;
94
+ const base = { ok: true, kind: 'npm', current: cur, latest, notes: [], manual: cmd };
95
+ const c = cmpSemver(latest, cur);
96
+ if (c !== null && c <= 0) return { ...base, upToDate: true, ahead: c < 0, count: 0 };
97
+ let count = 1;
98
+ const all = await d.run('npm', ['view', NPM_PACKAGE, 'versions', '--json'], { timeout: 20000 });
99
+ try {
100
+ const list = JSON.parse(all.stdout);
101
+ const n = (Array.isArray(list) ? list : [list]).filter((x) => cmpSemver(x, cur) === 1 && cmpSemver(x, latest) <= 0).length;
102
+ if (n > 0) count = n;
103
+ } catch { /* keep 1 */ }
104
+ return { ...base, upToDate: false, count, command: { cmd: 'npm', args: ['install', '-g', `${NPM_PACKAGE}@${latest}`], display: cmd } };
105
+ }
106
+
107
+ async function checkGit(info, d) {
108
+ const { root, kind } = info;
109
+ const f = await d.run('git', ['-C', root, 'fetch', 'origin', 'main', '--quiet'], { timeout: 30000 });
110
+ if (f.code !== 0) return { ok: false, kind, error: `git -C ${root} fetch origin main failed: ${lastLine(f.stderr) || `exit ${f.code}`}` };
111
+ const current = await gitOutA(d, root, ['rev-parse', '--short', 'HEAD']) || '?';
112
+ const latest = await gitOutA(d, root, ['rev-parse', '--short', 'origin/main']) || '?';
113
+ const count = Number(await gitOutA(d, root, ['rev-list', '--count', 'HEAD..origin/main']) || 0) || 0;
114
+ const pull = `git -C ${root} pull --ff-only`;
115
+ const shared = kind === 'shared-git';
116
+ const manual = shared ? 'quox-cli-refresh' : pull;
117
+ const base = { ok: true, kind, root, current, latest, count, manual };
118
+ if (count === 0) return { ...base, upToDate: true, notes: [] };
119
+ const notes = (await gitOutA(d, root, ['log', '--format=%s', '-n', String(NOTES_MAX), 'HEAD..origin/main']) || '').split('\n').filter(Boolean);
120
+ if (shared) {
121
+ return { ...base, upToDate: false, notes, scope: `This runs quox-cli-refresh, which updates quox for every session on this box (${root}).`, command: { cmd: info.refresh, args: [], display: 'quox-cli-refresh' } };
122
+ }
123
+ let refused = null;
124
+ if (info.branch !== 'main') refused = `${root} is on branch ${info.branch || '(unknown)'}, not main. Switch to main yourself, then update again. Nothing was changed.`;
125
+ else {
126
+ const dirty = (await gitOutA(d, root, ['status', '--porcelain', '--untracked-files=no']) || '').split('\n').filter(Boolean);
127
+ if (dirty.length) refused = `${root} has ${dirty.length} file${dirty.length === 1 ? '' : 's'} with uncommitted changes. Commit or move them, then update again. Nothing was changed.`;
128
+ }
129
+ return { ...base, upToDate: false, notes, refused, command: refused ? null : { cmd: 'git', args: ['-C', root, 'pull', '--ff-only'], display: pull } };
130
+ }
131
+
132
+ /**
133
+ * What an update would do. -> { ok, kind, current, latest, count, notes[], upToDate, command?, manual, scope?, refused?, error? }
134
+ * refused: this install has its own updater (or is not in a state to update): say why and the manual way.
135
+ */
136
+ export async function checkUpdate(info, { deps = {} } = {}) {
137
+ const d = withDeps(deps, asyncRun);
138
+ switch (info?.kind) {
139
+ case 'npm': return checkNpm(info, d);
140
+ case 'git': case 'shared-git': return checkGit(info, d);
141
+ case 'server': return { ok: true, kind: 'server', current: info.tag, manual: 'quox server update', refused: `This quox is part of your self-hosted Quox install (${info.tag} at ${info.root}), so it updates with the server: run quox server update (it shows the latest release and asks first).` };
142
+ case 'bundle': return { ok: true, kind: 'bundle', manual: null, refused: `This quox is the dev-server bundle in ${info.root}; the Quox server refreshes it while dev mode is on. Ask your admin, or install your own copy with npm install -g ${NPM_PACKAGE}@latest.` };
143
+ default: return { ok: true, kind: 'unknown', manual: `npm install -g ${NPM_PACKAGE}@latest`, refused: `Could not tell how this quox was installed (${info?.root || 'unknown path'}). To install the published one: npm install -g ${NPM_PACKAGE}@latest` };
144
+ }
145
+ }
146
+
147
+ /** Run the update with the terminal handed over (prompts, sudo). -> { ok, code } or { ok:false, error } */
148
+ export function runUpdate(plan, { spawn = spawnSync, env = process.env } = {}) {
149
+ const r = spawn(plan.command.cmd, plan.command.args, { stdio: 'inherit', env });
150
+ if (r.error) return { ok: false, error: `${r.error.code || 'failed'}: ${r.error.message}` };
151
+ const code = r.status ?? (r.signal ? 128 + (os.constants.signals[r.signal] || 0) : 1);
152
+ return { ok: code === 0, code };
153
+ }
154
+
155
+ const isGit = (p) => p.kind === 'git' || p.kind === 'shared-git';
156
+
157
+ /** The lines shown before the ask (or the one line when there is nothing to do). */
158
+ export function updateLines(plan) {
159
+ if (!plan.ok) return [`Could not check for updates: ${plan.error}`];
160
+ if (plan.refused && plan.kind !== 'git') return [plan.refused];
161
+ if (plan.upToDate) return [isGit(plan) ? `quox ${plan.current} is up to date with origin/main.` : plan.ahead ? `quox ${plan.current} is newer than the published ${plan.latest}. Nothing to update.` : `quox ${plan.current} is the latest.`];
162
+ const out = [];
163
+ const notes = plan.notes || [];
164
+ if (notes.length) out.push(`What's new (${isGit(plan) ? 'commits on origin/main' : 'releases'}):`, ...notes.slice(0, NOTES_MAX).map((n) => ` · ${n}`));
165
+ if (plan.count > notes.length && notes.length) out.push(` · and ${plan.count - notes.length} more`);
166
+ if (plan.kind === 'npm') out.push(`Runs: ${plan.command.display}. Release notes show after the restart (/whatsnew).`);
167
+ if (plan.scope) out.push(plan.scope);
168
+ if (plan.refused) out.push(`quox ${plan.current} -> ${plan.latest}: ${plan.count} change${plan.count === 1 ? '' : 's'}, not updated. ${plan.refused}`);
169
+ return out;
170
+ }
171
+
172
+ /** "▶ quox <cur> -> <latest>: <n> changes. Update now and restart? y/N/c" (the shared ask shape). */
173
+ export function updateAsk(plan, { restart = true } = {}) {
174
+ const n = plan.count;
175
+ return askText(`Update now${restart ? ' and restart' : ''}?`, { reason: `quox ${plan.current} -> ${plan.latest}: ${n} change${n === 1 ? '' : 's'}.` });
176
+ }
177
+
178
+ /** `quox --update [...]` is `quox update [...]` when it comes before any subcommand. */
179
+ export function rewriteUpdateFlag(argv) {
180
+ const VALUE = new Set(['-p', '--profile', '-o', '--output', '--org']);
181
+ for (let i = 0; i < argv.length; i++) {
182
+ const a = argv[i];
183
+ if (a === '--update') return [...argv.slice(0, i), 'update', ...argv.slice(i + 1)];
184
+ if (VALUE.has(a)) { i++; continue; }
185
+ if (!a.startsWith('-')) return argv; // a subcommand came first: its own flags are its business
186
+ }
187
+ return argv;
188
+ }
@@ -15,7 +15,7 @@
15
15
  * released on every exit path.
16
16
  * Pure parts take injected tmux output / held slots / attach lookup.
17
17
  */
18
- import { spawn as realSpawn, spawnSync as realSpawnSync } from 'node:child_process';
18
+ import { spawnSync as realSpawnSync } from 'node:child_process';
19
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, statSync, appendFileSync, truncateSync } from 'node:fs';
20
20
  import { homedir } from 'node:os';
21
21
  import { join } from 'node:path';
@@ -70,9 +70,10 @@ export function buildBeat({ probe, backend = null, agent = null, slot = null, he
70
70
  };
71
71
  }
72
72
 
73
- /** Live probe of one tmux session (exact match). null when it does not exist. */
73
+ /** Live probe of one tmux session (exact match). null when it does not exist.
74
+ * `=name:` (session form): tmux 3.4 answers a bare `=name` with an empty line. */
74
75
  export function probeTmux(name, spawnSync = realSpawnSync) {
75
- const r = spawnSync('tmux', ['display-message', '-p', '-t', `=${name}`, PROBE_FORMAT], { encoding: 'utf8' });
76
+ const r = spawnSync('tmux', ['display-message', '-p', '-t', `=${name}:`, PROBE_FORMAT], { encoding: 'utf8' });
76
77
  if (!r || r.error || r.status !== 0) return null;
77
78
  const p = parseProbe(r.stdout);
78
79
  return p && p.name === name ? p : null;
@@ -177,16 +178,19 @@ export function fileLogger(name, dir = HB_DIR()) {
177
178
  * Start the detached heartbeat child for a tmux session the wrapper launched.
178
179
  * The child is this same quox binary; it survives the wrapper exiting on detach.
179
180
  */
180
- export function startHeartbeat({ tmuxSession, backend, agent, slot }, { spawn = realSpawn, argv = process.argv, execPath = process.execPath } = {}) {
181
+ // Spawned by a short-lived node that exits at once, so the heartbeat is never
182
+ // the wrapper's child: the wrapper blocks in spawnSync(tmux) for the session's
183
+ // life and could not reap it (ZOMBIES 2026-10-24, one defunct per wrapper).
184
+ const DETACH_LAUNCHER = "const{spawn}=require('node:child_process');const c=spawn(process.argv[1],process.argv.slice(2),{detached:true,stdio:'ignore'});c.on('error',()=>{});c.unref();";
185
+
186
+ export function startHeartbeat({ tmuxSession, backend, agent, slot }, { spawnSync = realSpawnSync, argv = process.argv, execPath = process.execPath } = {}) {
181
187
  if (!argv[1]) return false;
182
188
  const args = [argv[1], 'sessions', 'heartbeat', tmuxSession, '--loop'];
183
189
  if (backend) args.push('--backend', backend);
184
190
  if (agent) args.push('--agent', agent);
185
191
  if (slot) args.push('--slot', slot);
186
192
  try {
187
- const child = spawn(execPath, args, { detached: true, stdio: 'ignore' });
188
- if (child && child.on) child.on('error', () => {});
189
- if (child && child.unref) child.unref();
190
- return true;
193
+ const r = spawnSync(execPath, ['-e', DETACH_LAUNCHER, execPath, ...args], { stdio: 'ignore', timeout: 10_000 });
194
+ return !!r && !r.error && r.status === 0;
191
195
  } catch { return false; }
192
196
  }
@@ -107,6 +107,70 @@ export function overviewLines(index, { now = Date.now(), localHost = null, all =
107
107
  return out;
108
108
  }
109
109
 
110
+ /**
111
+ * SESSIONS (owner 2026-10-15): the quox conversation client's bare `/sessions`.
112
+ * Human first: every row leads with the session NAME and WHERE it runs, then
113
+ * how long it has been up and when it was last active. Ids and attach commands
114
+ * only appear with `refs` (one dim line under each row). Sections by what the
115
+ * thing is (terminal sessions, slots, work, chats), newest activity first.
116
+ * A row with no host says "host unknown"; nothing is guessed.
117
+ * -> [{ text, tone, state? }]
118
+ */
119
+ const SECTIONS = [
120
+ { title: 'Terminal sessions', test: (r) => r.kind === 'tmux' },
121
+ { title: 'Slots', test: (r) => r.kind === 'slot' },
122
+ { title: 'Work', test: (r) => r.kind !== 'tmux' && r.kind !== 'slot' && r.kind !== 'conversation' },
123
+ { title: 'Chats', test: (r) => r.kind === 'conversation' },
124
+ ];
125
+
126
+ function whereLabel(r, localHost) {
127
+ if (r.kind === 'conversation') return 'on the Quox server';
128
+ if (!r.host) return 'host unknown';
129
+ return localHost && r.host === localHost ? `on ${r.host} (this machine)` : `on ${r.host}`;
130
+ }
131
+
132
+ function sessionName(r) {
133
+ if (r.kind === 'slot') return r.holder_session || r.name || '(unnamed slot)';
134
+ return r.name || '(unnamed)';
135
+ }
136
+
137
+ export function humanSessionLines(index, { now = Date.now(), localHost = null, all = false, refs = false, names = {} } = {}) {
138
+ const prepared = prepareIndex(index, { all, names });
139
+ const rows = prepared.groups.flatMap((g) => g.rows).sort((a, b) => timeOf(b) - timeOf(a));
140
+ const counts = index.counts || {};
141
+ const ago = (v) => { const t = Date.parse(v); return Number.isFinite(t) ? formatAge((now - t) / 1000) : null; };
142
+ const out = [
143
+ { text: stripAnsi(line('quox', 'sessions', { result: `${counts.total || 0} live` })), tone: 'accent' },
144
+ { text: `${counts.working || 0} working · ${counts.waiting || 0} waiting · ${counts.resting || 0} resting · ${counts.failing || 0} failing`, tone: 'dim' },
145
+ ];
146
+ if (!rows.length) out.push({ text: 'No live sessions in this org.', tone: 'dim' });
147
+ for (const sec of SECTIONS) {
148
+ const rs = rows.filter(sec.test);
149
+ if (!rs.length) continue;
150
+ out.push({ text: `${sec.title} (${rs.length})`, tone: 'accent' });
151
+ for (const r of rs) {
152
+ const bits = [whereLabel(r, localHost)];
153
+ if (r.kind === 'slot' && r.holder_session && r.name && r.name !== r.holder_session) bits.push(`holds slot ${r.name}`);
154
+ const up = r.started_at ? ago(r.started_at) : null;
155
+ if (up) bits.push(`up ${up}`);
156
+ const act = ago(r.last_active);
157
+ bits.push(act ? `active ${act} ago` : 'no activity recorded');
158
+ const who = [r.agent ? `agent ${r.agent}` : null, r.kind === 'tmux' && r.slot ? `slot ${r.slot}` : null, r.backend || null].filter(Boolean);
159
+ if (who.length) bits.push(who.join(', '));
160
+ if (r.unread) bits.push(`${r.unread} unread`);
161
+ if (r.turns) bits.push(`${r.turns} turn${r.turns === 1 ? '' : 's'}`);
162
+ if (r.state_reason && r.state === 'failing') bits.push(r.state_reason);
163
+ out.push({ text: ` ${GLYPH[r.state] || '?'} ${sessionName(r)} ${bits.join(' · ')}`, tone: TONE[r.state] || 'dim', state: r.state });
164
+ if (refs) out.push({ text: ` ${[r.attach_hint || null, r.id ? `id ${r.id}` : null].filter(Boolean).join(' · ')}`, tone: 'dim' });
165
+ }
166
+ }
167
+ if (prepared.hiddenConversations > 0) out.push({ text: ` +${prepared.hiddenConversations} older chats (last 24 h) · /sessions all`, tone: 'dim' });
168
+ const bad = (index.sources || []).filter((s) => s.state !== 'connected');
169
+ if (bad.length) out.push({ text: `could not read: ${bad.map((s) => `${s.id} (${s.state}${s.note ? `: ${s.note}` : ''})`).join(' · ')}`, tone: 'dim' });
170
+ if (!refs && rows.length) out.push({ text: '/sessions refs adds ids and attach commands · /sessions all includes every chat', tone: 'dim' });
171
+ return out;
172
+ }
173
+
110
174
  export function renderOverview(index, { now = Date.now(), localHost = osHostname(), color = false, all = false, names = {} } = {}) {
111
175
  return overviewLines(index, { now, localHost, all, names }).map((l) => {
112
176
  if (!color) return l.text;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `quox shortcut add|remove` (QUOXON-Q): a short command (default `q`) that is a symlink to this quox.
3
+ *
4
+ * Rules: never shadow an existing command (a `q` already on PATH that is not ours is refused and named);
5
+ * a user-writable directory only (the one quox lives in, else ~/.local/bin); no sudo; no shell rc edits (a symlink
6
+ * works in every shell and script); idempotent; remove only ever removes a link that points at this quox.
7
+ * Local only: nothing is sent anywhere.
8
+ */
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ export const DEFAULT_NAME = 'q';
15
+ export const NAME_OK = /^[A-Za-z][A-Za-z0-9_.-]{0,30}$/;
16
+ export const QUOX_ENTRY = fileURLToPath(new URL('../../bin/quox.js', import.meta.url));
17
+
18
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return null; } };
19
+ const isExec = (p) => { try { fs.accessSync(p, fs.constants.X_OK); return fs.statSync(p).isFile(); } catch { return false; } };
20
+ const writable = (d) => { try { fs.accessSync(d, fs.constants.W_OK); return fs.statSync(d).isDirectory(); } catch { return false; } };
21
+ const pathDirs = (env) => String(env.PATH || '').split(path.delimiter).filter(Boolean);
22
+
23
+ /** `command -v name` over PATH -> the first executable path, or null. */
24
+ export function whichOnPath(name, env = process.env) {
25
+ for (const d of pathDirs(env)) { const p = path.join(d, name); if (isExec(p)) return p; }
26
+ return null;
27
+ }
28
+
29
+ /** Where the link goes: { dir, onPath, target }. target is what `q` will point at. */
30
+ export function planShortcut({ env = process.env, home = os.homedir(), entry = QUOX_ENTRY } = {}) {
31
+ const realEntry = real(entry) || entry;
32
+ const quoxOnPath = whichOnPath('quox', env);
33
+ // The `quox` on PATH may be a wrapper script rather than a link to this entry: it is still "this quox".
34
+ const onPathReal = quoxOnPath;
35
+ const target = onPathReal || realEntry;
36
+ const reals = new Set([realEntry, quoxOnPath && real(quoxOnPath)].filter(Boolean));
37
+ const dirs = pathDirs(env).map(d => path.resolve(d));
38
+ const local = path.join(home, '.local', 'bin');
39
+ const cands = [];
40
+ if (onPathReal) cands.push(path.dirname(onPathReal));
41
+ cands.push(local);
42
+ const dir = cands.find(writable) || (fs.existsSync(local) ? null : local);
43
+ return { dir, onPath: dir ? dirs.includes(path.resolve(dir)) : false, target, realEntry, reals };
44
+ }
45
+
46
+ const ours = (p, reals) => { try { return fs.lstatSync(p).isSymbolicLink() && reals.has(real(p)); } catch { return false; } };
47
+
48
+ /** -> { ok, status: 'created'|'exists'|'refused'|'error', message, link?, onPath? } */
49
+ export function addShortcut({ name = DEFAULT_NAME, env = process.env, home = os.homedir(), entry = QUOX_ENTRY } = {}) {
50
+ if (!NAME_OK.test(name)) return { ok: false, status: 'error', message: `"${name}" is not a usable command name (letters, digits, _ . -, starting with a letter).` };
51
+ const plan = planShortcut({ env, home, entry });
52
+ const found = whichOnPath(name, env);
53
+ if (found && plan.reals.has(real(found))) return { ok: true, status: 'exists', link: found, message: `${name} already runs quox (${found}). Nothing changed.` };
54
+ if (found) return { ok: false, status: 'refused', message: `Not created: "${name}" already runs ${found}. Quox never shadows an existing command. Pick another name with: quox shortcut add --name <name>` };
55
+ if (!plan.dir) return { ok: false, status: 'error', message: 'No writable directory for the link (tried the one quox lives in and ~/.local/bin). Nothing changed.' };
56
+ const link = path.join(plan.dir, name);
57
+ if (ours(link, plan.reals)) return { ok: true, status: 'exists', link, onPath: plan.onPath, message: `${name} already points at quox (${link}).${plan.onPath ? '' : ` ${plan.dir} is not on your PATH, so ${name} will not run yet.`}` };
58
+ let present = false;
59
+ try { fs.lstatSync(link); present = true; } catch { /* free */ }
60
+ if (present) return { ok: false, status: 'refused', message: `Not created: ${link} already exists and is not a link to quox. Left alone.` };
61
+ try {
62
+ fs.mkdirSync(plan.dir, { recursive: true });
63
+ fs.symlinkSync(plan.target, link);
64
+ } catch (e) { return { ok: false, status: 'error', message: `Could not create ${link}: ${e.message}` }; }
65
+ const warn = plan.onPath ? '' : ` ${plan.dir} is not on your PATH, so ${name} will not run until you add it.`;
66
+ return { ok: true, status: 'created', link, onPath: plan.onPath, message: `Created ${link} -> ${plan.target}.${warn}` };
67
+ }
68
+
69
+ /** Removes the link only if it points at this quox. */
70
+ export function removeShortcut({ name = DEFAULT_NAME, env = process.env, home = os.homedir(), entry = QUOX_ENTRY } = {}) {
71
+ if (!NAME_OK.test(name)) return { ok: false, status: 'error', message: `"${name}" is not a usable command name.` };
72
+ const plan = planShortcut({ env, home, entry });
73
+ const dirs = [...new Set([...(plan.dir ? [plan.dir] : []), ...pathDirs(env), path.join(home, '.local', 'bin')])];
74
+ for (const d of dirs) {
75
+ const link = path.join(d, name);
76
+ if (ours(link, plan.reals)) {
77
+ try { fs.unlinkSync(link); } catch (e) { return { ok: false, status: 'error', message: `Could not remove ${link}: ${e.message}` }; }
78
+ return { ok: true, status: 'removed', link, message: `Removed ${link}.` };
79
+ }
80
+ }
81
+ const other = whichOnPath(name, env);
82
+ return { ok: true, status: 'none', message: other ? `Nothing removed: ${name} (${other}) is not a link to quox, so it is left alone.` : `Nothing to remove: no ${name} link to quox found.` };
83
+ }