quoxcli 0.0.0-stage → 0.2.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 (348) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +142 -2
  3. package/bin/quox.js +292 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +125 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agents.js +56 -0
  9. package/lib/api/endpoints/assistants.js +50 -0
  10. package/lib/api/endpoints/audit.js +14 -0
  11. package/lib/api/endpoints/auth.js +50 -0
  12. package/lib/api/endpoints/awareness.js +34 -0
  13. package/lib/api/endpoints/backup-schedules.js +20 -0
  14. package/lib/api/endpoints/backups.js +86 -0
  15. package/lib/api/endpoints/brain.js +140 -0
  16. package/lib/api/endpoints/budgets.js +62 -0
  17. package/lib/api/endpoints/calendarq.js +71 -0
  18. package/lib/api/endpoints/catalog.js +14 -0
  19. package/lib/api/endpoints/chat.js +54 -0
  20. package/lib/api/endpoints/consent.js +26 -0
  21. package/lib/api/endpoints/contactsq.js +66 -0
  22. package/lib/api/endpoints/conversations.js +32 -0
  23. package/lib/api/endpoints/custom-roles.js +92 -0
  24. package/lib/api/endpoints/divisions.js +56 -0
  25. package/lib/api/endpoints/domains.js +38 -0
  26. package/lib/api/endpoints/engagement.js +231 -0
  27. package/lib/api/endpoints/engine.js +11 -0
  28. package/lib/api/endpoints/entity.js +50 -0
  29. package/lib/api/endpoints/executions.js +32 -0
  30. package/lib/api/endpoints/file.js +20 -0
  31. package/lib/api/endpoints/files.js +26 -0
  32. package/lib/api/endpoints/github.js +21 -0
  33. package/lib/api/endpoints/governance.js +43 -0
  34. package/lib/api/endpoints/hat.js +36 -0
  35. package/lib/api/endpoints/inbox.js +32 -0
  36. package/lib/api/endpoints/inboxq.js +120 -0
  37. package/lib/api/endpoints/index.js +228 -0
  38. package/lib/api/endpoints/invites.js +19 -0
  39. package/lib/api/endpoints/llm-policy.js +43 -0
  40. package/lib/api/endpoints/lucid.js +89 -0
  41. package/lib/api/endpoints/members.js +32 -0
  42. package/lib/api/endpoints/memory.js +75 -0
  43. package/lib/api/endpoints/mfa.js +32 -0
  44. package/lib/api/endpoints/mon.js +178 -0
  45. package/lib/api/endpoints/notesq.js +43 -0
  46. package/lib/api/endpoints/objectives.js +62 -0
  47. package/lib/api/endpoints/orgs.js +38 -0
  48. package/lib/api/endpoints/plugins.js +134 -0
  49. package/lib/api/endpoints/powers.js +75 -0
  50. package/lib/api/endpoints/prompts.js +69 -0
  51. package/lib/api/endpoints/qlarity.js +122 -0
  52. package/lib/api/endpoints/quoxbox.js +55 -0
  53. package/lib/api/endpoints/quoxchat.js +54 -0
  54. package/lib/api/endpoints/quoxflow.js +32 -0
  55. package/lib/api/endpoints/quoxplan.js +114 -0
  56. package/lib/api/endpoints/quoxseo.js +108 -0
  57. package/lib/api/endpoints/reach.js +47 -0
  58. package/lib/api/endpoints/runs.js +38 -0
  59. package/lib/api/endpoints/safety.js +46 -0
  60. package/lib/api/endpoints/scim.js +46 -0
  61. package/lib/api/endpoints/scopes.js +14 -0
  62. package/lib/api/endpoints/security.js +69 -0
  63. package/lib/api/endpoints/setup.js +90 -0
  64. package/lib/api/endpoints/skills.js +180 -0
  65. package/lib/api/endpoints/sso.js +67 -0
  66. package/lib/api/endpoints/tags.js +32 -0
  67. package/lib/api/endpoints/teams.js +56 -0
  68. package/lib/api/endpoints/telephony.js +46 -0
  69. package/lib/api/endpoints/tg.js +56 -0
  70. package/lib/api/endpoints/tools.js +50 -0
  71. package/lib/api/endpoints/traces.js +157 -0
  72. package/lib/api/endpoints/triggers.js +70 -0
  73. package/lib/api/endpoints/user-library.js +41 -0
  74. package/lib/api/endpoints/users.js +20 -0
  75. package/lib/api/endpoints/v1.js +499 -0
  76. package/lib/api/endpoints/vault.js +192 -0
  77. package/lib/api/endpoints/voice.js +22 -0
  78. package/lib/api/endpoints/volt.js +84 -0
  79. package/lib/api/endpoints/ward.js +40 -0
  80. package/lib/api/endpoints/webhooks.js +46 -0
  81. package/lib/api/endpoints/workflows.js +162 -0
  82. package/lib/api/endpoints/x.js +24 -0
  83. package/lib/api/endpoints.js +15 -0
  84. package/lib/commands/admin.js +359 -0
  85. package/lib/commands/aee.js +99 -0
  86. package/lib/commands/agent.js +201 -0
  87. package/lib/commands/agentAttach.js +1440 -0
  88. package/lib/commands/approvals.js +47 -0
  89. package/lib/commands/assistant.js +114 -0
  90. package/lib/commands/auth.js +458 -0
  91. package/lib/commands/awareness.js +88 -0
  92. package/lib/commands/backup.js +373 -0
  93. package/lib/commands/brain.js +994 -0
  94. package/lib/commands/budget.js +241 -0
  95. package/lib/commands/build.js +156 -0
  96. package/lib/commands/calendar.js +216 -0
  97. package/lib/commands/capabilities.js +34 -0
  98. package/lib/commands/chat.js +240 -0
  99. package/lib/commands/claude.js +354 -0
  100. package/lib/commands/config-cmd.js +106 -0
  101. package/lib/commands/consent.js +84 -0
  102. package/lib/commands/contacts.js +967 -0
  103. package/lib/commands/contactsSelftest.js +132 -0
  104. package/lib/commands/conversation.js +74 -0
  105. package/lib/commands/department.js +75 -0
  106. package/lib/commands/dev.js +620 -0
  107. package/lib/commands/devResident.js +314 -0
  108. package/lib/commands/devSupervisor.js +1050 -0
  109. package/lib/commands/discord.js +96 -0
  110. package/lib/commands/doctor.js +181 -0
  111. package/lib/commands/domain.js +114 -0
  112. package/lib/commands/done.js +277 -0
  113. package/lib/commands/engagement.js +586 -0
  114. package/lib/commands/entity.js +119 -0
  115. package/lib/commands/facebook.js +212 -0
  116. package/lib/commands/file.js +128 -0
  117. package/lib/commands/fleet.js +882 -0
  118. package/lib/commands/governance.js +638 -0
  119. package/lib/commands/gripe.js +324 -0
  120. package/lib/commands/hat.js +205 -0
  121. package/lib/commands/helpers.js +79 -0
  122. package/lib/commands/inbox.js +122 -0
  123. package/lib/commands/instagram.js +264 -0
  124. package/lib/commands/integration.js +100 -0
  125. package/lib/commands/inventory.js +115 -0
  126. package/lib/commands/invites.js +45 -0
  127. package/lib/commands/licence.js +280 -0
  128. package/lib/commands/linkedin.js +178 -0
  129. package/lib/commands/llm-policy.js +117 -0
  130. package/lib/commands/llm.js +45 -0
  131. package/lib/commands/lucid.js +591 -0
  132. package/lib/commands/mail.js +531 -0
  133. package/lib/commands/memory.js +284 -0
  134. package/lib/commands/mon.js +514 -0
  135. package/lib/commands/monitoring.js +86 -0
  136. package/lib/commands/motion.js +22 -0
  137. package/lib/commands/notes.js +1246 -0
  138. package/lib/commands/notesBatch.js +160 -0
  139. package/lib/commands/notesBridges.js +56 -0
  140. package/lib/commands/notesLinks.js +60 -0
  141. package/lib/commands/notification.js +57 -0
  142. package/lib/commands/nucleus.js +92 -0
  143. package/lib/commands/objective.js +229 -0
  144. package/lib/commands/onboard.js +328 -0
  145. package/lib/commands/open.js +56 -0
  146. package/lib/commands/org.js +266 -0
  147. package/lib/commands/plan.js +906 -0
  148. package/lib/commands/plugin-doctor.js +1091 -0
  149. package/lib/commands/plugin.js +539 -0
  150. package/lib/commands/policy.js +78 -0
  151. package/lib/commands/powers.js +296 -0
  152. package/lib/commands/prompt.js +350 -0
  153. package/lib/commands/qlarity.js +690 -0
  154. package/lib/commands/quoxbox.js +270 -0
  155. package/lib/commands/quoxchat.js +266 -0
  156. package/lib/commands/quoxseo.js +340 -0
  157. package/lib/commands/reach.js +158 -0
  158. package/lib/commands/role.js +286 -0
  159. package/lib/commands/run.js +132 -0
  160. package/lib/commands/safety.js +107 -0
  161. package/lib/commands/scim.js +88 -0
  162. package/lib/commands/scope.js +58 -0
  163. package/lib/commands/security.js +157 -0
  164. package/lib/commands/service.js +89 -0
  165. package/lib/commands/settings.js +99 -0
  166. package/lib/commands/setup.js +543 -0
  167. package/lib/commands/skillManifest.js +113 -0
  168. package/lib/commands/skillRightsFormat.js +38 -0
  169. package/lib/commands/skills.js +644 -0
  170. package/lib/commands/slot.js +1198 -0
  171. package/lib/commands/sso.js +229 -0
  172. package/lib/commands/status.js +137 -0
  173. package/lib/commands/tag.js +91 -0
  174. package/lib/commands/team.js +140 -0
  175. package/lib/commands/tg.js +240 -0
  176. package/lib/commands/tiktok.js +218 -0
  177. package/lib/commands/tool.js +216 -0
  178. package/lib/commands/trace.js +525 -0
  179. package/lib/commands/trigger.js +128 -0
  180. package/lib/commands/vault.js +1424 -0
  181. package/lib/commands/voice.js +182 -0
  182. package/lib/commands/volt.js +528 -0
  183. package/lib/commands/watch.js +115 -0
  184. package/lib/commands/webhook.js +137 -0
  185. package/lib/commands/workflow.js +503 -0
  186. package/lib/commands/x.js +439 -0
  187. package/lib/core/actingAs.js +136 -0
  188. package/lib/core/aee.js +53 -0
  189. package/lib/core/agentAlias.js +163 -0
  190. package/lib/core/approvalContext.js +61 -0
  191. package/lib/core/auth.js +350 -0
  192. package/lib/core/banner.js +143 -0
  193. package/lib/core/brainPrime.js +90 -0
  194. package/lib/core/callerSource.js +129 -0
  195. package/lib/core/claudeLs.js +94 -0
  196. package/lib/core/client.js +584 -0
  197. package/lib/core/completions.js +163 -0
  198. package/lib/core/config.js +551 -0
  199. package/lib/core/confirm.js +37 -0
  200. package/lib/core/deeplinks.js +76 -0
  201. package/lib/core/doneEngineFetch.js +219 -0
  202. package/lib/core/errors.js +132 -0
  203. package/lib/core/global-flags.js +90 -0
  204. package/lib/core/governedSession.js +402 -0
  205. package/lib/core/lazyCommands.js +119 -0
  206. package/lib/core/liveSessionsReport.js +112 -0
  207. package/lib/core/org-override.js +52 -0
  208. package/lib/core/output.js +239 -0
  209. package/lib/core/prodEnv.js +7 -0
  210. package/lib/core/quoxglyph.js +158 -0
  211. package/lib/core/residentAlarm.js +282 -0
  212. package/lib/core/residentAuth.js +243 -0
  213. package/lib/core/residentPump.js +229 -0
  214. package/lib/core/residentSeed.js +104 -0
  215. package/lib/core/sessionHeartbeat.js +192 -0
  216. package/lib/core/sessionsOverview.js +160 -0
  217. package/lib/core/slotKeeper.js +282 -0
  218. package/lib/core/supervisorEngine.js +864 -0
  219. package/lib/core/systemdInstaller.js +256 -0
  220. package/lib/core/version.js +87 -0
  221. package/lib/core/websiteSession.js +148 -0
  222. package/lib/lucid/LucidIntro.jsx +105 -0
  223. package/lib/lucid/blind.js +71 -0
  224. package/lib/lucid/deck.js +32 -0
  225. package/lib/lucid/fmt.js +139 -0
  226. package/lib/lucid/sitrep.js +173 -0
  227. package/lib/plugins/loader.js +93 -0
  228. package/lib/quoxon/activity.js +104 -0
  229. package/lib/quoxon/api.js +473 -0
  230. package/lib/quoxon/app.js +1262 -0
  231. package/lib/quoxon/capabilities.js +75 -0
  232. package/lib/quoxon/commands.js +154 -0
  233. package/lib/quoxon/connect.js +467 -0
  234. package/lib/quoxon/connectCard.js +28 -0
  235. package/lib/quoxon/context.js +68 -0
  236. package/lib/quoxon/copy.js +33 -0
  237. package/lib/quoxon/demo.fixtures.js +24 -0
  238. package/lib/quoxon/demo.js +35 -0
  239. package/lib/quoxon/egg.js +36 -0
  240. package/lib/quoxon/events.js +458 -0
  241. package/lib/quoxon/gallery.js +223 -0
  242. package/lib/quoxon/indicator.js +193 -0
  243. package/lib/quoxon/intent.js +41 -0
  244. package/lib/quoxon/keys.js +121 -0
  245. package/lib/quoxon/main.js +244 -0
  246. package/lib/quoxon/markdown.js +317 -0
  247. package/lib/quoxon/model.js +205 -0
  248. package/lib/quoxon/motion/clock.js +69 -0
  249. package/lib/quoxon/motion/color.js +35 -0
  250. package/lib/quoxon/motion/frame.js +14 -0
  251. package/lib/quoxon/motion/reveal.js +54 -0
  252. package/lib/quoxon/motion/ring.js +79 -0
  253. package/lib/quoxon/objects/agent.js +61 -0
  254. package/lib/quoxon/objects/crystal.js +100 -0
  255. package/lib/quoxon/objects/fetch.js +170 -0
  256. package/lib/quoxon/objects/index.js +85 -0
  257. package/lib/quoxon/objects/ladder.js +71 -0
  258. package/lib/quoxon/objects/memory.js +44 -0
  259. package/lib/quoxon/objects/model.js +118 -0
  260. package/lib/quoxon/objects/proof.js +110 -0
  261. package/lib/quoxon/objects/skill.js +105 -0
  262. package/lib/quoxon/objects/view.js +76 -0
  263. package/lib/quoxon/objects/workflow.js +119 -0
  264. package/lib/quoxon/observers.js +42 -0
  265. package/lib/quoxon/onboarding/copy.js +235 -0
  266. package/lib/quoxon/onboarding/credential.js +95 -0
  267. package/lib/quoxon/onboarding/detect.js +22 -0
  268. package/lib/quoxon/onboarding/finish.js +119 -0
  269. package/lib/quoxon/onboarding/index.js +67 -0
  270. package/lib/quoxon/onboarding/installRunner.js +225 -0
  271. package/lib/quoxon/onboarding/intent.js +53 -0
  272. package/lib/quoxon/onboarding/journey.js +73 -0
  273. package/lib/quoxon/onboarding/preflight.js +142 -0
  274. package/lib/quoxon/onboarding/runner.js +58 -0
  275. package/lib/quoxon/onboarding/view.js +625 -0
  276. package/lib/quoxon/print.js +94 -0
  277. package/lib/quoxon/qart.js +379 -0
  278. package/lib/quoxon/qglyph.js +143 -0
  279. package/lib/quoxon/qhello.js +58 -0
  280. package/lib/quoxon/qmotion.js +111 -0
  281. package/lib/quoxon/render.js +648 -0
  282. package/lib/quoxon/roster.js +221 -0
  283. package/lib/quoxon/route.js +38 -0
  284. package/lib/quoxon/rules.js +130 -0
  285. package/lib/quoxon/session.js +68 -0
  286. package/lib/quoxon/skills.js +30 -0
  287. package/lib/quoxon/slash/compact.js +90 -0
  288. package/lib/quoxon/slash/ledger.js +83 -0
  289. package/lib/quoxon/slash/listView.js +28 -0
  290. package/lib/quoxon/slash/palette.js +104 -0
  291. package/lib/quoxon/slash/paletteController.js +59 -0
  292. package/lib/quoxon/slash/panels.js +101 -0
  293. package/lib/quoxon/slash/picker.js +38 -0
  294. package/lib/quoxon/slash/restart.js +144 -0
  295. package/lib/quoxon/slash/resume.js +59 -0
  296. package/lib/quoxon/slash/settings.js +50 -0
  297. package/lib/quoxon/sound/index.js +186 -0
  298. package/lib/quoxon/sound/mapping.js +119 -0
  299. package/lib/quoxon/sound/output.js +52 -0
  300. package/lib/quoxon/sound/synth.js +122 -0
  301. package/lib/quoxon/term.js +106 -0
  302. package/lib/quoxon/textBuffer.js +242 -0
  303. package/lib/quoxon/theme.js +253 -0
  304. package/lib/quoxon/transcript.js +150 -0
  305. package/lib/quoxon/web/qface.js +81 -0
  306. package/lib/quoxon/welcome.js +52 -0
  307. package/lib/tui/app.jsx +289 -0
  308. package/lib/tui/components/Breadcrumb.jsx +42 -0
  309. package/lib/tui/components/ChatInput.jsx +68 -0
  310. package/lib/tui/components/Confirm.jsx +32 -0
  311. package/lib/tui/components/DataTable.jsx +270 -0
  312. package/lib/tui/components/DetailPanel.jsx +241 -0
  313. package/lib/tui/components/HelpOverlay.jsx +67 -0
  314. package/lib/tui/components/Layout.jsx +129 -0
  315. package/lib/tui/components/Palette.jsx +141 -0
  316. package/lib/tui/components/SearchInput.jsx +125 -0
  317. package/lib/tui/components/Sparkline.jsx +55 -0
  318. package/lib/tui/components/StatusBar.jsx +133 -0
  319. package/lib/tui/components/Toast.jsx +52 -0
  320. package/lib/tui/hooks/useApi.js +44 -0
  321. package/lib/tui/hooks/useHealth.js +90 -0
  322. package/lib/tui/hooks/usePolledApi.js +86 -0
  323. package/lib/tui/hooks/useTerminalSize.js +46 -0
  324. package/lib/tui/index.js +56 -0
  325. package/lib/tui/intro/Intro.jsx +332 -0
  326. package/lib/tui/jsx-loader.js +32 -0
  327. package/lib/tui/keys.js +87 -0
  328. package/lib/tui/registry.js +49 -0
  329. package/lib/tui/theme.js +103 -0
  330. package/lib/tui/views/AdminView.jsx +398 -0
  331. package/lib/tui/views/AgentView.jsx +204 -0
  332. package/lib/tui/views/BackupView.jsx +225 -0
  333. package/lib/tui/views/BrainView.jsx +223 -0
  334. package/lib/tui/views/ChatView.jsx +468 -0
  335. package/lib/tui/views/FilesView.jsx +218 -0
  336. package/lib/tui/views/FleetView.jsx +234 -0
  337. package/lib/tui/views/GovernanceView.jsx +453 -0
  338. package/lib/tui/views/HealthView.jsx +216 -0
  339. package/lib/tui/views/InboxView.jsx +228 -0
  340. package/lib/tui/views/LucidDeck.jsx +727 -0
  341. package/lib/tui/views/MainMenu.jsx +123 -0
  342. package/lib/tui/views/MemoryView.jsx +377 -0
  343. package/lib/tui/views/ObjectivesView.jsx +230 -0
  344. package/lib/tui/views/OrgView.jsx +229 -0
  345. package/lib/tui/views/ToolView.jsx +221 -0
  346. package/lib/tui/views/VaultView.jsx +422 -0
  347. package/lib/tui/views/WorkflowView.jsx +329 -0
  348. package/package.json +72 -4
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `quox discord` — Quox Discord Pro from the canonical CLI (quoxcli-parity-gate:
3
+ * nothing ships without quox CLI support).
4
+ *
5
+ * Same principle as `quox done`: this is a thin wrapper over the daemon's own
6
+ * control CLI (`qdp`, quox-discord-pro/bin/qdp.mjs), not a reimplementation.
7
+ * The daemon already exposes status | instances | bindings | bind | unbind |
8
+ * gaps | health over its token-gated loopback control API, and qdp is the one
9
+ * client for it. Two clients for one control surface is how a control surface
10
+ * drifts, so `quox discord <verb>` shells straight through to qdp with the
11
+ * arguments unchanged, and qdp's exit code is passed through.
12
+ *
13
+ * Host-local, like `quox done` shelling to the DoneEngine: the operator CLI
14
+ * runs where the daemon runs (one daemon per QuoxCORE instance). Set
15
+ * QDP_CLI_PATH if qdp lives elsewhere.
16
+ */
17
+
18
+ import { spawnSync } from 'node:child_process';
19
+ import fs from 'node:fs';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
22
+
23
+ const QDP = process.env.QDP_CLI_PATH
24
+ || path.join(os.homedir(), 'quox-discord-pro', 'bin', 'qdp.mjs');
25
+
26
+ /**
27
+ * Per-verb usage, mirrored from qdp.mjs's own header doc block (bin/qdp.mjs
28
+ * in the quox-discord-pro repo). qdp itself has no --help handling of its
29
+ * own, and this command hand-rolls arg dispatch (allowUnknownOption +
30
+ * helpOption(false), see below) rather than commander subcommands, so
31
+ * `--help`/`-h` was previously just another unrecognised arg forwarded
32
+ * straight through to qdp — which silently ran the verb instead of showing
33
+ * help. Worst case for a mutating verb (bind/unbind/provision): "--help"
34
+ * would have actually bound/unbound/provisioned a channel. This map is
35
+ * checked BEFORE qdp is ever spawned so that can't happen (quox#603).
36
+ */
37
+ const VERB_USAGE = {
38
+ status: 'qdp status daemon liveness, instances, binding count',
39
+ instances: 'qdp instances configured instances and their targets',
40
+ bindings: 'qdp bindings every channel binding',
41
+ gaps: 'qdp gaps open support-gap items awaiting an official answer',
42
+ health: 'qdp health one-line OK/actionable summary',
43
+ doctor: 'qdp doctor per-binding view/webhook/liveness check; exits\n'
44
+ + ' non-zero if any binding is non-functional',
45
+ bind: 'qdp bind <channelId> <mode> [key=value ...] bind an EXISTING channel',
46
+ unbind: 'qdp unbind <channelId>',
47
+ provision: 'qdp provision <agent-id> [options] create/reuse a channel AND bind it\n'
48
+ + ' --mode <feed|bridge|team|roundtable> default: feed\n'
49
+ + ' --channel <name> default: feed-<agent-id> for feed, <agent-id> otherwise\n'
50
+ + ' --host <hostname-or-ip> dev server the agent runs on (sets bridge_remote_host)\n'
51
+ + ' --inbox <name> remote session-mail inbox (default <agent-id>-inbox)\n'
52
+ + ' --outbox <name> remote outbox (default discord-outbox)\n'
53
+ + ' --two-way allow authorised humans to talk back (bridge only)\n'
54
+ + ' --persona <name> webhook display name (default: titlecased agent id)\n'
55
+ + ' --roster <ids> roundtable only: comma-separated "id" or "id:role" list\n'
56
+ + ' --dry-run print exactly what it WOULD do, change nothing',
57
+ };
58
+
59
+ const GENERAL_USAGE = 'Usage: quox discord <verb> [args...]\n\n'
60
+ + Object.values(VERB_USAGE).map((u) => ` ${u}`).join('\n')
61
+ + '\n\nRun `quox discord <verb> --help` for a single verb\'s usage.';
62
+
63
+ function printVerbHelp(verb) {
64
+ if (verb && VERB_USAGE[verb]) {
65
+ process.stdout.write(`Usage:\n ${VERB_USAGE[verb]}\n`);
66
+ } else {
67
+ process.stdout.write(`${GENERAL_USAGE}\n`);
68
+ }
69
+ }
70
+
71
+ export function registerDiscordCommands(program) {
72
+ program
73
+ .command('discord [args...]')
74
+ .description('Quox Discord Pro daemon: status | instances | bindings | bind | unbind | gaps | health')
75
+ .allowUnknownOption(true)
76
+ .helpOption(false)
77
+ .action((args = []) => {
78
+ if (args.includes('--help') || args.includes('-h')) {
79
+ printVerbHelp(args[0]);
80
+ process.exit(0);
81
+ }
82
+ if (!fs.existsSync(QDP)) {
83
+ process.stderr.write(
84
+ `Discord Pro control CLI (qdp) not found at ${QDP}\n` +
85
+ 'Set QDP_CLI_PATH if it lives elsewhere, or install the quox-discord-pro daemon on this host.\n',
86
+ );
87
+ process.exit(2);
88
+ }
89
+ const r = spawnSync(process.execPath, [QDP, ...args], { stdio: 'inherit' });
90
+ if (r.error) {
91
+ process.stderr.write(`qdp failed to run: ${r.error.message}\n`);
92
+ process.exit(2);
93
+ }
94
+ process.exit(r.status ?? 2);
95
+ });
96
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * quox doctor — Platform Diagnostics
3
+ *
4
+ * Deep-checks every service, database, agent daemon, and credential.
5
+ * Reports pass/warning/critical with actionable details.
6
+ *
7
+ * CLI-3b (Stream CLI Phase 3)
8
+ */
9
+
10
+ import chalk from 'chalk';
11
+ import { SERVICE_PORTS } from '../core/config.js';
12
+ import { emitCliEvent } from '../core/aee.js';
13
+ import { resolveCliVersion, checkCliDrift } from '../core/version.js';
14
+
15
+ const CHECKS = [
16
+ // Core services
17
+ { name: 'Auth service', service: 'auth', port: 3101, path: '/health', critical: true },
18
+ { name: 'Memory service', service: 'memory', port: 3103, path: '/health' },
19
+ { name: 'Collector', service: 'collector', port: 9848, path: '/health', critical: true },
20
+ { name: 'QuoxFlow', service: 'quoxflow', port: 3200, path: '/health' },
21
+ { name: 'Files service', service: 'files', port: 3102, path: '/health' },
22
+ // External services
23
+ { name: 'QuoxAgent daemon', port: 9847, path: '/health', url: 'http://localhost:9847' },
24
+ { name: 'Qdrant vectors', port: 6333, path: '/', url: 'http://localhost:6333' },
25
+ ];
26
+
27
+ async function checkEndpoint(url, path, timeoutMs = 3000) {
28
+ const start = Date.now();
29
+ try {
30
+ const resp = await fetch(`${url}${path}`, {
31
+ signal: AbortSignal.timeout(timeoutMs),
32
+ });
33
+ const latency = Date.now() - start;
34
+ if (resp.ok) {
35
+ return { status: 'pass', latency_ms: latency };
36
+ }
37
+ return { status: 'warn', latency_ms: latency, detail: `HTTP ${resp.status}` };
38
+ } catch (err) {
39
+ return { status: 'fail', latency_ms: null, detail: err.message };
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Every doctor check, in this process: services, auth token, CLI version. Shared by `quox doctor` and the
45
+ * conversation's /doctor so the two can never disagree. -> [{ name, status, detail?, latency_ms?, ... }].
46
+ */
47
+ export async function runDoctorChecks({ config }) {
48
+ // Check all services in parallel — per-item try/catch so one failure doesn't lose all results
49
+ const checks = await Promise.all(
50
+ CHECKS.map(async (check) => {
51
+ try {
52
+ const url = check.url || config.getServiceUrl(check.service);
53
+ let result = await checkEndpoint(url, check.path);
54
+ // #623: doctor was crying CRITICAL when the configured URL was just
55
+ // WRONG (stale profile override / wrong scheme+host) while the
56
+ // service was up on its loopback bind. On a connection failure,
57
+ // probe http://127.0.0.1:<port> — if THAT answers, the service is
58
+ // UP and the verdict is "misconfigured URL", not "service down".
59
+ // Downgrade to a warning that names the reachable bind + the bad
60
+ // URL, so an operator fixes the config instead of chasing an outage.
61
+ if (result.status === 'fail' && check.port) {
62
+ const loopbackUrl = `http://127.0.0.1:${check.port}`;
63
+ if (url !== loopbackUrl) {
64
+ const alt = await checkEndpoint(loopbackUrl, check.path);
65
+ if (alt.status === 'pass') {
66
+ result = {
67
+ status: 'warn',
68
+ latency_ms: alt.latency_ms,
69
+ detail: `reachable at ${loopbackUrl} but configured URL ${url} failed — profile URL likely stale/wrong (misconfig, not an outage)`,
70
+ };
71
+ }
72
+ }
73
+ }
74
+ return {
75
+ name: check.name,
76
+ port: check.port,
77
+ ...result,
78
+ critical: check.critical || false,
79
+ };
80
+ } catch {
81
+ return {
82
+ name: check.name,
83
+ port: check.port,
84
+ ok: false,
85
+ status: 'error',
86
+ ms: 0,
87
+ critical: check.critical || false,
88
+ };
89
+ }
90
+ })
91
+ );
92
+
93
+ // Check auth token. fleet #479: the CURRENT profile (persisted
94
+ // default, or a -p/QUOX_PROFILE override) is config._effectiveProfile,
95
+ // never config._data.activeProfile (that's always the persisted
96
+ // default now, and would report the wrong profile under an override).
97
+ const auth = config.getAuth?.() || config._data?.profiles?.[config._effectiveProfile]?.auth;
98
+ if (auth?.access_token) {
99
+ const expired = auth.expires_at && Date.now() > auth.expires_at;
100
+ if (expired) {
101
+ checks.push({ name: 'Auth token', status: 'warn', detail: 'expired' });
102
+ } else {
103
+ const remaining = auth.expires_at ? Math.round((auth.expires_at - Date.now()) / 3600000) : '?';
104
+ checks.push({ name: 'Auth token', status: 'pass', detail: `valid (expires in ${remaining}h)` });
105
+ }
106
+ } else {
107
+ checks.push({ name: 'Auth token', status: 'warn', detail: 'not logged in' });
108
+ }
109
+
110
+ // #610 Part 3: which build is this, and is it drifted from origin/main?
111
+ // A silently-stale CLI (the worktree was 32 commits behind and nothing
112
+ // said so) is a real warning, so a stale drift maps to 'warn' and bumps
113
+ // the exit code like any other. unknown/skip are informational (pass) —
114
+ // never fail a health check because we could not compute drift. The
115
+ // drift count reflects the last fetch (no network fetch here).
116
+ const cliVersion = resolveCliVersion();
117
+ const drift = checkCliDrift();
118
+ checks.push({
119
+ name: 'CLI version',
120
+ status: drift.status === 'stale' ? 'warn' : 'pass',
121
+ detail: `${cliVersion} · ${drift.detail}`,
122
+ });
123
+
124
+ return checks;
125
+ }
126
+
127
+ export function registerDoctorCommand(program, ctx) {
128
+ const { config, client } = ctx;
129
+
130
+ program
131
+ .command('doctor')
132
+ .description('Run platform health diagnostics')
133
+ .action(async (opts, cmd) => {
134
+ const outputFormat = cmd.parent?.opts()?.output || 'table';
135
+ const isJson = outputFormat === 'json' || cmd.parent?.opts()?.json;
136
+
137
+ const checks = await runDoctorChecks({ config });
138
+
139
+ // Summary
140
+ const passed = checks.filter(c => c.status === 'pass').length;
141
+ const warnings = checks.filter(c => c.status === 'warn').length;
142
+ const failed = checks.filter(c => c.status === 'fail').length;
143
+
144
+ if (isJson) {
145
+ console.log(JSON.stringify({ checks, summary: { passed, warnings, failed } }, null, 2));
146
+ return;
147
+ }
148
+
149
+ // Render
150
+ console.log('');
151
+ for (const c of checks) {
152
+ const icon = c.status === 'pass' ? chalk.green(' ✓')
153
+ : c.status === 'warn' ? chalk.yellow(' ⚠')
154
+ : chalk.red(' ✗');
155
+
156
+ const latency = c.latency_ms != null ? chalk.dim(` (${c.latency_ms}ms)`) : '';
157
+ const detail = c.detail ? chalk.dim(` ${c.detail}`) : '';
158
+ const port = c.port ? chalk.dim(` ${c.port}`) : '';
159
+
160
+ console.log(`${icon} ${c.name.padEnd(20)}${port}${latency}${detail}`);
161
+ }
162
+
163
+ console.log('');
164
+ const parts = [];
165
+ if (passed) parts.push(chalk.green(`${passed} passed`));
166
+ if (warnings) parts.push(chalk.yellow(`${warnings} warning${warnings > 1 ? 's' : ''}`));
167
+ if (failed) parts.push(chalk.red(`${failed} critical`));
168
+ console.log(` ${parts.join(' ')}`);
169
+ console.log('');
170
+
171
+ emitCliEvent(client, {
172
+ command: 'doctor',
173
+ intent: 'quox.cli.doctor',
174
+ payload: { passed, warnings, failed, total: checks.length },
175
+ status: failed > 0 ? 'error' : 'success',
176
+ });
177
+
178
+ if (failed > 0) process.exitCode = 2;
179
+ else if (warnings > 0) process.exitCode = 1;
180
+ });
181
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Domain commands: org domain verification (DNS TXT record ownership proof)
3
+ *
4
+ * quox domain list|add|verify|remove
5
+ *
6
+ * Backend: services/auth/routes/domains.js, mounted at plain `/api`
7
+ * (org-scoped IN the URL: /api/orgs/:orgId/domains...). Team-tier+ +
8
+ * ORG_MANAGE_SETTINGS gated — expect 402/403 on non-team orgs.
9
+ *
10
+ * `add` returns a verification token and human-readable DNS instructions;
11
+ * `verify` re-checks DNS and returns 202 (retryable) rather than an error
12
+ * when the TXT record isn't there yet or hasn't propagated.
13
+ */
14
+
15
+ import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
16
+ import { resolveOrgId } from './helpers.js';
17
+ import { resolveEndpoint } from '../api/endpoints.js';
18
+
19
+ export function registerDomainCommands(program, ctx) {
20
+ const { client, auth } = ctx;
21
+
22
+ const domain = program
23
+ .command('domain')
24
+ .description('Manage org domain verification (team+ — DNS ownership proof)');
25
+
26
+ // ── domain list ──────────────────────────────────────────────────────────────
27
+
28
+ domain
29
+ .command('list')
30
+ .description('List domains claimed by the active org')
31
+ .action(async () => {
32
+ auth.requireAuth();
33
+ const orgId = resolveOrgId(auth, program);
34
+ const ep = resolveEndpoint('domains.list', { orgId });
35
+ const data = await client.get(ep.path, { service: ep.service });
36
+ const rows = data.domains || data || [];
37
+
38
+ formatOutput({
39
+ // fleet #392: column is verified_at (a timestamp), not a `verified` flag.
40
+ columns: ['id', 'domain', 'verified_at', 'created_at'],
41
+ headers: ['ID', 'Domain', 'Verified', 'Created'],
42
+ rows,
43
+ }, { format: program.opts().output, emptyMessage: 'No domains claimed for this org.' });
44
+ });
45
+
46
+ // ── domain add ───────────────────────────────────────────────────────────────
47
+
48
+ domain
49
+ .command('add')
50
+ .description('Claim a domain and get its DNS verification instructions')
51
+ .argument('<domain>', 'domain name (e.g. "example.com")')
52
+ .action(async (domainName) => {
53
+ auth.requireAuth();
54
+ const orgId = resolveOrgId(auth, program);
55
+ const body = { domain: domainName };
56
+
57
+ const ep = resolveEndpoint('domains.create', { orgId });
58
+ const data = await client.post(ep.path, { service: ep.service, body });
59
+ printSuccess(`Domain "${domainName}" claim started.`);
60
+ formatDetail(data.domain || {}, { format: program.opts().output });
61
+ if (data.instructions && program.opts().output !== 'json') {
62
+ printInfo(data.instructions);
63
+ }
64
+ });
65
+
66
+ // ── domain verify ────────────────────────────────────────────────────────────
67
+
68
+ domain
69
+ .command('verify')
70
+ .description('Re-check DNS for a claimed domain')
71
+ .argument('<domainId>', 'domain ID')
72
+ .action(async (domainId) => {
73
+ auth.requireAuth();
74
+ const orgId = resolveOrgId(auth, program);
75
+ const ep = resolveEndpoint('domains.verify', { orgId, domainId });
76
+ const data = await client.post(ep.path, { service: ep.service });
77
+
78
+ if (data.verified) {
79
+ printSuccess('Domain verified.');
80
+ } else {
81
+ printWarning(`Not verified yet (reason: ${data.reason || 'unknown'}).${data.retryable ? ' Retryable — DNS changes can take up to 48 hours.' : ''}`);
82
+ }
83
+ });
84
+
85
+ // ── domain remove ────────────────────────────────────────────────────────────
86
+
87
+ domain
88
+ .command('remove')
89
+ .description('Remove a claimed domain')
90
+ .argument('<domainId>', 'domain ID')
91
+ .option('--force', 'skip confirmation')
92
+ .action(async (domainId, options) => {
93
+ auth.requireAuth();
94
+ const orgId = resolveOrgId(auth, program);
95
+
96
+ if (!options.force && !program.opts().yes) {
97
+ const { default: inquirer } = await import('inquirer');
98
+ const ans = await inquirer.prompt([{
99
+ type: 'confirm',
100
+ name: 'confirm',
101
+ message: `Remove domain ${domainId}? SSO/user-domain routing tied to it will stop working.`,
102
+ default: false,
103
+ }]);
104
+ if (!ans.confirm) {
105
+ printInfo('Cancelled.');
106
+ return;
107
+ }
108
+ }
109
+
110
+ const ep = resolveEndpoint('domains.delete', { orgId, domainId });
111
+ await client.delete(ep.path, { service: ep.service });
112
+ printSuccess('Domain removed.');
113
+ });
114
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * `quox done` — Quox DoneEngine from the terminal.
3
+ *
4
+ * The house gate is that nothing ships without CLI support, and the north star
5
+ * is Claude running Quox headless. That matters more here than for most
6
+ * surfaces: DoneEngine's primary users are AGENTS, not people. "Is this
7
+ * actually done?" is a question something asks before it acts, so a completion
8
+ * engine reachable only from a dashboard fails its own premise.
9
+ *
10
+ * This is a thin wrapper over the quox-gauge engine rather than a
11
+ * reimplementation. Two implementations of a rule set is how a rule set
12
+ * drifts, which is the thing DoneEngine exists to catch, so there is exactly
13
+ * one engine and the CLI shells out to it.
14
+ *
15
+ * ENGINE RESOLUTION (#456, extended by the fetch follow-on below): defaults
16
+ * to ~/quox-gauge/gauge.mjs, which only ever existed on the box quox-gauge
17
+ * was developed on, so every other dev-enabled host got a bare "not found"
18
+ * error. `quox dev enable` delivers the engine to /opt/quoxgauge and points
19
+ * QUOX_DONEENGINE_PATH at it via the generated CLI wrapper (see
20
+ * devServerRefresh.js), so a fresh dev-enabled host resolves it there
21
+ * without any local checkout.
22
+ *
23
+ * FETCH FOLLOW-ON: enable is correctly human-gated (requireHumanAdmin), so a
24
+ * RESIDENT agent with a missing or stale engine on a host nobody has run
25
+ * `quox dev enable` on had no self-service path — permanently stuck waiting
26
+ * on a human. `quox done fetch` closes that: it pulls the same public bundle
27
+ * (/download/quoxgauge-bundle.tar.gz) directly and installs it, no human in
28
+ * the loop, no new permission (see cli/lib/core/doneEngineFetch.js for the
29
+ * fetch/extract/verify logic). Resolution now also checks a user-local
30
+ * fallback (~/.local/share/quox/gauge/gauge.mjs), which is where fetch lands
31
+ * when /opt/quoxgauge is not writable and there is no passwordless sudo. A
32
+ * missing engine or packs directory names `quox done fetch` first (the
33
+ * self-service fix) and `quox dev enable` second.
34
+ *
35
+ * EXIT CODES are the contract, and `check` passes the engine's through
36
+ * unchanged:
37
+ * 0 CLEAR every gate in the pack passes and the running code agrees
38
+ * 1 REFUSED a named gate fails; reasons are printed
39
+ * 2 UNKNOWN insufficient evidence to say either way
40
+ *
41
+ * UNKNOWN is deliberately not folded into either neighbour. Treating "no
42
+ * evidence" as approval is the exact failure this engine was built after, and
43
+ * treating it as refusal trains people to ignore refusals.
44
+ *
45
+ * ADVISORY. `quox done check` never blocks anything. Whether DoneEngine may
46
+ * refuse a deploy is an open owner decision on the DONEENGINE stream, and a
47
+ * tool that started refusing other people's deploys before that was settled
48
+ * would be taking a governance power nobody granted it.
49
+ */
50
+
51
+ import { spawnSync } from 'node:child_process';
52
+ import fs from 'node:fs';
53
+ import os from 'node:os';
54
+ import path from 'node:path';
55
+ import {
56
+ resolveFetchDestination,
57
+ fetchEngine,
58
+ OPT_GAUGE_DIR,
59
+ userLocalGaugeDir,
60
+ legacyGaugeDir,
61
+ } from '../core/doneEngineFetch.js';
62
+ import { printSuccess, printInfo } from '../core/output.js';
63
+
64
+ /**
65
+ * Engine resolution order (see the FETCH FOLLOW-ON note above):
66
+ * 1. QUOX_DONEENGINE_PATH (explicit override, wins even if the file is
67
+ * missing — the honest "not found" error below still needs a concrete
68
+ * path to name)
69
+ * 2. /opt/quoxgauge/gauge.mjs — where `quox dev enable` and a sudo-capable
70
+ * `quox done fetch` both put it
71
+ * 3. ~/.local/share/quox/gauge/gauge.mjs — where `quox done fetch` puts it
72
+ * when /opt/quoxgauge is not writable and there is no passwordless sudo
73
+ * 4. ~/quox-gauge/gauge.mjs — legacy default, kept as the final fallback so
74
+ * the error message always names a concrete path
75
+ *
76
+ * Exported (rather than inlined into the ENGINE constant) so this order is
77
+ * unit-testable with an injected existsSync/env/home instead of the real
78
+ * filesystem.
79
+ */
80
+ export function resolveEnginePath({ env = process.env, existsSync = fs.existsSync, home = os.homedir() } = {}) {
81
+ if (env.QUOX_DONEENGINE_PATH) return env.QUOX_DONEENGINE_PATH;
82
+ const optPath = path.join(OPT_GAUGE_DIR, 'gauge.mjs');
83
+ if (existsSync(optPath)) return optPath;
84
+ const userLocalPath = path.join(userLocalGaugeDir(home), 'gauge.mjs');
85
+ if (existsSync(userLocalPath)) return userLocalPath;
86
+ return path.join(legacyGaugeDir(home), 'gauge.mjs');
87
+ }
88
+
89
+ const ENGINE = resolveEnginePath();
90
+
91
+ function runEngine(args) {
92
+ if (!fs.existsSync(ENGINE)) {
93
+ process.stderr.write(
94
+ `DoneEngine not found at ${ENGINE}\n` +
95
+ 'Self-heal: `quox done fetch` pulls the engine bundle straight from ' +
96
+ 'the collector, no human in the loop. Otherwise `quox dev enable` ' +
97
+ 'delivers it to /opt/quoxgauge as part of full dev-server ' +
98
+ 'provisioning. Or set QUOX_DONEENGINE_PATH to a checkout.\n',
99
+ );
100
+ // 2, not 1: a missing engine is "cannot tell", not "refused".
101
+ return 2;
102
+ }
103
+ const r = spawnSync(process.execPath, [ENGINE, ...args], { stdio: 'inherit' });
104
+ if (r.error) {
105
+ process.stderr.write(`DoneEngine failed to run: ${r.error.message}\n`);
106
+ return 2;
107
+ }
108
+ return r.status ?? 2;
109
+ }
110
+
111
+ function defaultExecFn(cmd, args) {
112
+ return spawnSync(cmd, args, { encoding: 'utf8' });
113
+ }
114
+
115
+ function defaultIsWritable(dir) {
116
+ let probe = dir;
117
+ while (!fs.existsSync(probe)) {
118
+ const parent = path.dirname(probe);
119
+ if (parent === probe) break;
120
+ probe = parent;
121
+ }
122
+ try {
123
+ fs.accessSync(probe, fs.constants.W_OK);
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ function defaultFsOps() {
131
+ return {
132
+ existsSync: fs.existsSync,
133
+ readdirSync: fs.readdirSync,
134
+ writeFileSync: fs.writeFileSync,
135
+ rmSync: fs.rmSync,
136
+ isWritable: defaultIsWritable,
137
+ mkdtemp: () => fs.mkdtempSync(path.join(os.tmpdir(), 'quoxgauge-backup-')),
138
+ tmpFile: () => path.join(os.tmpdir(), `quoxgauge-bundle-${process.pid}-${Date.now()}.tar.gz`),
139
+ };
140
+ }
141
+
142
+ export function registerDoneCommands(program, ctx = {}) {
143
+ const done = program
144
+ .command('done')
145
+ .description('Quox DoneEngine: is this actually done, and what is the evidence?');
146
+
147
+ done
148
+ .command('check [subject]')
149
+ .description('Can this ship? exit 0 clear, 1 refused with reasons, 2 unknown. Advisory: never blocks.')
150
+ .option('--pack <id>', 'rule pack to judge against', 'launch-spine')
151
+ .option('--json', 'structured verdict for agents')
152
+ .option('--root <path>', 'judge <subject> in this repo, reading its own STATUS map with the selected pack (e.g. a plugin with --pack plugin)')
153
+ .action((subject, options) => {
154
+ const args = ['--can-ship', subject || 'all', '--pack', options.pack];
155
+ if (options.root) {
156
+ if (!subject) {
157
+ process.stderr.write('quox done check: --root needs a subject (the surface to judge in that repo).\n');
158
+ process.exitCode = 2;
159
+ return;
160
+ }
161
+ args.push('--subject', subject, '--root', path.resolve(options.root));
162
+ }
163
+ if (options.json) args.push('--json');
164
+ process.exitCode = runEngine(args);
165
+ });
166
+
167
+ done
168
+ .command('checklist [pack]')
169
+ .description('The builder checklist GENERATED from a pack (every gate the engine judges); --check verifies a doc that embeds it')
170
+ .option('--check <file>', 'exit 1 if the checklist block in this file has drifted from the pack')
171
+ .action((pack, options) => {
172
+ const args = ['--checklist', pack || 'plugin'];
173
+ if (options.check) args.push('--check', path.resolve(options.check));
174
+ process.exitCode = runEngine(args);
175
+ });
176
+
177
+ done
178
+ .command('board')
179
+ .description('Estate scoreboard: every surface, score, attested gates, deploy state')
180
+ .option('--json', 'machine-readable, includes raw deploy state')
181
+ .option('--dod', 'per-surface gates, split by evidence class')
182
+ .option('--no-deploy', 'skip docker/git; Deployed Ready becomes unknown, which is not a pass')
183
+ .action((options) => {
184
+ const args = [];
185
+ if (options.json) args.push('--json');
186
+ if (options.dod) args.push('--dod');
187
+ if (options.deploy === false) args.push('--no-deploy');
188
+ process.exitCode = runEngine(args);
189
+ });
190
+
191
+ done
192
+ .command('bar [pack]')
193
+ .description('One rule pack against its subjects: BOTH finish lines, build and deployed')
194
+ .option('--snapshot', 'append this reading to the snapshot history')
195
+ .option('--org <id>', 'measure FOR this org: plan evidence reads as it, the snapshot is stamped with it (default: the pack\'s own org field)')
196
+ .action((pack, options) => {
197
+ const args = pack ? ['--pack', pack] : ['--launch'];
198
+ if (options.snapshot) args.push('--snapshot');
199
+ if (options.org) args.push('--org', options.org);
200
+ process.exitCode = runEngine(args);
201
+ });
202
+
203
+ done
204
+ .command('history [pack]')
205
+ .description('Movement between snapshots, regressions listed first')
206
+ .action((pack) => {
207
+ process.exitCode = runEngine(pack ? ['--history', '--pack', pack] : ['--history']);
208
+ });
209
+
210
+ done
211
+ .command('packs')
212
+ .description('List available rule packs')
213
+ .action(() => {
214
+ const dir = path.join(path.dirname(ENGINE), 'packs');
215
+ if (!fs.existsSync(dir)) {
216
+ process.stderr.write(
217
+ `No packs directory at ${dir}\n` +
218
+ 'This usually means the engine delivery is missing or stale. ' +
219
+ 'Run `quox done fetch` to self-heal it (no human needed), or ' +
220
+ 're-run `quox dev enable`, or set QUOX_DONEENGINE_PATH to a ' +
221
+ 'checkout that has one.\n',
222
+ );
223
+ process.exitCode = 2;
224
+ return;
225
+ }
226
+ for (const f of fs.readdirSync(dir).filter((x) => x.endsWith('.json')).sort()) {
227
+ try {
228
+ const p = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
229
+ const scoped = p.subjects ? `${p.subjects.length} subjects` : 'base pack';
230
+ process.stdout.write(`${(p.id || f).padEnd(16)} v${p.version || '?'} ${scoped} ${p.name || ''}\n`);
231
+ } catch {
232
+ process.stdout.write(`${f} (unreadable)\n`);
233
+ }
234
+ }
235
+ });
236
+
237
+ done
238
+ .command('fetch')
239
+ .description('Self-fetch the DoneEngine bundle, no human in the loop (quox#456 follow-on)')
240
+ .option('--dest <path>', 'install destination (default: $QUOX_DONEENGINE_HOME, else /opt/quoxgauge if writable/sudo-reachable, else ~/.local/share/quox/gauge)')
241
+ .option('--force', 'refetch and reinstall even if the current install already looks healthy')
242
+ .action(async (options) => {
243
+ const client = ctx.client;
244
+ if (!client) {
245
+ process.stderr.write('quox done fetch: no HTTP client available in this context.\n');
246
+ process.exitCode = 2;
247
+ return;
248
+ }
249
+ const execFn = ctx.execFn || defaultExecFn;
250
+ const isWritable = ctx.isWritable || defaultIsWritable;
251
+ const fsOps = ctx.fsOps || defaultFsOps();
252
+
253
+ const { dest, reason, needsSudo } = resolveFetchDestination({
254
+ destOption: options.dest,
255
+ env: process.env,
256
+ isWritable,
257
+ execFn,
258
+ });
259
+ process.stdout.write(`Destination: ${dest} (${reason})\n`);
260
+
261
+ try {
262
+ const result = await fetchEngine({ dest, needsSudo, force: !!options.force, client, execFn, fsOps });
263
+ if (result.status === 'noop') {
264
+ printInfo(result.message);
265
+ } else {
266
+ printSuccess(result.message);
267
+ }
268
+ } catch (err) {
269
+ process.stderr.write(`quox done fetch failed: ${err.message}\n`);
270
+ process.exitCode = 1;
271
+ }
272
+ });
273
+
274
+ return done;
275
+ }
276
+
277
+ export default registerDoneCommands;