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,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox done fetch` engine (quox#456 follow-on).
|
|
3
|
+
*
|
|
4
|
+
* quox#456 pointed `quox done` at /opt/quoxgauge, which is delivered ONLY by
|
|
5
|
+
* `quox dev enable` (see cli/lib/commands/dev.js and
|
|
6
|
+
* services/collector/lib/devServerRefresh.js buildGaugeRefreshCommand).
|
|
7
|
+
* Enable is correctly human-gated (requireHumanAdmin), so a RESIDENT agent
|
|
8
|
+
* with a missing or stale engine had no self-service path and was stuck
|
|
9
|
+
* waiting on a human forever. The bundle download endpoint is already
|
|
10
|
+
* public (see services/collector/utils/collectorAuthGate.js PUBLIC_EXACT:
|
|
11
|
+
* /download/quoxgauge-bundle.tar.gz), so a resident-driven fetch needs no
|
|
12
|
+
* new permission grant, only this verb.
|
|
13
|
+
*
|
|
14
|
+
* Everything here is pure or takes its side effects (HTTP client, exec,
|
|
15
|
+
* filesystem reads/writes) as injected dependencies, so
|
|
16
|
+
* cli/lib/commands/done.js is a thin wrapper and every path below is driven
|
|
17
|
+
* from tests with no real network call, no real `tar`/`sudo`, and no writes
|
|
18
|
+
* outside a scratch fsOps fake.
|
|
19
|
+
*
|
|
20
|
+
* Extraction shape mirrors the collector's own refresh command exactly:
|
|
21
|
+
* flat `tar -xzf <bundle> -C <dest>`, never into a parent directory.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
|
|
27
|
+
export const OPT_GAUGE_DIR = '/opt/quoxgauge';
|
|
28
|
+
|
|
29
|
+
export function userLocalGaugeDir(home = os.homedir()) {
|
|
30
|
+
return path.join(home, '.local', 'share', 'quox', 'gauge');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function legacyGaugeDir(home = os.homedir()) {
|
|
34
|
+
return path.join(home, 'quox-gauge');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Decide where a fetched engine should land, and whether getting there
|
|
39
|
+
* needs privilege escalation.
|
|
40
|
+
*
|
|
41
|
+
* Order:
|
|
42
|
+
* 1. --dest (explicit, always wins, never needs sudo)
|
|
43
|
+
* 2. $QUOX_DONEENGINE_HOME (an override for an already server-controlled
|
|
44
|
+
* path, same precedent as QUOX_DONEENGINE_PATH — not a new feature flag)
|
|
45
|
+
* 3. /opt/quoxgauge, if directly writable, or reachable via passwordless
|
|
46
|
+
* sudo (`sudo -n true`) — matching where `quox dev enable` puts it, so
|
|
47
|
+
* a resident does not silently diverge from the host-wide install
|
|
48
|
+
* 4. ~/.local/share/quox/gauge — always-writable fallback
|
|
49
|
+
*
|
|
50
|
+
* `isWritable` and `execFn` are injected so this stays testable without a
|
|
51
|
+
* real filesystem probe or a real `sudo` spawn.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveFetchDestination({ destOption, env = process.env, isWritable, execFn } = {}) {
|
|
54
|
+
if (destOption) {
|
|
55
|
+
return { dest: destOption, reason: '--dest', needsSudo: false };
|
|
56
|
+
}
|
|
57
|
+
if (env.QUOX_DONEENGINE_HOME) {
|
|
58
|
+
return { dest: env.QUOX_DONEENGINE_HOME, reason: 'QUOX_DONEENGINE_HOME override', needsSudo: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const directlyWritable = typeof isWritable === 'function' ? isWritable(OPT_GAUGE_DIR) : false;
|
|
62
|
+
if (directlyWritable) {
|
|
63
|
+
return { dest: OPT_GAUGE_DIR, reason: `${OPT_GAUGE_DIR} is writable`, needsSudo: false };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let sudoAvailable = false;
|
|
67
|
+
if (typeof execFn === 'function') {
|
|
68
|
+
try {
|
|
69
|
+
const r = execFn('sudo', ['-n', 'true']);
|
|
70
|
+
sudoAvailable = !!r && r.status === 0;
|
|
71
|
+
} catch {
|
|
72
|
+
sudoAvailable = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (sudoAvailable) {
|
|
76
|
+
return {
|
|
77
|
+
dest: OPT_GAUGE_DIR,
|
|
78
|
+
reason: `${OPT_GAUGE_DIR} is not directly writable, using passwordless sudo (matches quox dev enable's install)`,
|
|
79
|
+
needsSudo: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
dest: userLocalGaugeDir(),
|
|
85
|
+
reason: `${OPT_GAUGE_DIR} is not writable and no passwordless sudo is available; falling back to a user-writable path`,
|
|
86
|
+
needsSudo: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Inspect an install directory. Never throws: an unreadable or missing
|
|
92
|
+
* directory just reports unhealthy, it is not itself an error.
|
|
93
|
+
*/
|
|
94
|
+
export function inspectInstall(dest, fsOps) {
|
|
95
|
+
const gaugePath = path.join(dest, 'gauge.mjs');
|
|
96
|
+
const packsPath = path.join(dest, 'packs');
|
|
97
|
+
const gaugeExists = fsOps.existsSync(gaugePath);
|
|
98
|
+
const packsDirExists = fsOps.existsSync(packsPath);
|
|
99
|
+
let packsCount = 0;
|
|
100
|
+
if (packsDirExists) {
|
|
101
|
+
try {
|
|
102
|
+
packsCount = fsOps.readdirSync(packsPath).filter((f) => f.endsWith('.json')).length;
|
|
103
|
+
} catch {
|
|
104
|
+
packsCount = 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
healthy: gaugeExists && packsDirExists && packsCount > 0,
|
|
109
|
+
gaugeExists,
|
|
110
|
+
packsDirExists,
|
|
111
|
+
packsCount,
|
|
112
|
+
gaugePath,
|
|
113
|
+
packsPath,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Fetch the bundle from the collector and extract it flat into `dest`.
|
|
119
|
+
*
|
|
120
|
+
* A healthy existing install is a no-op unless `force` is set. On failure
|
|
121
|
+
* after a previous install existed, the previous install is restored so a
|
|
122
|
+
* resident is never left worse off than it started (leave the previous
|
|
123
|
+
* install intact where possible).
|
|
124
|
+
*
|
|
125
|
+
* `client` is the CLI's existing HTTP client (already injectable in every
|
|
126
|
+
* command via ctx) — reused here rather than a bare fetch so auth headers,
|
|
127
|
+
* retries and service-url resolution stay identical to every other command.
|
|
128
|
+
* `execFn(cmd, args)` is the child-process seam (mirrors ctx.spawnFn in
|
|
129
|
+
* lib/commands/devSupervisor.js): all destination mutation (mkdir/mv/rm/tar)
|
|
130
|
+
* goes through it, prefixed with `sudo -n` when `needsSudo` is set, so the
|
|
131
|
+
* privileged and unprivileged paths share one code path.
|
|
132
|
+
*/
|
|
133
|
+
export async function fetchEngine({ dest, needsSudo, force, client, execFn, fsOps } = {}) {
|
|
134
|
+
if (!force) {
|
|
135
|
+
const existing = inspectInstall(dest, fsOps);
|
|
136
|
+
if (existing.healthy) {
|
|
137
|
+
return {
|
|
138
|
+
status: 'noop',
|
|
139
|
+
dest,
|
|
140
|
+
packsCount: existing.packsCount,
|
|
141
|
+
message: `DoneEngine already installed and healthy at ${dest} (${existing.packsCount} packs). `
|
|
142
|
+
+ 'Nothing to do; pass --force to re-fetch anyway.',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const res = await client.get('/download/quoxgauge-bundle.tar.gz', { service: 'collector', raw: true });
|
|
148
|
+
if (!res || !res.ok) {
|
|
149
|
+
throw new Error(`Bundle download failed (status ${res ? res.status : 'no response'}) from /download/quoxgauge-bundle.tar.gz`);
|
|
150
|
+
}
|
|
151
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
152
|
+
const tmpFile = fsOps.tmpFile();
|
|
153
|
+
fsOps.writeFileSync(tmpFile, buffer);
|
|
154
|
+
|
|
155
|
+
const run = (cmd, args) => (needsSudo ? execFn('sudo', ['-n', cmd, ...args]) : execFn(cmd, args));
|
|
156
|
+
const requireOk = (r, label) => {
|
|
157
|
+
if (!r || r.status !== 0) {
|
|
158
|
+
throw new Error(`${label} failed: ${(r && (r.stderr || r.stdout)) || 'unknown error'}`);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const hadPrevious = fsOps.existsSync(dest);
|
|
163
|
+
const backupDest = `${dest}.pre-fetch`;
|
|
164
|
+
// A writable dest inside a non-writable parent (e.g. control-owned
|
|
165
|
+
// /opt/quoxgauge under root:root /opt) cannot be renamed to a sibling.
|
|
166
|
+
// Back up its contents into a temp dir instead and mutate dest in place.
|
|
167
|
+
const inPlace = hadPrevious && !needsSudo
|
|
168
|
+
&& typeof fsOps.isWritable === 'function'
|
|
169
|
+
&& !fsOps.isWritable(path.dirname(dest));
|
|
170
|
+
let inPlaceBackup = null;
|
|
171
|
+
const emptyDest = () => run('find', [dest, '-mindepth', '1', '-delete']);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
if (inPlace) {
|
|
175
|
+
inPlaceBackup = fsOps.mkdtemp();
|
|
176
|
+
requireOk(run('cp', ['-a', `${dest}/.`, `${inPlaceBackup}/`]), 'backing up previous install in place');
|
|
177
|
+
requireOk(emptyDest(), 'emptying previous install');
|
|
178
|
+
} else if (hadPrevious) {
|
|
179
|
+
if (fsOps.existsSync(backupDest)) requireOk(run('rm', ['-rf', backupDest]), 'clearing stale backup');
|
|
180
|
+
requireOk(run('mv', [dest, backupDest]), 'backing up previous install');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
requireOk(run('mkdir', ['-p', dest]), 'creating destination');
|
|
184
|
+
requireOk(run('tar', ['-xzf', tmpFile, '-C', dest]), 'extracting bundle');
|
|
185
|
+
|
|
186
|
+
const verified = inspectInstall(dest, fsOps);
|
|
187
|
+
if (!verified.healthy) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Extracted bundle at ${dest} failed verification `
|
|
190
|
+
+ `(gauge.mjs present: ${verified.gaugeExists}, packs: ${verified.packsCount}). `
|
|
191
|
+
+ 'Left the previous install in place where one existed.',
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (inPlace) requireOk(run('rm', ['-rf', inPlaceBackup]), 'cleaning up backup');
|
|
196
|
+
else if (hadPrevious) requireOk(run('rm', ['-rf', backupDest]), 'cleaning up backup');
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
status: 'updated',
|
|
200
|
+
dest,
|
|
201
|
+
packsCount: verified.packsCount,
|
|
202
|
+
message: `DoneEngine installed at ${dest} (${verified.packsCount} packs).`,
|
|
203
|
+
};
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (inPlaceBackup) {
|
|
206
|
+
emptyDest();
|
|
207
|
+
run('cp', ['-a', `${inPlaceBackup}/.`, `${dest}/`]);
|
|
208
|
+
} else if (hadPrevious && fsOps.existsSync(backupDest)) {
|
|
209
|
+
run('rm', ['-rf', dest]);
|
|
210
|
+
run('mv', [backupDest, dest]);
|
|
211
|
+
}
|
|
212
|
+
throw err;
|
|
213
|
+
} finally {
|
|
214
|
+
if (inPlaceBackup) {
|
|
215
|
+
try { run('rm', ['-rf', inPlaceBackup]); } catch { /* best effort cleanup */ }
|
|
216
|
+
}
|
|
217
|
+
try { fsOps.rmSync(tmpFile, { force: true }); } catch { /* best effort cleanup */ }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error types and handler for QuoxCLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
export class QuoxError extends Error {
|
|
8
|
+
constructor(message, exitCode = 1) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'QuoxError';
|
|
11
|
+
this.exitCode = exitCode;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class QuoxApiError extends QuoxError {
|
|
16
|
+
/**
|
|
17
|
+
* @param {number} status HTTP status
|
|
18
|
+
* @param {string} code machine-readable error code from the body
|
|
19
|
+
* @param {string} message human-readable message
|
|
20
|
+
* @param {*} [detail] the body's `detail`/`details` field, if any
|
|
21
|
+
* @param {object} [body] LL-3d: the WHOLE parsed JSON error body. Lifting
|
|
22
|
+
* only message/detail threw away everything else the server said, and the
|
|
23
|
+
* licensing endpoints say a lot: which instance holds the seat, how many
|
|
24
|
+
* of the licensed servers are in use, which plugin is unlicensed. Command
|
|
25
|
+
* handlers read this to render something actionable; --json emits it raw.
|
|
26
|
+
*/
|
|
27
|
+
constructor(status, code, message, detail, body) {
|
|
28
|
+
super(message, statusToExitCode(status));
|
|
29
|
+
this.name = 'QuoxApiError';
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.detail = detail;
|
|
33
|
+
if (body && typeof body === 'object') this.body = body;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class QuoxAuthError extends QuoxError {
|
|
38
|
+
constructor(message = 'Not authenticated. Run `quox login` first.') {
|
|
39
|
+
super(message, 3);
|
|
40
|
+
this.name = 'QuoxAuthError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A re-authentication that failed for a reason expected to clear by itself
|
|
46
|
+
* (auth service restarting, 429 rate_limited on the resident token exchange,
|
|
47
|
+
* 502/503/504, network fault). It IS a QuoxAuthError, so every verb that does
|
|
48
|
+
* not know about it behaves exactly as before; only long-lived listeners
|
|
49
|
+
* (`quox slot wait`) choose to back off and keep going. `retryAfterMs` carries
|
|
50
|
+
* the server's Retry-After when it sent one.
|
|
51
|
+
*/
|
|
52
|
+
export class QuoxTransientAuthError extends QuoxAuthError {
|
|
53
|
+
constructor(message, retryAfterMs = null) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = 'QuoxTransientAuthError';
|
|
56
|
+
this.transient = true;
|
|
57
|
+
this.retryAfterMs = retryAfterMs;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class QuoxNetworkError extends QuoxError {
|
|
62
|
+
constructor(message = 'Could not connect to QuoxCORE services.', cause) {
|
|
63
|
+
super(message, 7);
|
|
64
|
+
this.name = 'QuoxNetworkError';
|
|
65
|
+
this.cause = cause;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class QuoxValidationError extends QuoxError {
|
|
70
|
+
constructor(message) {
|
|
71
|
+
super(message, 2);
|
|
72
|
+
this.name = 'QuoxValidationError';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function statusToExitCode(status) {
|
|
77
|
+
if (status === 401) return 3;
|
|
78
|
+
if (status === 403) return 4;
|
|
79
|
+
if (status === 404) return 5;
|
|
80
|
+
if (status === 409) return 6;
|
|
81
|
+
if (status >= 500) return 8;
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const STATUS_HINTS = {
|
|
86
|
+
401: 'Run `quox login` to authenticate.',
|
|
87
|
+
403: 'You may not have permission. Try `quox org switch` to change context.',
|
|
88
|
+
404: 'Resource not found. Use the `list` subcommand to see what exists.',
|
|
89
|
+
409: 'Resource conflict. The item may already exist.',
|
|
90
|
+
429: 'Rate limited. Wait a moment and try again.',
|
|
91
|
+
500: 'Server error. Use --debug for details.',
|
|
92
|
+
502: 'Service temporarily unavailable. Check `quox service health`.',
|
|
93
|
+
503: 'Service temporarily unavailable. Check `quox service health`.',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export function handleError(err, outputFormat = 'table') {
|
|
97
|
+
if (outputFormat === 'json') {
|
|
98
|
+
const obj = {
|
|
99
|
+
error: true,
|
|
100
|
+
code: err.code || err.name,
|
|
101
|
+
message: err.message,
|
|
102
|
+
};
|
|
103
|
+
if (err.status) obj.status = err.status;
|
|
104
|
+
if (err.detail) obj.detail = err.detail;
|
|
105
|
+
// LL-3d: scripts parsing --json get the server's own body verbatim, not
|
|
106
|
+
// our prose rendering of it.
|
|
107
|
+
if (err.body && typeof err.body === 'object') obj.body = err.body;
|
|
108
|
+
process.stderr.write(JSON.stringify(obj) + '\n');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (err instanceof QuoxApiError) {
|
|
113
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
114
|
+
if (err.detail) console.error(chalk.dim(` Detail: ${err.detail}`));
|
|
115
|
+
console.error(chalk.dim(` Status: ${err.status} ${err.code || ''}`));
|
|
116
|
+
const hint = STATUS_HINTS[err.status];
|
|
117
|
+
if (hint) console.error(chalk.yellow(` Hint: ${hint}`));
|
|
118
|
+
} else if (err instanceof QuoxAuthError) {
|
|
119
|
+
console.error(chalk.red(`Auth: ${err.message}`));
|
|
120
|
+
} else if (err instanceof QuoxNetworkError) {
|
|
121
|
+
console.error(chalk.red(`Network: ${err.message}`));
|
|
122
|
+
console.error(chalk.yellow(' Hint: Check that services are running with `quox service health`.'));
|
|
123
|
+
} else if (err instanceof QuoxValidationError) {
|
|
124
|
+
console.error(chalk.red(`Validation: ${err.message}`));
|
|
125
|
+
} else {
|
|
126
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
127
|
+
if (process.env.DEBUG || process.argv.includes('--debug')) {
|
|
128
|
+
const sanitized = (err.stack || '').replace(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, '[JWT_REDACTED]');
|
|
129
|
+
console.error(chalk.dim(sanitized));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* global-flags.js — repair for commander's silent global-flag shadowing.
|
|
3
|
+
*
|
|
4
|
+
* Commander 13 resolves a long flag against the ROOT program first. When a
|
|
5
|
+
* subcommand redeclares a flag the root already declares, the root definition
|
|
6
|
+
* wins: the typed value lands on `program.opts()` and the subcommand's own
|
|
7
|
+
* options object holds `undefined` forever. Nothing warns. Nothing throws.
|
|
8
|
+
*
|
|
9
|
+
* That is a silent-wrong-answer bug, and it had already shipped in 13 places:
|
|
10
|
+
*
|
|
11
|
+
* - `quox gripe --yes` read `options.yes === undefined`, so it never told
|
|
12
|
+
* GRIPE to file directly and then advised re-running with the flag the
|
|
13
|
+
* user had just passed (fleet #441). Headless sessions could not file
|
|
14
|
+
* a gripe at all.
|
|
15
|
+
* - `--dry-run` on `vault migrate`, `admin` and `backup restore` read
|
|
16
|
+
* `undefined`, meaning a requested dry run EXECUTED FOR REAL. That one
|
|
17
|
+
* can cost data, which is why this is a scaffold fix and not a patch to
|
|
18
|
+
* gripe.js alone.
|
|
19
|
+
* - `--org` on fleet/skills/governance/done and `--json` on done/qlarity
|
|
20
|
+
* silently fell back to the active org / table output.
|
|
21
|
+
*
|
|
22
|
+
* The fix is a merge-down, not a de-duplication. Removing the subcommand's
|
|
23
|
+
* declaration would also work, but commander does not list parent options in
|
|
24
|
+
* subcommand help (verified on 13.1.0), so `quox vault migrate --help` would
|
|
25
|
+
* stop mentioning `--dry-run` while still honouring it. Keeping the local
|
|
26
|
+
* declaration keeps it discoverable; this hook makes it actually arrive.
|
|
27
|
+
*
|
|
28
|
+
* Only options the subcommand ITSELF declares are merged, so a command that
|
|
29
|
+
* never declared `--yes` does not silently gain it.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Copy shadowed global values down onto the subcommand that declared them.
|
|
34
|
+
*
|
|
35
|
+
* @param {import('commander').Command} rootCommand - the root program
|
|
36
|
+
* @param {import('commander').Command} actionCommand - the subcommand running
|
|
37
|
+
* @returns {string[]} attribute names that were merged (for tests/diagnostics)
|
|
38
|
+
*/
|
|
39
|
+
export function mergeShadowedGlobals(rootCommand, actionCommand) {
|
|
40
|
+
if (!rootCommand || !actionCommand || actionCommand === rootCommand) return [];
|
|
41
|
+
|
|
42
|
+
const rootOpts = rootCommand.opts();
|
|
43
|
+
const merged = [];
|
|
44
|
+
|
|
45
|
+
for (const option of actionCommand.options) {
|
|
46
|
+
const name = option.attributeName();
|
|
47
|
+
// A shadowed option can never carry a user value (commander gave it to
|
|
48
|
+
// the root), so an undefined here is unambiguous: nothing local to lose.
|
|
49
|
+
// A subcommand default is a real value and is left alone.
|
|
50
|
+
if (actionCommand.getOptionValue(name) === undefined && rootOpts[name] !== undefined) {
|
|
51
|
+
actionCommand.setOptionValue(name, rootOpts[name]);
|
|
52
|
+
merged.push(name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return merged;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Walk a command tree and report every subcommand option that collides with a
|
|
61
|
+
* root option. Used by the regression test to keep the blast radius visible:
|
|
62
|
+
* new collisions are fine (the hook handles them) but they should be a
|
|
63
|
+
* deliberate, seen decision rather than a surprise.
|
|
64
|
+
*
|
|
65
|
+
* @param {import('commander').Command} rootCommand
|
|
66
|
+
* @returns {Array<{command: string, flag: string, attribute: string}>}
|
|
67
|
+
*/
|
|
68
|
+
export function findShadowedOptions(rootCommand) {
|
|
69
|
+
const rootAttrs = new Map();
|
|
70
|
+
for (const option of rootCommand.options) {
|
|
71
|
+
rootAttrs.set(option.attributeName(), option.flags);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const found = [];
|
|
75
|
+
const walk = (cmd, path) => {
|
|
76
|
+
for (const sub of cmd.commands) {
|
|
77
|
+
const subPath = [...path, sub.name()];
|
|
78
|
+
for (const option of sub.options) {
|
|
79
|
+
const attr = option.attributeName();
|
|
80
|
+
if (rootAttrs.has(attr)) {
|
|
81
|
+
found.push({ command: subPath.join(' '), flag: option.flags, attribute: attr });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
walk(sub, subPath);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
walk(rootCommand, []);
|
|
88
|
+
|
|
89
|
+
return found;
|
|
90
|
+
}
|