quoxcli 0.0.0-stage → 0.3.1
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 +144 -2
- package/bin/quox.js +265 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +151 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +63 -0
- package/lib/api/endpoints/assistants.js +62 -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/away.js +15 -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 +61 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +68 -0
- package/lib/api/endpoints/conversations.js +46 -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 +45 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +40 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +66 -0
- package/lib/api/endpoints/inbox.js +52 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +242 -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 +94 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +184 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +68 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +152 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/presence.js +33 -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 +121 -0
- package/lib/api/endpoints/quoxseo.js +114 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +53 -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 +200 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/stepup.js +20 -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 +96 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +585 -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 +47 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +169 -0
- package/lib/api/endpoints/x.js +32 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +375 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +337 -0
- package/lib/commands/agentAttach.js +1528 -0
- package/lib/commands/approvals.js +119 -0
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +169 -0
- package/lib/commands/auth.js +464 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +377 -0
- package/lib/commands/brain.js +1085 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +170 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +295 -0
- package/lib/commands/claude.js +380 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +1016 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +105 -0
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +96 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +326 -0
- package/lib/commands/devSupervisor.js +1062 -0
- package/lib/commands/discord.js +132 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +347 -0
- package/lib/commands/engagement.js +597 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +165 -0
- package/lib/commands/fleet.js +1189 -0
- package/lib/commands/governance.js +647 -0
- package/lib/commands/gripe.js +355 -0
- package/lib/commands/hat.js +374 -0
- package/lib/commands/helpers.js +96 -0
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +173 -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 +139 -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 +597 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +361 -0
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +581 -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 +123 -0
- package/lib/commands/objective.js +240 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +329 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +276 -0
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +944 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +649 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +706 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +275 -0
- package/lib/commands/quoxseo.js +378 -0
- package/lib/commands/reach.js +159 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +187 -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/server.js +292 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +569 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +687 -0
- package/lib/commands/slot.js +1451 -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 +142 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +304 -0
- package/lib/commands/trace.js +530 -0
- package/lib/commands/trigger.js +223 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +1447 -0
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +213 -0
- package/lib/commands/volt.js +570 -0
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +167 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +660 -0
- package/lib/commands/x.js +448 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/activeSummary.js +186 -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/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +501 -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/cliBuild.js +62 -0
- package/lib/core/client.js +693 -0
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +669 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/customHats.js +168 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +227 -0
- package/lib/core/errors.js +441 -0
- package/lib/core/global-flags.js +190 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +122 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +239 -0
- package/lib/core/peek.js +126 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/repoPath.js +11 -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 +106 -0
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionHeartbeat.js +192 -0
- package/lib/core/sessionsOverview.js +224 -0
- package/lib/core/slotKeeper.js +527 -0
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +933 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +92 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +116 -0
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +952 -0
- package/lib/quoxon/app.js +4032 -0
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +82 -0
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +479 -0
- package/lib/quoxon/connect.js +654 -0
- package/lib/quoxon/connectCard.js +45 -0
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +34 -0
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +76 -0
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +36 -0
- package/lib/quoxon/events.js +677 -0
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +237 -0
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +197 -0
- package/lib/quoxon/intent.js +52 -0
- package/lib/quoxon/keys.js +164 -0
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +176 -0
- package/lib/quoxon/markdown.js +341 -0
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +311 -0
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +70 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +182 -0
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +106 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/proof.js +110 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/skill.js +105 -0
- package/lib/quoxon/objects/view.js +87 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +263 -0
- package/lib/quoxon/onboarding/credential.js +108 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +134 -0
- package/lib/quoxon/onboarding/index.js +94 -0
- package/lib/quoxon/onboarding/installRunner.js +284 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +156 -0
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +682 -0
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +98 -0
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +348 -0
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +132 -0
- package/lib/quoxon/render.js +1167 -0
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +307 -0
- package/lib/quoxon/route.js +39 -0
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +238 -0
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/skills.js +30 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +90 -0
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +88 -0
- package/lib/quoxon/slash/listView.js +30 -0
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +118 -0
- package/lib/quoxon/slash/paletteController.js +169 -0
- package/lib/quoxon/slash/panels.js +116 -0
- package/lib/quoxon/slash/picker.js +56 -0
- package/lib/quoxon/slash/restart.js +202 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +60 -0
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +120 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +141 -0
- package/lib/quoxon/textBuffer.js +416 -0
- package/lib/quoxon/theme.js +286 -0
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +188 -0
- package/lib/quoxon/web/qface.js +89 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -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/HyperionDeck.jsx +79 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +719 -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 +75 -4
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Window-scoped org (#1034 root cause). `/org switch` in the TUI used to write the new org and token into the SHARED
|
|
3
|
+
* profile (config.json) and move the shared server session's org, so every other window, script and proof run on the
|
|
4
|
+
* same profile (`-p moo`) silently ran in the switched org. Now the org-scoped token lives in THIS window's memory only:
|
|
5
|
+
*
|
|
6
|
+
* - the switch asks the server for a token without moving the session (`scope: 'window'`); an older server that
|
|
7
|
+
* moves it anyway gets the session put straight back on the profile's org;
|
|
8
|
+
* - the profile, the file and every other process keep the profile's org and token;
|
|
9
|
+
* - when the window token expires it is re-minted for the window's org, never the profile's.
|
|
10
|
+
*
|
|
11
|
+
* `quox org switch` from the shell is still the explicit way to change a profile's default org.
|
|
12
|
+
*/
|
|
13
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
14
|
+
import { RESTART_ORG_ENV } from './slash/restart.js';
|
|
15
|
+
|
|
16
|
+
export { RESTART_ORG_ENV };
|
|
17
|
+
|
|
18
|
+
const expiryOf = (token) => {
|
|
19
|
+
try { const p = JSON.parse(Buffer.from(String(token).split('.')[1], 'base64url').toString('utf8')); return p?.exp ? p.exp * 1000 : null; }
|
|
20
|
+
catch { return null; }
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} args
|
|
25
|
+
* @param {object} args.auth the profile auth (createAuth(config))
|
|
26
|
+
* @param {object} args.baseClient a client on the PROFILE identity (createClient(config, auth)), used only to mint
|
|
27
|
+
* @returns {{ auth, switchTo(orgId), current(), _expireForTest() }}
|
|
28
|
+
*/
|
|
29
|
+
export function createWindowOrg({ auth, baseClient, now = () => Date.now() }) {
|
|
30
|
+
let overlay = null; // { id, name, slug, role, access_token, expires_at } for this window only
|
|
31
|
+
const ep = resolveEndpoint('auth.switch-org');
|
|
32
|
+
const profileOrgId = () => { try { return auth.getOrgId(); } catch { return null; } };
|
|
33
|
+
|
|
34
|
+
async function mint(orgId) {
|
|
35
|
+
const data = await baseClient.post(ep.path, { service: ep.service, body: { org_id: orgId, scope: 'window' } });
|
|
36
|
+
if (!data?.org?.id || !data?.access_token) { const e = new Error('The server did not return a token for that org.'); e.code = 'switch_failed'; throw e; }
|
|
37
|
+
if (data.scope !== 'window') {
|
|
38
|
+
// An older server moved the shared session's org (the #1034 leak). Put it back on the profile's org so the other
|
|
39
|
+
// users of this profile do not refresh into the window's org. The token that call returns is discarded.
|
|
40
|
+
const home = profileOrgId();
|
|
41
|
+
if (home && home !== data.org.id) { try { await baseClient.post(ep.path, { service: ep.service, body: { org_id: home } }); } catch { /* best effort */ } }
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
id: data.org.id, name: data.org.name || data.org.slug || data.org.id, slug: data.org.slug || null, role: data.role || null,
|
|
45
|
+
access_token: data.access_token, expires_at: expiryOf(data.access_token) ?? now() + 50 * 60 * 1000,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const over = {
|
|
50
|
+
getTokens() {
|
|
51
|
+
const t = auth.getTokens();
|
|
52
|
+
if (!overlay) return t;
|
|
53
|
+
return { ...(t || {}), access_token: overlay.access_token, expires_at: overlay.expires_at, org_id: overlay.id, org_name: overlay.name, org_slug: overlay.slug, org_role: overlay.role };
|
|
54
|
+
},
|
|
55
|
+
isExpired() { return overlay ? now() > overlay.expires_at : auth.isExpired(); },
|
|
56
|
+
getOrgId(o) { if (o) return o; return overlay ? overlay.id : auth.getOrgId(); },
|
|
57
|
+
async refresh(baseUrl) {
|
|
58
|
+
if (!overlay) return auth.refresh(baseUrl);
|
|
59
|
+
if (auth.isExpired()) await auth.refresh(baseUrl); // the profile's own token, in the profile's org
|
|
60
|
+
overlay = await mint(overlay.id);
|
|
61
|
+
return { access_token: overlay.access_token };
|
|
62
|
+
},
|
|
63
|
+
// A new sign-in or sign-out in this window starts from the profile again.
|
|
64
|
+
async login(...a) { overlay = null; return auth.login(...a); },
|
|
65
|
+
async logout(...a) { overlay = null; return auth.logout(...a); },
|
|
66
|
+
};
|
|
67
|
+
const wrapped = new Proxy(auth, {
|
|
68
|
+
get(t, k) {
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(over, k)) return over[k];
|
|
70
|
+
const v = t[k];
|
|
71
|
+
return typeof v === 'function' ? v.bind(t) : v;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
auth: wrapped,
|
|
77
|
+
/** Move THIS window to orgId. Back to the profile's own org just drops the window token. -> { id, name } */
|
|
78
|
+
async switchTo(orgId) {
|
|
79
|
+
if (orgId && orgId === profileOrgId()) {
|
|
80
|
+
overlay = null;
|
|
81
|
+
const a = auth.getTokens() || {};
|
|
82
|
+
return { id: orgId, name: a.org_name || a.org_slug || orgId };
|
|
83
|
+
}
|
|
84
|
+
const m = await mint(orgId);
|
|
85
|
+
overlay = m;
|
|
86
|
+
return { id: m.id, name: m.name };
|
|
87
|
+
},
|
|
88
|
+
/** The window's own org when it differs from the profile's, else null. */
|
|
89
|
+
current() { return overlay ? { id: overlay.id, name: overlay.name } : null; },
|
|
90
|
+
_expireForTest() { if (overlay) overlay.expires_at = now() - 1; },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The client a TUI window should use: requests carry the window's org token when one is set.
|
|
96
|
+
* -> { client, auth, windowOrg } ready for createApi({ ...this, config }).
|
|
97
|
+
*/
|
|
98
|
+
export function createWindowClient({ config, auth, createClient }) {
|
|
99
|
+
const windowOrg = createWindowOrg({ auth, baseClient: createClient(config, auth) });
|
|
100
|
+
return { client: createClient(config, windowOrg.auth), auth: windowOrg.auth, windowOrg };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* /restart hand-over: the predecessor's window org arrives in RESTART_ORG_ENV. Re-enter it in this window, once.
|
|
105
|
+
* -> null (nothing carried) | { id, name } | { error, id }
|
|
106
|
+
*/
|
|
107
|
+
export async function carryOrg(api, env = process.env) {
|
|
108
|
+
const id = env[RESTART_ORG_ENV] || null;
|
|
109
|
+
delete env[RESTART_ORG_ENV];
|
|
110
|
+
if (!id || !api?.switchOrg) return null;
|
|
111
|
+
try { return await api.switchOrg(id); }
|
|
112
|
+
catch (e) { return { error: e?.message || 'refused', id }; }
|
|
113
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C4: `quox server diag` = a redacted support bundle, written locally, never uploaded.
|
|
3
|
+
*
|
|
4
|
+
* Redaction is ALLOWLIST-only, never a denylist (a denylist only knows the secret shapes its author
|
|
5
|
+
* thought of; 2026-08-09 a nested access_token slipped one):
|
|
6
|
+
* - JSON log lines keep only the fields named in LOG_FIELDS; everything else is dropped.
|
|
7
|
+
* - Free-text log lines keep a token only if it has an allowed SHAPE (a plain word, a small number,
|
|
8
|
+
* a timestamp, a URL path with no query and no id-like segment, an HTTP method/status); a key=value
|
|
9
|
+
* keeps its value only when the key is in VALUE_KEYS. Every other token becomes [redacted].
|
|
10
|
+
* - .env: key names only, plus the values of ENV_VALUE_KEYS (non-secret by construction).
|
|
11
|
+
*/
|
|
12
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { envKeyNames, readEnvKeys } from './install.js';
|
|
15
|
+
|
|
16
|
+
export const LOG_FIELDS = ['time', 'timestamp', 'ts', 'level', 'severity', 'service', 'component', 'module', 'msg', 'message', 'event', 'type',
|
|
17
|
+
'status', 'statusCode', 'code', 'method', 'route', 'path', 'duration_ms', 'durationMs', 'latency_ms', 'attempt', 'phase', 'errorName', 'err_name'];
|
|
18
|
+
export const VALUE_KEYS = ['status', 'code', 'method', 'level', 'service', 'port', 'attempt', 'duration', 'ms', 'count', 'exit', 'signal', 'version', 'route', 'path', 'phase', 'state'];
|
|
19
|
+
export const ENV_VALUE_KEYS = ['NODE_ENV', 'LOG_LEVEL', 'QUOXCORE_VERSION', 'COMPOSE_PROJECT_NAME', 'TZ'];
|
|
20
|
+
const R = '[redacted]';
|
|
21
|
+
const TEXT_FIELDS = new Set(['msg', 'message']);
|
|
22
|
+
|
|
23
|
+
const ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
24
|
+
const SHAPES = [
|
|
25
|
+
/^[A-Za-z][A-Za-z'’-]{0,24}$/, // a plain word
|
|
26
|
+
/^-?\d{1,6}(\.\d{1,3})?(ms|s|m|h|%|kb|mb|gb|KB|MB|GB)?$/, // a small number or duration
|
|
27
|
+
/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2}(\.\d{1,6})?)?(Z|[+-]\d{2}:?\d{2})?)?$/, // a date / ISO timestamp
|
|
28
|
+
/^\d{2}:\d{2}(:\d{2}(\.\d{1,6})?)?$/, // a clock time
|
|
29
|
+
/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/,
|
|
30
|
+
/^[[\](){}<>:;,.!?=|/*#&+-]{1,4}$/ // punctuation runs
|
|
31
|
+
];
|
|
32
|
+
const PUNCT_EDGE = /^([("'`[{<]*)(.*?)([)"'`\]}>:;,.!?]*)$/s;
|
|
33
|
+
|
|
34
|
+
/** A URL path with no query; any id-like segment (digits+letters, 12+ chars, hex 8+) is masked. */
|
|
35
|
+
function safePath(p) {
|
|
36
|
+
if (!/^\/[A-Za-z0-9/_.:-]{0,200}$/.test(p)) return null;
|
|
37
|
+
return p.split('/').map((seg) => (seg === '' || SHAPES[0].test(seg) || /^v\d{1,2}$/.test(seg) || /^\d{1,6}$/.test(seg) ? seg : ':id')).join('/');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function scrubToken(tok) {
|
|
41
|
+
const m = PUNCT_EDGE.exec(tok); const [, pre, core, post] = m || ['', '', tok, ''];
|
|
42
|
+
if (!core) return tok;
|
|
43
|
+
const kv = /^([A-Za-z_][\w.-]{0,40})([=:])(.*)$/.exec(core);
|
|
44
|
+
if (kv && !/^\/\//.test(kv[3])) {
|
|
45
|
+
const key = kv[1].toLowerCase().replace(/^.*[._-]/, '');
|
|
46
|
+
const val = kv[3].replace(/^["']|["']$/g, '');
|
|
47
|
+
const keep = VALUE_KEYS.includes(kv[1].toLowerCase()) || VALUE_KEYS.includes(key);
|
|
48
|
+
if (val === '') return `${pre}${kv[1]}${kv[2]}${post}`;
|
|
49
|
+
return `${pre}${kv[1]}${kv[2]}${keep && SHAPES.some((s) => s.test(val)) ? val : keep && safePath(val) ? safePath(val) : R}${post}`;
|
|
50
|
+
}
|
|
51
|
+
if (SHAPES.some((s) => s.test(core))) return `${pre}${core}${post}`;
|
|
52
|
+
if (core.startsWith('/')) { const sp = safePath(core); if (sp) return `${pre}${sp}${post}`; }
|
|
53
|
+
return `${pre}${R}${post}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Free text -> only allowed token shapes survive. */
|
|
57
|
+
export function scrubText(line) {
|
|
58
|
+
return String(line || '').replace(ANSI, '').split(/(\s+)/).map((t) => (/^\s+$/.test(t) || t === '' ? t : scrubToken(t))).join('');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One log line -> its redacted form. JSON lines keep allowlisted fields only. */
|
|
62
|
+
export function redactLine(line) {
|
|
63
|
+
const raw = String(line || '').replace(ANSI, '');
|
|
64
|
+
// docker compose prefixes "service-1 | "
|
|
65
|
+
const pm = /^(\S+\s+\|\s?)(.*)$/.exec(raw);
|
|
66
|
+
const prefix = pm ? pm[1] : ''; const body = pm ? pm[2] : raw;
|
|
67
|
+
const t = body.trim();
|
|
68
|
+
if (t.startsWith('{')) {
|
|
69
|
+
try {
|
|
70
|
+
const o = JSON.parse(t);
|
|
71
|
+
const kept = {};
|
|
72
|
+
for (const k of LOG_FIELDS) {
|
|
73
|
+
const v = o[k];
|
|
74
|
+
if (v === undefined || v === null || typeof v === 'object') continue;
|
|
75
|
+
if (typeof v === 'string') {
|
|
76
|
+
if (k === 'path' || k === 'route') kept[k] = safePath(v.split('?')[0]) || R;
|
|
77
|
+
else kept[k] = TEXT_FIELDS.has(k) ? scrubText(v) : (SHAPES.some((s) => s.test(v)) ? v : R);
|
|
78
|
+
} else kept[k] = v;
|
|
79
|
+
}
|
|
80
|
+
return prefix + JSON.stringify(kept);
|
|
81
|
+
} catch { /* not JSON after all */ }
|
|
82
|
+
}
|
|
83
|
+
return prefix + scrubText(body);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const INTERESTING = /\b(error|err|fail(ed|ure)?|fatal|exception|panic|warn(ing)?|refused|timeout|unhealthy|denied)\b/i;
|
|
87
|
+
|
|
88
|
+
/** Recent error/warning lines only, newest last, at most `max`. */
|
|
89
|
+
export function recentErrors(text, max = 200) {
|
|
90
|
+
return String(text || '').split('\n').filter((l) => INTERESTING.test(l)).slice(-max).map(redactLine);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Write the bundle. data = { versions, status rows, doctor rows }. Logs come from `docker compose logs`.
|
|
95
|
+
* -> { ok, file, files } ; the directory is tarred into <out>/quox-diag-<stamp>.tar.gz and removed.
|
|
96
|
+
*/
|
|
97
|
+
export async function writeDiagBundle({ install, sys, out, data, now = () => new Date(), services = null, tail = 2000 }) {
|
|
98
|
+
const stamp = now().toISOString().replace(/[:.]/g, '-').replace(/-\d{3}Z$/, 'Z');
|
|
99
|
+
const name = `quox-diag-${stamp}`;
|
|
100
|
+
const dir = join(out, name);
|
|
101
|
+
mkdirSync(join(dir, 'logs'), { recursive: true, mode: 0o700 });
|
|
102
|
+
const files = [];
|
|
103
|
+
const put = (rel, body) => { writeFileSync(join(dir, rel), typeof body === 'string' ? body : JSON.stringify(body, null, 2) + '\n', { mode: 0o600 }); files.push(rel); };
|
|
104
|
+
|
|
105
|
+
put('README.txt', [
|
|
106
|
+
'Quox support bundle (quox server diag). Written on this machine; nothing was uploaded.',
|
|
107
|
+
'Redaction is allowlist-only: JSON log lines keep only named fields; free-text lines keep only plain',
|
|
108
|
+
'words, small numbers, timestamps and id-free URL paths; everything else reads [redacted].',
|
|
109
|
+
'.env: key names only, plus the values of NODE_ENV, LOG_LEVEL, QUOXCORE_VERSION, COMPOSE_PROJECT_NAME, TZ.',
|
|
110
|
+
'Look through it before you share it.', ''
|
|
111
|
+
].join('\n'));
|
|
112
|
+
put('versions.json', data.versions || {});
|
|
113
|
+
put('status.json', data.status || {});
|
|
114
|
+
put('doctor.json', data.doctor || []);
|
|
115
|
+
put('env.json', { keys: envKeyNames(install.repoDir) || [], values: readEnvKeys(install.repoDir, ENV_VALUE_KEYS) });
|
|
116
|
+
|
|
117
|
+
const names = services || (data.status?.services?.ok ? data.status.services.services.map((s) => s.service) : []);
|
|
118
|
+
for (const s of names) {
|
|
119
|
+
if (!/^[\w.-]+$/.test(s)) continue;
|
|
120
|
+
const r = await sys.run('docker', ['compose', '-p', install.project, 'logs', '--no-color', '--tail', String(tail), s], { cwd: install.repoDir });
|
|
121
|
+
const lines = r.code === 0 ? recentErrors(`${r.stdout}\n${r.stderr}`) : [`could not read logs (exit ${r.code})`];
|
|
122
|
+
put(join('logs', `${s}.log`), lines.join('\n') + '\n');
|
|
123
|
+
}
|
|
124
|
+
const file = join(out, `${name}.tar.gz`);
|
|
125
|
+
const t = await sys.run('tar', ['czf', file, '-C', out, name]);
|
|
126
|
+
if (t.code !== 0) return { ok: true, file: dir, files, tarred: false };
|
|
127
|
+
rmSync(dir, { recursive: true, force: true });
|
|
128
|
+
return { ok: true, file, files, tarred: true };
|
|
129
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C4: `quox server doctor` = the `quox doctor` checks (runDoctorChecks, extracted in f491a2394) plus the
|
|
3
|
+
* server-side checks only the box itself can answer. All read-only. Same row shape as runDoctorChecks:
|
|
4
|
+
* { name, status: 'pass'|'warn'|'fail'|'skip', detail }.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { envKeyNames, readInstalled } from './install.js';
|
|
9
|
+
import { composeServices, diskFree, runningVersion } from './status.js';
|
|
10
|
+
import { latestSnapshot } from './snapshot.js';
|
|
11
|
+
|
|
12
|
+
export const REQUIRED_ENV = ['JWT_SECRET', 'POSTGRES_PASSWORD', 'INTERNAL_SERVICE_KEY'];
|
|
13
|
+
|
|
14
|
+
export async function serverChecks({ install, sys, fetchImpl = globalThis.fetch, urls, backups, now = () => Date.now() }) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const add = (name, status, detail) => out.push({ name, status, detail });
|
|
17
|
+
|
|
18
|
+
const dk = await sys.run('docker', ['info', '--format', '{{.ServerVersion}}']);
|
|
19
|
+
add('Docker engine', dk.code === 0 ? 'pass' : 'fail', dk.code === 0 ? `server ${dk.stdout.trim()}` : 'docker info failed (is the daemon running, is this user in the docker group?)');
|
|
20
|
+
|
|
21
|
+
const svc = await composeServices(install, sys);
|
|
22
|
+
if (!svc.ok) add('Compose services', 'fail', svc.error);
|
|
23
|
+
else {
|
|
24
|
+
const down = svc.services.filter((s) => s.state !== 'running');
|
|
25
|
+
const sick = svc.services.filter((s) => s.state === 'running' && s.health && s.health !== 'healthy');
|
|
26
|
+
add('Compose services', down.length ? 'fail' : sick.length ? 'warn' : 'pass',
|
|
27
|
+
`${svc.services.length - down.length} of ${svc.services.length} running${down.length ? `; not running: ${down.map((s) => s.service).join(', ')}` : ''}${sick.length ? `; unhealthy: ${sick.map((s) => `${s.service} (${s.health})`).join(', ')}` : ''}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const disk = await diskFree(install.root, sys);
|
|
31
|
+
add('Disk space', !disk.ok ? 'warn' : disk.freeGB < 2 ? 'fail' : disk.freeGB < 5 ? 'warn' : 'pass', disk.ok ? `${disk.freeGB} GB free at ${disk.path}` : disk.error);
|
|
32
|
+
|
|
33
|
+
const keys = envKeyNames(install.repoDir);
|
|
34
|
+
if (!keys) add('Config (.env)', 'fail', `no .env in ${install.repoDir}`);
|
|
35
|
+
else {
|
|
36
|
+
const missing = REQUIRED_ENV.filter((k) => !keys.includes(k));
|
|
37
|
+
add('Config (.env)', missing.length ? 'fail' : 'pass', missing.length ? `missing ${missing.join(', ')}` : `${keys.length} keys, required ones present (values not read)`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const cert = join(install.repoDir, 'ssl', 'quox.crt');
|
|
41
|
+
if (!existsSync(cert)) add('TLS certificate', 'warn', `no ${cert}`);
|
|
42
|
+
else {
|
|
43
|
+
const r = await sys.run('openssl', ['x509', '-enddate', '-noout', '-in', cert]);
|
|
44
|
+
const end = r.code === 0 ? Date.parse(r.stdout.replace(/^notAfter=/, '').trim()) : NaN;
|
|
45
|
+
if (!Number.isFinite(end)) add('TLS certificate', 'warn', 'could not read its expiry (openssl)');
|
|
46
|
+
else {
|
|
47
|
+
const days = Math.floor((end - now()) / 86400000);
|
|
48
|
+
add('TLS certificate', days < 0 ? 'fail' : days < 14 ? 'warn' : 'pass', days < 0 ? `expired ${-days} days ago` : `expires in ${days} days`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const installed = await readInstalled(install, sys);
|
|
53
|
+
add('Checkout', installed.dirty ? 'warn' : installed.dirty === null ? 'warn' : 'pass',
|
|
54
|
+
installed.dirty === null ? 'not a git checkout; quox server update cannot move it' : `${installed.release || '?'} at ${installed.commit}${installed.dirty ? ', with local changes (update will refuse without --force)' : ', clean'}`);
|
|
55
|
+
|
|
56
|
+
const rv = await runningVersion(urls.collector, fetchImpl);
|
|
57
|
+
if (!rv.ok) add('Running build', 'warn', `collector /version: ${rv.error}`);
|
|
58
|
+
else if (rv.stamped === false) add('Running build', 'warn', 'the collector build is unstamped (0.0.0); rebuild with QUOXCORE_VERSION set');
|
|
59
|
+
else {
|
|
60
|
+
const same = installed.commit && rv.commit && (String(rv.commit).startsWith(installed.commit) || String(installed.commit).startsWith(rv.commit));
|
|
61
|
+
add('Running build', same || !installed.commit ? 'pass' : 'warn', same ? `${rv.commit}, matches the checkout` : `running ${rv.commit}, checkout is ${installed.commit} (rebuilt since? quox server update rebuilds)`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const snap = backups ? latestSnapshot(backups) : null;
|
|
65
|
+
if (!snap) add('Backups', 'warn', `no snapshot yet in ${backups}; quox server update takes one first`);
|
|
66
|
+
else {
|
|
67
|
+
const days = Math.floor((now() - Date.parse(snap.createdAt)) / 86400000);
|
|
68
|
+
add('Backups', days > 30 ? 'warn' : 'pass', `latest ${snap.createdAt} (${days} days ago, ${snap.release || snap.commit})`);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C4 lifecycle: which install are we talking about, what version is it, what is available.
|
|
3
|
+
*
|
|
4
|
+
* Layout (get.quox.ai -> install.sh -> scripts/setup.sh): $QUOX_DIR/{quox,quoxflow,quoxmcp}; the quox
|
|
5
|
+
* checkout holds docker-compose.yml, VERSION, .env and ssl/. Compose project = the checkout's dir name
|
|
6
|
+
* (setup.sh runs `docker compose` there), unless the running containers say otherwise.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join, dirname, basename, resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
export const PUBLIC_REPO = 'quoxai/quoxcore';
|
|
13
|
+
export const SIBLINGS = ['quoxflow', 'quoxmcp'];
|
|
14
|
+
|
|
15
|
+
export function backupsRoot(env = process.env, home = homedir()) {
|
|
16
|
+
if (env.QUOX_SERVER_BACKUPS) return resolve(env.QUOX_SERVER_BACKUPS);
|
|
17
|
+
return join(env.XDG_DATA_HOME || join(home, '.local', 'share'), 'quox', 'server-backups');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalize(p) {
|
|
21
|
+
if (!p) return null;
|
|
22
|
+
const abs = resolve(p);
|
|
23
|
+
if (existsSync(join(abs, 'docker-compose.yml'))) return { repoDir: abs, root: dirname(abs) };
|
|
24
|
+
if (existsSync(join(abs, 'quox', 'docker-compose.yml'))) return { repoDir: join(abs, 'quox'), root: abs };
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** install.json that scripts/setup.sh writes: { dir } (the install root or the quox checkout). */
|
|
29
|
+
export function installRecordPath(env = process.env, home = homedir()) {
|
|
30
|
+
return join(env.XDG_CONFIG_HOME || join(home, '.config'), 'quox', 'install.json');
|
|
31
|
+
}
|
|
32
|
+
function readRecordedDir(env, home) {
|
|
33
|
+
try { const d = JSON.parse(readFileSync(installRecordPath(env, home), 'utf8'))?.dir; return typeof d === 'string' && d.startsWith('/') ? d : null; } catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Where the install lives: --dir, then QUOX_INSTALL_DIR, then the Getting started journey
|
|
38
|
+
* (~/.config/quox/onboarding.json installDir), then ~/.config/quox/install.json (written by setup.sh), then
|
|
39
|
+
* the running containers' compose label, then the installer's default ~/quox-install.
|
|
40
|
+
* -> { repoDir, root, project, source } | { error }
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveInstall({ dir, env = process.env, sys, journey = null, home = homedir() } = {}) {
|
|
43
|
+
if (dir) {
|
|
44
|
+
const n = normalize(dir);
|
|
45
|
+
if (!n) return { error: `${dir} is not a Quox install (no docker-compose.yml there or in ${join(dir, 'quox')})` };
|
|
46
|
+
return { ...n, project: await projectFor(n.repoDir, sys), source: '--dir' };
|
|
47
|
+
}
|
|
48
|
+
if (env.QUOX_INSTALL_DIR) {
|
|
49
|
+
const n = normalize(env.QUOX_INSTALL_DIR);
|
|
50
|
+
if (n) return { ...n, project: await projectFor(n.repoDir, sys), source: 'QUOX_INSTALL_DIR' };
|
|
51
|
+
}
|
|
52
|
+
let j = journey;
|
|
53
|
+
if (j === null) { try { const { loadJourney } = await import('../quoxon/onboarding/journey.js'); j = loadJourney(); } catch { j = null; } }
|
|
54
|
+
if (j?.installDir) {
|
|
55
|
+
const n = normalize(j.installDir);
|
|
56
|
+
if (n) return { ...n, project: await projectFor(n.repoDir, sys), source: 'Getting started' };
|
|
57
|
+
}
|
|
58
|
+
const rec = normalize(readRecordedDir(env, home));
|
|
59
|
+
if (rec) return { ...rec, project: await projectFor(rec.repoDir, sys), source: 'install.json' };
|
|
60
|
+
if (sys) {
|
|
61
|
+
const r = await sys.run('docker', ['inspect', 'quox-auth', '--format', '{{index .Config.Labels "com.docker.compose.project.working_dir"}}|{{index .Config.Labels "com.docker.compose.project"}}']);
|
|
62
|
+
if (r.code === 0) {
|
|
63
|
+
const [wd, project] = r.stdout.trim().split('|');
|
|
64
|
+
const n = normalize(wd);
|
|
65
|
+
if (n) return { ...n, project: project || basename(n.repoDir), source: 'running containers' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const def = normalize(join(home, 'quox-install'));
|
|
69
|
+
if (def) return { ...def, project: await projectFor(def.repoDir, sys), source: 'default ~/quox-install' };
|
|
70
|
+
return { error: 'no Quox install found here. Pass --dir <path>, or set QUOX_INSTALL_DIR (setup.sh records it in ~/.config/quox/install.json).' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function projectFor(repoDir, sys) {
|
|
74
|
+
if (sys) {
|
|
75
|
+
const r = await sys.run('docker', ['inspect', 'quox-auth', '--format', '{{index .Config.Labels "com.docker.compose.project.working_dir"}}|{{index .Config.Labels "com.docker.compose.project"}}']);
|
|
76
|
+
if (r.code === 0) {
|
|
77
|
+
const [wd, project] = r.stdout.trim().split('|');
|
|
78
|
+
if (wd && resolve(wd) === repoDir && project) return project;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return basename(repoDir);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Read named keys from .env. ALLOWLIST: only the keys asked for come back; nothing else is parsed out. */
|
|
85
|
+
export function readEnvKeys(repoDir, keys) {
|
|
86
|
+
const out = {};
|
|
87
|
+
let text = '';
|
|
88
|
+
try { text = readFileSync(join(repoDir, '.env'), 'utf8'); } catch { return out; }
|
|
89
|
+
for (const line of text.split('\n')) {
|
|
90
|
+
const m = /^([A-Z0-9_]+)=(.*)$/.exec(line.trim());
|
|
91
|
+
if (m && keys.includes(m[1])) out[m[1]] = m[2].replace(/^"(.*)"$/, '$1');
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Names only (never values) of every key in .env. */
|
|
97
|
+
export function envKeyNames(repoDir) {
|
|
98
|
+
try {
|
|
99
|
+
return readFileSync(join(repoDir, '.env'), 'utf8').split('\n').map((l) => /^([A-Z0-9_]+)=/.exec(l.trim())?.[1]).filter(Boolean);
|
|
100
|
+
} catch { return null; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** -> { release, commit, tag, dirty, siblings: { quoxflow: commit|null, ... } } (read-only git). */
|
|
104
|
+
export async function readInstalled({ repoDir, root }, sys) {
|
|
105
|
+
let release = null;
|
|
106
|
+
try { release = readFileSync(join(repoDir, 'VERSION'), 'utf8').trim() || null; } catch { /* no VERSION */ }
|
|
107
|
+
const git = async (dir, args) => { const r = await sys.run('git', ['-C', dir, ...args]); return r.code === 0 ? r.stdout.trim() : null; };
|
|
108
|
+
const commit = await git(repoDir, ['rev-parse', '--short', 'HEAD']);
|
|
109
|
+
const tag = await git(repoDir, ['describe', '--tags', '--exact-match', 'HEAD']);
|
|
110
|
+
const porcelain = await git(repoDir, ['status', '--porcelain', '--untracked-files=no']);
|
|
111
|
+
const siblings = {};
|
|
112
|
+
for (const s of SIBLINGS) {
|
|
113
|
+
const d = join(root, s);
|
|
114
|
+
siblings[s] = existsSync(join(d, '.git')) ? await git(d, ['rev-parse', '--short', 'HEAD']) : null;
|
|
115
|
+
}
|
|
116
|
+
return { release, commit, tag, dirty: porcelain === null ? null : porcelain.length > 0, siblings };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function parseSemver(v) {
|
|
120
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(String(v || '').trim());
|
|
121
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
122
|
+
}
|
|
123
|
+
export function cmpSemver(a, b) {
|
|
124
|
+
const x = parseSemver(a); const y = parseSemver(b);
|
|
125
|
+
if (!x || !y) return null;
|
|
126
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Tags on the public repo (read-only, unauthenticated GitHub API). Status checked before parsing.
|
|
132
|
+
* -> { ok: true, latest, tags } | { ok: false, error }
|
|
133
|
+
*/
|
|
134
|
+
export async function fetchAvailable({ fetchImpl = globalThis.fetch, repo = PUBLIC_REPO, timeoutMs = 8000 } = {}) {
|
|
135
|
+
const url = `https://api.github.com/repos/${repo}/tags?per_page=100`;
|
|
136
|
+
let r;
|
|
137
|
+
try { r = await fetchImpl(url, { headers: { accept: 'application/vnd.github+json', 'user-agent': 'quox-cli' }, signal: AbortSignal.timeout(timeoutMs) }); } catch (e) { return { ok: false, error: `could not reach ${url}: ${e?.message || e}` }; }
|
|
138
|
+
if (!r.ok) return { ok: false, error: `${url} answered HTTP ${r.status}` };
|
|
139
|
+
let body;
|
|
140
|
+
try { body = await r.json(); } catch { return { ok: false, error: `${url} did not return JSON` }; }
|
|
141
|
+
if (!Array.isArray(body)) return { ok: false, error: `${url} returned an unexpected shape` };
|
|
142
|
+
const tags = body.map((t) => t?.name).filter((n) => parseSemver(n)).sort((a, b) => cmpSemver(b, a));
|
|
143
|
+
if (!tags.length) return { ok: false, error: `no release tags on ${repo}` };
|
|
144
|
+
return { ok: true, latest: tags[0], tags };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const releaseNotesUrl = (tag, repo = PUBLIC_REPO) => `https://github.com/${repo}/releases/tag/${tag}`;
|
|
148
|
+
export const compareUrl = (from, to, repo = PUBLIC_REPO) => `https://github.com/${repo}/compare/${from}...${to}`;
|
|
149
|
+
export const vtag = (v) => (String(v).startsWith('v') ? String(v) : `v${v}`);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLUGIN-IMG #1012: `quox plugin enable <id>` starts a paid plugin from its
|
|
3
|
+
* prebuilt PRIVATE image on a self-hosted install.
|
|
4
|
+
*
|
|
5
|
+
* Order, and why:
|
|
6
|
+
* 1. Licence check FIRST. Not entitled: refuse with a clear message and
|
|
7
|
+
* run nothing on the box (no login, no pull, no compose). The entitlement
|
|
8
|
+
* read is the same resolved set auth's has-plugin uses (verified,
|
|
9
|
+
* instance-bound, unexpired leases), via the user-facing
|
|
10
|
+
* /api/setup/licensing/entitlements read.
|
|
11
|
+
* 2. Pull credential (TODO hook below): the licence server will issue a
|
|
12
|
+
* short-lived, package-scoped GHCR credential to an instance whose lease
|
|
13
|
+
* covers the plugin. Until that endpoint ships, the hook returns a clear
|
|
14
|
+
* "pending" error and enable stops there, unless the image is already
|
|
15
|
+
* present locally (for example a proving VM where it was loaded by hand),
|
|
16
|
+
* in which case it starts with --pull never.
|
|
17
|
+
* 3. docker compose -f docker-compose.yml -f docker-compose.images.yml
|
|
18
|
+
* -f docker-compose.plugins.yml --profile <profile> up -d <service>
|
|
19
|
+
* 4. Health: poll the container's healthcheck until healthy (or time out).
|
|
20
|
+
*
|
|
21
|
+
* Everything that touches the host goes through an injected `sys`
|
|
22
|
+
* (lib/server/sys.js), so tests prove what would run without running it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Plugins that ship as prebuilt images. QuoxPlan only for now. */
|
|
26
|
+
export const PLUGIN_IMAGES = Object.freeze({
|
|
27
|
+
quoxplan: Object.freeze({
|
|
28
|
+
service: 'quoxplan',
|
|
29
|
+
profile: 'with-quoxplan',
|
|
30
|
+
container: 'quox-quoxplan',
|
|
31
|
+
image: 'ghcr.io/quoxai/quoxplan',
|
|
32
|
+
registry: 'ghcr.io',
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const COMPOSE_FILES = Object.freeze(['docker-compose.yml', 'docker-compose.images.yml', 'docker-compose.plugins.yml']);
|
|
37
|
+
|
|
38
|
+
/** The image tag: the install's release tag (QUOX_IMAGE_TAG in .env), else latest. */
|
|
39
|
+
export function imageRef(spec, tag) {
|
|
40
|
+
return `${spec.image}:${tag || 'latest'}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The docker compose argv `enable` runs (after `docker`). */
|
|
44
|
+
export function composeUpArgs({ project }, spec, { pullNever = false } = {}) {
|
|
45
|
+
return [
|
|
46
|
+
'compose',
|
|
47
|
+
...(project ? ['-p', project] : []),
|
|
48
|
+
...COMPOSE_FILES.flatMap((f) => ['-f', f]),
|
|
49
|
+
'--profile', spec.profile,
|
|
50
|
+
'up', '-d',
|
|
51
|
+
...(pullNever ? ['--pull', 'never'] : []),
|
|
52
|
+
spec.service,
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* TODO(PLUGIN-IMG #1012, owner-gated): fetch a short-lived, package-scoped
|
|
58
|
+
* pull credential from the licence server for this instance's lease. The
|
|
59
|
+
* endpoint does not exist yet (licence-server change, owner deploy word).
|
|
60
|
+
* Contract when it lands: -> { ok: true, registry, username, token, expires_at }
|
|
61
|
+
* and enable logs in with `docker login <registry> -u <username> --password-stdin`.
|
|
62
|
+
*/
|
|
63
|
+
export async function getPluginPullCredential(_pluginId, _ctx = {}) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
pending: true,
|
|
67
|
+
error: 'pull credential pending licence-server endpoint: the licence server does not issue plugin image pull credentials yet (#1012)',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Same rule as `quox plugin licence`: a governed plugin is entitled when the
|
|
73
|
+
* resolved entitlements carry it (bare or banded) and, when it is banded,
|
|
74
|
+
* this org holds a seat.
|
|
75
|
+
*/
|
|
76
|
+
export function entitlementFrom(data, pluginId) {
|
|
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
|
+
const band = (data?.org_bands && data.org_bands[pluginId]) || null;
|
|
81
|
+
if (!governed) return { entitled: true, governed: false };
|
|
82
|
+
if (!match) return { entitled: false, governed: true, reason: 'no_entitlement' };
|
|
83
|
+
if (band && band.org_licensed === false) return { entitled: false, governed: true, reason: 'org_not_in_band' };
|
|
84
|
+
return { entitled: true, governed: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {object} o
|
|
89
|
+
* @param {string} o.pluginId
|
|
90
|
+
* @param {(pluginId: string) => Promise<{ entitled: boolean, reason?: string }>} o.checkEntitlement
|
|
91
|
+
* @param {{ repoDir: string, project?: string }} o.install
|
|
92
|
+
* @param {{ run: Function }} o.sys
|
|
93
|
+
* @param {string} [o.tag] image tag (QUOX_IMAGE_TAG)
|
|
94
|
+
* @param {Function} [o.getPullCredential]
|
|
95
|
+
* @param {(ms: number) => Promise<void>} [o.sleep]
|
|
96
|
+
* @param {number} [o.healthTries]
|
|
97
|
+
* @param {(line: string) => void} [o.log]
|
|
98
|
+
* @returns {Promise<{ ok: boolean, stage: string, message: string, ran: string[][] }>}
|
|
99
|
+
*/
|
|
100
|
+
export async function enablePlugin({
|
|
101
|
+
pluginId, checkEntitlement, install, sys, tag,
|
|
102
|
+
getPullCredential = getPluginPullCredential,
|
|
103
|
+
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
104
|
+
healthTries = 30, healthIntervalMs = 2000,
|
|
105
|
+
log = () => {},
|
|
106
|
+
}) {
|
|
107
|
+
const ran = [];
|
|
108
|
+
const run = async (cmd, args, opts) => { ran.push([cmd, ...args]); return sys.run(cmd, args, opts); };
|
|
109
|
+
const spec = PLUGIN_IMAGES[pluginId];
|
|
110
|
+
if (!spec) {
|
|
111
|
+
const known = Object.keys(PLUGIN_IMAGES).join(', ');
|
|
112
|
+
return { ok: false, stage: 'unknown', message: `${pluginId} does not ship as a prebuilt plugin image yet (available: ${known}).`, ran };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let ent;
|
|
116
|
+
try {
|
|
117
|
+
ent = await checkEntitlement(pluginId);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return { ok: false, stage: 'licence', message: `Could not check the ${pluginId} licence (${err.message}). Nothing was pulled or started.`, ran };
|
|
120
|
+
}
|
|
121
|
+
if (!ent || !ent.entitled) {
|
|
122
|
+
const why = ent?.reason === 'org_not_in_band' ? ' This organisation does not hold one of its seats.' : '';
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
stage: 'licence',
|
|
126
|
+
message: `${pluginId} is not licensed on this install.${why} Nothing was pulled or started. Run \`quox plugin licence ${pluginId}\` to see the entitlement, then renew or buy the licence.`,
|
|
127
|
+
ran,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const ref = imageRef(spec, tag);
|
|
132
|
+
let pullNever = false;
|
|
133
|
+
const cred = await getPullCredential(pluginId, { install });
|
|
134
|
+
if (cred && cred.ok) {
|
|
135
|
+
const login = await run('docker', ['login', cred.registry || spec.registry, '-u', cred.username, '--password-stdin'], { stdinText: cred.token });
|
|
136
|
+
if (login.code !== 0) return { ok: false, stage: 'login', message: `docker login to ${cred.registry || spec.registry} failed: ${(login.stderr || '').trim().slice(0, 300)}`, ran };
|
|
137
|
+
} else {
|
|
138
|
+
const local = await run('docker', ['image', 'inspect', ref, '--format', '{{.Id}}']);
|
|
139
|
+
if (local.code !== 0) {
|
|
140
|
+
return { ok: false, stage: 'pull-credential', message: `${pluginId} is licensed, but its image ${ref} is private and ${cred?.error || 'no pull credential is available'}. Nothing was started.`, ran };
|
|
141
|
+
}
|
|
142
|
+
pullNever = true;
|
|
143
|
+
log(`${ref} is already present locally; starting it without a pull.`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const up = await run('docker', composeUpArgs(install, spec, { pullNever }), { cwd: install.repoDir });
|
|
147
|
+
if (up.code !== 0) return { ok: false, stage: 'compose', message: `docker compose up for ${spec.service} failed: ${(up.stderr || '').trim().slice(-400)}`, ran };
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < healthTries; i += 1) {
|
|
150
|
+
const h = await run('docker', ['inspect', '--format', '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}', spec.container]);
|
|
151
|
+
const status = (h.stdout || '').trim();
|
|
152
|
+
if (h.code === 0 && status === 'healthy') {
|
|
153
|
+
return { ok: true, stage: 'healthy', message: `${pluginId} is running (${ref}) and healthy.`, ran };
|
|
154
|
+
}
|
|
155
|
+
if (h.code === 0 && status === 'unhealthy') {
|
|
156
|
+
return { ok: false, stage: 'health', message: `${spec.container} started but reports unhealthy. Check \`docker logs ${spec.container}\`.`, ran };
|
|
157
|
+
}
|
|
158
|
+
await sleep(healthIntervalMs);
|
|
159
|
+
}
|
|
160
|
+
return { ok: false, stage: 'health', message: `${spec.container} did not report healthy in time. Check \`docker logs ${spec.container}\`.`, ran };
|
|
161
|
+
}
|