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.
- package/LICENSE +35 -0
- package/README.md +142 -2
- package/bin/quox.js +292 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +125 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agents.js +56 -0
- package/lib/api/endpoints/assistants.js +50 -0
- package/lib/api/endpoints/audit.js +14 -0
- package/lib/api/endpoints/auth.js +50 -0
- package/lib/api/endpoints/awareness.js +34 -0
- package/lib/api/endpoints/backup-schedules.js +20 -0
- package/lib/api/endpoints/backups.js +86 -0
- package/lib/api/endpoints/brain.js +140 -0
- package/lib/api/endpoints/budgets.js +62 -0
- package/lib/api/endpoints/calendarq.js +71 -0
- package/lib/api/endpoints/catalog.js +14 -0
- package/lib/api/endpoints/chat.js +54 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +66 -0
- package/lib/api/endpoints/conversations.js +32 -0
- package/lib/api/endpoints/custom-roles.js +92 -0
- package/lib/api/endpoints/divisions.js +56 -0
- package/lib/api/endpoints/domains.js +38 -0
- package/lib/api/endpoints/engagement.js +231 -0
- package/lib/api/endpoints/engine.js +11 -0
- package/lib/api/endpoints/entity.js +50 -0
- package/lib/api/endpoints/executions.js +32 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +26 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +36 -0
- package/lib/api/endpoints/inbox.js +32 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +228 -0
- package/lib/api/endpoints/invites.js +19 -0
- package/lib/api/endpoints/llm-policy.js +43 -0
- package/lib/api/endpoints/lucid.js +89 -0
- package/lib/api/endpoints/members.js +32 -0
- package/lib/api/endpoints/memory.js +75 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mon.js +178 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +62 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +134 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/prompts.js +69 -0
- package/lib/api/endpoints/qlarity.js +122 -0
- package/lib/api/endpoints/quoxbox.js +55 -0
- package/lib/api/endpoints/quoxchat.js +54 -0
- package/lib/api/endpoints/quoxflow.js +32 -0
- package/lib/api/endpoints/quoxplan.js +114 -0
- package/lib/api/endpoints/quoxseo.js +108 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +46 -0
- package/lib/api/endpoints/scim.js +46 -0
- package/lib/api/endpoints/scopes.js +14 -0
- package/lib/api/endpoints/security.js +69 -0
- package/lib/api/endpoints/setup.js +90 -0
- package/lib/api/endpoints/skills.js +180 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/tags.js +32 -0
- package/lib/api/endpoints/teams.js +56 -0
- package/lib/api/endpoints/telephony.js +46 -0
- package/lib/api/endpoints/tg.js +56 -0
- package/lib/api/endpoints/tools.js +50 -0
- package/lib/api/endpoints/traces.js +157 -0
- package/lib/api/endpoints/triggers.js +70 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +499 -0
- package/lib/api/endpoints/vault.js +192 -0
- package/lib/api/endpoints/voice.js +22 -0
- package/lib/api/endpoints/volt.js +84 -0
- package/lib/api/endpoints/ward.js +40 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +162 -0
- package/lib/api/endpoints/x.js +24 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/admin.js +359 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +201 -0
- package/lib/commands/agentAttach.js +1440 -0
- package/lib/commands/approvals.js +47 -0
- package/lib/commands/assistant.js +114 -0
- package/lib/commands/auth.js +458 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +373 -0
- package/lib/commands/brain.js +994 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +156 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +240 -0
- package/lib/commands/claude.js +354 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +967 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +74 -0
- package/lib/commands/department.js +75 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +314 -0
- package/lib/commands/devSupervisor.js +1050 -0
- package/lib/commands/discord.js +96 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +277 -0
- package/lib/commands/engagement.js +586 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/file.js +128 -0
- package/lib/commands/fleet.js +882 -0
- package/lib/commands/governance.js +638 -0
- package/lib/commands/gripe.js +324 -0
- package/lib/commands/hat.js +205 -0
- package/lib/commands/helpers.js +79 -0
- package/lib/commands/inbox.js +122 -0
- package/lib/commands/instagram.js +264 -0
- package/lib/commands/integration.js +100 -0
- package/lib/commands/inventory.js +115 -0
- package/lib/commands/invites.js +45 -0
- package/lib/commands/licence.js +280 -0
- package/lib/commands/linkedin.js +178 -0
- package/lib/commands/llm-policy.js +117 -0
- package/lib/commands/llm.js +45 -0
- package/lib/commands/lucid.js +591 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +284 -0
- package/lib/commands/mon.js +514 -0
- package/lib/commands/monitoring.js +86 -0
- package/lib/commands/motion.js +22 -0
- package/lib/commands/notes.js +1246 -0
- package/lib/commands/notesBatch.js +160 -0
- package/lib/commands/notesBridges.js +56 -0
- package/lib/commands/notesLinks.js +60 -0
- package/lib/commands/notification.js +57 -0
- package/lib/commands/nucleus.js +92 -0
- package/lib/commands/objective.js +229 -0
- package/lib/commands/onboard.js +328 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +266 -0
- package/lib/commands/plan.js +906 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +539 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/qlarity.js +690 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +266 -0
- package/lib/commands/quoxseo.js +340 -0
- package/lib/commands/reach.js +158 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/run.js +132 -0
- package/lib/commands/safety.js +107 -0
- package/lib/commands/scim.js +88 -0
- package/lib/commands/scope.js +58 -0
- package/lib/commands/security.js +157 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +543 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +644 -0
- package/lib/commands/slot.js +1198 -0
- package/lib/commands/sso.js +229 -0
- package/lib/commands/status.js +137 -0
- package/lib/commands/tag.js +91 -0
- package/lib/commands/team.js +140 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tool.js +216 -0
- package/lib/commands/trace.js +525 -0
- package/lib/commands/trigger.js +128 -0
- package/lib/commands/vault.js +1424 -0
- package/lib/commands/voice.js +182 -0
- package/lib/commands/volt.js +528 -0
- package/lib/commands/watch.js +115 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/workflow.js +503 -0
- package/lib/commands/x.js +439 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/aee.js +53 -0
- package/lib/core/agentAlias.js +163 -0
- package/lib/core/approvalContext.js +61 -0
- package/lib/core/auth.js +350 -0
- package/lib/core/banner.js +143 -0
- package/lib/core/brainPrime.js +90 -0
- package/lib/core/callerSource.js +129 -0
- package/lib/core/claudeLs.js +94 -0
- package/lib/core/client.js +584 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +551 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +219 -0
- package/lib/core/errors.js +132 -0
- package/lib/core/global-flags.js +90 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/lazyCommands.js +119 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/output.js +239 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/residentAlarm.js +282 -0
- package/lib/core/residentAuth.js +243 -0
- package/lib/core/residentPump.js +229 -0
- package/lib/core/residentSeed.js +104 -0
- package/lib/core/sessionHeartbeat.js +192 -0
- package/lib/core/sessionsOverview.js +160 -0
- package/lib/core/slotKeeper.js +282 -0
- package/lib/core/supervisorEngine.js +864 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/websiteSession.js +148 -0
- package/lib/lucid/LucidIntro.jsx +105 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +104 -0
- package/lib/quoxon/api.js +473 -0
- package/lib/quoxon/app.js +1262 -0
- package/lib/quoxon/capabilities.js +75 -0
- package/lib/quoxon/commands.js +154 -0
- package/lib/quoxon/connect.js +467 -0
- package/lib/quoxon/connectCard.js +28 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +33 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +35 -0
- package/lib/quoxon/egg.js +36 -0
- package/lib/quoxon/events.js +458 -0
- package/lib/quoxon/gallery.js +223 -0
- package/lib/quoxon/indicator.js +193 -0
- package/lib/quoxon/intent.js +41 -0
- package/lib/quoxon/keys.js +121 -0
- package/lib/quoxon/main.js +244 -0
- package/lib/quoxon/markdown.js +317 -0
- package/lib/quoxon/model.js +205 -0
- package/lib/quoxon/motion/clock.js +69 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +170 -0
- package/lib/quoxon/objects/index.js +85 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/proof.js +110 -0
- package/lib/quoxon/objects/skill.js +105 -0
- package/lib/quoxon/objects/view.js +76 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +235 -0
- package/lib/quoxon/onboarding/credential.js +95 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/finish.js +119 -0
- package/lib/quoxon/onboarding/index.js +67 -0
- package/lib/quoxon/onboarding/installRunner.js +225 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +142 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +625 -0
- package/lib/quoxon/print.js +94 -0
- package/lib/quoxon/qart.js +379 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +111 -0
- package/lib/quoxon/render.js +648 -0
- package/lib/quoxon/roster.js +221 -0
- package/lib/quoxon/route.js +38 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/session.js +68 -0
- package/lib/quoxon/skills.js +30 -0
- package/lib/quoxon/slash/compact.js +90 -0
- package/lib/quoxon/slash/ledger.js +83 -0
- package/lib/quoxon/slash/listView.js +28 -0
- package/lib/quoxon/slash/palette.js +104 -0
- package/lib/quoxon/slash/paletteController.js +59 -0
- package/lib/quoxon/slash/panels.js +101 -0
- package/lib/quoxon/slash/picker.js +38 -0
- package/lib/quoxon/slash/restart.js +144 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/settings.js +50 -0
- package/lib/quoxon/sound/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +119 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/term.js +106 -0
- package/lib/quoxon/textBuffer.js +242 -0
- package/lib/quoxon/theme.js +253 -0
- package/lib/quoxon/transcript.js +150 -0
- package/lib/quoxon/web/qface.js +81 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/tui/app.jsx +289 -0
- package/lib/tui/components/Breadcrumb.jsx +42 -0
- package/lib/tui/components/ChatInput.jsx +68 -0
- package/lib/tui/components/Confirm.jsx +32 -0
- package/lib/tui/components/DataTable.jsx +270 -0
- package/lib/tui/components/DetailPanel.jsx +241 -0
- package/lib/tui/components/HelpOverlay.jsx +67 -0
- package/lib/tui/components/Layout.jsx +129 -0
- package/lib/tui/components/Palette.jsx +141 -0
- package/lib/tui/components/SearchInput.jsx +125 -0
- package/lib/tui/components/Sparkline.jsx +55 -0
- package/lib/tui/components/StatusBar.jsx +133 -0
- package/lib/tui/components/Toast.jsx +52 -0
- package/lib/tui/hooks/useApi.js +44 -0
- package/lib/tui/hooks/useHealth.js +90 -0
- package/lib/tui/hooks/usePolledApi.js +86 -0
- package/lib/tui/hooks/useTerminalSize.js +46 -0
- package/lib/tui/index.js +56 -0
- package/lib/tui/intro/Intro.jsx +332 -0
- package/lib/tui/jsx-loader.js +32 -0
- package/lib/tui/keys.js +87 -0
- package/lib/tui/registry.js +49 -0
- package/lib/tui/theme.js +103 -0
- package/lib/tui/views/AdminView.jsx +398 -0
- package/lib/tui/views/AgentView.jsx +204 -0
- package/lib/tui/views/BackupView.jsx +225 -0
- package/lib/tui/views/BrainView.jsx +223 -0
- package/lib/tui/views/ChatView.jsx +468 -0
- package/lib/tui/views/FilesView.jsx +218 -0
- package/lib/tui/views/FleetView.jsx +234 -0
- package/lib/tui/views/GovernanceView.jsx +453 -0
- package/lib/tui/views/HealthView.jsx +216 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +727 -0
- package/lib/tui/views/MainMenu.jsx +123 -0
- package/lib/tui/views/MemoryView.jsx +377 -0
- package/lib/tui/views/ObjectivesView.jsx +230 -0
- package/lib/tui/views/OrgView.jsx +229 -0
- package/lib/tui/views/ToolView.jsx +221 -0
- package/lib/tui/views/VaultView.jsx +422 -0
- package/lib/tui/views/WorkflowView.jsx +329 -0
- 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
|
+
}
|