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,88 @@
1
+ /**
2
+ * `quox awareness post|list|summary|remove`: QuoxAwareness ledger, the
3
+ * cross-surface situational-awareness store. Collector routes under
4
+ * /api/v1/awareness (org-scoped).
5
+ */
6
+ import { formatOutput, printSuccess } from '../core/output.js';
7
+ import { resolveEndpoint } from '../api/endpoints.js';
8
+ import { resolveOrgId } from './helpers.js';
9
+
10
+ export function registerAwarenessCommands(program, ctx) {
11
+ const { client, auth } = ctx;
12
+
13
+ const aw = program.command('awareness').description('QuoxAwareness: cross-surface situational-awareness ledger');
14
+
15
+ aw
16
+ .command('post <key> <state>')
17
+ .description('Post or update an awareness entry (upsert by key)')
18
+ .option('--title <text>', 'short title')
19
+ .option('--detail <text>', 'longer detail')
20
+ .option('--source <text>', 'who is posting this entry', 'manual')
21
+ .option('--severity <level>', 'info|warn|critical', 'info')
22
+ .option('--ttl <seconds>', 'expire the entry after this many seconds')
23
+ .action(async (key, state, options) => {
24
+ auth.requireAuth();
25
+ const ep = resolveEndpoint('awareness.post');
26
+ const body = {
27
+ key,
28
+ state,
29
+ title: options.title,
30
+ detail: options.detail,
31
+ source: options.source,
32
+ severity: options.severity,
33
+ ttl_s: options.ttl ? Number(options.ttl) : undefined,
34
+ };
35
+ const data = await client.post(ep.path, { service: ep.service, body, query: { org_id: resolveOrgId(auth, program) } });
36
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
37
+ printSuccess(`Posted awareness: ${key}`);
38
+ });
39
+
40
+ aw
41
+ .command('list')
42
+ .description('List awareness entries for the active org')
43
+ .option('--source <text>', 'filter by source')
44
+ .action(async (options) => {
45
+ auth.requireAuth();
46
+ const ep = resolveEndpoint('awareness.list');
47
+ const data = await client.get(ep.path, { service: ep.service, query: { org_id: resolveOrgId(auth, program), source: options.source } });
48
+ const entries = data.entries || [];
49
+ formatOutput(entries.map((e) => ({
50
+ KEY: e.key,
51
+ TITLE: e.title || '',
52
+ STATE: e.state,
53
+ SEV: e.severity || '',
54
+ SOURCE: e.source || '',
55
+ UPDATED: e.updated_at || '',
56
+ })), {
57
+ format: program.opts().output,
58
+ columns: ['KEY', 'TITLE', 'STATE', 'SEV', 'SOURCE', 'UPDATED'],
59
+ emptyMessage: 'No awareness entries. Post one with `quox awareness post <key> <state>`.',
60
+ raw: entries,
61
+ });
62
+ });
63
+
64
+ aw
65
+ .command('summary')
66
+ .description('Current-state briefing (what a reader like the voice receptionist sees)')
67
+ .action(async () => {
68
+ auth.requireAuth();
69
+ const ep = resolveEndpoint('awareness.summary');
70
+ const data = await client.get(ep.path, { service: ep.service, query: { org_id: resolveOrgId(auth, program) } });
71
+ if (program.opts().output === 'json') {
72
+ formatOutput(data, { format: 'json', raw: data });
73
+ return;
74
+ }
75
+ console.log(data.text);
76
+ });
77
+
78
+ aw
79
+ .command('remove <key>')
80
+ .description('Remove an awareness entry')
81
+ .action(async (key) => {
82
+ auth.requireAuth();
83
+ const ep = resolveEndpoint('awareness.remove', { key });
84
+ const data = await client.delete(ep.path, { service: ep.service, query: { org_id: resolveOrgId(auth, program) } });
85
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
86
+ printSuccess(`Removed ${key}`);
87
+ });
88
+ }
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Backup commands: CRUD, verify, restore, schedule
3
+ *
4
+ * TWO LANES, deliberately distinct (G2d slice 3):
5
+ * `quox backup …` — the per-ORG tenant lane (one org's data).
6
+ * `quox backup server …` — the SERVER-WIDE lane: the whole box, every org,
7
+ * instance-admin only, executed by the governed
8
+ * host-side runner. Its restore is the most
9
+ * destructive verb in the product, so it is
10
+ * dry-run by default and needs both --execute and
11
+ * an interactive confirmation.
12
+ */
13
+
14
+ import chalk from 'chalk';
15
+ import { createHash } from 'crypto';
16
+ import { createWriteStream } from 'fs';
17
+ import { Readable } from 'stream';
18
+ import { pipeline } from 'stream/promises';
19
+ import * as path from 'path';
20
+ import { formatOutput, formatDetail, printSuccess, printInfo, printWarning } from '../core/output.js';
21
+ import { resolveOrgId } from './helpers.js';
22
+ import { resolveEndpoint } from '../api/endpoints.js';
23
+
24
+ export function registerBackupCommands(program, ctx) {
25
+ const { client, auth } = ctx;
26
+ const sleep = ctx.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
27
+
28
+ const backup = program
29
+ .command('backup')
30
+ .description('Manage backups');
31
+
32
+ backup
33
+ .command('list')
34
+ .description('List backups')
35
+ .option('--limit <n>', 'max results', '20')
36
+ .action(async (options) => {
37
+ auth.requireAuth();
38
+ const orgId = resolveOrgId(auth, program);
39
+ const data = await client.get(resolveEndpoint('backups.list', { orgId }).path, { service: resolveEndpoint('backups.list', { orgId }).service, query: { limit: options.limit } });
40
+ formatOutput({
41
+ // fleet #392: backups has name/backup_type/archive_size_bytes.
42
+ columns: ['id', 'name', 'backup_type', 'archive_size_bytes', 'status', 'created_at'],
43
+ headers: ['ID', 'Name', 'Type', 'Size (bytes)', 'Status', 'Created'],
44
+ rows: data.items || data.backups || data,
45
+ }, { format: program.opts().output });
46
+ });
47
+
48
+ backup
49
+ .command('get')
50
+ .description('Get backup details')
51
+ .argument('<backupId>', 'backup ID')
52
+ .action(async (backupId) => {
53
+ auth.requireAuth();
54
+ const orgId = resolveOrgId(auth, program);
55
+ const data = await client.get(resolveEndpoint('backups.get', { orgId, backupId }).path, { service: resolveEndpoint('backups.get', { orgId, backupId }).service });
56
+ formatDetail(data.backup || data, { format: program.opts().output });
57
+ });
58
+
59
+ backup
60
+ .command('create')
61
+ .description('Create a backup')
62
+ .option('--label <label>', 'backup label')
63
+ .option('--type <type>', 'backup type: full, incremental', 'full')
64
+ .option('--components <list>', 'comma-separated components to include')
65
+ .action(async (options) => {
66
+ auth.requireAuth();
67
+ const orgId = resolveOrgId(auth, program);
68
+ const body = { type: options.type };
69
+ if (options.label) body.label = options.label;
70
+ if (options.components) body.components = options.components.split(',').map(c => c.trim());
71
+
72
+ const ora = (await import('ora')).default;
73
+ const spinner = ora('Creating backup...').start();
74
+ try {
75
+ const data = await client.post(resolveEndpoint('backups.create', { orgId }).path, { service: resolveEndpoint('backups.create', { orgId }).service, body, timeout: 300000 });
76
+ spinner.succeed('Backup created');
77
+ if (data.backup) formatDetail(data.backup, { format: program.opts().output });
78
+ } catch (err) {
79
+ spinner.fail('Backup failed');
80
+ throw err;
81
+ }
82
+ });
83
+
84
+ backup
85
+ .command('delete')
86
+ .description('Delete a backup')
87
+ .argument('<backupId>', 'backup ID')
88
+ .action(async (backupId) => {
89
+ auth.requireAuth();
90
+ const orgId = resolveOrgId(auth, program);
91
+ await client.delete(resolveEndpoint('backups.delete', { orgId, backupId }).path, { service: resolveEndpoint('backups.delete', { orgId, backupId }).service });
92
+ printSuccess('Backup deleted.');
93
+ });
94
+
95
+ backup
96
+ .command('verify')
97
+ .description('Verify backup integrity')
98
+ .argument('<backupId>', 'backup ID')
99
+ .action(async (backupId) => {
100
+ auth.requireAuth();
101
+ const orgId = resolveOrgId(auth, program);
102
+ const data = await client.post(resolveEndpoint('backups.verify', { orgId, backupId }).path, { service: resolveEndpoint('backups.verify', { orgId, backupId }).service });
103
+ let result = data;
104
+ // The auth route verifies ASYNC ("Verification started", status verifying) and
105
+ // writes the outcome to the backup row. Reading `valid` off the start reply
106
+ // reported "failed: unknown" for a backup that went on to verify cleanly.
107
+ if (data && data.valid === undefined && data.status === 'verifying') {
108
+ const ep = resolveEndpoint('backups.get', { orgId, backupId });
109
+ const deadline = Date.now() + 30 * 60 * 1000;
110
+ for (;;) {
111
+ const g = await client.get(ep.path, { service: ep.service });
112
+ const b = (g && g.backup) || g || {};
113
+ if (b.status && b.status !== 'verifying') {
114
+ let v = null;
115
+ try { v = b.verification_json ? JSON.parse(b.verification_json) : (b.verification || null); } catch { v = null; }
116
+ result = { valid: b.status === 'verified' && (!v || v.valid !== false), errors: v && (v.errors || (v.mismatches && v.mismatches.length ? v.mismatches : undefined)), status: b.status };
117
+ break;
118
+ }
119
+ if (Date.now() > deadline) throw new Error('Backup verification still running after 30 minutes; check `quox backup get ' + backupId + '`');
120
+ await sleep(5000);
121
+ }
122
+ }
123
+ if (result.valid) {
124
+ printSuccess('Backup is valid.');
125
+ } else {
126
+ const detail = result.errors ? JSON.stringify(result.errors) : (result.status ? `status ${result.status}` : 'unknown');
127
+ throw new Error(`Backup verification failed: ${detail}`);
128
+ }
129
+ });
130
+
131
+ backup
132
+ .command('restore')
133
+ .description('Restore from a backup')
134
+ .argument('<backupId>', 'backup ID')
135
+ .option('--dry-run', 'show what would be restored without doing it')
136
+ .option('--components <list>', 'comma-separated components to restore')
137
+ .action(async (backupId, options) => {
138
+ auth.requireAuth();
139
+ const orgId = resolveOrgId(auth, program);
140
+ const body = {};
141
+ if (options.dryRun) body.dry_run = true;
142
+ if (options.components) body.components = options.components.split(',').map(c => c.trim());
143
+
144
+ if (options.dryRun) {
145
+ printInfo('DRY RUN - No changes will be made');
146
+ }
147
+
148
+ const ora = (await import('ora')).default;
149
+ const spinner = ora('Restoring...').start();
150
+ try {
151
+ const data = await client.post(resolveEndpoint('backups.restore', { orgId, backupId }).path, { service: resolveEndpoint('backups.restore', { orgId, backupId }).service, body, timeout: 300000 });
152
+ spinner.succeed('Restore complete');
153
+ if (data.restore) formatDetail(data.restore, { format: program.opts().output });
154
+ } catch (err) {
155
+ spinner.fail('Restore failed');
156
+ throw err;
157
+ }
158
+ });
159
+
160
+ // Schedule subcommand
161
+ const schedule = backup
162
+ .command('schedule')
163
+ .description('Manage backup schedules');
164
+
165
+ schedule
166
+ .command('list')
167
+ .description('List backup schedules')
168
+ .action(async () => {
169
+ auth.requireAuth();
170
+ const orgId = resolveOrgId(auth, program);
171
+ const data = await client.get(resolveEndpoint('backup-schedules.list', { orgId }).path, { service: resolveEndpoint('backup-schedules.list', { orgId }).service });
172
+ formatOutput({
173
+ // fleet #392: backup_schedules has cron_expression/backup_type/next_run_at.
174
+ columns: ['id', 'name', 'cron_expression', 'backup_type', 'enabled', 'next_run_at'],
175
+ headers: ['ID', 'Name', 'Cron', 'Type', 'Enabled', 'Next Run'],
176
+ rows: data.schedules || [data.schedule].filter(Boolean) || data,
177
+ }, { format: program.opts().output });
178
+ });
179
+
180
+ schedule
181
+ .command('create')
182
+ .description('Create a backup schedule')
183
+ .requiredOption('--name <name>', 'schedule name')
184
+ .requiredOption('--cron <expr>', 'cron expression')
185
+ .option('--type <type>', 'backup type', 'full')
186
+ .option('--retention <days>', 'retention in days', '30')
187
+ .action(async (options) => {
188
+ auth.requireAuth();
189
+ const orgId = resolveOrgId(auth, program);
190
+ // Field names match services/auth/routes/backups.js POST /orgs/:orgId/backup-schedules
191
+ // (name required; cron -> cron_expression; type -> backup_type).
192
+ await client.post(resolveEndpoint('backup-schedules.create', { orgId }).path, {
193
+ service: resolveEndpoint('backup-schedules.create', { orgId }).service,
194
+ body: {
195
+ name: options.name,
196
+ cron_expression: options.cron,
197
+ backup_type: options.type,
198
+ retention_days: parseInt(options.retention, 10),
199
+ },
200
+ });
201
+ printSuccess('Backup schedule created.');
202
+ });
203
+
204
+ // ── `quox backup server …` — the instance-admin, SERVER-WIDE lane ────────
205
+ //
206
+ // Style-matched to `quox build` (lib/commands/build.js): the dispatch verbs
207
+ // say plainly that a 200 is a QUEUE acknowledgement and not a finished job,
208
+ // and every read verb names which half of the picture it is showing.
209
+ //
210
+ // Every route behind these verbs is instance-admin gated server-side
211
+ // (services/auth/routes/admin-backups.js); nothing here re-implements that
212
+ // check, it just surfaces the refusal honestly.
213
+ const server = backup
214
+ .command('server')
215
+ .description('Server-wide backup/restore (whole box, every org; instance admin only)');
216
+
217
+ server
218
+ .command('create')
219
+ .description('Dispatch a server-wide backup on the configured runner host')
220
+ .option('--mode <mode>', 'backup mode: full, quick', 'full')
221
+ .action(async (options) => {
222
+ auth.requireAuth();
223
+ if (!['full', 'quick'].includes(options.mode)) {
224
+ throw new Error(`--mode must be one of: full, quick (got "${options.mode}")`);
225
+ }
226
+ const endpoint = resolveEndpoint('backups.server.create');
227
+ const data = await client.post(endpoint.path, {
228
+ service: endpoint.service,
229
+ body: { mode: options.mode },
230
+ });
231
+ formatDetail(data, { format: program.opts().output, title: 'Server-wide backup dispatched' });
232
+ printSuccess(`${options.mode} backup queued on ${data.runner_host || 'the runner host'}.`);
233
+ printWarning('Queued is not backed up: a 200 here is the dispatch acknowledgement only. '
234
+ + 'Check `quox backup server status` and the receipt before claiming the box is backed up.');
235
+ });
236
+
237
+ server
238
+ .command('status')
239
+ .description('Runner readiness and live state, plus the latest durable receipts')
240
+ .action(async () => {
241
+ auth.requireAuth();
242
+ const endpoint = resolveEndpoint('backups.server.status');
243
+ const data = await client.get(endpoint.path, { service: endpoint.service });
244
+ formatDetail(data.runner || {}, {
245
+ format: program.opts().output,
246
+ title: `Runner state: ${data.runner_host || 'unknown host'}`,
247
+ });
248
+ if (data.receipts_error) {
249
+ // Never render a clean board when a half could not be read.
250
+ printWarning(`Receipts could not be read: ${data.receipts_error}`);
251
+ return;
252
+ }
253
+ formatOutput(data.receipts || [], {
254
+ format: program.opts().output,
255
+ columns: ['created_at', 'kind', 'result', 'host', 'archive_id', 'archive_size_bytes', 'duration_seconds'],
256
+ emptyMessage: 'No backup receipts recorded yet.',
257
+ });
258
+ });
259
+
260
+ server
261
+ .command('list')
262
+ .description('Archives the runner host has registered')
263
+ .action(async () => {
264
+ auth.requireAuth();
265
+ const endpoint = resolveEndpoint('backups.server.list');
266
+ const data = await client.get(endpoint.path, { service: endpoint.service });
267
+ const rows = (data.archives || []).map((a) => ({
268
+ id: a.id,
269
+ size_bytes: a.size_bytes,
270
+ created_at: a.created_at,
271
+ sha256: typeof a.sha256 === 'string' ? `${a.sha256.replace(/^sha256:/, '').slice(0, 12)}…` : '-',
272
+ }));
273
+ formatOutput(rows, {
274
+ format: program.opts().output,
275
+ columns: ['id', 'size_bytes', 'created_at', 'sha256'],
276
+ headers: ['Archive ID', 'Size (bytes)', 'Created', 'SHA-256'],
277
+ emptyMessage: 'No archives registered on the runner host.',
278
+ });
279
+ });
280
+
281
+ server
282
+ .command('download')
283
+ .description('Download a server-wide archive to a local file')
284
+ .argument('<archiveId>', 'archive id from `quox backup server list`')
285
+ .option('--out <path>', 'output file (default ./<archiveId>.tar.gz)')
286
+ .action(async (archiveId, options) => {
287
+ auth.requireAuth();
288
+ const endpoint = resolveEndpoint('backups.server.download', { archiveId });
289
+ const res = await client.get(endpoint.path, { service: endpoint.service, raw: true, timeout: 1800000 });
290
+ if (!res.ok) {
291
+ let detail = '';
292
+ try { detail = JSON.stringify(await res.json()); } catch { detail = res.statusText; }
293
+ throw new Error(`Download failed with status ${res.status}: ${detail}`);
294
+ }
295
+
296
+ const outPath = path.resolve(options.out || `./${archiveId.endsWith('.tar.gz') ? archiveId : `${archiveId}.tar.gz`}`);
297
+ // Hash WHAT WAS WRITTEN, not what the server said it would send: the
298
+ // operator needs to compare the bytes on their disk against the
299
+ // archive's registered sha256, and a hash taken anywhere else proves
300
+ // nothing about the file they now hold.
301
+ const hash = createHash('sha256');
302
+ let bytes = 0;
303
+ const sink = createWriteStream(outPath);
304
+ const source = Readable.fromWeb(res.body);
305
+ source.on('data', (chunk) => { bytes += chunk.length; hash.update(chunk); });
306
+ await pipeline(source, sink);
307
+
308
+ printSuccess(`Wrote ${bytes} bytes to ${outPath}`);
309
+ printInfo(`sha256(${path.basename(outPath)}) = ${hash.digest('hex')}`);
310
+ printInfo('Compare that against the SHA-256 column in `quox backup server list` before trusting the copy.');
311
+ });
312
+
313
+ server
314
+ .command('restore')
315
+ .description('Restore the whole box from a server-wide archive (DRY RUN unless --execute)')
316
+ .argument('<archiveId>', 'archive id from `quox backup server list`')
317
+ .option('--execute', 'perform a REAL restore (destroys current data), not a dry run')
318
+ .option('--stores <list>', 'comma-separated store names to restore (default: all in the archive)')
319
+ .action(async (archiveId, options) => {
320
+ auth.requireAuth();
321
+ const body = {};
322
+ if (options.stores) body.stores = options.stores.split(',').map((s) => s.trim()).filter(Boolean);
323
+
324
+ if (!options.execute) {
325
+ // dry_run is left ABSENT rather than sent as true: the engine's own
326
+ // default is dry-run, and asserting a posture the operator did not
327
+ // state is how a real restore eventually happens by accident.
328
+ printInfo('DRY RUN: nothing will be written. Re-run with --execute to restore for real.');
329
+ } else {
330
+ printWarning(`REAL RESTORE of ${archiveId}. This OVERWRITES this instance's data for EVERY org `
331
+ + 'from the archive. Current data not in the archive is lost.');
332
+ const { confirmDestructive } = await import('../core/confirm.js');
333
+ const ok = await confirmDestructive(
334
+ program,
335
+ options,
336
+ `Restore ${archiveId} over this instance, destroying current data?`,
337
+ );
338
+ if (!ok) {
339
+ printInfo('Aborted. Nothing was dispatched.');
340
+ return;
341
+ }
342
+ body.dry_run = false;
343
+ body.confirm = 'RESTORE';
344
+ }
345
+
346
+ const endpoint = resolveEndpoint('backups.server.restore', { archiveId });
347
+ const data = await client.post(endpoint.path, { service: endpoint.service, body });
348
+ formatDetail(data, { format: program.opts().output, title: `Restore dispatched: ${archiveId}` });
349
+ printWarning('Dispatched, not finished: poll `quox backup server restore-status` for the outcome.');
350
+ });
351
+
352
+ server
353
+ .command('restore-status')
354
+ .description('Live restore-engine state on the runner host, plus recent restore receipts')
355
+ .action(async () => {
356
+ auth.requireAuth();
357
+ const endpoint = resolveEndpoint('backups.server.restoreStatus');
358
+ const data = await client.get(endpoint.path, { service: endpoint.service });
359
+ formatDetail(data.runner || {}, {
360
+ format: program.opts().output,
361
+ title: `Restore engine: ${data.runner_host || 'unknown host'}`,
362
+ });
363
+ if (data.receipts_error) {
364
+ printWarning(`Restore receipts could not be read: ${data.receipts_error}`);
365
+ return;
366
+ }
367
+ formatOutput(data.receipts || [], {
368
+ format: program.opts().output,
369
+ columns: ['created_at', 'result', 'archive_id', 'dry_run', 'host', 'duration_seconds'],
370
+ emptyMessage: 'No restore receipts recorded yet.',
371
+ });
372
+ });
373
+ }