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,328 @@
1
+ /**
2
+ * `quox onboard <host>`: guided, idempotent, resumable tenant-onboarding
3
+ * wizard.
4
+ *
5
+ * This is NET-NEW GLUE over verbs that already exist (dev doctor, org create,
6
+ * plan roadmap create, engagement create/target/verify/gripe-target/schedule,
7
+ * vault list). It does NOT reimplement any of their logic: every REST call
8
+ * goes through ctx.client + resolveEndpoint, exactly as the individual verbs
9
+ * do. The wizard's only job is to run the mechanical steps in the right order,
10
+ * skip anything already done, and STOP cleanly at the two human-auth gates
11
+ * (dev-enable + profile-verify, and the GitHub PAT) with the exact copy-paste
12
+ * command the operator must run before re-running the wizard.
13
+ *
14
+ * Design rules baked in here:
15
+ * - Idempotent: every step CHECKs before it acts, so a second run creates no
16
+ * duplicates and simply resumes at the first incomplete step.
17
+ * - Honest gates: a gated step is never reported as done. It prints a WAITING
18
+ * banner and the wizard returns (exit 0, since a gate is a pause, not an error).
19
+ * - Secret-free: the GitHub PAT is instruct-and-stop in v1. The token never
20
+ * enters this process; the operator adds it with `quox vault create
21
+ * --credentials-stdin` and re-runs.
22
+ */
23
+
24
+ import { resolveEndpoint } from '../api/endpoints.js';
25
+ import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
26
+
27
+ // ── status line markers ────────────────────────────────────────────────────
28
+ // One line per step. [ok] already there, [+] created/changed, [wait] a human
29
+ // gate blocks progress. Kept as plain console.log (not the chalk helpers) so
30
+ // the markers are trivially assertable in tests and scannable in a wall of
31
+ // tool output.
32
+ function ok(msg) { console.log(` [ok] ${msg}`); }
33
+ function created(msg) { console.log(` [+] ${msg}`); }
34
+ function waiting(msg) { console.log(` [wait] needs you: ${msg}`); }
35
+
36
+ // ── small pure helpers (exported for unit tests) ───────────────────────────
37
+
38
+ /** Slug for a display name: lowercase, non-alphanumeric collapsed to '-'. */
39
+ export function slugify(name) {
40
+ return String(name || '')
41
+ .toLowerCase()
42
+ .replace(/[^a-z0-9]+/g, '-')
43
+ .replace(/^-+|-+$/g, '')
44
+ || 'tenant';
45
+ }
46
+
47
+ /**
48
+ * Infer an engagement target type from its value.
49
+ * - an ipv4 or ipv4/cidr -> 'cidr'
50
+ * - a scheme://... or host:port -> 'url'
51
+ * - a bare hostname -> 'domain'
52
+ * Default for a bare IP is 'cidr' (the engagement scope model treats a single
53
+ * host as a /32 cidr target).
54
+ */
55
+ export function inferTargetType(value) {
56
+ const v = String(value || '').trim();
57
+ // ipv4 with optional /nn suffix
58
+ if (/^\d{1,3}(\.\d{1,3}){3}(\/\d{1,2})?$/.test(v)) return 'cidr';
59
+ // explicit scheme, or a host:port pair
60
+ if (/:\/\//.test(v) || /^[^\s/]+:\d+$/.test(v)) return 'url';
61
+ return 'domain';
62
+ }
63
+
64
+ /** curl install command for a host that is not yet a fleet-enrolled dev server. */
65
+ export function buildOnboardInstallCommand(collectorBase, host) {
66
+ const base = String(collectorBase || '').replace(/\/+$/, '');
67
+ return `curl -sSL ${base}/install | sudo bash -s -- --host-id ${host}`;
68
+ }
69
+
70
+ // ── the wizard ──────────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Run the onboarding orchestration. Returns { status: 'COMPLETE' | 'WAITING' }.
74
+ * Exported so it can be unit-tested directly with an injected fake client, and
75
+ * so a caller can branch on the outcome.
76
+ */
77
+ export async function runOnboard(host, options, ctx) {
78
+ const { client, auth, config } = ctx;
79
+ auth.requireAuth();
80
+
81
+ const name = options.name;
82
+ const targetValue = options.target || host;
83
+ const envTier = options.environmentTier || 'dev';
84
+ const cron = options.scheduleCron || '0 2 * * 3';
85
+ const tool = options.tool || 'vuln_scan';
86
+ const slug = slugify(name);
87
+
88
+ printEvent(glyphLine('quox', `onboarding ${host}`));
89
+
90
+ // ── Step 1: AGENT + DEV-ENABLE gate ──────────────────────────────────────
91
+ // Read real registry state; never infer readiness from an enable having
92
+ // returned 200. Not enrolled -> install. Enrolled but not dev-enabled or not
93
+ // profile_verified -> the enable/seam-key gate.
94
+ const orgIdForDev = auth.getOrgId(ctx.program?.opts?.().org);
95
+ const serversEp = resolveEndpoint('v1.dev.servers.list');
96
+ const serversResp = await client.get(serversEp.path, {
97
+ service: serversEp.service,
98
+ query: orgIdForDev ? { org_id: orgIdForDev } : {},
99
+ });
100
+ const servers = Array.isArray(serversResp?.servers) ? serversResp.servers : [];
101
+ const server = servers.find((s) => s.host_id === host);
102
+
103
+ if (!server) {
104
+ const collectorBase = config?.getServiceUrl ? config.getServiceUrl('collector') : '<collector-url>';
105
+ waiting(`${host} is not a fleet-enrolled dev server. Install the agent, then re-run:`);
106
+ console.log(` ${buildOnboardInstallCommand(collectorBase, host)}`);
107
+ return stop(host);
108
+ }
109
+
110
+ const enabledCaps = Array.isArray(server.enabled_capabilities) ? server.enabled_capabilities : [];
111
+ if (!server.profile_verified || enabledCaps.length === 0) {
112
+ const why = !server.profile_verified ? 'profile not verified' : 'no capabilities enabled';
113
+ waiting(`${host} is enrolled but not ready (${why}). Run (as an owner/admin):`);
114
+ console.log(` quox dev enable ${host}`);
115
+ console.log(' profile_verified needs the seam key plus an agent restart.');
116
+ console.log(` Confirm with: quox dev doctor ${host}`);
117
+ return stop(host);
118
+ }
119
+ ok(`${host} is a dev-enabled, profile-verified dev server`);
120
+
121
+ // ── Step 2: ORG ──────────────────────────────────────────────────────────
122
+ const orgsEp = resolveEndpoint('orgs.list');
123
+ const orgsResp = await client.get(orgsEp.path, { service: orgsEp.service });
124
+ const orgs = orgsResp?.orgs || orgsResp?.organizations || orgsResp || [];
125
+ let org = (Array.isArray(orgs) ? orgs : []).find(
126
+ (o) => o.name === name || o.slug === slug || o.id === name,
127
+ );
128
+ let orgId;
129
+ if (org) {
130
+ orgId = org.id;
131
+ ok(`org "${name}" exists (${orgId})`);
132
+ } else {
133
+ const createEp = resolveEndpoint('orgs.create');
134
+ const createResp = await client.post(createEp.path, {
135
+ service: createEp.service,
136
+ body: { name, slug },
137
+ });
138
+ org = createResp?.organization || createResp?.org || createResp;
139
+ orgId = org.id;
140
+ created(`org "${name}" (${orgId})`);
141
+ }
142
+
143
+ // ── Step 3: ROADMAP ───────────────────────────────────────────────────────
144
+ const roadmapId = slug;
145
+ const roadmapsEp = resolveEndpoint('quoxplan.roadmaps.list');
146
+ const roadmapsResp = await client.get(roadmapsEp.path, { service: roadmapsEp.service });
147
+ const roadmaps = roadmapsResp?.roadmaps || [];
148
+ const existingRoadmap = roadmaps.find((r) => r.id === roadmapId || r.org_id === orgId);
149
+ if (existingRoadmap) {
150
+ ok(`roadmap "${existingRoadmap.id}" exists for this tenant`);
151
+ } else {
152
+ const rmCreateEp = resolveEndpoint('quoxplan.roadmaps.create');
153
+ await client.post(rmCreateEp.path, {
154
+ service: rmCreateEp.service,
155
+ body: { org_id: orgId, id: roadmapId, name: `${name} delivery`, actor: 'quox-cli' },
156
+ });
157
+ created(`roadmap "${roadmapId}" for org ${orgId}`);
158
+ }
159
+
160
+ // ── Step 4: ENGAGEMENT ────────────────────────────────────────────────────
161
+ const engagementName = `${name} security`;
162
+ const engListEp = resolveEndpoint('engagement.list');
163
+ const engListResp = await client.get(engListEp.path, { service: engListEp.service });
164
+ const engagements = engListResp?.engagements || [];
165
+ let engagement = engagements.find((e) => e.name === engagementName);
166
+ let engagementId;
167
+ if (engagement) {
168
+ engagementId = engagement.id;
169
+ ok(`engagement "${engagementName}" exists (${engagementId})`);
170
+ } else {
171
+ const engCreateEp = resolveEndpoint('engagement.create');
172
+ const engResp = await client.post(engCreateEp.path, {
173
+ service: engCreateEp.service,
174
+ body: { name: engagementName, environment_tier: envTier, risk_ceiling: 'safe_active' },
175
+ });
176
+ engagement = engResp.engagement;
177
+ engagementId = engagement.id;
178
+ created(`engagement "${engagementName}" (${engagementId})`);
179
+ }
180
+
181
+ // ── Step 5: TARGET ────────────────────────────────────────────────────────
182
+ const engGetEp = resolveEndpoint('engagement.get', { id: engagementId });
183
+ const engDetail = await client.get(engGetEp.path, { service: engGetEp.service });
184
+ const currentEngagement = engDetail?.engagement || engagement;
185
+ const targets = Array.isArray(engDetail?.targets) ? engDetail.targets : [];
186
+ let target = targets.find((t) => t.value === targetValue);
187
+ let targetVerified = false;
188
+
189
+ if (!target) {
190
+ const type = inferTargetType(targetValue);
191
+ const addEp = resolveEndpoint('engagement.target.create', { id: engagementId });
192
+ const addResp = await client.post(addEp.path, {
193
+ service: addEp.service,
194
+ body: { type, value: targetValue, verification_method: 'quoxagent_install' },
195
+ });
196
+ target = addResp.target;
197
+ created(`target ${targetValue} (${type}) added`);
198
+ const verEp = resolveEndpoint('engagement.target.verify', { id: engagementId, targetId: target.id });
199
+ const verResp = await client.post(verEp.path, { service: verEp.service, body: {} });
200
+ targetVerified = verResp?.target?.verification_status === 'verified';
201
+ if (targetVerified) created(`target ${targetValue} verified`);
202
+ else waiting(`target ${targetValue} did not verify (status: ${verResp?.target?.verification_status || 'unknown'})`);
203
+ } else if (target.verification_status === 'verified') {
204
+ targetVerified = true;
205
+ ok(`target ${targetValue} already verified`);
206
+ } else {
207
+ const verEp = resolveEndpoint('engagement.target.verify', { id: engagementId, targetId: target.id });
208
+ const verResp = await client.post(verEp.path, { service: verEp.service, body: {} });
209
+ targetVerified = verResp?.target?.verification_status === 'verified';
210
+ if (targetVerified) created(`target ${targetValue} verified`);
211
+ else waiting(`target ${targetValue} did not verify (status: ${verResp?.target?.verification_status || 'unknown'})`);
212
+ }
213
+
214
+ // ── Step 6: GITHUB PAT gate ───────────────────────────────────────────────
215
+ // The repo is only meaningful if a customer GitHub connector token exists to
216
+ // file into it. Check the org vault for a github credential; if absent, STOP
217
+ // with the exact instruct-and-stop command (secret via --credentials-stdin,
218
+ // NEVER argv), and do NOT touch gripe target or schedule.
219
+ const repo = options.repo;
220
+ if (repo) {
221
+ const vaultEp = resolveEndpoint('vault.list', { orgId });
222
+ const vaultResp = await client.get(vaultEp.path, {
223
+ service: vaultEp.service,
224
+ query: { type: 'github' },
225
+ });
226
+ const creds = vaultResp?.credentials || vaultResp || [];
227
+ const githubCred = (Array.isArray(creds) ? creds : []).find((c) => c.integration_type === 'github');
228
+ if (!githubCred) {
229
+ waiting(`no GitHub connector credential in org ${orgId}. Add the fine-grained PAT, then re-run:`);
230
+ console.log(` printf '%s' '<PAT>' | quox vault create --org ${orgId} --type github --label "${name} github" --credentials-stdin`);
231
+ console.log(` (the JSON on stdin is {"token":"<PAT>"}; scope the PAT to ${repo} with Issues read/write)`);
232
+ return stop(host, { orgId, roadmapId, engagementId, targetValue, targetVerified });
233
+ }
234
+ ok(`GitHub connector credential present in org ${orgId}`);
235
+
236
+ // ── Step 7: GRIPE TARGET ────────────────────────────────────────────────
237
+ const wantRepo = repo;
238
+ const wantConnector = orgId;
239
+ if (currentEngagement.gripe_repo === wantRepo && currentEngagement.gripe_connector_org_id === wantConnector) {
240
+ ok(`gripe target already ${wantRepo} via org ${wantConnector}`);
241
+ } else {
242
+ const gtEp = resolveEndpoint('engagement.gripe_target.set', { id: engagementId });
243
+ await client.patch(gtEp.path, {
244
+ service: gtEp.service,
245
+ body: { gripe_repo: wantRepo, gripe_connector_org_id: wantConnector },
246
+ });
247
+ created(`gripe target -> ${wantRepo} via org ${wantConnector}`);
248
+ }
249
+ } else {
250
+ ok('no --repo given; skipping GitHub connector and gripe target');
251
+ }
252
+
253
+ // ── Step 8: SCHEDULE ──────────────────────────────────────────────────────
254
+ const schedListEp = resolveEndpoint('engagement.schedule.list', { id: engagementId });
255
+ const schedResp = await client.get(schedListEp.path, { service: schedListEp.service });
256
+ const schedules = Array.isArray(schedResp?.schedules) ? schedResp.schedules : [];
257
+ let schedule = schedules.find((s) => s.target_value === targetValue && s.tool === tool);
258
+ if (schedule) {
259
+ ok(`schedule for ${tool} on ${targetValue} exists (next: ${schedule.next_run_at || 'n/a'})`);
260
+ } else {
261
+ const schedCreateEp = resolveEndpoint('engagement.schedule.create', { id: engagementId });
262
+ const createResp = await client.post(schedCreateEp.path, {
263
+ service: schedCreateEp.service,
264
+ body: { target_value: targetValue, tool, schedule_kind: 'cron', cron_expression: cron },
265
+ });
266
+ schedule = createResp.schedule;
267
+ created(`schedule ${tool} on ${targetValue} @ "${cron}" (next: ${schedule?.next_run_at || 'n/a'})`);
268
+ }
269
+
270
+ // ── Step 9: SUMMARY ───────────────────────────────────────────────────────
271
+ // Every human gate that could stop the wizard has already returned by here,
272
+ // so the only thing left that can hold onboarding open is an unverified
273
+ // target (verification is best-effort and can legitimately not land yet).
274
+ const complete = targetVerified;
275
+ printSummary({
276
+ status: complete ? 'COMPLETE' : 'WAITING',
277
+ host,
278
+ orgId,
279
+ roadmapId,
280
+ engagementId,
281
+ targetValue,
282
+ targetVerified,
283
+ gripeRepo: repo || null,
284
+ schedule: schedule ? { cron, next: schedule.next_run_at } : null,
285
+ remainingGate: targetVerified ? null : 'target not verified',
286
+ });
287
+ return { status: complete ? 'COMPLETE' : 'WAITING' };
288
+ }
289
+
290
+ /** Shared gate-stop: print the WAITING banner + resume hint, return WAITING. */
291
+ function stop(host, partial) {
292
+ console.log('');
293
+ console.log(' ===== WAITING on a human gate =====');
294
+ console.log(` Do the step above, then re-run: quox onboard ${host} ... to continue.`);
295
+ if (partial) {
296
+ console.log(` Progress so far: org ${partial.orgId}, roadmap ${partial.roadmapId}, engagement ${partial.engagementId}, target ${partial.targetValue} (verified: ${partial.targetVerified}).`);
297
+ }
298
+ return { status: 'WAITING' };
299
+ }
300
+
301
+ function printSummary(s) {
302
+ console.log('');
303
+ console.log(` ===== onboarding ${s.status} =====`);
304
+ console.log(` host: ${s.host}`);
305
+ console.log(` org: ${s.orgId}`);
306
+ console.log(` roadmap: ${s.roadmapId}`);
307
+ console.log(` engagement: ${s.engagementId}`);
308
+ console.log(` target: ${s.targetValue} (verified: ${s.targetVerified})`);
309
+ console.log(` gripe repo: ${s.gripeRepo || '(none)'}`);
310
+ if (s.schedule) console.log(` schedule: "${s.schedule.cron}" (next: ${s.schedule.next || 'n/a'})`);
311
+ if (s.remainingGate) console.log(` remaining: ${s.remainingGate}`);
312
+ }
313
+
314
+ export function registerOnboardCommands(program, ctx) {
315
+ program
316
+ .command('onboard <host>')
317
+ .description('Guided, idempotent, resumable wizard to onboard a customer site as a governed Quox tenant')
318
+ .requiredOption('--name <customerName>', 'the org display name')
319
+ .option('--repo <owner/name>', 'the customer GitHub repo for auto-filed security issues')
320
+ .option('--target <hostOrIp>', 'the scan target (defaults to <host>)')
321
+ .option('--environment-tier <tier>', 'dev | staging | production', 'dev')
322
+ .option('--schedule-cron <expr>', 'cron cadence for the recurring scan', '0 2 * * 3')
323
+ .option('--tool <name>', 'capability tool to schedule', 'vuln_scan')
324
+ .option('--yes', 'run non-interactively: do every automatable step and print gate instructions without pausing')
325
+ .action(async (host, options) => {
326
+ await runOnboard(host, options, ctx);
327
+ });
328
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * quox open — Resolve quox:// URIs
3
+ *
4
+ * Opens a quox:// URI in the dashboard (browser) or prints the
5
+ * equivalent CLI command.
6
+ *
7
+ * CLI-3f (Stream CLI Phase 3)
8
+ */
9
+
10
+ import chalk from 'chalk';
11
+ import { resolveQuoxUri } from '../core/deeplinks.js';
12
+ import { execFile } from 'node:child_process';
13
+ import { emitCliEvent } from '../core/aee.js';
14
+
15
+ function openBrowser(url) {
16
+ const cmd = process.platform === 'darwin' ? 'open'
17
+ : process.platform === 'win32' ? 'cmd'
18
+ : 'xdg-open';
19
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
20
+ execFile(cmd, args, () => {});
21
+ }
22
+
23
+ export function registerOpenCommand(program, ctx) {
24
+ const { config, client } = ctx;
25
+
26
+ program
27
+ .command('open')
28
+ .description('Open a quox:// URI in the dashboard or show CLI equivalent')
29
+ .argument('<uri>', 'quox:// URI (e.g., quox://fleet/docker01)')
30
+ .option('--cli', 'show CLI command instead of opening browser')
31
+ .action(async (uri, opts) => {
32
+ const baseUrl = config.getBaseUrl();
33
+ const resolved = resolveQuoxUri(uri, baseUrl);
34
+
35
+ if (!resolved) {
36
+ console.error(chalk.red(`Invalid URI: ${uri}`));
37
+ console.error(chalk.dim('Format: quox://<type>/<id> (e.g., quox://fleet/docker01)'));
38
+ process.exitCode = 2;
39
+ return;
40
+ }
41
+
42
+ if (opts.cli) {
43
+ console.log(resolved.cliCommand);
44
+ } else {
45
+ console.log(chalk.dim(`Opening ${resolved.dashboardUrl}`));
46
+ openBrowser(resolved.dashboardUrl);
47
+ console.log(chalk.dim(`CLI equivalent: ${resolved.cliCommand}`));
48
+ }
49
+
50
+ emitCliEvent(client, {
51
+ command: 'open',
52
+ intent: 'quox.cli.open',
53
+ payload: { uri, type: resolved.type, id: resolved.id },
54
+ });
55
+ });
56
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Organization commands: CRUD, switch, members, divisions, audit
3
+ */
4
+
5
+ import { QuoxError } from '../core/errors.js';
6
+ import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
7
+ import { resolveOrgId } from './helpers.js';
8
+ import { resolveEndpoint } from '../api/endpoints.js';
9
+
10
+ export function registerOrgCommands(program, ctx) {
11
+ const { client, auth, config } = ctx;
12
+
13
+ const org = program
14
+ .command('org')
15
+ .description('Manage organizations');
16
+
17
+ org
18
+ .command('list')
19
+ .description('List organizations')
20
+ .action(async () => {
21
+ auth.requireAuth();
22
+ const data = await client.get(resolveEndpoint('orgs.list').path, { service: resolveEndpoint('orgs.list').service });
23
+ formatOutput({
24
+ columns: ['id', 'name', 'slug', 'plan', 'created_at'],
25
+ headers: ['ID', 'Name', 'Slug', 'Plan', 'Created'],
26
+ rows: data.orgs || data.organizations || data,
27
+ }, { format: program.opts().output });
28
+ });
29
+
30
+ org
31
+ .command('get')
32
+ .description('Get organization details')
33
+ .argument('<orgId>', 'organization ID')
34
+ .action(async (orgId) => {
35
+ auth.requireAuth();
36
+ const data = await client.get(resolveEndpoint('orgs.get', { orgId }).path, { service: resolveEndpoint('orgs.get', { orgId }).service });
37
+ const org = data.organization || data.org || data;
38
+ formatDetail(org, {
39
+ format: program.opts().output,
40
+ title: org.name || data.name,
41
+ });
42
+ });
43
+
44
+ org
45
+ .command('create')
46
+ .description('Create a new organization')
47
+ .requiredOption('--name <name>', 'organization name')
48
+ .option('--slug <slug>', 'URL-friendly slug')
49
+ .option('--description <desc>', 'organization description')
50
+ .action(async (options) => {
51
+ auth.requireAuth();
52
+ const data = await client.post(resolveEndpoint('orgs.create').path, {
53
+ service: resolveEndpoint('orgs.create').service,
54
+ body: {
55
+ name: options.name,
56
+ slug: options.slug,
57
+ description: options.description,
58
+ },
59
+ });
60
+ const created = data.organization || data.org;
61
+ printSuccess(`Created organization: ${options.name}${created?.slug ? ` (${created.slug})` : ''}`);
62
+ });
63
+
64
+ org
65
+ .command('update')
66
+ .description('Update an organization')
67
+ .argument('<orgId>', 'organization ID')
68
+ .option('--name <name>', 'new name')
69
+ .option('--description <desc>', 'new description')
70
+ .action(async (orgId, options) => {
71
+ auth.requireAuth();
72
+ const body = {};
73
+ if (options.name) body.name = options.name;
74
+ if (options.description) body.description = options.description;
75
+ await client.patch(resolveEndpoint('orgs.update', { orgId }).path, { service: resolveEndpoint('orgs.update', { orgId }).service, body });
76
+ printSuccess(`Organization updated.`);
77
+ });
78
+
79
+ org
80
+ .command('delete')
81
+ .description('Delete an organization')
82
+ .argument('<orgId>', 'organization ID')
83
+ .action(async (orgId) => {
84
+ auth.requireAuth();
85
+ await client.delete(resolveEndpoint('orgs.delete', { orgId }).path, { service: resolveEndpoint('orgs.delete', { orgId }).service });
86
+ printSuccess('Organization deleted.');
87
+ });
88
+
89
+ org
90
+ .command('switch')
91
+ .description('Switch active organization context')
92
+ .argument('<orgId>', 'organization ID or slug')
93
+ .action(async (orgId) => {
94
+ auth.requireAuth();
95
+ // The server matches memberships by org ID only. Accept a slug (or exact name) by
96
+ // resolving it against the caller's org list first; a 26-char ULID passes through.
97
+ let targetId = orgId;
98
+ if (!/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(orgId)) {
99
+ const list = await client.get(resolveEndpoint('orgs.list').path, { service: resolveEndpoint('orgs.list').service });
100
+ const orgs = Array.isArray(list) ? list : (list?.organizations || list?.orgs || list?.data || []);
101
+ const hit = orgs.find((o) => o.slug === orgId) || orgs.find((o) => String(o.name || '').toLowerCase() === orgId.toLowerCase());
102
+ if (!hit) throw new QuoxError(`No organisation with slug or name '${orgId}'. Run \`quox org list\` and pass its ID.`, 1);
103
+ targetId = hit.id;
104
+ }
105
+ // Call switch-org endpoint
106
+ const data = await client.post(resolveEndpoint('auth.switch-org').path, {
107
+ service: resolveEndpoint('auth.switch-org').service,
108
+ body: { org_id: targetId },
109
+ });
110
+
111
+ // Update stored auth
112
+ const authData = config.getAuth();
113
+ if (data.org) {
114
+ authData.org_id = data.org.id;
115
+ authData.org_name = data.org.name;
116
+ authData.org_slug = data.org.slug;
117
+ authData.org_role = data.role;
118
+ }
119
+ if (data.access_token) authData.access_token = data.access_token;
120
+ config.setAuth(authData);
121
+
122
+ printSuccess(`Switched to ${data.org?.name || orgId}`);
123
+ });
124
+
125
+ // Members subcommands
126
+ const members = org
127
+ .command('members')
128
+ .description('Manage organization members');
129
+
130
+ members
131
+ .command('list')
132
+ .description('List organization members')
133
+ .action(async () => {
134
+ auth.requireAuth();
135
+ const orgId = resolveOrgId(auth, program);
136
+ const data = await client.get(resolveEndpoint('members.list', { orgId }).path, { service: resolveEndpoint('members.list', { orgId }).service });
137
+ formatOutput({
138
+ // fleet #392: member rows key on id, not user_id.
139
+ columns: ['id', 'email', 'display_name', 'role', 'status', 'joined_at'],
140
+ headers: ['User ID', 'Email', 'Name', 'Role', 'Status', 'Joined'],
141
+ rows: data.members || data,
142
+ }, { format: program.opts().output });
143
+ });
144
+
145
+ members
146
+ .command('add')
147
+ .description('Invite a member to the organization')
148
+ .requiredOption('--email <email>', 'email to invite')
149
+ .option('--role <role>', 'role: owner, admin, builder, viewer', 'viewer')
150
+ .action(async (options) => {
151
+ auth.requireAuth();
152
+ const orgId = resolveOrgId(auth, program);
153
+ await client.post(resolveEndpoint('members.create', { orgId }).path, {
154
+ service: resolveEndpoint('members.create', { orgId }).service,
155
+ body: { email: options.email, role: options.role },
156
+ });
157
+ printSuccess(`Invited ${options.email} as ${options.role}.`);
158
+ });
159
+
160
+ members
161
+ .command('update')
162
+ .description('Update a member role')
163
+ .argument('<userId>', 'user ID')
164
+ .requiredOption('--role <role>', 'new role')
165
+ .action(async (userId, options) => {
166
+ auth.requireAuth();
167
+ const orgId = resolveOrgId(auth, program);
168
+ await client.patch(resolveEndpoint('members.update', { orgId, userId }).path, {
169
+ service: resolveEndpoint('members.update', { orgId, userId }).service,
170
+ body: { role: options.role },
171
+ });
172
+ printSuccess(`Member role updated to ${options.role}.`);
173
+ });
174
+
175
+ members
176
+ .command('remove')
177
+ .description('Remove a member from the organization')
178
+ .argument('<userId>', 'user ID')
179
+ .action(async (userId) => {
180
+ auth.requireAuth();
181
+ const orgId = resolveOrgId(auth, program);
182
+ await client.delete(resolveEndpoint('members.delete', { orgId, userId }).path, { service: resolveEndpoint('members.delete', { orgId, userId }).service });
183
+ printSuccess('Member removed.');
184
+ });
185
+
186
+ // Divisions subcommands
187
+ const divisions = org
188
+ .command('divisions')
189
+ .description('Manage divisions within the organization');
190
+
191
+ divisions
192
+ .command('list')
193
+ .description('List divisions')
194
+ .action(async () => {
195
+ auth.requireAuth();
196
+ const orgId = resolveOrgId(auth, program);
197
+ const data = await client.get(resolveEndpoint('divisions.list', { orgId }).path, { service: resolveEndpoint('divisions.list', { orgId }).service });
198
+ formatOutput({
199
+ columns: ['id', 'name', 'description', 'created_at'],
200
+ headers: ['ID', 'Name', 'Description', 'Created'],
201
+ rows: data.divisions || data,
202
+ }, { format: program.opts().output });
203
+ });
204
+
205
+ divisions
206
+ .command('create')
207
+ .description('Create a division')
208
+ .requiredOption('--name <name>', 'division name')
209
+ .option('--description <desc>', 'description')
210
+ .action(async (options) => {
211
+ auth.requireAuth();
212
+ const orgId = resolveOrgId(auth, program);
213
+ await client.post(resolveEndpoint('divisions.create', { orgId }).path, {
214
+ service: resolveEndpoint('divisions.create', { orgId }).service,
215
+ body: { name: options.name, description: options.description },
216
+ });
217
+ printSuccess(`Division "${options.name}" created.`);
218
+ });
219
+
220
+ divisions
221
+ .command('update')
222
+ .description('Update a division')
223
+ .argument('<divId>', 'division ID')
224
+ .option('--name <name>', 'new name')
225
+ .option('--description <desc>', 'new description')
226
+ .action(async (divId, options) => {
227
+ auth.requireAuth();
228
+ const orgId = resolveOrgId(auth, program);
229
+ const body = {};
230
+ if (options.name) body.name = options.name;
231
+ if (options.description) body.description = options.description;
232
+ await client.patch(resolveEndpoint('divisions.update', { orgId, divId }).path, { service: resolveEndpoint('divisions.update', { orgId, divId }).service, body });
233
+ printSuccess('Division updated.');
234
+ });
235
+
236
+ divisions
237
+ .command('delete')
238
+ .description('Delete a division')
239
+ .argument('<divId>', 'division ID')
240
+ .action(async (divId) => {
241
+ auth.requireAuth();
242
+ const orgId = resolveOrgId(auth, program);
243
+ await client.delete(resolveEndpoint('divisions.delete', { orgId, divId }).path, { service: resolveEndpoint('divisions.delete', { orgId, divId }).service });
244
+ printSuccess('Division deleted.');
245
+ });
246
+
247
+ // Audit subcommand
248
+ org
249
+ .command('audit')
250
+ .description('View organization audit log')
251
+ .option('--action <action>', 'filter by action type')
252
+ .option('--limit <n>', 'max results', '50')
253
+ .action(async (options) => {
254
+ auth.requireAuth();
255
+ const orgId = resolveOrgId(auth, program);
256
+ const query = { limit: options.limit };
257
+ if (options.action) query.action = options.action;
258
+ const data = await client.get(resolveEndpoint('audit.list', { orgId }).path, { service: resolveEndpoint('audit.list', { orgId }).service, query });
259
+ formatOutput({
260
+ // fleet #392: audit_events uses created_at/actor_email/metadata.
261
+ columns: ['created_at', 'actor_email', 'action', 'metadata'],
262
+ headers: ['Time', 'User', 'Action', 'Detail'],
263
+ rows: data.events || data.entries || data,
264
+ }, { format: program.opts().output });
265
+ });
266
+ }