quoxcli 0.2.0 → 0.4.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 +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +46 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +14 -0
- package/lib/api/endpoints/contactsq.js +6 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +44 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +26 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +179 -0
- package/lib/api/endpoints/objectives.js +12 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +12 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +120 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +131 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +170 -27
- package/lib/commands/agentAttach.js +140 -13
- package/lib/commands/approvals.js +78 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +146 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +180 -26
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +35 -38
- package/lib/commands/contacts.js +134 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +107 -6
- package/lib/commands/evidence.js +154 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +336 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +254 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/kuma.js +1050 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +52 -155
- package/lib/commands/loop.js +291 -0
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +59 -1
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +30 -3
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +217 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +69 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/research.js +79 -0
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +367 -0
- package/lib/commands/run.js +82 -10
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +405 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +68 -9
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actingAs.js +2 -2
- package/lib/core/actionItems.js +115 -0
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +152 -35
- package/lib/core/commandRegistrars.js +130 -0
- package/lib/core/config.js +143 -25
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +105 -5
- package/lib/core/governedSession.js +1 -3
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +533 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +11 -11
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- 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 +12 -8
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/shortcut.js +83 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +366 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/EXTENSIONS.md +23 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +690 -25
- package/lib/quoxon/app.js +3586 -250
- package/lib/quoxon/approvalFacts.js +96 -0
- package/lib/quoxon/approvalSentence.js +44 -0
- package/lib/quoxon/approvalsView.js +384 -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 +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +124 -0
- package/lib/quoxon/commands.js +429 -39
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +20 -0
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +46 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +192 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonCompanion.js +255 -0
- package/lib/quoxon/dragonModes.js +151 -0
- package/lib/quoxon/dragonMoments.js +82 -0
- package/lib/quoxon/dragonReads.js +68 -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 +2 -2
- package/lib/quoxon/events.js +266 -26
- package/lib/quoxon/extensions.js +276 -0
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- 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/inboxView.js +79 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +51 -8
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/loopCard.js +456 -0
- package/lib/quoxon/loopCards.js +180 -0
- package/lib/quoxon/loopStream.js +87 -0
- package/lib/quoxon/main.js +101 -160
- package/lib/quoxon/markdown.js +45 -21
- 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 +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +222 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +46 -7
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/proof.js +12 -4
- 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/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +59 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +12 -5
- 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 +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +62 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +75 -37
- package/lib/quoxon/render.js +740 -124
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +117 -17
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +954 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +182 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -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 +94 -0
- package/lib/quoxon/slash/cliLeaves.json +1023 -0
- package/lib/quoxon/slash/compact.js +1 -1
- 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/evidenceCmd.js +34 -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 +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +22 -8
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +95 -13
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- 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/todoCmd.js +122 -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/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +90 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +134 -0
- package/lib/quoxon/windowOrg.js +123 -0
- package/lib/server/devServers.js +137 -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/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +5 -1
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
- package/lib/core/websiteSession.js +0 -148
package/lib/core/vm.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared engine behind `quox vm` and the QUOXON `/vm` command: drives the
|
|
3
|
+
* HYPERION (agent id atlas) Proxmox tools through the governed tool route
|
|
4
|
+
* (POST /api/v1/tools/execute). Pure functions over an injected `exec(tool, input)`
|
|
5
|
+
* so the CLI group and the TUI share one implementation and one set of tests.
|
|
6
|
+
*
|
|
7
|
+
* Tool names/inputs are the contract with the server side:
|
|
8
|
+
* proxmox_vms, proxmox_containers, proxmox_vm_resources, proxmox_vm_action,
|
|
9
|
+
* proxmox_vm_snapshot, proxmox_vm_resolve {name|vmid}, proxmox_pool_status,
|
|
10
|
+
* proxmox_pool_lease, proxmox_pool_release, proxmox_pool_configure,
|
|
11
|
+
* proxmox_node_power, proxmox_rehearse {vmid, node, command, checks?, keep_twin?} (starts, returns {rehearsal_id, state:'running'}),
|
|
12
|
+
* proxmox_rehearsal_status {rehearsal_id}.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
19
|
+
|
|
20
|
+
export const VM_ACTIONS = ['start', 'stop', 'reboot', 'shutdown'];
|
|
21
|
+
export const NODE_ACTIONS = ['reboot', 'shutdown'];
|
|
22
|
+
|
|
23
|
+
/** A write was held for a human. Callers print the hint and never re-submit. */
|
|
24
|
+
export class VmPendingError extends Error {
|
|
25
|
+
constructor(tool, data) {
|
|
26
|
+
super(`${tool} is awaiting approval`);
|
|
27
|
+
this.name = 'VmPendingError';
|
|
28
|
+
this.tool = tool;
|
|
29
|
+
this.approvalId = data?.approvalId || data?.approval_id || data?._approval?.id || null;
|
|
30
|
+
this.inboxItemId = data?.inbox_item_id || null;
|
|
31
|
+
this.rehearsalId = data?.rehearsal_id || null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function pendingHint(e) {
|
|
36
|
+
const id = e.approvalId || e.inboxItemId;
|
|
37
|
+
if (!id) return `${e.tool} is awaiting approval. Check: quox approvals`;
|
|
38
|
+
return `${e.tool} is awaiting approval (approval id ${id}). After a human approves, run: quox approvals result ${id}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* DEVFLEET-READ: HYPERION (atlas) is granted ONE read tool, proxmox_read, not the per-view read
|
|
43
|
+
* tools. Callers keep using the old names; the wire call is proxmox_read with the matching view.
|
|
44
|
+
* Writes (proxmox_vm_action, _snapshot, _rehearse, _vm_doctor, pool lease/release/config) pass through.
|
|
45
|
+
*/
|
|
46
|
+
export const PROXMOX_READ_VIEW_OF = {
|
|
47
|
+
proxmox_vms: 'vms', proxmox_containers: 'containers', proxmox_cluster_status: 'cluster',
|
|
48
|
+
proxmox_ha_status: 'ha', proxmox_templates_list: 'templates', proxmox_storage: 'storage',
|
|
49
|
+
proxmox_node_resources: 'node', proxmox_vm_resources: 'vm', proxmox_snapshots_list: 'snapshots',
|
|
50
|
+
proxmox_backup_status: 'backups', proxmox_vm_resolve: 'resolve', proxmox_pool_status: 'pool',
|
|
51
|
+
proxmox_rehearsal_status: 'rehearsal', proxmox_doctor_report: 'doctor_report',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Tools that fan out many Proxmox reads server-side and outlive the client's 30s default.
|
|
55
|
+
// VM Doctor runs 12 reads in parallel; over Bastion pvesh on a slow node that is ~30s.
|
|
56
|
+
export const SLOW_TOOL_TIMEOUT_MS = { proxmox_vm_doctor: 120000, proxmox_snapshots_list: 90000 };
|
|
57
|
+
|
|
58
|
+
/** Build an exec(tool, input) over an API client. Throws VmPendingError on a held call. */
|
|
59
|
+
export function makeExec({ client, orgId }) {
|
|
60
|
+
return async function exec(tool, input = {}) {
|
|
61
|
+
const ep = resolveEndpoint('v1.tools.execute');
|
|
62
|
+
// HYPERION (atlas) owns these tools; without agent_id the call lands on the
|
|
63
|
+
// orchestrator, which is not granted them. An attach token's agent claim still wins server-side.
|
|
64
|
+
const view = PROXMOX_READ_VIEW_OF[tool];
|
|
65
|
+
const body = view ? { tool_name: 'proxmox_read', input: { view, ...input }, agent_id: 'atlas' } : { tool_name: tool, input, agent_id: 'atlas' };
|
|
66
|
+
if (orgId) body.org_id = orgId;
|
|
67
|
+
const timeout = SLOW_TOOL_TIMEOUT_MS[tool];
|
|
68
|
+
let data = await client.post(ep.path, { service: ep.service, body, ...(timeout ? { timeout } : {}) });
|
|
69
|
+
// The caller's own token identity wins over agent_id server-side (e.g. an attached session runs
|
|
70
|
+
// as itself, which holds the legacy reads like proxmox_vms but not proxmox_read). When proxmox_read
|
|
71
|
+
// is not granted to that identity, retry once with the legacy per-view tool it does hold.
|
|
72
|
+
if (view && data && data.success === false && /not granted/i.test(String(data.error || ''))) {
|
|
73
|
+
const legacy = { tool_name: tool, input, agent_id: 'atlas' };
|
|
74
|
+
if (orgId) legacy.org_id = orgId;
|
|
75
|
+
data = await client.post(ep.path, { service: ep.service, body: legacy });
|
|
76
|
+
}
|
|
77
|
+
if (data && data.pending) throw new VmPendingError(tool, data);
|
|
78
|
+
if (data && data.success === false) {
|
|
79
|
+
const err = new Error(`${tool} via POST ${ep.path} returned success:false: ${data.error || 'unknown error'}`);
|
|
80
|
+
err.toolError = data.error || 'unknown error';
|
|
81
|
+
err.tool = tool;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
return data;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The tool's payload, whatever envelope it came wrapped in. */
|
|
89
|
+
export function payloadOf(data) {
|
|
90
|
+
let p = data;
|
|
91
|
+
if (p && typeof p === 'object' && !Array.isArray(p)) {
|
|
92
|
+
for (const k of ['result', 'output', 'data']) {
|
|
93
|
+
if (p[k] !== undefined && p[k] !== null) { p = p[k]; break; }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (typeof p === 'string') { try { return JSON.parse(p); } catch { return p; } }
|
|
97
|
+
return p;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function arrayOf(p, ...keys) {
|
|
101
|
+
if (Array.isArray(p)) return p;
|
|
102
|
+
for (const k of [...keys, 'items', 'results', 'resources']) if (Array.isArray(p?.[k])) return p[k];
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const normVm = (v, fallbackType) => ({
|
|
107
|
+
vmid: Number(v.vmid ?? v.id),
|
|
108
|
+
name: v.name ?? v.hostname ?? '',
|
|
109
|
+
node: v.node ?? null,
|
|
110
|
+
type: v.type === 'lxc' || v.type === 'qemu' ? v.type : (fallbackType || 'qemu'),
|
|
111
|
+
status: v.status ?? v.state ?? '',
|
|
112
|
+
raw: v,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const isUnknownTool = (e) => e?.status === 404
|
|
116
|
+
|| /unknown tool|tool .*not (found|available|registered)|no such tool|unsupported tool|not implemented/i.test(e?.toolError || e?.message || '');
|
|
117
|
+
|
|
118
|
+
export async function listVms(exec, { node, type } = {}) {
|
|
119
|
+
if (type && !['qemu', 'lxc'].includes(type)) throw new Error('--type must be qemu or lxc');
|
|
120
|
+
const out = [];
|
|
121
|
+
if (type !== 'lxc') {
|
|
122
|
+
const p = payloadOf(await exec('proxmox_vms', node ? { node } : {}));
|
|
123
|
+
for (const v of arrayOf(p, 'vms')) out.push(normVm(v, 'qemu'));
|
|
124
|
+
}
|
|
125
|
+
if (type === 'lxc') {
|
|
126
|
+
const p = payloadOf(await exec('proxmox_containers', node ? { node } : {}));
|
|
127
|
+
for (const v of arrayOf(p, 'containers', 'lxc')) out.push(normVm(v, 'lxc'));
|
|
128
|
+
}
|
|
129
|
+
return node ? out.filter((v) => !v.node || v.node === node) : out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* name|vmid -> { vmid, node, type, name }. Tool proxmox_vm_resolve first; if the server
|
|
134
|
+
* does not know it, fall back to client-side exact match over proxmox_vms + proxmox_containers.
|
|
135
|
+
* Ambiguity is an error listing candidates; never a guess.
|
|
136
|
+
*/
|
|
137
|
+
/** DEVFLEET INV-1: list rows for `quox vm list`, with each VM's Proxmox tags as one cell ('' when untagged). */
|
|
138
|
+
export function vmListRows(vms) {
|
|
139
|
+
return vms.map(({ raw, ...r }) => ({ ...r, tags: Array.isArray(raw && raw.tags) ? raw.tags.join(', ') : '' }));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function resolveTarget(exec, target) {
|
|
143
|
+
const t = String(target ?? '').trim();
|
|
144
|
+
if (!t) throw new Error('a VM name or vmid is required');
|
|
145
|
+
const numeric = /^\d+$/.test(t);
|
|
146
|
+
const input = numeric ? { vmid: Number(t) } : { name: t };
|
|
147
|
+
let matches = null;
|
|
148
|
+
try {
|
|
149
|
+
const p = payloadOf(await exec('proxmox_vm_resolve', input));
|
|
150
|
+
const list = Array.isArray(p) ? p : (p?.matches || p?.candidates || (p && (p.vmid !== undefined || p.id !== undefined) ? [p] : []));
|
|
151
|
+
matches = list.map((v) => normVm(v));
|
|
152
|
+
if (!matches.length) throw new Error(`no VM matches "${t}" (proxmox_vm_resolve)`);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (e instanceof VmPendingError || !isUnknownTool(e)) throw e;
|
|
155
|
+
matches = null;
|
|
156
|
+
}
|
|
157
|
+
if (matches === null) {
|
|
158
|
+
const all = await listVms(exec, {});
|
|
159
|
+
try { all.push(...await listVms(exec, { type: 'lxc' })); } catch (e) { if (e instanceof VmPendingError) throw e; /* containers are optional */ }
|
|
160
|
+
matches = all.filter((v) => (numeric ? v.vmid === Number(t) : v.name === t));
|
|
161
|
+
if (!matches.length) throw new Error(`no VM matches "${t}" (checked proxmox_vms and proxmox_containers; exact name match only)`);
|
|
162
|
+
}
|
|
163
|
+
if (matches.length > 1) {
|
|
164
|
+
const lines = matches.map((v) => ` ${v.vmid} ${v.name} ${v.node || '?'} ${v.type}`).join('\n');
|
|
165
|
+
throw new Error(`"${t}" is ambiguous, ${matches.length} candidates (use the vmid):\n${lines}`);
|
|
166
|
+
}
|
|
167
|
+
const m = matches[0];
|
|
168
|
+
if (!m.node) throw new Error(`could not determine the node for "${t}" (vmid ${m.vmid})`);
|
|
169
|
+
return { vmid: m.vmid, node: m.node, type: m.type, name: m.name };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const withReason = (input, reason) => (reason ? { ...input, _reason: reason } : input);
|
|
173
|
+
|
|
174
|
+
export async function vmInfo(exec, target) {
|
|
175
|
+
const vm = await resolveTarget(exec, target);
|
|
176
|
+
const data = await exec('proxmox_vm_resources', { vmid: vm.vmid, node: vm.node, type: vm.type });
|
|
177
|
+
return { vm, resources: payloadOf(data) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function vmAction(exec, action, target, { reason } = {}) {
|
|
181
|
+
if (!VM_ACTIONS.includes(action)) throw new Error(`action must be one of ${VM_ACTIONS.join(', ')}`);
|
|
182
|
+
const vm = await resolveTarget(exec, target);
|
|
183
|
+
const data = await exec('proxmox_vm_action', withReason({ vmid: vm.vmid, node: vm.node, action, type: vm.type }, reason));
|
|
184
|
+
return { vm, result: payloadOf(data) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function vmSnapshot(exec, target, { name, reason } = {}) {
|
|
188
|
+
const vm = await resolveTarget(exec, target);
|
|
189
|
+
// proxmox_vm_snapshot requires snapname (Proxmox: letter first, then [A-Za-z0-9_-]).
|
|
190
|
+
const snapname = name || `quox-${new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').slice(0, 15)}`;
|
|
191
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,39}$/.test(snapname)) throw new Error('snapshot name must start with a letter and use only letters, digits, _ or -');
|
|
192
|
+
const input = { vmid: vm.vmid, node: vm.node, snapname };
|
|
193
|
+
const data = await exec('proxmox_vm_snapshot', withReason(input, reason));
|
|
194
|
+
return { vm, result: payloadOf(data) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* `url:http://127.0.0.1:8080/health` | `unit:nginx` | `cmd:test -f /etc/x` -> a proxmox_rehearse check.
|
|
199
|
+
* Only the shape is checked here; the server validates the characters strictly.
|
|
200
|
+
*/
|
|
201
|
+
export function parseCheck(spec) {
|
|
202
|
+
const m = /^(url|unit|cmd):(.+)$/.exec(String(spec ?? '').trim());
|
|
203
|
+
if (!m) throw new Error(`--check must look like url:http://127.0.0.1:PORT/path, unit:NAME or cmd:COMMAND (got "${spec}")`);
|
|
204
|
+
if (m[1] === 'url') return { type: 'url', url: m[2] };
|
|
205
|
+
if (m[1] === 'unit') return { type: 'unit', name: m[2] };
|
|
206
|
+
return { type: 'cmd', run: m[2] };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Start a rehearsal on a network-isolated twin of the VM (HYPERION proxmox_rehearse). Returns at once; the run takes minutes (a full disk copy). */
|
|
210
|
+
export async function vmRehearse(exec, target, command, { checks = [], keepTwin = false } = {}) {
|
|
211
|
+
const c = String(command ?? '').trim();
|
|
212
|
+
if (!c) throw new Error('a command is required after --');
|
|
213
|
+
if (c.includes("'")) throw new Error('the command may not contain single quotes (it runs inside bash -c, use double quotes)');
|
|
214
|
+
const vm = await resolveTarget(exec, target);
|
|
215
|
+
const input = { vmid: vm.vmid, node: vm.node, command: c };
|
|
216
|
+
if (checks.length) input.checks = checks.map(parseCheck);
|
|
217
|
+
if (keepTwin) input.keep_twin = true;
|
|
218
|
+
let data;
|
|
219
|
+
try { data = await exec('proxmox_rehearse', input); } catch (e) {
|
|
220
|
+
// The rehearsal needs ONE human approval (the card lists every host command). The proposal exists and
|
|
221
|
+
// the run starts by itself once approved, so hand back the id instead of failing.
|
|
222
|
+
if (e instanceof VmPendingError && e.rehearsalId) {
|
|
223
|
+
return { vm, result: { rehearsal_id: e.rehearsalId, state: 'awaiting_approval', approval_id: e.approvalId, inbox_item_id: e.inboxItemId, hint: pendingHint(e) } };
|
|
224
|
+
}
|
|
225
|
+
throw e;
|
|
226
|
+
}
|
|
227
|
+
return { vm, result: payloadOf(data) };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const REHEARSAL_POLL_MS = 10000;
|
|
231
|
+
export const REHEARSAL_TIMEOUT_MS = 50 * 60 * 1000;
|
|
232
|
+
|
|
233
|
+
/** Poll proxmox_rehearsal_status until the rehearsal is done (awaiting_approval and running both keep waiting). Throws on timeout (the id stays valid for a later look). */
|
|
234
|
+
export async function waitRehearsal(exec, rehearsalId, { intervalMs = REHEARSAL_POLL_MS, timeoutMs = REHEARSAL_TIMEOUT_MS, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), onTick } = {}) {
|
|
235
|
+
if (!/^RH_[0-9A-F]{16}$/.test(String(rehearsalId || ''))) throw new Error('the server did not return a rehearsal id');
|
|
236
|
+
const t0 = Date.now();
|
|
237
|
+
for (;;) {
|
|
238
|
+
const rec = payloadOf(await exec('proxmox_rehearsal_status', { rehearsal_id: rehearsalId }));
|
|
239
|
+
const r = rec?.rehearsal || rec;
|
|
240
|
+
if (r && r.state === 'done') return r;
|
|
241
|
+
if (Date.now() - t0 > timeoutMs) throw new Error(`rehearsal ${rehearsalId} is still running after ${Math.round(timeoutMs / 60000)} minutes; check later with the proxmox_rehearsal_status tool`);
|
|
242
|
+
if (onTick) onTick(r);
|
|
243
|
+
await sleep(intervalMs);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Human REST surface for the pool. */
|
|
248
|
+
export function makePoolRest({ client }) {
|
|
249
|
+
const call = (name, params, body, timeout) => {
|
|
250
|
+
const ep = resolveEndpoint(name, params);
|
|
251
|
+
const m = ep.method.toLowerCase();
|
|
252
|
+
const t = timeout ? { timeout } : {};
|
|
253
|
+
return m === 'get' ? client.get(ep.path, { service: ep.service, ...t }) : client[m](ep.path, { service: ep.service, body: body || {}, ...t });
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
status: () => call('v1.proxmox.pool'),
|
|
257
|
+
configure: (body) => call('v1.proxmox.pool.config.put', {}, body),
|
|
258
|
+
// A warm lease probes ssh, installs the key and refreshes the IP; a slow auth service stretches that past 30s.
|
|
259
|
+
lease: (body) => call('v1.proxmox.pool.lease', {}, body, 180000),
|
|
260
|
+
release: (vmid) => call('v1.proxmox.pool.release', { vmid }),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function poolStatus(rest) {
|
|
265
|
+
return rest.status();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function defaultLessee(env = process.env) {
|
|
269
|
+
return env.QUOX_SESSION || os.hostname();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** --pubkey-file, else ~/.ssh/id_ed25519.pub when present, else a clear error. Reads the PUBLIC key only. */
|
|
273
|
+
export function readPubkey(file, { home = os.homedir() } = {}) {
|
|
274
|
+
const f = file || path.join(home, '.ssh', 'id_ed25519.pub');
|
|
275
|
+
if (!file && !fs.existsSync(f)) throw new Error('no ssh public key: pass --pubkey-file <path> (default ~/.ssh/id_ed25519.pub was not found)');
|
|
276
|
+
let txt;
|
|
277
|
+
try { txt = fs.readFileSync(f, 'utf8').trim(); } catch (e) { throw new Error(`cannot read public key ${f}: ${e.message}`); }
|
|
278
|
+
if (!/^(ssh-|ecdsa-|sk-)/.test(txt)) throw new Error(`${f} does not look like an SSH public key (refusing to send it)`);
|
|
279
|
+
return txt;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function poolLease(rest, { lessee, pubkey, cores, memoryGb, diskGb, ttlH, purpose, avx, node } = {}) {
|
|
283
|
+
const input = { lessee: lessee || defaultLessee(), ssh_pubkey: pubkey };
|
|
284
|
+
if (cores != null) input.cores = Number(cores);
|
|
285
|
+
if (memoryGb != null) input.memory_gb = Number(memoryGb);
|
|
286
|
+
if (diskGb != null) input.disk_gb = Number(diskGb);
|
|
287
|
+
if (ttlH != null) input.ttl_h = Number(ttlH);
|
|
288
|
+
if (purpose) input.purpose = purpose;
|
|
289
|
+
if (avx) input.require_avx2 = true; // set when only some nodes have AVX2; the server errors if no lane can take it
|
|
290
|
+
if (node) input.node = node;
|
|
291
|
+
return rest.lease(input); // 201 {lease} when a warm VM fits, 202 {vmid,state:'building'} when built to order
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Host key lines for a returned lease: the fingerprint to compare, and a ready known_hosts line. Never invents a key. */
|
|
295
|
+
export function poolHostKeyLines(p) {
|
|
296
|
+
const l = p && p.lease;
|
|
297
|
+
if (!l || typeof l !== 'object') return [];
|
|
298
|
+
if (l.host_key_line && l.host_key_fingerprint) {
|
|
299
|
+
return [`Host key ${l.host_key_fingerprint}`, 'known_hosts line:', `${l.ip} ${l.host_key_line}`];
|
|
300
|
+
}
|
|
301
|
+
return [`host key not read: ${l.host_key_error || 'the server returned none'}`];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function poolDone(rest, vmid) {
|
|
305
|
+
if (!/^\d+$/.test(String(vmid ?? ''))) throw new Error('vmid must be a number');
|
|
306
|
+
return rest.release(Number(vmid));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function poolConfig(rest, input = {}) {
|
|
310
|
+
return rest.configure(input);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export async function nodePower(exec, action, node, { reason } = {}) {
|
|
314
|
+
if (!NODE_ACTIONS.includes(action)) throw new Error(`node action must be one of ${NODE_ACTIONS.join(', ')}`);
|
|
315
|
+
if (!node) throw new Error('a node name is required');
|
|
316
|
+
// The proxmox_node_power tool is not built yet. Fail loudly instead of calling a missing tool.
|
|
317
|
+
void exec; void reason;
|
|
318
|
+
throw new Error('node power (reboot, shutdown) is not built yet; there is no proxmox_node_power tool');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Plain-text lines for a VM list. */
|
|
322
|
+
export function vmListLines(vms) {
|
|
323
|
+
if (!vms.length) return ['No VMs found.'];
|
|
324
|
+
return vms.map((v) => `${String(v.vmid).padEnd(6)} ${String(v.name).padEnd(28)} ${String(v.node || '?').padEnd(8)} ${String(v.type).padEnd(5)} ${v.status}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function textOf(p) {
|
|
328
|
+
if (p === undefined || p === null) return 'done';
|
|
329
|
+
return typeof p === 'string' ? p : JSON.stringify(p, null, 2);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* VM Doctor (DEVFLEET-DOCTOR): read-only diagnosis. The server resolves the name or vmid itself
|
|
334
|
+
* (proxmox_vm_doctor), so the target goes straight through. -> the report object.
|
|
335
|
+
*/
|
|
336
|
+
export async function vmDoctor(exec, target) {
|
|
337
|
+
const t = String(target ?? '').trim();
|
|
338
|
+
if (!t) throw new Error('a VM name or vmid is required');
|
|
339
|
+
const p = payloadOf(await exec('proxmox_vm_doctor', { target: t }));
|
|
340
|
+
if (!p || typeof p !== 'object' || !Array.isArray(p.findings)) throw new Error('proxmox_vm_doctor returned no report');
|
|
341
|
+
return p;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const SEV_LABEL = { critical: 'CRITICAL', warning: 'WARNING', info: 'INFO' };
|
|
345
|
+
const showVal = (v) => (typeof v === 'number' ? String(Math.round(v * 1000) / 1000) : String(v));
|
|
346
|
+
|
|
347
|
+
/** Human lines for a doctor report: verdict, each finding (cause / evidence / fix), then gaps. */
|
|
348
|
+
export function doctorLines(report) {
|
|
349
|
+
const vm = report.vm || {};
|
|
350
|
+
const lines = [`VERDICT ${String(report.verdict || 'unknown').toUpperCase()} ${vm.name || vm.vmid} (vmid ${vm.vmid} on ${vm.node}, ${vm.status || 'unknown'})`];
|
|
351
|
+
if (!report.findings.length) lines.push('No problems found in what could be read.');
|
|
352
|
+
for (const f of report.findings) {
|
|
353
|
+
lines.push('', `${SEV_LABEL[f.severity] || String(f.severity).toUpperCase()} ${f.title}: ${f.cause}`);
|
|
354
|
+
for (const e of f.evidence || []) {
|
|
355
|
+
lines.push(` evidence: ${e.metric} = ${showVal(e.value)} (threshold ${showVal(e.threshold)}, ${e.window}, ${e.source})`);
|
|
356
|
+
}
|
|
357
|
+
lines.push(` fix: ${f.fix && f.fix.text}`);
|
|
358
|
+
if (f.fix && f.fix.action) lines.push(` gated action: ${f.fix.action.tool} ${JSON.stringify(f.fix.action.input)} (asks for approval; not run)`);
|
|
359
|
+
}
|
|
360
|
+
if (report.gaps && report.gaps.length) {
|
|
361
|
+
lines.push('', 'Could not check:');
|
|
362
|
+
for (const g of report.gaps) lines.push(` ${g}`);
|
|
363
|
+
}
|
|
364
|
+
if (report.report_id) lines.push('', `report ${report.report_id}`);
|
|
365
|
+
return lines;
|
|
366
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUOXON `/vm <verb> <target> [args]`: parse the words, call the shared engine
|
|
3
|
+
* (lib/core/vm.js, same as `quox vm`), return { lines, tone } for say().
|
|
4
|
+
* A held call becomes the approval hint (tone 'warn'); other failures throw.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
VM_ACTIONS, NODE_ACTIONS, VmPendingError, pendingHint, makeExec,
|
|
8
|
+
listVms, vmInfo, vmDoctor, doctorLines, vmAction, vmSnapshot, poolStatus, poolLease, poolDone, poolConfig,
|
|
9
|
+
nodePower, readPubkey, vmListLines, textOf,
|
|
10
|
+
} from './vm.js';
|
|
11
|
+
|
|
12
|
+
/** Split argv into positionals and --flag value pairs. */
|
|
13
|
+
export function splitFlags(argv) {
|
|
14
|
+
const pos = []; const flags = {};
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
const a = argv[i];
|
|
17
|
+
if (a.startsWith('--')) {
|
|
18
|
+
const eq = a.indexOf('=');
|
|
19
|
+
if (eq > 0) flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
20
|
+
else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('--')) flags[a.slice(2)] = argv[++i];
|
|
21
|
+
else flags[a.slice(2)] = true;
|
|
22
|
+
} else pos.push(a);
|
|
23
|
+
}
|
|
24
|
+
return { pos, flags };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const asLines = (t) => String(t).split('\n');
|
|
28
|
+
export const VM_USAGE = 'Usage: /vm <list|info|doctor|start|stop|reboot|shutdown|snapshot|pool|node> <target> [args]';
|
|
29
|
+
|
|
30
|
+
export async function runVmCommand({ client, orgId, argv, exec: injected }) {
|
|
31
|
+
const exec = injected || makeExec({ client, orgId });
|
|
32
|
+
const { pos, flags } = splitFlags(argv || []);
|
|
33
|
+
const [verb, ...rest] = pos;
|
|
34
|
+
const reason = typeof flags.reason === 'string' ? flags.reason : undefined;
|
|
35
|
+
const need = (v, what) => { if (!v) throw new Error(`${what} is required. ${VM_USAGE}`); return v; };
|
|
36
|
+
try {
|
|
37
|
+
if (!verb) return { lines: [VM_USAGE], tone: 'warn' };
|
|
38
|
+
if (verb === 'list') {
|
|
39
|
+
return { lines: vmListLines(await listVms(exec, { node: flags.node, type: flags.type })) };
|
|
40
|
+
}
|
|
41
|
+
if (verb === 'info') {
|
|
42
|
+
const { vm, resources } = await vmInfo(exec, need(rest[0], 'a VM name or vmid'));
|
|
43
|
+
return { lines: [`${vm.name || vm.vmid} vmid ${vm.vmid} node ${vm.node} ${vm.type}`, ...asLines(textOf(resources))] };
|
|
44
|
+
}
|
|
45
|
+
if (verb === 'doctor') {
|
|
46
|
+
const report = await vmDoctor(exec, need(rest[0], 'a VM name or vmid'));
|
|
47
|
+
return { lines: doctorLines(report), tone: report.verdict === 'healthy' ? undefined : 'warn' };
|
|
48
|
+
}
|
|
49
|
+
if (VM_ACTIONS.includes(verb)) {
|
|
50
|
+
const { vm, result } = await vmAction(exec, verb, need(rest[0], 'a VM name or vmid'), { reason });
|
|
51
|
+
return { lines: [`${verb} ${vm.name || vm.vmid} (vmid ${vm.vmid} on ${vm.node})`, ...asLines(textOf(result))] };
|
|
52
|
+
}
|
|
53
|
+
if (verb === 'snapshot') {
|
|
54
|
+
const { vm, result } = await vmSnapshot(exec, need(rest[0], 'a VM name or vmid'), { name: typeof flags.name === 'string' ? flags.name : undefined, reason });
|
|
55
|
+
return { lines: [`snapshot ${vm.name || vm.vmid} (vmid ${vm.vmid})`, ...asLines(textOf(result))] };
|
|
56
|
+
}
|
|
57
|
+
if (verb === 'pool') {
|
|
58
|
+
const sub = rest[0];
|
|
59
|
+
if (sub === 'status') return { lines: asLines(textOf(await poolStatus(exec))) };
|
|
60
|
+
if (sub === 'lease') {
|
|
61
|
+
const pubkey = readPubkey(typeof flags['pubkey-file'] === 'string' ? flags['pubkey-file'] : undefined);
|
|
62
|
+
const p = await poolLease(exec, {
|
|
63
|
+
lessee: flags.lessee, pubkey, cores: flags.cores, memoryGb: flags['memory-gb'],
|
|
64
|
+
diskGb: flags['disk-gb'], purpose: flags.purpose, reason,
|
|
65
|
+
});
|
|
66
|
+
return { lines: asLines(textOf(p)) };
|
|
67
|
+
}
|
|
68
|
+
if (sub === 'done') return { lines: asLines(textOf(await poolDone(exec, need(rest[1], 'a vmid'), { reason }))) };
|
|
69
|
+
if (sub === 'config') {
|
|
70
|
+
let input = {};
|
|
71
|
+
if (flags.set) { try { input = JSON.parse(flags.set); } catch { throw new Error('--set must be a JSON object'); } }
|
|
72
|
+
return { lines: asLines(textOf(await poolConfig(exec, input, { reason }))) };
|
|
73
|
+
}
|
|
74
|
+
return { lines: ['Usage: /vm pool <status|lease|done <vmid>|config>'], tone: 'warn' };
|
|
75
|
+
}
|
|
76
|
+
if (verb === 'node') {
|
|
77
|
+
const [action, name] = rest;
|
|
78
|
+
if (!NODE_ACTIONS.includes(action)) return { lines: ['Usage: /vm node <reboot|shutdown> <node>'], tone: 'warn' };
|
|
79
|
+
return { lines: asLines(textOf(await nodePower(exec, action, need(name, 'a node name'), { reason }))) };
|
|
80
|
+
}
|
|
81
|
+
return { lines: [`Unknown /vm verb "${verb}". ${VM_USAGE}`], tone: 'warn' };
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (e instanceof VmPendingError) return { lines: [pendingHint(e)], tone: 'warn' };
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WALKTHROUGH for a plain terminal (`quox approvals walkthrough`, `quox inbox walkthrough`): the waiting decisions one
|
|
3
|
+
* card at a time, using the same card and reply rules as the TUI (quoxon/approvalsView.js). Identity is the decision id.
|
|
4
|
+
* io = { readLine(prompt) -> Promise<string|null>, write(line), answer(item, key, note) -> Promise<void> }.
|
|
5
|
+
*/
|
|
6
|
+
import { isDecision, walkCard, walkReply, walkNotYesNo, walkNoCatch, walkSummary } from '../quoxon/approvalsView.js';
|
|
7
|
+
|
|
8
|
+
export async function runWalkthrough(items, io) {
|
|
9
|
+
const decs = (items || []).filter(isDecision);
|
|
10
|
+
if (!decs.length) { io.write('No question is waiting for you.'); return { answered: [], skipped: [], left: [] }; }
|
|
11
|
+
const st = { answered: [], skipped: [], left: [] };
|
|
12
|
+
io.write(`${decs.length} question${decs.length === 1 ? '' : 's'} waiting for you. One at a time: a choice, then an optional note.`);
|
|
13
|
+
for (let i = 0; i < decs.length; i++) {
|
|
14
|
+
const it = decs[i]; const card = walkCard(it, i + 1, decs.length);
|
|
15
|
+
io.write(''); card.lines.forEach((l) => io.write(l));
|
|
16
|
+
for (;;) {
|
|
17
|
+
const line = await io.readLine('Your choice? ');
|
|
18
|
+
if (line == null) { for (let j = i; j < decs.length; j++) st.left.push(decs[j].title || decs[j].id); io.write(''); walkSummary(st).forEach((l) => io.write(l)); return st; }
|
|
19
|
+
const rep = walkReply(it, line);
|
|
20
|
+
if (!rep) { io.write(walkNoCatch(it)); continue; }
|
|
21
|
+
if (rep.notYesNo) { io.write(walkNotYesNo(it)); continue; }
|
|
22
|
+
if (rep.defer) { st.skipped.push(it.title || it.id); break; }
|
|
23
|
+
if (rep.ask) { io.write(` Asked by ${it.payload.session || 'a session'} (id ${it.id}). ${it.payload.why || 'No further reason was given.'}`); continue; }
|
|
24
|
+
if (it.payload.high_risk === true && rep.key !== 'n') { io.write(' High risk: a yes needs your PIN, which this plain terminal cannot take. Answer it in `quox` with /approvals walkthrough, or skip it here.'); continue; }
|
|
25
|
+
try { await io.answer(it, rep.key, rep.note || null); }
|
|
26
|
+
catch (e) { io.write(` Not answered (${e.message}). The question stays open.`); continue; }
|
|
27
|
+
const label = it.payload.options.find((o) => o.key === rep.key).label;
|
|
28
|
+
io.write(`✔ ${it.title || it.id}: ${label}`);
|
|
29
|
+
st.answered.push({ title: it.title || it.id, word: label });
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
io.write(''); walkSummary(st).forEach((l) => io.write(l));
|
|
34
|
+
return st;
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The companion view Quoxon actually uses: same contract as dragonlab's integration/companions-v1/view.mjs
|
|
3
|
+
* (initial / reduce / view -> {frame, caption, hidden, name, scene}), same reducer, same character, same scene and caption
|
|
4
|
+
* logic, but sized to the classic dragon's footprint and played from a frame cache filled by a worker (frames.js).
|
|
5
|
+
* view() never renders; it reads. `ready` resolves once the worker has loaded the art.
|
|
6
|
+
*/
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { createPlayhead, createWorkerBackend, companionSlot, quantise } from './frames.js';
|
|
9
|
+
|
|
10
|
+
export async function loadCompanionView(root, id, { still = false, arrival = true, mono = false, persist = true, makeBackend = createWorkerBackend, onFrame = () => {} } = {}) {
|
|
11
|
+
const url = (p) => pathToFileURL(`${root}/integration/${p}`).href;
|
|
12
|
+
const [chars, act] = await Promise.all([import(url('companions-v1/characters.mjs')), import(url('hyperion-v3/activity.mjs'))]);
|
|
13
|
+
const spec = chars.character(id);
|
|
14
|
+
const backend = await makeBackend({ root, id, still, mono });
|
|
15
|
+
const play = createPlayhead(backend, { onFirst: onFrame });
|
|
16
|
+
return {
|
|
17
|
+
id, name: spec.name, arrivalMs: spec.arrival.duration,
|
|
18
|
+
initial: act.initialActivity,
|
|
19
|
+
reduce: act.activityEvent,
|
|
20
|
+
playhead: play,
|
|
21
|
+
view(state, now, { cols = 80, rows = 24, mood = null } = {}) {
|
|
22
|
+
if (!persist && !still && now - state.started >= spec.arrival.duration && !state.approvals.length) return { frame: null, caption: null, hidden: true, name: spec.name, scene: null };
|
|
23
|
+
const slot = companionSlot(cols, Math.max(1, rows));
|
|
24
|
+
let v = chars.activityView(id, state, now, { arrival: arrival && !still });
|
|
25
|
+
// The dragon's job read found something wrong (a down route, an unhealthy host, a failed check): it acts concerned, the same scene
|
|
26
|
+
// a recorded issue gets. Never the other way round: a good read does not pick a happier scene than the activity allows.
|
|
27
|
+
if (mood === 'concerned' && !state.approvals.length && !state.issue) { const c = chars.activityView(id, { ...state, issue: true }, now, { arrival: false }); v = { ...v, scene: c.scene, time: c.time, motionTime: c.motionTime }; }
|
|
28
|
+
const frame = slot.showArt ? play.frame({ scene: v.scene, time: v.time, motionTime: v.motionTime, cols: slot.cols, rows: slot.rows }, { still }) : null;
|
|
29
|
+
return { frame, caption: v.caption, slot, name: spec.name, scene: v.scene };
|
|
30
|
+
},
|
|
31
|
+
dispose() { play.dispose(); },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export { companionSlot, quantise };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smooth companion playback. Rendering a dragonlab frame costs 100 ms to over a second (Hyperion), and the old path did it
|
|
3
|
+
* synchronously inside every Ink render, so the whole TUI hitched and the dragon juddered. The classic Q dragon is smooth
|
|
4
|
+
* because its frames are cheap and the shared clock paces them. This does the same for companions: time is quantised to
|
|
5
|
+
* the clock's frame step, frames are rendered OFF the main thread (a worker, see renderWorker.js) a few steps ahead of the
|
|
6
|
+
* playhead into a cache, and the renderer only ever READS the cache. A frame that is not ready yet is held (the last one
|
|
7
|
+
* shown stays), never rendered inline. The same quantised time returns the same frame object, so an unchanged frame costs
|
|
8
|
+
* Ink no output at all.
|
|
9
|
+
*/
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
|
+
|
|
12
|
+
/** One animation step, the shared clock's FRAME_MS (about 12 fps). */
|
|
13
|
+
export const STEP_MS = 83;
|
|
14
|
+
/** How many steps ahead of the playhead the worker is asked to fill. */
|
|
15
|
+
export const LOOKAHEAD = 3;
|
|
16
|
+
const CACHE_MAX = 240;
|
|
17
|
+
|
|
18
|
+
export const quantise = (ms) => Math.max(0, Math.round((Number(ms) || 0) / STEP_MS) * STEP_MS);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Where the art goes. Sized to the classic dragon's footprint (15 rows when the terminal is short, 22 when it is tall
|
|
22
|
+
* enough that the live region still fits), not the old 39x11 cap. Below ART_MIN a one-line label is used instead.
|
|
23
|
+
*/
|
|
24
|
+
export const ART_MIN_COLS = 64, ART_MIN_ROWS = 30;
|
|
25
|
+
export function companionSlot(cols, rows) {
|
|
26
|
+
const big = cols >= ART_MIN_COLS && rows >= ART_MIN_ROWS;
|
|
27
|
+
if (!big) return { showArt: false, cols: 0, rows: 0 };
|
|
28
|
+
const tall = rows >= 40;
|
|
29
|
+
return { showArt: true, cols: Math.max(28, Math.min(tall ? 66 : 52, cols - 26)), rows: tall ? 22 : 15 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Cache + playhead. `backend.render(req) -> Promise<frame>` does the real work somewhere else; at most one request is in
|
|
34
|
+
* flight, and the next one asked for is always the soonest frame not cached yet (current step first, then lookahead).
|
|
35
|
+
*/
|
|
36
|
+
export function createPlayhead(backend, { onFirst = () => {}, lookahead = LOOKAHEAD, clock = () => Date.now() } = {}) {
|
|
37
|
+
const cache = new Map(), pending = new Set();
|
|
38
|
+
const conc = Math.max(1, backend.size || 1); // the backend may be a pool: keep every worker busy
|
|
39
|
+
let last = null, disposed = false, failed = null, renders = 0, ema = 100;
|
|
40
|
+
const keyOf = (r) => `${r.scene}|${r.time}|${r.motionTime}|${r.cols}x${r.rows}`;
|
|
41
|
+
const put = (k, f) => { cache.set(k, f); if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value); };
|
|
42
|
+
function pump(wants) {
|
|
43
|
+
for (const next of wants) {
|
|
44
|
+
if (pending.size >= conc || disposed || failed) return;
|
|
45
|
+
const k = keyOf(next);
|
|
46
|
+
if (cache.has(k) || pending.has(k)) continue;
|
|
47
|
+
pending.add(k); renders++;
|
|
48
|
+
const began = clock();
|
|
49
|
+
Promise.resolve().then(() => backend.render(next)).then((f) => {
|
|
50
|
+
ema = ema * 0.7 + (clock() - began) * 0.3;
|
|
51
|
+
if (f) { put(k, f); const first = !last; last = f; if (first) onFirst(); }
|
|
52
|
+
}, (e) => { failed = e; }).finally(() => { pending.delete(k); });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
/**
|
|
57
|
+
* The frame to draw for this scene/time: the cached one for this step, else the newest earlier one (held), else the last
|
|
58
|
+
* one shown. null before the first ever arrives. The worker is asked for the step the playhead will be at when the
|
|
59
|
+
* render lands (now + the measured render time), so a slow character still plays at the rate it can render, never stalls the TUI.
|
|
60
|
+
*/
|
|
61
|
+
frame(req, { still = false } = {}) {
|
|
62
|
+
const base = (i) => ({ ...req, time: quantise(req.time) + i * STEP_MS, motionTime: quantise(req.motionTime) + i * STEP_MS });
|
|
63
|
+
const lead = still ? 0 : Math.min(6, Math.ceil(ema / STEP_MS));
|
|
64
|
+
pump(still ? [base(0)] : Array.from({ length: Math.max(lookahead, conc * 2) + 1 }, (_, i) => base(lead + i)));
|
|
65
|
+
for (let back = 0; back <= 4; back++) {
|
|
66
|
+
const hit = cache.get(keyOf(base(-back)));
|
|
67
|
+
if (hit) { last = hit; return hit; }
|
|
68
|
+
}
|
|
69
|
+
return last;
|
|
70
|
+
},
|
|
71
|
+
get error() { return failed; },
|
|
72
|
+
get renders() { return renders; },
|
|
73
|
+
get size() { return cache.size; },
|
|
74
|
+
get busy() { return pending.size; },
|
|
75
|
+
get lead() { return Math.min(6, Math.ceil(ema / STEP_MS)); },
|
|
76
|
+
dispose() { disposed = true; cache.clear(); backend.dispose?.(); },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The real backend: worker threads that load the character once and render on request (a small pool, because the heavier
|
|
82
|
+
* characters take several frame steps to render and the pool renders upcoming steps in parallel). Rejects if no worker can start.
|
|
83
|
+
*/
|
|
84
|
+
export async function createWorkerBackend({ root, id, still = false, mono = false, size = null }, { Worker, cpus = null } = {}) {
|
|
85
|
+
const W = Worker || (await import('node:worker_threads')).Worker;
|
|
86
|
+
const avail = cpus ?? (await import('node:os')).availableParallelism?.() ?? 2;
|
|
87
|
+
const n = Math.max(1, Math.min(size ?? 3, avail - 1));
|
|
88
|
+
const file = fileURLToPath(new URL('./renderWorker.js', import.meta.url));
|
|
89
|
+
const waiting = new Map();
|
|
90
|
+
let seq = 0, dead = null;
|
|
91
|
+
const fail = (e) => { dead = e; for (const w of waiting.values()) w.reject(e); waiting.clear(); };
|
|
92
|
+
const spawn = () => new Promise((resolve, reject) => {
|
|
93
|
+
const worker = new W(file, { workerData: { root, id } });
|
|
94
|
+
worker.unref?.();
|
|
95
|
+
worker.once('error', (e) => { fail(e); reject(e); });
|
|
96
|
+
worker.on('message', (m) => {
|
|
97
|
+
if (m.type === 'ready') return resolve({ worker, load: 0 });
|
|
98
|
+
const w = waiting.get(m.seq); if (!w) return;
|
|
99
|
+
waiting.delete(m.seq); w.slot.load--;
|
|
100
|
+
if (m.type === 'frame') w.resolve(m.frame); else w.reject(new Error(m.code || 'render failed'));
|
|
101
|
+
});
|
|
102
|
+
worker.once('exit', (c) => { fail(new Error(`render worker exited (${c})`)); reject(dead); });
|
|
103
|
+
});
|
|
104
|
+
const slots = await Promise.all(Array.from({ length: n }, spawn));
|
|
105
|
+
return {
|
|
106
|
+
size: n,
|
|
107
|
+
render: (req) => new Promise((resolve, reject) => {
|
|
108
|
+
if (dead) return reject(dead);
|
|
109
|
+
const slot = slots.reduce((a, b) => (b.load < a.load ? b : a));
|
|
110
|
+
const s = ++seq; slot.load++; waiting.set(s, { resolve, reject, slot });
|
|
111
|
+
slot.worker.postMessage({ seq: s, req: { ...req, still, mono } });
|
|
112
|
+
}),
|
|
113
|
+
dispose() { fail(new Error('disposed')); for (const s of slots) s.worker.terminate(); },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Test/fallback helper: path-to-URL for a dragonlab module. */
|
|
118
|
+
export const labModule = (root, ...p) => pathToFileURL([root, 'integration', 'companions-v1', ...p].join('/')).href;
|