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,539 @@
1
+ /**
2
+ * Plugin commands: org-scoped plugin install registry (PD-1..PD-8)
3
+ *
4
+ * quox plugin list|get|install|update|uninstall|mine
5
+ * quox plugin data list|get|set|delete
6
+ * quox plugin secrets list|set|reveal|delete
7
+ * quox plugin assign list|add|remove
8
+ * quox plugin trusted-keys list|add|remove
9
+ *
10
+ * Backend: services/auth/routes/plugins.js, mounted org-scoped at
11
+ * `/api/orgs/:orgId/...` (server.js). Install/update/uninstall and all
12
+ * secrets writes are admin-only per the PD stream's locked "admin-only
13
+ * install model" decision; read endpoints require org membership only.
14
+ */
15
+
16
+ import { registerPluginDoctor } from './plugin-doctor.js';
17
+ import { readFileSync } from 'node:fs';
18
+ import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
19
+ import { resolveOrgId, parseJSON } from './helpers.js';
20
+ import { resolveEndpoint } from '../api/endpoints.js';
21
+
22
+ /** Read + JSON.parse a file, wrapping errors with the file path for context. */
23
+ function readJsonFile(filePath, label) {
24
+ let raw;
25
+ try {
26
+ raw = readFileSync(filePath, 'utf-8');
27
+ } catch (err) {
28
+ throw new Error(`Could not read ${label} file "${filePath}": ${err.message}`);
29
+ }
30
+ return parseJSON(raw, label);
31
+ }
32
+
33
+ /** Read a file and base64-encode its bytes (bundle_b64 / css_b64 shape). */
34
+ function readBase64File(filePath, label) {
35
+ try {
36
+ return readFileSync(filePath).toString('base64');
37
+ } catch (err) {
38
+ throw new Error(`Could not read ${label} file "${filePath}": ${err.message}`);
39
+ }
40
+ }
41
+
42
+ /** Best-effort JSON parse for a data value; falls back to the raw string. */
43
+ function coerceDataValue(raw) {
44
+ try {
45
+ return JSON.parse(raw);
46
+ } catch {
47
+ return raw;
48
+ }
49
+ }
50
+
51
+ async function confirmOrSkip(program, message) {
52
+ const { default: inquirer } = await import('inquirer');
53
+ const ans = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message, default: false }]);
54
+ return ans.confirm;
55
+ }
56
+
57
+ export function registerPluginCommands(program, ctx) {
58
+ const { client, auth } = ctx;
59
+
60
+ const plugin = program
61
+ .command('plugin')
62
+ .description('Manage org-installed plugins');
63
+
64
+ registerPluginDoctor(plugin);
65
+
66
+ // ── plugin licence ──────────────────────────────────────────────────────────
67
+
68
+ plugin
69
+ .command('licence')
70
+ .alias('license')
71
+ .description('Show whether a plugin is licence governed and entitled for this org (band, org id, entitlement errors)')
72
+ .argument('<pluginId>', 'plugin id, e.g. notesq')
73
+ .action(async (pluginId) => {
74
+ auth.requireAuth();
75
+ const ep = resolveEndpoint('setup.licensing.entitlements.list');
76
+ const data = await client.get(ep.path, { service: ep.service });
77
+ const entitlements = Array.isArray(data.entitlements) ? data.entitlements : [];
78
+ const governed = (Array.isArray(data.governed) ? data.governed : []).includes(pluginId);
79
+ const match = entitlements.find((e) => e === pluginId || String(e).startsWith(`${pluginId}@`)) || null;
80
+ // `entitlements` is the resolved (band-stripped) list; the band string lives on the licence itself.
81
+ const licenceEnts = (Array.isArray(data.licenses) ? data.licenses : []).flatMap((l) => (Array.isArray(l.entitlements) ? l.entitlements : []));
82
+ const banded = licenceEnts.find((e) => String(e).startsWith(`${pluginId}@`)) || null;
83
+ const bandInfo = (data.org_bands && data.org_bands[pluginId]) || null;
84
+ // A plugin outside the governed set is never licence gated (same rule as the server).
85
+ const entitled = governed ? match !== null : true;
86
+ const summary = {
87
+ plugin: pluginId,
88
+ governed,
89
+ entitled,
90
+ band: banded || (match && match.includes('@') ? match : null),
91
+ seats_used: bandInfo && typeof bandInfo.assigned_count === 'number' ? bandInfo.assigned_count : null,
92
+ seats_allowed: bandInfo && typeof bandInfo.allowance === 'number' ? bandInfo.allowance : null,
93
+ seat_holders: bandInfo && Array.isArray(bandInfo.assigned_orgs) ? bandInfo.assigned_orgs : [],
94
+ org_id: data.org_id || null,
95
+ org_licensed: bandInfo ? bandInfo.org_licensed ?? null : null,
96
+ entitlement_errors: Array.isArray(data.entitlement_errors) ? data.entitlement_errors : [],
97
+ };
98
+ if (program.opts().output === 'json') {
99
+ console.log(JSON.stringify(summary, null, 2));
100
+ return;
101
+ }
102
+ formatDetail({
103
+ plugin: summary.plugin,
104
+ governed: summary.governed ? 'yes' : 'no',
105
+ entitled: summary.entitled ? 'yes' : 'no',
106
+ band: summary.band || '(none)',
107
+ org_seats: summary.seats_allowed === null ? '(no band)' : `${summary.seats_used} of ${summary.seats_allowed} used`,
108
+ org_id: summary.org_id || '(instance scoped)',
109
+ org_licensed: summary.org_licensed === null ? '(unknown)' : summary.org_licensed ? 'yes' : 'no',
110
+ entitlement_errors: summary.entitlement_errors.length
111
+ ? summary.entitlement_errors.map((e) => (typeof e === 'string' ? e : JSON.stringify(e))).join('; ')
112
+ : '(none)',
113
+ }, { title: `Licence: ${pluginId}` });
114
+ });
115
+
116
+ // ── plugin list ─────────────────────────────────────────────────────────────
117
+
118
+ plugin
119
+ .command('list')
120
+ .description('List installed plugins for the active org')
121
+ .action(async () => {
122
+ auth.requireAuth();
123
+ const orgId = resolveOrgId(auth, program);
124
+ const ep = resolveEndpoint('plugins.list', { orgId });
125
+ const data = await client.get(ep.path, { service: ep.service });
126
+ const plugins = data.plugins || data || [];
127
+ formatOutput({
128
+ columns: ['plugin_id', 'version', 'is_enabled', 'installed_by', 'installed_at', 'upgraded_at'],
129
+ headers: ['Plugin ID', 'Version', 'Enabled', 'Installed By', 'Installed', 'Upgraded'],
130
+ rows: plugins,
131
+ }, { format: program.opts().output, emptyMessage: 'No plugins installed.' });
132
+ });
133
+
134
+ // ── plugin get ──────────────────────────────────────────────────────────────
135
+
136
+ plugin
137
+ .command('get')
138
+ .description('Show plugin install details')
139
+ .argument('<pluginId>', 'plugin ID (slug) or install-record id')
140
+ .action(async (pluginId) => {
141
+ auth.requireAuth();
142
+ const orgId = resolveOrgId(auth, program);
143
+ const getFor = (id) => {
144
+ const ep = resolveEndpoint('plugins.get', { orgId, pluginId: id });
145
+ return client.get(ep.path, { service: ep.service });
146
+ };
147
+ let resolvedId = pluginId;
148
+ let data;
149
+ try {
150
+ data = await getFor(resolvedId);
151
+ } catch (err) {
152
+ // quox-plugin-sdk#2(B): `plugin list --json` emits the install-record
153
+ // ULID as `id`, but get resolves by plugin_id (slug) and 404s on the
154
+ // ULID — so `plugin list --json | jq .id | xargs quox plugin get` broke.
155
+ // On a 404, treat the arg as an install id and map it to its plugin_id
156
+ // via the list, so BOTH identifiers round-trip. Slugs stay the fast path.
157
+ if (err && err.status === 404) {
158
+ const listEp = resolveEndpoint('plugins.list', { orgId });
159
+ const listData = await client.get(listEp.path, { service: listEp.service });
160
+ const rows = listData.plugins || listData || [];
161
+ const match = rows.find(p => p.id === pluginId || p.install_id === pluginId);
162
+ if (match && match.plugin_id && match.plugin_id !== pluginId) {
163
+ resolvedId = match.plugin_id;
164
+ data = await getFor(resolvedId);
165
+ } else {
166
+ throw err;
167
+ }
168
+ } else {
169
+ throw err;
170
+ }
171
+ }
172
+ formatDetail(data.plugin || data, {
173
+ format: program.opts().output,
174
+ title: `Plugin: ${resolvedId}`,
175
+ fields: ['id', 'plugin_id', 'version', 'is_enabled', 'bundle_sha256', 'installed_by', 'installed_at', 'upgraded_at', 'manifest', 'signature'],
176
+ labels: {
177
+ id: 'Install ID', plugin_id: 'Plugin ID', version: 'Version', is_enabled: 'Enabled',
178
+ bundle_sha256: 'Bundle SHA256', installed_by: 'Installed By', installed_at: 'Installed',
179
+ upgraded_at: 'Upgraded', manifest: 'Manifest', signature: 'Signature',
180
+ },
181
+ });
182
+ });
183
+
184
+ // ── plugin install ──────────────────────────────────────────────────────────
185
+
186
+ plugin
187
+ .command('install')
188
+ .description('Install a plugin (admin only)')
189
+ .requiredOption('--manifest <file>', 'path to a manifest JSON file (must include id, name, version)')
190
+ .requiredOption('--bundle <file>', 'path to the plugin bundle JS file')
191
+ .option('--css <file>', 'path to a CSS file for the plugin')
192
+ .option('--signature <file>', 'path to a signature JSON file (required if the org has trusted keys)')
193
+ .action(async (options) => {
194
+ auth.requireAuth();
195
+ const orgId = resolveOrgId(auth, program);
196
+
197
+ const manifest = readJsonFile(options.manifest, '--manifest');
198
+ const body = { manifest, bundle_b64: readBase64File(options.bundle, '--bundle') };
199
+ if (options.css) body.css_b64 = readBase64File(options.css, '--css');
200
+ if (options.signature) body.signature = readJsonFile(options.signature, '--signature');
201
+
202
+ const ep = resolveEndpoint('plugins.install', { orgId });
203
+ const data = await client.post(ep.path, { service: ep.service, body });
204
+ printSuccess(`Plugin "${manifest.id}" installed.`);
205
+ formatDetail(data.plugin || data, { format: program.opts().output });
206
+ });
207
+
208
+ // ── plugin update ───────────────────────────────────────────────────────────
209
+
210
+ plugin
211
+ .command('update')
212
+ .description('Upgrade a plugin bundle and/or toggle its enabled state (admin only)')
213
+ .argument('<pluginId>', 'plugin ID')
214
+ .option('--enable', 'enable the plugin')
215
+ .option('--disable', 'disable the plugin')
216
+ .option('--manifest <file>', 'path to a new manifest JSON file (required together with --bundle)')
217
+ .option('--bundle <file>', 'path to a new bundle JS file (required together with --manifest)')
218
+ .option('--css <file>', 'path to a new CSS file')
219
+ .option('--signature <file>', 'path to a new signature JSON file')
220
+ .action(async (pluginId, options) => {
221
+ auth.requireAuth();
222
+ const orgId = resolveOrgId(auth, program);
223
+
224
+ if (options.enable && options.disable) {
225
+ throw new Error('Specify only one of --enable or --disable.');
226
+ }
227
+
228
+ const body = {};
229
+ if (options.enable) body.is_enabled = true;
230
+ if (options.disable) body.is_enabled = false;
231
+
232
+ if (options.manifest || options.bundle) {
233
+ if (!options.manifest || !options.bundle) {
234
+ throw new Error('--manifest and --bundle must be provided together for an upgrade.');
235
+ }
236
+ body.manifest = readJsonFile(options.manifest, '--manifest');
237
+ body.bundle_b64 = readBase64File(options.bundle, '--bundle');
238
+ if (options.css) body.css_b64 = readBase64File(options.css, '--css');
239
+ if (options.signature) body.signature = readJsonFile(options.signature, '--signature');
240
+ }
241
+
242
+ if (Object.keys(body).length === 0) {
243
+ throw new Error('Specify --enable, --disable, or --manifest/--bundle to upgrade.');
244
+ }
245
+
246
+ const ep = resolveEndpoint('plugins.update', { orgId, pluginId });
247
+ const data = await client.patch(ep.path, { service: ep.service, body });
248
+ printSuccess('Plugin updated.');
249
+ formatDetail(data.plugin || data, { format: program.opts().output });
250
+ });
251
+
252
+ // ── plugin uninstall ────────────────────────────────────────────────────────
253
+
254
+ plugin
255
+ .command('uninstall')
256
+ .description('Uninstall a plugin (admin only)')
257
+ .argument('<pluginId>', 'plugin ID')
258
+ .option('--force', 'skip confirmation')
259
+ .action(async (pluginId, options) => {
260
+ auth.requireAuth();
261
+ const orgId = resolveOrgId(auth, program);
262
+
263
+ if (!options.force && !program.opts().yes) {
264
+ const ok = await confirmOrSkip(program, `Uninstall plugin ${pluginId}? This cannot be undone.`);
265
+ if (!ok) { printInfo('Cancelled.'); return; }
266
+ }
267
+
268
+ const ep = resolveEndpoint('plugins.uninstall', { orgId, pluginId });
269
+ await client.delete(ep.path, { service: ep.service });
270
+ printSuccess('Plugin uninstalled.');
271
+ });
272
+
273
+ // ── plugin mine ─────────────────────────────────────────────────────────────
274
+
275
+ plugin
276
+ .command('mine')
277
+ .description('List plugins visible to the current user (respects assignments)')
278
+ .action(async () => {
279
+ auth.requireAuth();
280
+ const orgId = resolveOrgId(auth, program);
281
+ const ep = resolveEndpoint('plugins.mine.list', { orgId });
282
+ const data = await client.get(ep.path, { service: ep.service });
283
+ const plugins = data.plugins || data || [];
284
+ formatOutput({
285
+ columns: ['plugin_id', 'version', 'is_enabled', 'installed_at'],
286
+ headers: ['Plugin ID', 'Version', 'Enabled', 'Installed'],
287
+ rows: plugins,
288
+ }, { format: program.opts().output, emptyMessage: 'No plugins visible to you in this org.' });
289
+ });
290
+
291
+ // ── plugin data ─────────────────────────────────────────────────────────────
292
+
293
+ const data = plugin
294
+ .command('data')
295
+ .description('Manage plugin-scoped durable data (KV)');
296
+
297
+ data
298
+ .command('list')
299
+ .description('List data keys for a plugin')
300
+ .argument('<pluginId>', 'plugin ID')
301
+ .option('--scope <scope>', 'org|user', 'org')
302
+ .action(async (pluginId, options) => {
303
+ auth.requireAuth();
304
+ const orgId = resolveOrgId(auth, program);
305
+ const ep = resolveEndpoint('plugins.data.list', { orgId, pluginId });
306
+ const result = await client.get(ep.path, { service: ep.service, query: { scope: options.scope } });
307
+ const keys = result.keys || [];
308
+ if (keys.length === 0) {
309
+ printInfo('No data keys found.');
310
+ return;
311
+ }
312
+ formatOutput({ columns: ['key'], headers: ['Key'], rows: keys.map(key => ({ key })) }, { format: program.opts().output });
313
+ });
314
+
315
+ data
316
+ .command('get')
317
+ .description('Get a plugin data value')
318
+ .argument('<pluginId>', 'plugin ID')
319
+ .argument('<key>', 'data key')
320
+ .option('--scope <scope>', 'org|user', 'org')
321
+ .action(async (pluginId, key, options) => {
322
+ auth.requireAuth();
323
+ const orgId = resolveOrgId(auth, program);
324
+ const ep = resolveEndpoint('plugins.data.get', { orgId, pluginId, key });
325
+ const row = await client.get(ep.path, { service: ep.service, query: { scope: options.scope } });
326
+ formatDetail(row, { format: program.opts().output });
327
+ });
328
+
329
+ data
330
+ .command('set')
331
+ .description('Set a plugin data value')
332
+ .argument('<pluginId>', 'plugin ID')
333
+ .argument('<key>', 'data key')
334
+ .requiredOption('--value <value>', 'value to store (JSON is parsed automatically; otherwise stored as a string)')
335
+ .option('--scope <scope>', 'org|user', 'org')
336
+ .action(async (pluginId, key, options) => {
337
+ auth.requireAuth();
338
+ const orgId = resolveOrgId(auth, program);
339
+ const ep = resolveEndpoint('plugins.data.set', { orgId, pluginId, key });
340
+ const row = await client.put(ep.path, {
341
+ service: ep.service,
342
+ query: { scope: options.scope },
343
+ body: { value: coerceDataValue(options.value) },
344
+ });
345
+ printSuccess(`Data key "${key}" set.`);
346
+ formatDetail(row, { format: program.opts().output });
347
+ });
348
+
349
+ data
350
+ .command('delete')
351
+ .description('Delete a plugin data value')
352
+ .argument('<pluginId>', 'plugin ID')
353
+ .argument('<key>', 'data key')
354
+ .option('--scope <scope>', 'org|user', 'org')
355
+ .action(async (pluginId, key, options) => {
356
+ auth.requireAuth();
357
+ const orgId = resolveOrgId(auth, program);
358
+ const ep = resolveEndpoint('plugins.data.delete', { orgId, pluginId, key });
359
+ await client.delete(ep.path, { service: ep.service, query: { scope: options.scope } });
360
+ printSuccess('Data key deleted.');
361
+ });
362
+
363
+ // ── plugin secrets ──────────────────────────────────────────────────────────
364
+
365
+ const secrets = plugin
366
+ .command('secrets')
367
+ .description('Manage plugin-scoped encrypted secrets');
368
+
369
+ secrets
370
+ .command('list')
371
+ .description('List secret metadata for a plugin (no plaintext)')
372
+ .argument('<pluginId>', 'plugin ID')
373
+ .action(async (pluginId) => {
374
+ auth.requireAuth();
375
+ const orgId = resolveOrgId(auth, program);
376
+ const ep = resolveEndpoint('plugins.secrets.list', { orgId, pluginId });
377
+ const result = await client.get(ep.path, { service: ep.service });
378
+ const rows = result.secrets || result || [];
379
+ formatOutput({
380
+ columns: ['name', 'description', 'created_by', 'created_at', 'rotated_at'],
381
+ headers: ['Name', 'Description', 'Created By', 'Created', 'Rotated'],
382
+ rows,
383
+ }, { format: program.opts().output, emptyMessage: 'No secrets set for this plugin.' });
384
+ });
385
+
386
+ secrets
387
+ .command('set')
388
+ .description('Set (or rotate) a plugin secret (admin only)')
389
+ .argument('<pluginId>', 'plugin ID')
390
+ .argument('<name>', 'secret name')
391
+ .requiredOption('--value <text>', 'secret value')
392
+ .option('--description <text>', 'secret description')
393
+ .action(async (pluginId, name, options) => {
394
+ auth.requireAuth();
395
+ const orgId = resolveOrgId(auth, program);
396
+ const body = { value: options.value };
397
+ if (options.description) body.description = options.description;
398
+ const ep = resolveEndpoint('plugins.secrets.set', { orgId, pluginId, name });
399
+ const result = await client.put(ep.path, { service: ep.service, body });
400
+ printSuccess(`Secret "${name}" set.`);
401
+ formatDetail(result.secret || result, { format: program.opts().output });
402
+ });
403
+
404
+ secrets
405
+ .command('reveal')
406
+ .description('Reveal a plugin secret\'s plaintext value (audited)')
407
+ .argument('<pluginId>', 'plugin ID')
408
+ .argument('<name>', 'secret name')
409
+ .action(async (pluginId, name) => {
410
+ auth.requireAuth();
411
+ const orgId = resolveOrgId(auth, program);
412
+ printWarning('This reveals a plaintext secret and is recorded in the audit log.');
413
+ const ep = resolveEndpoint('plugins.secrets.reveal', { orgId, pluginId, name });
414
+ const result = await client.post(ep.path, { service: ep.service });
415
+ formatDetail(result, { format: program.opts().output });
416
+ });
417
+
418
+ secrets
419
+ .command('delete')
420
+ .description('Delete a plugin secret (admin only)')
421
+ .argument('<pluginId>', 'plugin ID')
422
+ .argument('<name>', 'secret name')
423
+ .action(async (pluginId, name) => {
424
+ auth.requireAuth();
425
+ const orgId = resolveOrgId(auth, program);
426
+ const ep = resolveEndpoint('plugins.secrets.delete', { orgId, pluginId, name });
427
+ await client.delete(ep.path, { service: ep.service });
428
+ printSuccess('Secret deleted.');
429
+ });
430
+
431
+ // ── plugin assign ───────────────────────────────────────────────────────────
432
+
433
+ const assign = plugin
434
+ .command('assign')
435
+ .description('Manage plugin visibility assignments (admin only)');
436
+
437
+ assign
438
+ .command('list')
439
+ .description('List assignments for a plugin')
440
+ .argument('<pluginId>', 'plugin ID')
441
+ .action(async (pluginId) => {
442
+ auth.requireAuth();
443
+ const orgId = resolveOrgId(auth, program);
444
+ const ep = resolveEndpoint('plugins.assignments.list', { orgId, pluginId });
445
+ const result = await client.get(ep.path, { service: ep.service });
446
+ const rows = result.assignments || result || [];
447
+ formatOutput({
448
+ columns: ['assignee_type', 'assignee_id', 'assigned_by', 'assigned_at'],
449
+ headers: ['Type', 'Assignee', 'Assigned By', 'Assigned'],
450
+ rows,
451
+ }, { format: program.opts().output, emptyMessage: 'No assignments set (plugin uses default visibility).' });
452
+ });
453
+
454
+ assign
455
+ .command('add')
456
+ .description('Assign a plugin to all org members, a user, or a team')
457
+ .argument('<pluginId>', 'plugin ID')
458
+ .requiredOption('--type <assigneeType>', 'all_org_members|user|team')
459
+ .option('--id <assigneeId>', 'assignee ID (user or team ID; omit for all_org_members)')
460
+ .action(async (pluginId, options) => {
461
+ auth.requireAuth();
462
+ const orgId = resolveOrgId(auth, program);
463
+ const body = { assignee_type: options.type };
464
+ if (options.id) body.assignee_id = options.id;
465
+ const ep = resolveEndpoint('plugins.assignments.create', { orgId, pluginId });
466
+ const result = await client.post(ep.path, { service: ep.service, body });
467
+ printSuccess('Assignment added.');
468
+ formatDetail(result.assignment || result, { format: program.opts().output });
469
+ });
470
+
471
+ assign
472
+ .command('remove')
473
+ .description('Remove a plugin assignment')
474
+ .argument('<pluginId>', 'plugin ID')
475
+ .argument('<assigneeType>', 'all_org_members|user|team')
476
+ .argument('[assigneeId]', 'assignee ID (omit for all_org_members)')
477
+ .action(async (pluginId, assigneeType, assigneeId) => {
478
+ auth.requireAuth();
479
+ const orgId = resolveOrgId(auth, program);
480
+ const ep = resolveEndpoint('plugins.assignments.delete', { orgId, pluginId, assigneeType, assigneeId });
481
+ await client.delete(ep.path, { service: ep.service });
482
+ printSuccess('Assignment removed.');
483
+ });
484
+
485
+ // ── plugin trusted-keys ─────────────────────────────────────────────────────
486
+
487
+ const trustedKeys = plugin
488
+ .command('trusted-keys')
489
+ .description('Manage per-org trusted Ed25519 signing keys (admin only)');
490
+
491
+ trustedKeys
492
+ .command('list')
493
+ .description('List trusted keys')
494
+ .action(async () => {
495
+ auth.requireAuth();
496
+ const orgId = resolveOrgId(auth, program);
497
+ const ep = resolveEndpoint('plugins.trusted-keys.list', { orgId });
498
+ const result = await client.get(ep.path, { service: ep.service });
499
+ const rows = result.trusted_keys || result || [];
500
+ formatOutput({
501
+ columns: ['id', 'label', 'public_key_hex', 'added_by', 'added_at'],
502
+ headers: ['ID', 'Label', 'Public Key', 'Added By', 'Added'],
503
+ rows,
504
+ }, { format: program.opts().output, emptyMessage: 'No trusted keys configured (unsigned installs are accepted).' });
505
+ });
506
+
507
+ trustedKeys
508
+ .command('add')
509
+ .description('Add a trusted signing key')
510
+ .requiredOption('--label <label>', 'key label')
511
+ .requiredOption('--public-key <hex>', 'Ed25519 public key, hex-encoded')
512
+ .action(async (options) => {
513
+ auth.requireAuth();
514
+ const orgId = resolveOrgId(auth, program);
515
+ const body = { label: options.label, public_key_hex: options.publicKey };
516
+ const ep = resolveEndpoint('plugins.trusted-keys.create', { orgId });
517
+ const result = await client.post(ep.path, { service: ep.service, body });
518
+ printSuccess('Trusted key added.');
519
+ formatDetail(result.trusted_key || result, { format: program.opts().output });
520
+ });
521
+
522
+ trustedKeys
523
+ .command('remove')
524
+ .description('Remove a trusted signing key')
525
+ .argument('<keyId>', 'trusted key ID')
526
+ .option('--confirm-unsigned', 'WARNING: required to remove the LAST trusted key; the org will then accept UNSIGNED plugin bundles')
527
+ .action(async (keyId, opts) => {
528
+ auth.requireAuth();
529
+ const orgId = resolveOrgId(auth, program);
530
+ const ep = resolveEndpoint('plugins.trusted-keys.delete', { orgId, keyId });
531
+ const reqOpts = { service: ep.service };
532
+ if (opts && opts.confirmUnsigned) {
533
+ console.error('WARNING: confirming downgrade. This org will accept UNSIGNED plugin bundles after this key is removed.');
534
+ reqOpts.query = { confirm_unsigned: '1' };
535
+ }
536
+ await client.delete(ep.path, reqOpts);
537
+ printSuccess('Trusted key removed.');
538
+ });
539
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `quox policy` — the governance compiler from the terminal (OPGOV quox#582).
3
+ *
4
+ * compile: plain-language rule -> deterministic quox-policy.v1 DRAFT, with
5
+ * the compiled policy printed in full for review (a policy nobody read is
6
+ * not governance). arm: moves a reviewed draft to armed; the collector
7
+ * refuses arming without a logged-in human. Honesty: enforcement is phase 3
8
+ * (IMPACT #491) and is NOT wired; an armed policy is a reviewed intent, not
9
+ * yet a running gate, and this command says so.
10
+ */
11
+
12
+ import { resolveEndpoint } from '../api/endpoints.js';
13
+ import { resolveOrgId } from './helpers.js';
14
+
15
+ function isJsonOutput(program) {
16
+ const o = program.opts();
17
+ return o.json === true || o.output === 'json';
18
+ }
19
+
20
+ const NOT_ENFORCED_NOTE = 'note: enforcement is phase 3 (IMPACT #491) and is not wired yet; armed = reviewed intent, not a running gate.';
21
+
22
+ export function registerPolicyCommands(program, ctx) {
23
+ const { client, auth } = ctx;
24
+
25
+ const p = program
26
+ .command('policy')
27
+ .description('Governance compiler: plain-language rule -> reviewed, armable policy (OPGOV)');
28
+
29
+ p.command('compile <rule>')
30
+ .description('Compile a plain-language rule to a quox-policy.v1 draft (deterministic grammar; out-of-grammar rules are refused, never approximated)')
31
+ .action(async (rule) => {
32
+ const orgId = resolveOrgId(auth, program);
33
+ const ep = resolveEndpoint('governance.policy.compile', { orgId });
34
+ try {
35
+ const data = await client.post(ep.path, { service: ep.service, body: { rule } });
36
+ if (isJsonOutput(program)) { console.log(JSON.stringify(data, null, 2)); return; }
37
+ const rec = data.record;
38
+ console.log(`compiled DRAFT ${rec.id} (${rec.policy.effect})`);
39
+ console.log(JSON.stringify(rec.policy, null, 2));
40
+ console.log(`review it, then: quox policy arm ${rec.id}`);
41
+ console.log(NOT_ENFORCED_NOTE);
42
+ } catch (err) {
43
+ const msg = err?.message || String(err);
44
+ if (/422|unknown/i.test(msg)) {
45
+ console.log('NOT COMPILED: the rule is outside the deterministic grammar and was refused rather than approximated.');
46
+ }
47
+ throw err;
48
+ }
49
+ });
50
+
51
+ p.command('list')
52
+ .description('List compiled policies for this org')
53
+ .option('--state <s>', 'draft | armed | retired')
54
+ .action(async (opts) => {
55
+ const orgId = resolveOrgId(auth, program);
56
+ const ep = resolveEndpoint('governance.policy.list', { orgId, state: opts.state });
57
+ const data = await client.get(ep.path, { service: ep.service });
58
+ if (isJsonOutput(program)) { console.log(JSON.stringify(data, null, 2)); return; }
59
+ const items = data.policies || [];
60
+ if (!items.length) { console.log('No policies. Compile one: quox policy compile "<rule>"'); return; }
61
+ for (const r of items) {
62
+ console.log(`${r.id} [${r.state}] ${r.policy?.effect ?? '?'} ${r.rule_text}${r.approved_by ? ` (armed by ${r.approved_by})` : ''}`);
63
+ }
64
+ console.log(NOT_ENFORCED_NOTE);
65
+ });
66
+
67
+ p.command('arm <id>')
68
+ .description('Arm a reviewed draft (requires a logged-in human; service keys are refused by design)')
69
+ .action(async (id) => {
70
+ const orgId = resolveOrgId(auth, program);
71
+ const ep = resolveEndpoint('governance.policy.arm', { orgId, id });
72
+ const data = await client.post(ep.path, { service: ep.service, body: {} });
73
+ if (isJsonOutput(program)) { console.log(JSON.stringify(data, null, 2)); return; }
74
+ const rec = data.record;
75
+ console.log(`ARMED ${rec.id} (${rec.policy.effect}) approved by ${rec.approved_by}`);
76
+ console.log(NOT_ENFORCED_NOTE);
77
+ });
78
+ }