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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox discord` — Quox Discord Pro from the canonical CLI (quoxcli-parity-gate:
|
|
3
|
+
* nothing ships without quox CLI support).
|
|
4
|
+
*
|
|
5
|
+
* Same principle as `quox done`: this is a thin wrapper over the daemon's own
|
|
6
|
+
* control CLI (`qdp`, quox-discord-pro/bin/qdp.mjs), not a reimplementation.
|
|
7
|
+
* The daemon already exposes status | instances | bindings | bind | unbind |
|
|
8
|
+
* gaps | health over its token-gated loopback control API, and qdp is the one
|
|
9
|
+
* client for it. Two clients for one control surface is how a control surface
|
|
10
|
+
* drifts, so `quox discord <verb>` shells straight through to qdp with the
|
|
11
|
+
* arguments unchanged, and qdp's exit code is passed through.
|
|
12
|
+
*
|
|
13
|
+
* Host-local, like `quox done` shelling to the DoneEngine: the operator CLI
|
|
14
|
+
* runs where the daemon runs (one daemon per QuoxCORE instance). Set
|
|
15
|
+
* QDP_CLI_PATH if qdp lives elsewhere.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
const QDP = process.env.QDP_CLI_PATH
|
|
24
|
+
|| path.join(os.homedir(), 'quox-discord-pro', 'bin', 'qdp.mjs');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Per-verb usage, mirrored from qdp.mjs's own header doc block (bin/qdp.mjs
|
|
28
|
+
* in the quox-discord-pro repo). qdp itself has no --help handling of its
|
|
29
|
+
* own, and this command hand-rolls arg dispatch (allowUnknownOption +
|
|
30
|
+
* helpOption(false), see below) rather than commander subcommands, so
|
|
31
|
+
* `--help`/`-h` was previously just another unrecognised arg forwarded
|
|
32
|
+
* straight through to qdp — which silently ran the verb instead of showing
|
|
33
|
+
* help. Worst case for a mutating verb (bind/unbind/provision): "--help"
|
|
34
|
+
* would have actually bound/unbound/provisioned a channel. This map is
|
|
35
|
+
* checked BEFORE qdp is ever spawned so that can't happen (quox#603).
|
|
36
|
+
*/
|
|
37
|
+
const VERB_USAGE = {
|
|
38
|
+
status: 'qdp status daemon liveness, instances, binding count',
|
|
39
|
+
instances: 'qdp instances configured instances and their targets',
|
|
40
|
+
bindings: 'qdp bindings every channel binding',
|
|
41
|
+
gaps: 'qdp gaps open support-gap items awaiting an official answer',
|
|
42
|
+
health: 'qdp health one-line OK/actionable summary',
|
|
43
|
+
doctor: 'qdp doctor per-binding view/webhook/liveness check; exits\n'
|
|
44
|
+
+ ' non-zero if any binding is non-functional',
|
|
45
|
+
bind: 'qdp bind <channelId> <mode> [key=value ...] bind an EXISTING channel',
|
|
46
|
+
unbind: 'qdp unbind <channelId>',
|
|
47
|
+
provision: 'qdp provision <agent-id> [options] create/reuse a channel AND bind it\n'
|
|
48
|
+
+ ' --mode <feed|bridge|team|roundtable> default: feed\n'
|
|
49
|
+
+ ' --channel <name> default: feed-<agent-id> for feed, <agent-id> otherwise\n'
|
|
50
|
+
+ ' --host <hostname-or-ip> dev server the agent runs on (sets bridge_remote_host)\n'
|
|
51
|
+
+ ' --inbox <name> remote session-mail inbox (default <agent-id>-inbox)\n'
|
|
52
|
+
+ ' --outbox <name> remote outbox (default discord-outbox)\n'
|
|
53
|
+
+ ' --two-way allow authorised humans to talk back (bridge only)\n'
|
|
54
|
+
+ ' --persona <name> webhook display name (default: titlecased agent id)\n'
|
|
55
|
+
+ ' --roster <ids> roundtable only: comma-separated "id" or "id:role" list\n'
|
|
56
|
+
+ ' --dry-run print exactly what it WOULD do, change nothing',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const GENERAL_USAGE = 'Usage: quox discord <verb> [args...]\n\n'
|
|
60
|
+
+ Object.values(VERB_USAGE).map((u) => ` ${u}`).join('\n')
|
|
61
|
+
+ '\n\nRun `quox discord <verb> --help` for a single verb\'s usage.';
|
|
62
|
+
|
|
63
|
+
function printVerbHelp(verb) {
|
|
64
|
+
if (verb && VERB_USAGE[verb]) {
|
|
65
|
+
process.stdout.write(`Usage:\n ${VERB_USAGE[verb]}\n`);
|
|
66
|
+
} else {
|
|
67
|
+
process.stdout.write(`${GENERAL_USAGE}\n`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function registerDiscordCommands(program) {
|
|
72
|
+
program
|
|
73
|
+
.command('discord [args...]')
|
|
74
|
+
.description('Quox Discord Pro daemon: status | instances | bindings | bind | unbind | gaps | health')
|
|
75
|
+
.allowUnknownOption(true)
|
|
76
|
+
.helpOption(false)
|
|
77
|
+
.action((args = []) => {
|
|
78
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
79
|
+
printVerbHelp(args[0]);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
if (!fs.existsSync(QDP)) {
|
|
83
|
+
process.stderr.write(
|
|
84
|
+
`Discord Pro control CLI (qdp) not found at ${QDP}\n` +
|
|
85
|
+
'Set QDP_CLI_PATH if it lives elsewhere, or install the quox-discord-pro daemon on this host.\n',
|
|
86
|
+
);
|
|
87
|
+
process.exit(2);
|
|
88
|
+
}
|
|
89
|
+
const r = spawnSync(process.execPath, [QDP, ...args], { stdio: 'inherit' });
|
|
90
|
+
if (r.error) {
|
|
91
|
+
process.stderr.write(`qdp failed to run: ${r.error.message}\n`);
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
process.exit(r.status ?? 2);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quox doctor — Platform Diagnostics
|
|
3
|
+
*
|
|
4
|
+
* Deep-checks every service, database, agent daemon, and credential.
|
|
5
|
+
* Reports pass/warning/critical with actionable details.
|
|
6
|
+
*
|
|
7
|
+
* CLI-3b (Stream CLI Phase 3)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { SERVICE_PORTS } from '../core/config.js';
|
|
12
|
+
import { emitCliEvent } from '../core/aee.js';
|
|
13
|
+
import { resolveCliVersion, checkCliDrift } from '../core/version.js';
|
|
14
|
+
|
|
15
|
+
const CHECKS = [
|
|
16
|
+
// Core services
|
|
17
|
+
{ name: 'Auth service', service: 'auth', port: 3101, path: '/health', critical: true },
|
|
18
|
+
{ name: 'Memory service', service: 'memory', port: 3103, path: '/health' },
|
|
19
|
+
{ name: 'Collector', service: 'collector', port: 9848, path: '/health', critical: true },
|
|
20
|
+
{ name: 'QuoxFlow', service: 'quoxflow', port: 3200, path: '/health' },
|
|
21
|
+
{ name: 'Files service', service: 'files', port: 3102, path: '/health' },
|
|
22
|
+
// External services
|
|
23
|
+
{ name: 'QuoxAgent daemon', port: 9847, path: '/health', url: 'http://localhost:9847' },
|
|
24
|
+
{ name: 'Qdrant vectors', port: 6333, path: '/', url: 'http://localhost:6333' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
async function checkEndpoint(url, path, timeoutMs = 3000) {
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
try {
|
|
30
|
+
const resp = await fetch(`${url}${path}`, {
|
|
31
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
32
|
+
});
|
|
33
|
+
const latency = Date.now() - start;
|
|
34
|
+
if (resp.ok) {
|
|
35
|
+
return { status: 'pass', latency_ms: latency };
|
|
36
|
+
}
|
|
37
|
+
return { status: 'warn', latency_ms: latency, detail: `HTTP ${resp.status}` };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return { status: 'fail', latency_ms: null, detail: err.message };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Every doctor check, in this process: services, auth token, CLI version. Shared by `quox doctor` and the
|
|
45
|
+
* conversation's /doctor so the two can never disagree. -> [{ name, status, detail?, latency_ms?, ... }].
|
|
46
|
+
*/
|
|
47
|
+
export async function runDoctorChecks({ config }) {
|
|
48
|
+
// Check all services in parallel — per-item try/catch so one failure doesn't lose all results
|
|
49
|
+
const checks = await Promise.all(
|
|
50
|
+
CHECKS.map(async (check) => {
|
|
51
|
+
try {
|
|
52
|
+
const url = check.url || config.getServiceUrl(check.service);
|
|
53
|
+
let result = await checkEndpoint(url, check.path);
|
|
54
|
+
// #623: doctor was crying CRITICAL when the configured URL was just
|
|
55
|
+
// WRONG (stale profile override / wrong scheme+host) while the
|
|
56
|
+
// service was up on its loopback bind. On a connection failure,
|
|
57
|
+
// probe http://127.0.0.1:<port> — if THAT answers, the service is
|
|
58
|
+
// UP and the verdict is "misconfigured URL", not "service down".
|
|
59
|
+
// Downgrade to a warning that names the reachable bind + the bad
|
|
60
|
+
// URL, so an operator fixes the config instead of chasing an outage.
|
|
61
|
+
if (result.status === 'fail' && check.port) {
|
|
62
|
+
const loopbackUrl = `http://127.0.0.1:${check.port}`;
|
|
63
|
+
if (url !== loopbackUrl) {
|
|
64
|
+
const alt = await checkEndpoint(loopbackUrl, check.path);
|
|
65
|
+
if (alt.status === 'pass') {
|
|
66
|
+
result = {
|
|
67
|
+
status: 'warn',
|
|
68
|
+
latency_ms: alt.latency_ms,
|
|
69
|
+
detail: `reachable at ${loopbackUrl} but configured URL ${url} failed — profile URL likely stale/wrong (misconfig, not an outage)`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
name: check.name,
|
|
76
|
+
port: check.port,
|
|
77
|
+
...result,
|
|
78
|
+
critical: check.critical || false,
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
return {
|
|
82
|
+
name: check.name,
|
|
83
|
+
port: check.port,
|
|
84
|
+
ok: false,
|
|
85
|
+
status: 'error',
|
|
86
|
+
ms: 0,
|
|
87
|
+
critical: check.critical || false,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Check auth token. fleet #479: the CURRENT profile (persisted
|
|
94
|
+
// default, or a -p/QUOX_PROFILE override) is config._effectiveProfile,
|
|
95
|
+
// never config._data.activeProfile (that's always the persisted
|
|
96
|
+
// default now, and would report the wrong profile under an override).
|
|
97
|
+
const auth = config.getAuth?.() || config._data?.profiles?.[config._effectiveProfile]?.auth;
|
|
98
|
+
if (auth?.access_token) {
|
|
99
|
+
const expired = auth.expires_at && Date.now() > auth.expires_at;
|
|
100
|
+
if (expired) {
|
|
101
|
+
checks.push({ name: 'Auth token', status: 'warn', detail: 'expired' });
|
|
102
|
+
} else {
|
|
103
|
+
const remaining = auth.expires_at ? Math.round((auth.expires_at - Date.now()) / 3600000) : '?';
|
|
104
|
+
checks.push({ name: 'Auth token', status: 'pass', detail: `valid (expires in ${remaining}h)` });
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
checks.push({ name: 'Auth token', status: 'warn', detail: 'not logged in' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// #610 Part 3: which build is this, and is it drifted from origin/main?
|
|
111
|
+
// A silently-stale CLI (the worktree was 32 commits behind and nothing
|
|
112
|
+
// said so) is a real warning, so a stale drift maps to 'warn' and bumps
|
|
113
|
+
// the exit code like any other. unknown/skip are informational (pass) —
|
|
114
|
+
// never fail a health check because we could not compute drift. The
|
|
115
|
+
// drift count reflects the last fetch (no network fetch here).
|
|
116
|
+
const cliVersion = resolveCliVersion();
|
|
117
|
+
const drift = checkCliDrift();
|
|
118
|
+
checks.push({
|
|
119
|
+
name: 'CLI version',
|
|
120
|
+
status: drift.status === 'stale' ? 'warn' : 'pass',
|
|
121
|
+
detail: `${cliVersion} · ${drift.detail}`,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return checks;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function registerDoctorCommand(program, ctx) {
|
|
128
|
+
const { config, client } = ctx;
|
|
129
|
+
|
|
130
|
+
program
|
|
131
|
+
.command('doctor')
|
|
132
|
+
.description('Run platform health diagnostics')
|
|
133
|
+
.action(async (opts, cmd) => {
|
|
134
|
+
const outputFormat = cmd.parent?.opts()?.output || 'table';
|
|
135
|
+
const isJson = outputFormat === 'json' || cmd.parent?.opts()?.json;
|
|
136
|
+
|
|
137
|
+
const checks = await runDoctorChecks({ config });
|
|
138
|
+
|
|
139
|
+
// Summary
|
|
140
|
+
const passed = checks.filter(c => c.status === 'pass').length;
|
|
141
|
+
const warnings = checks.filter(c => c.status === 'warn').length;
|
|
142
|
+
const failed = checks.filter(c => c.status === 'fail').length;
|
|
143
|
+
|
|
144
|
+
if (isJson) {
|
|
145
|
+
console.log(JSON.stringify({ checks, summary: { passed, warnings, failed } }, null, 2));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Render
|
|
150
|
+
console.log('');
|
|
151
|
+
for (const c of checks) {
|
|
152
|
+
const icon = c.status === 'pass' ? chalk.green(' ✓')
|
|
153
|
+
: c.status === 'warn' ? chalk.yellow(' ⚠')
|
|
154
|
+
: chalk.red(' ✗');
|
|
155
|
+
|
|
156
|
+
const latency = c.latency_ms != null ? chalk.dim(` (${c.latency_ms}ms)`) : '';
|
|
157
|
+
const detail = c.detail ? chalk.dim(` ${c.detail}`) : '';
|
|
158
|
+
const port = c.port ? chalk.dim(` ${c.port}`) : '';
|
|
159
|
+
|
|
160
|
+
console.log(`${icon} ${c.name.padEnd(20)}${port}${latency}${detail}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log('');
|
|
164
|
+
const parts = [];
|
|
165
|
+
if (passed) parts.push(chalk.green(`${passed} passed`));
|
|
166
|
+
if (warnings) parts.push(chalk.yellow(`${warnings} warning${warnings > 1 ? 's' : ''}`));
|
|
167
|
+
if (failed) parts.push(chalk.red(`${failed} critical`));
|
|
168
|
+
console.log(` ${parts.join(' ')}`);
|
|
169
|
+
console.log('');
|
|
170
|
+
|
|
171
|
+
emitCliEvent(client, {
|
|
172
|
+
command: 'doctor',
|
|
173
|
+
intent: 'quox.cli.doctor',
|
|
174
|
+
payload: { passed, warnings, failed, total: checks.length },
|
|
175
|
+
status: failed > 0 ? 'error' : 'success',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (failed > 0) process.exitCode = 2;
|
|
179
|
+
else if (warnings > 0) process.exitCode = 1;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain commands: org domain verification (DNS TXT record ownership proof)
|
|
3
|
+
*
|
|
4
|
+
* quox domain list|add|verify|remove
|
|
5
|
+
*
|
|
6
|
+
* Backend: services/auth/routes/domains.js, mounted at plain `/api`
|
|
7
|
+
* (org-scoped IN the URL: /api/orgs/:orgId/domains...). Team-tier+ +
|
|
8
|
+
* ORG_MANAGE_SETTINGS gated — expect 402/403 on non-team orgs.
|
|
9
|
+
*
|
|
10
|
+
* `add` returns a verification token and human-readable DNS instructions;
|
|
11
|
+
* `verify` re-checks DNS and returns 202 (retryable) rather than an error
|
|
12
|
+
* when the TXT record isn't there yet or hasn't propagated.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
|
|
16
|
+
import { resolveOrgId } from './helpers.js';
|
|
17
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
18
|
+
|
|
19
|
+
export function registerDomainCommands(program, ctx) {
|
|
20
|
+
const { client, auth } = ctx;
|
|
21
|
+
|
|
22
|
+
const domain = program
|
|
23
|
+
.command('domain')
|
|
24
|
+
.description('Manage org domain verification (team+ — DNS ownership proof)');
|
|
25
|
+
|
|
26
|
+
// ── domain list ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
domain
|
|
29
|
+
.command('list')
|
|
30
|
+
.description('List domains claimed by the active org')
|
|
31
|
+
.action(async () => {
|
|
32
|
+
auth.requireAuth();
|
|
33
|
+
const orgId = resolveOrgId(auth, program);
|
|
34
|
+
const ep = resolveEndpoint('domains.list', { orgId });
|
|
35
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
36
|
+
const rows = data.domains || data || [];
|
|
37
|
+
|
|
38
|
+
formatOutput({
|
|
39
|
+
// fleet #392: column is verified_at (a timestamp), not a `verified` flag.
|
|
40
|
+
columns: ['id', 'domain', 'verified_at', 'created_at'],
|
|
41
|
+
headers: ['ID', 'Domain', 'Verified', 'Created'],
|
|
42
|
+
rows,
|
|
43
|
+
}, { format: program.opts().output, emptyMessage: 'No domains claimed for this org.' });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── domain add ───────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
domain
|
|
49
|
+
.command('add')
|
|
50
|
+
.description('Claim a domain and get its DNS verification instructions')
|
|
51
|
+
.argument('<domain>', 'domain name (e.g. "example.com")')
|
|
52
|
+
.action(async (domainName) => {
|
|
53
|
+
auth.requireAuth();
|
|
54
|
+
const orgId = resolveOrgId(auth, program);
|
|
55
|
+
const body = { domain: domainName };
|
|
56
|
+
|
|
57
|
+
const ep = resolveEndpoint('domains.create', { orgId });
|
|
58
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
59
|
+
printSuccess(`Domain "${domainName}" claim started.`);
|
|
60
|
+
formatDetail(data.domain || {}, { format: program.opts().output });
|
|
61
|
+
if (data.instructions && program.opts().output !== 'json') {
|
|
62
|
+
printInfo(data.instructions);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ── domain verify ────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
domain
|
|
69
|
+
.command('verify')
|
|
70
|
+
.description('Re-check DNS for a claimed domain')
|
|
71
|
+
.argument('<domainId>', 'domain ID')
|
|
72
|
+
.action(async (domainId) => {
|
|
73
|
+
auth.requireAuth();
|
|
74
|
+
const orgId = resolveOrgId(auth, program);
|
|
75
|
+
const ep = resolveEndpoint('domains.verify', { orgId, domainId });
|
|
76
|
+
const data = await client.post(ep.path, { service: ep.service });
|
|
77
|
+
|
|
78
|
+
if (data.verified) {
|
|
79
|
+
printSuccess('Domain verified.');
|
|
80
|
+
} else {
|
|
81
|
+
printWarning(`Not verified yet (reason: ${data.reason || 'unknown'}).${data.retryable ? ' Retryable — DNS changes can take up to 48 hours.' : ''}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── domain remove ────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
domain
|
|
88
|
+
.command('remove')
|
|
89
|
+
.description('Remove a claimed domain')
|
|
90
|
+
.argument('<domainId>', 'domain ID')
|
|
91
|
+
.option('--force', 'skip confirmation')
|
|
92
|
+
.action(async (domainId, options) => {
|
|
93
|
+
auth.requireAuth();
|
|
94
|
+
const orgId = resolveOrgId(auth, program);
|
|
95
|
+
|
|
96
|
+
if (!options.force && !program.opts().yes) {
|
|
97
|
+
const { default: inquirer } = await import('inquirer');
|
|
98
|
+
const ans = await inquirer.prompt([{
|
|
99
|
+
type: 'confirm',
|
|
100
|
+
name: 'confirm',
|
|
101
|
+
message: `Remove domain ${domainId}? SSO/user-domain routing tied to it will stop working.`,
|
|
102
|
+
default: false,
|
|
103
|
+
}]);
|
|
104
|
+
if (!ans.confirm) {
|
|
105
|
+
printInfo('Cancelled.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ep = resolveEndpoint('domains.delete', { orgId, domainId });
|
|
111
|
+
await client.delete(ep.path, { service: ep.service });
|
|
112
|
+
printSuccess('Domain removed.');
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox done` — Quox DoneEngine from the terminal.
|
|
3
|
+
*
|
|
4
|
+
* The house gate is that nothing ships without CLI support, and the north star
|
|
5
|
+
* is Claude running Quox headless. That matters more here than for most
|
|
6
|
+
* surfaces: DoneEngine's primary users are AGENTS, not people. "Is this
|
|
7
|
+
* actually done?" is a question something asks before it acts, so a completion
|
|
8
|
+
* engine reachable only from a dashboard fails its own premise.
|
|
9
|
+
*
|
|
10
|
+
* This is a thin wrapper over the quox-gauge engine rather than a
|
|
11
|
+
* reimplementation. Two implementations of a rule set is how a rule set
|
|
12
|
+
* drifts, which is the thing DoneEngine exists to catch, so there is exactly
|
|
13
|
+
* one engine and the CLI shells out to it.
|
|
14
|
+
*
|
|
15
|
+
* ENGINE RESOLUTION (#456, extended by the fetch follow-on below): defaults
|
|
16
|
+
* to ~/quox-gauge/gauge.mjs, which only ever existed on the box quox-gauge
|
|
17
|
+
* was developed on, so every other dev-enabled host got a bare "not found"
|
|
18
|
+
* error. `quox dev enable` delivers the engine to /opt/quoxgauge and points
|
|
19
|
+
* QUOX_DONEENGINE_PATH at it via the generated CLI wrapper (see
|
|
20
|
+
* devServerRefresh.js), so a fresh dev-enabled host resolves it there
|
|
21
|
+
* without any local checkout.
|
|
22
|
+
*
|
|
23
|
+
* FETCH FOLLOW-ON: enable is correctly human-gated (requireHumanAdmin), so a
|
|
24
|
+
* RESIDENT agent with a missing or stale engine on a host nobody has run
|
|
25
|
+
* `quox dev enable` on had no self-service path — permanently stuck waiting
|
|
26
|
+
* on a human. `quox done fetch` closes that: it pulls the same public bundle
|
|
27
|
+
* (/download/quoxgauge-bundle.tar.gz) directly and installs it, no human in
|
|
28
|
+
* the loop, no new permission (see cli/lib/core/doneEngineFetch.js for the
|
|
29
|
+
* fetch/extract/verify logic). Resolution now also checks a user-local
|
|
30
|
+
* fallback (~/.local/share/quox/gauge/gauge.mjs), which is where fetch lands
|
|
31
|
+
* when /opt/quoxgauge is not writable and there is no passwordless sudo. A
|
|
32
|
+
* missing engine or packs directory names `quox done fetch` first (the
|
|
33
|
+
* self-service fix) and `quox dev enable` second.
|
|
34
|
+
*
|
|
35
|
+
* EXIT CODES are the contract, and `check` passes the engine's through
|
|
36
|
+
* unchanged:
|
|
37
|
+
* 0 CLEAR every gate in the pack passes and the running code agrees
|
|
38
|
+
* 1 REFUSED a named gate fails; reasons are printed
|
|
39
|
+
* 2 UNKNOWN insufficient evidence to say either way
|
|
40
|
+
*
|
|
41
|
+
* UNKNOWN is deliberately not folded into either neighbour. Treating "no
|
|
42
|
+
* evidence" as approval is the exact failure this engine was built after, and
|
|
43
|
+
* treating it as refusal trains people to ignore refusals.
|
|
44
|
+
*
|
|
45
|
+
* ADVISORY. `quox done check` never blocks anything. Whether DoneEngine may
|
|
46
|
+
* refuse a deploy is an open owner decision on the DONEENGINE stream, and a
|
|
47
|
+
* tool that started refusing other people's deploys before that was settled
|
|
48
|
+
* would be taking a governance power nobody granted it.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { spawnSync } from 'node:child_process';
|
|
52
|
+
import fs from 'node:fs';
|
|
53
|
+
import os from 'node:os';
|
|
54
|
+
import path from 'node:path';
|
|
55
|
+
import {
|
|
56
|
+
resolveFetchDestination,
|
|
57
|
+
fetchEngine,
|
|
58
|
+
OPT_GAUGE_DIR,
|
|
59
|
+
userLocalGaugeDir,
|
|
60
|
+
legacyGaugeDir,
|
|
61
|
+
} from '../core/doneEngineFetch.js';
|
|
62
|
+
import { printSuccess, printInfo } from '../core/output.js';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Engine resolution order (see the FETCH FOLLOW-ON note above):
|
|
66
|
+
* 1. QUOX_DONEENGINE_PATH (explicit override, wins even if the file is
|
|
67
|
+
* missing — the honest "not found" error below still needs a concrete
|
|
68
|
+
* path to name)
|
|
69
|
+
* 2. /opt/quoxgauge/gauge.mjs — where `quox dev enable` and a sudo-capable
|
|
70
|
+
* `quox done fetch` both put it
|
|
71
|
+
* 3. ~/.local/share/quox/gauge/gauge.mjs — where `quox done fetch` puts it
|
|
72
|
+
* when /opt/quoxgauge is not writable and there is no passwordless sudo
|
|
73
|
+
* 4. ~/quox-gauge/gauge.mjs — legacy default, kept as the final fallback so
|
|
74
|
+
* the error message always names a concrete path
|
|
75
|
+
*
|
|
76
|
+
* Exported (rather than inlined into the ENGINE constant) so this order is
|
|
77
|
+
* unit-testable with an injected existsSync/env/home instead of the real
|
|
78
|
+
* filesystem.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveEnginePath({ env = process.env, existsSync = fs.existsSync, home = os.homedir() } = {}) {
|
|
81
|
+
if (env.QUOX_DONEENGINE_PATH) return env.QUOX_DONEENGINE_PATH;
|
|
82
|
+
const optPath = path.join(OPT_GAUGE_DIR, 'gauge.mjs');
|
|
83
|
+
if (existsSync(optPath)) return optPath;
|
|
84
|
+
const userLocalPath = path.join(userLocalGaugeDir(home), 'gauge.mjs');
|
|
85
|
+
if (existsSync(userLocalPath)) return userLocalPath;
|
|
86
|
+
return path.join(legacyGaugeDir(home), 'gauge.mjs');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ENGINE = resolveEnginePath();
|
|
90
|
+
|
|
91
|
+
function runEngine(args) {
|
|
92
|
+
if (!fs.existsSync(ENGINE)) {
|
|
93
|
+
process.stderr.write(
|
|
94
|
+
`DoneEngine not found at ${ENGINE}\n` +
|
|
95
|
+
'Self-heal: `quox done fetch` pulls the engine bundle straight from ' +
|
|
96
|
+
'the collector, no human in the loop. Otherwise `quox dev enable` ' +
|
|
97
|
+
'delivers it to /opt/quoxgauge as part of full dev-server ' +
|
|
98
|
+
'provisioning. Or set QUOX_DONEENGINE_PATH to a checkout.\n',
|
|
99
|
+
);
|
|
100
|
+
// 2, not 1: a missing engine is "cannot tell", not "refused".
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
const r = spawnSync(process.execPath, [ENGINE, ...args], { stdio: 'inherit' });
|
|
104
|
+
if (r.error) {
|
|
105
|
+
process.stderr.write(`DoneEngine failed to run: ${r.error.message}\n`);
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
return r.status ?? 2;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function defaultExecFn(cmd, args) {
|
|
112
|
+
return spawnSync(cmd, args, { encoding: 'utf8' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function defaultIsWritable(dir) {
|
|
116
|
+
let probe = dir;
|
|
117
|
+
while (!fs.existsSync(probe)) {
|
|
118
|
+
const parent = path.dirname(probe);
|
|
119
|
+
if (parent === probe) break;
|
|
120
|
+
probe = parent;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
fs.accessSync(probe, fs.constants.W_OK);
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function defaultFsOps() {
|
|
131
|
+
return {
|
|
132
|
+
existsSync: fs.existsSync,
|
|
133
|
+
readdirSync: fs.readdirSync,
|
|
134
|
+
writeFileSync: fs.writeFileSync,
|
|
135
|
+
rmSync: fs.rmSync,
|
|
136
|
+
isWritable: defaultIsWritable,
|
|
137
|
+
mkdtemp: () => fs.mkdtempSync(path.join(os.tmpdir(), 'quoxgauge-backup-')),
|
|
138
|
+
tmpFile: () => path.join(os.tmpdir(), `quoxgauge-bundle-${process.pid}-${Date.now()}.tar.gz`),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function registerDoneCommands(program, ctx = {}) {
|
|
143
|
+
const done = program
|
|
144
|
+
.command('done')
|
|
145
|
+
.description('Quox DoneEngine: is this actually done, and what is the evidence?');
|
|
146
|
+
|
|
147
|
+
done
|
|
148
|
+
.command('check [subject]')
|
|
149
|
+
.description('Can this ship? exit 0 clear, 1 refused with reasons, 2 unknown. Advisory: never blocks.')
|
|
150
|
+
.option('--pack <id>', 'rule pack to judge against', 'launch-spine')
|
|
151
|
+
.option('--json', 'structured verdict for agents')
|
|
152
|
+
.option('--root <path>', 'judge <subject> in this repo, reading its own STATUS map with the selected pack (e.g. a plugin with --pack plugin)')
|
|
153
|
+
.action((subject, options) => {
|
|
154
|
+
const args = ['--can-ship', subject || 'all', '--pack', options.pack];
|
|
155
|
+
if (options.root) {
|
|
156
|
+
if (!subject) {
|
|
157
|
+
process.stderr.write('quox done check: --root needs a subject (the surface to judge in that repo).\n');
|
|
158
|
+
process.exitCode = 2;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
args.push('--subject', subject, '--root', path.resolve(options.root));
|
|
162
|
+
}
|
|
163
|
+
if (options.json) args.push('--json');
|
|
164
|
+
process.exitCode = runEngine(args);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
done
|
|
168
|
+
.command('checklist [pack]')
|
|
169
|
+
.description('The builder checklist GENERATED from a pack (every gate the engine judges); --check verifies a doc that embeds it')
|
|
170
|
+
.option('--check <file>', 'exit 1 if the checklist block in this file has drifted from the pack')
|
|
171
|
+
.action((pack, options) => {
|
|
172
|
+
const args = ['--checklist', pack || 'plugin'];
|
|
173
|
+
if (options.check) args.push('--check', path.resolve(options.check));
|
|
174
|
+
process.exitCode = runEngine(args);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
done
|
|
178
|
+
.command('board')
|
|
179
|
+
.description('Estate scoreboard: every surface, score, attested gates, deploy state')
|
|
180
|
+
.option('--json', 'machine-readable, includes raw deploy state')
|
|
181
|
+
.option('--dod', 'per-surface gates, split by evidence class')
|
|
182
|
+
.option('--no-deploy', 'skip docker/git; Deployed Ready becomes unknown, which is not a pass')
|
|
183
|
+
.action((options) => {
|
|
184
|
+
const args = [];
|
|
185
|
+
if (options.json) args.push('--json');
|
|
186
|
+
if (options.dod) args.push('--dod');
|
|
187
|
+
if (options.deploy === false) args.push('--no-deploy');
|
|
188
|
+
process.exitCode = runEngine(args);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
done
|
|
192
|
+
.command('bar [pack]')
|
|
193
|
+
.description('One rule pack against its subjects: BOTH finish lines, build and deployed')
|
|
194
|
+
.option('--snapshot', 'append this reading to the snapshot history')
|
|
195
|
+
.option('--org <id>', 'measure FOR this org: plan evidence reads as it, the snapshot is stamped with it (default: the pack\'s own org field)')
|
|
196
|
+
.action((pack, options) => {
|
|
197
|
+
const args = pack ? ['--pack', pack] : ['--launch'];
|
|
198
|
+
if (options.snapshot) args.push('--snapshot');
|
|
199
|
+
if (options.org) args.push('--org', options.org);
|
|
200
|
+
process.exitCode = runEngine(args);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
done
|
|
204
|
+
.command('history [pack]')
|
|
205
|
+
.description('Movement between snapshots, regressions listed first')
|
|
206
|
+
.action((pack) => {
|
|
207
|
+
process.exitCode = runEngine(pack ? ['--history', '--pack', pack] : ['--history']);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
done
|
|
211
|
+
.command('packs')
|
|
212
|
+
.description('List available rule packs')
|
|
213
|
+
.action(() => {
|
|
214
|
+
const dir = path.join(path.dirname(ENGINE), 'packs');
|
|
215
|
+
if (!fs.existsSync(dir)) {
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
`No packs directory at ${dir}\n` +
|
|
218
|
+
'This usually means the engine delivery is missing or stale. ' +
|
|
219
|
+
'Run `quox done fetch` to self-heal it (no human needed), or ' +
|
|
220
|
+
're-run `quox dev enable`, or set QUOX_DONEENGINE_PATH to a ' +
|
|
221
|
+
'checkout that has one.\n',
|
|
222
|
+
);
|
|
223
|
+
process.exitCode = 2;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
for (const f of fs.readdirSync(dir).filter((x) => x.endsWith('.json')).sort()) {
|
|
227
|
+
try {
|
|
228
|
+
const p = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
229
|
+
const scoped = p.subjects ? `${p.subjects.length} subjects` : 'base pack';
|
|
230
|
+
process.stdout.write(`${(p.id || f).padEnd(16)} v${p.version || '?'} ${scoped} ${p.name || ''}\n`);
|
|
231
|
+
} catch {
|
|
232
|
+
process.stdout.write(`${f} (unreadable)\n`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
done
|
|
238
|
+
.command('fetch')
|
|
239
|
+
.description('Self-fetch the DoneEngine bundle, no human in the loop (quox#456 follow-on)')
|
|
240
|
+
.option('--dest <path>', 'install destination (default: $QUOX_DONEENGINE_HOME, else /opt/quoxgauge if writable/sudo-reachable, else ~/.local/share/quox/gauge)')
|
|
241
|
+
.option('--force', 'refetch and reinstall even if the current install already looks healthy')
|
|
242
|
+
.action(async (options) => {
|
|
243
|
+
const client = ctx.client;
|
|
244
|
+
if (!client) {
|
|
245
|
+
process.stderr.write('quox done fetch: no HTTP client available in this context.\n');
|
|
246
|
+
process.exitCode = 2;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const execFn = ctx.execFn || defaultExecFn;
|
|
250
|
+
const isWritable = ctx.isWritable || defaultIsWritable;
|
|
251
|
+
const fsOps = ctx.fsOps || defaultFsOps();
|
|
252
|
+
|
|
253
|
+
const { dest, reason, needsSudo } = resolveFetchDestination({
|
|
254
|
+
destOption: options.dest,
|
|
255
|
+
env: process.env,
|
|
256
|
+
isWritable,
|
|
257
|
+
execFn,
|
|
258
|
+
});
|
|
259
|
+
process.stdout.write(`Destination: ${dest} (${reason})\n`);
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const result = await fetchEngine({ dest, needsSudo, force: !!options.force, client, execFn, fsOps });
|
|
263
|
+
if (result.status === 'noop') {
|
|
264
|
+
printInfo(result.message);
|
|
265
|
+
} else {
|
|
266
|
+
printSuccess(result.message);
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
process.stderr.write(`quox done fetch failed: ${err.message}\n`);
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return done;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default registerDoneCommands;
|