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,944 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuoxPlan commands: roadmaps, streams, phases, plans, timeline
|
|
3
|
+
* (STREAM_QUOXPLAN_PLAN.md, quoxcli parity gate P3b).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the standalone `quoxplan` CLI's verbs (quoxplan/cli)
|
|
6
|
+
* one-for-one, but through the collector's governed proxy
|
|
7
|
+
* (services/collector/server.js: /api/v1/quoxplan/* -> QuoxPlan server,
|
|
8
|
+
* port 3107) instead of hitting the QuoxPlan server directly — same auth,
|
|
9
|
+
* same profile, same --json/--insecure conventions as every other `quox`
|
|
10
|
+
* command group. Mutations send actor: "quox-cli" (the standalone CLI sends
|
|
11
|
+
* "cli") so the event log can tell the two clients apart.
|
|
12
|
+
*
|
|
13
|
+
* The render helpers (streamLine, renderStreamDetail, renderTimeline, etc.)
|
|
14
|
+
* are adapted from the standalone CLI's lib/render/*.js — same visual
|
|
15
|
+
* language (octahedron status glyphs per owner feedback 2026-07-25 point 5),
|
|
16
|
+
* rebuilt on chalk instead of hand-rolled ANSI since chalk is already this
|
|
17
|
+
* CLI's house convention.
|
|
18
|
+
*
|
|
19
|
+
* quox plan <subcommand> [options]
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import { writeFileSync, readFileSync, unlinkSync, mkdtempSync } from 'node:fs';
|
|
24
|
+
import { tmpdir } from 'node:os';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import chalk from 'chalk';
|
|
27
|
+
import { printSuccess } from '../core/output.js';
|
|
28
|
+
import { events, printEvent, celebrate, line as glyphLine } from '../core/quoxglyph.js';
|
|
29
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
30
|
+
import { QuoxApiError } from '../core/errors.js';
|
|
31
|
+
|
|
32
|
+
const VALID_LENSES = ['focus', 'tier', 'all'];
|
|
33
|
+
const VALID_STATUSES = ['NOT_STARTED', 'IN_PROGRESS', 'COMPLETE', 'BLOCKED', 'DECISION_NEEDED'];
|
|
34
|
+
|
|
35
|
+
const ID_WIDTH = 18;
|
|
36
|
+
const NAME_WIDTH = 36;
|
|
37
|
+
const WEEK_WIDTH = 4; // characters of bar per week, timeline view
|
|
38
|
+
|
|
39
|
+
// ── glyphs ───────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const STATUS_GLYPH = {
|
|
42
|
+
NOT_STARTED: { glyph: '◇', color: chalk.dim },
|
|
43
|
+
IN_PROGRESS: { glyph: '⬗', color: chalk.yellow },
|
|
44
|
+
BLOCKED: { glyph: '◆', color: chalk.red },
|
|
45
|
+
DECISION_NEEDED: { glyph: '◈', color: chalk.magenta },
|
|
46
|
+
COMPLETE: { glyph: '◆', color: chalk.dim },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function statusGlyph(status) {
|
|
50
|
+
return STATUS_GLYPH[status] || { glyph: '◇', color: chalk.dim };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const PHASE_GLYPH = {
|
|
54
|
+
TODO: { glyph: '□', color: chalk.dim },
|
|
55
|
+
IN_PROGRESS: { glyph: '▨', color: chalk.yellow },
|
|
56
|
+
DONE: { glyph: '■', color: chalk.green },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function phaseGlyph(status) {
|
|
60
|
+
return PHASE_GLYPH[status] || { glyph: '□', color: chalk.dim };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── small text helpers ───────────────────────────────────────────────────
|
|
64
|
+
// Pad/truncate the PLAIN string before applying chalk color — chalk wraps
|
|
65
|
+
// the string in fixed-length escape sequences, so padding an already-colored
|
|
66
|
+
// string with padEnd/padStart would count the escape bytes as visible width
|
|
67
|
+
// and break column alignment.
|
|
68
|
+
|
|
69
|
+
function pad(value, n) {
|
|
70
|
+
const s = String(value ?? '');
|
|
71
|
+
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function truncate(value, n) {
|
|
75
|
+
const s = String(value ?? '');
|
|
76
|
+
if (s.length <= n) return s;
|
|
77
|
+
return s.slice(0, Math.max(0, n - 1)) + '…';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function wrapText(text, width) {
|
|
81
|
+
const words = String(text || '').split(/\s+/);
|
|
82
|
+
const lines = [];
|
|
83
|
+
let line = '';
|
|
84
|
+
for (const w of words) {
|
|
85
|
+
if ((line + ' ' + w).trim().length > width) {
|
|
86
|
+
lines.push(line.trim());
|
|
87
|
+
line = w;
|
|
88
|
+
} else {
|
|
89
|
+
line = line ? line + ' ' + w : w;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (line.trim()) lines.push(line.trim());
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Light ANSI pass-through markdown: headings bold cyan, code dim. Not a real parser. */
|
|
97
|
+
function renderMarkdownLite(md) {
|
|
98
|
+
if (!md) return '';
|
|
99
|
+
const lines = String(md).split('\n');
|
|
100
|
+
const out = [];
|
|
101
|
+
let inFence = false;
|
|
102
|
+
for (let line of lines) {
|
|
103
|
+
if (/^\s*```/.test(line)) {
|
|
104
|
+
inFence = !inFence;
|
|
105
|
+
out.push(chalk.dim(line));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (inFence) {
|
|
109
|
+
out.push(chalk.dim(line));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (/^(#{1,6})\s+(.*)$/.test(line)) {
|
|
113
|
+
out.push(chalk.cyan.bold(line));
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
line = line.replace(/`([^`]+)`/g, (_, code) => chalk.dim('`' + code + '`'));
|
|
117
|
+
out.push(line);
|
|
118
|
+
}
|
|
119
|
+
return out.join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── render: roadmaps / stream lists ─────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
function renderRoadmapsList(roadmaps) {
|
|
125
|
+
const lines = [];
|
|
126
|
+
for (const r of roadmaps) {
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push(chalk.cyan.bold(` ${r.id}`) + ' ' + r.name);
|
|
129
|
+
if (r.description) lines.push(chalk.dim(` ${r.description}`));
|
|
130
|
+
const counts = r.status_counts || {};
|
|
131
|
+
const parts = Object.entries(counts).map(([status, n]) => {
|
|
132
|
+
const g = statusGlyph(status);
|
|
133
|
+
return g.color(`${g.glyph} ${n} ${status}`);
|
|
134
|
+
});
|
|
135
|
+
lines.push(` ${r.stream_count} streams ${parts.join(' ')}`);
|
|
136
|
+
}
|
|
137
|
+
return lines;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderSearchResults(data) {
|
|
141
|
+
const results = data.results || [];
|
|
142
|
+
if (results.length === 0) return [chalk.dim(` No streams match "${data.q}".`)];
|
|
143
|
+
const lines = [''];
|
|
144
|
+
for (const r of results) {
|
|
145
|
+
const g = statusGlyph(r.status);
|
|
146
|
+
const where = [...new Set((r.matches || []).map((m) => m.where))].join(', ');
|
|
147
|
+
const first = (r.matches || []).find((m) => m.snippet) || {};
|
|
148
|
+
lines.push(
|
|
149
|
+
` ${g.color(g.glyph)} ${pad(truncate(r.id, ID_WIDTH), ID_WIDTH)} ${pad(truncate(r.name, NAME_WIDTH), NAME_WIDTH)} `
|
|
150
|
+
+ `${pad(r.status, 15)} ${pad(where, 18)} ${chalk.dim(truncate(first.snippet || '', 60))}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
lines.push('', chalk.dim(` ${results.length} stream${results.length === 1 ? '' : 's'} matched "${data.q}"`));
|
|
154
|
+
return lines;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function streamLine(s, { indent = 0, showReasons = false } = {}) {
|
|
158
|
+
const prefix = indent > 0 ? ' '.repeat(indent) + '└ ' : ' ';
|
|
159
|
+
const g = statusGlyph(s.status);
|
|
160
|
+
const idPlain = pad(s.id, ID_WIDTH);
|
|
161
|
+
const idStr = indent > 0 ? chalk.dim(idPlain) : chalk.cyan(idPlain);
|
|
162
|
+
const nameStr = pad(truncate(s.name, NAME_WIDTH - 1), NAME_WIDTH);
|
|
163
|
+
const tierStr = chalk.dim(`T${s.tier}`);
|
|
164
|
+
const reasons =
|
|
165
|
+
showReasons && s.focus_reasons && s.focus_reasons.length
|
|
166
|
+
? ' ' + chalk.magenta(`[${s.focus_reasons.join(',')}]`)
|
|
167
|
+
: '';
|
|
168
|
+
return `${prefix}${g.color.bold(g.glyph)} ${idStr} ${nameStr} ${tierStr}${reasons}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function streamAndSubstreams(s, opts) {
|
|
172
|
+
const lines = [streamLine(s, opts)];
|
|
173
|
+
for (const sub of s.substreams || []) {
|
|
174
|
+
lines.push(streamLine(sub, { ...opts, indent: (opts.indent || 0) + 1 }));
|
|
175
|
+
}
|
|
176
|
+
return lines;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** lens: 'tier' | 'focus' | 'all' — matches the API's `streams` payload shape for that lens. */
|
|
180
|
+
function renderStreamList(payload, { lens = 'tier' } = {}) {
|
|
181
|
+
const lines = [];
|
|
182
|
+
const streams = (payload && payload.streams) || [];
|
|
183
|
+
|
|
184
|
+
if (lens === 'tier') {
|
|
185
|
+
if (streams.length === 0) {
|
|
186
|
+
lines.push(chalk.dim(' (no streams)'));
|
|
187
|
+
return lines;
|
|
188
|
+
}
|
|
189
|
+
for (const group of streams) {
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push(chalk.cyan.bold(` TIER ${group.tier}`));
|
|
192
|
+
for (const s of group.streams) {
|
|
193
|
+
lines.push(...streamAndSubstreams(s, { indent: 0 }));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return lines;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 'focus' and 'all' are both flat arrays (already nested with substreams).
|
|
200
|
+
if (streams.length === 0) {
|
|
201
|
+
lines.push(chalk.dim(lens === 'focus' ? ' Nothing needs attention right now.' : ' (no streams)'));
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
for (const s of streams) {
|
|
205
|
+
lines.push(...streamAndSubstreams(s, { indent: 0, showReasons: lens === 'focus' }));
|
|
206
|
+
}
|
|
207
|
+
return lines;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── render: stream detail ───────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
function renderStreamDetail(detail) {
|
|
213
|
+
const { stream, phases = [], plan, notes = [], depends_on: dependsOn = [], dependents = [] } = detail;
|
|
214
|
+
const lines = [];
|
|
215
|
+
const g = statusGlyph(stream.status);
|
|
216
|
+
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push(chalk.cyan.bold(` ${stream.id}`) + ' ' + chalk.bold(stream.name));
|
|
219
|
+
lines.push(
|
|
220
|
+
` ${g.color.bold(g.glyph)} ${g.color(stream.status)}` +
|
|
221
|
+
` ${chalk.dim(`tier ${stream.tier}`)} ${chalk.dim(`#${stream.priority}`)}` +
|
|
222
|
+
(stream.effort ? ` ${chalk.dim(stream.effort)}` : '')
|
|
223
|
+
);
|
|
224
|
+
if (stream.tags && stream.tags.length) {
|
|
225
|
+
lines.push(' ' + stream.tags.map((t) => chalk.dim(`[${t}]`)).join(' '));
|
|
226
|
+
}
|
|
227
|
+
lines.push('');
|
|
228
|
+
for (const line of wrapText(stream.summary, 78)) lines.push(` ${line}`);
|
|
229
|
+
|
|
230
|
+
if (phases.length) {
|
|
231
|
+
lines.push('');
|
|
232
|
+
lines.push(chalk.cyan.bold(' Phases'));
|
|
233
|
+
for (const p of phases) {
|
|
234
|
+
const pg = phaseGlyph(p.status);
|
|
235
|
+
lines.push(` ${pg.color(pg.glyph)} ${pad(`${p.seq}.`, 4)}${p.title}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (dependsOn.length || dependents.length) {
|
|
240
|
+
lines.push('');
|
|
241
|
+
lines.push(chalk.cyan.bold(' Dependencies'));
|
|
242
|
+
if (dependsOn.length) {
|
|
243
|
+
lines.push(
|
|
244
|
+
' depends on: ' +
|
|
245
|
+
dependsOn
|
|
246
|
+
.map((d) => `${chalk.cyan(d.id)} ${statusGlyph(d.status).color(`(${d.status})`)}`)
|
|
247
|
+
.join(', ')
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (dependents.length) {
|
|
251
|
+
lines.push(
|
|
252
|
+
' blocks: ' +
|
|
253
|
+
dependents
|
|
254
|
+
.map((d) => `${chalk.cyan(d.id)} ${statusGlyph(d.status).color(`(${d.status})`)}`)
|
|
255
|
+
.join(', ')
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (plan && plan.body) {
|
|
261
|
+
lines.push('');
|
|
262
|
+
lines.push(chalk.cyan.bold(` Plan (v${plan.version})`));
|
|
263
|
+
lines.push('');
|
|
264
|
+
lines.push(renderMarkdownLite(plan.body));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (notes.length) {
|
|
268
|
+
lines.push('');
|
|
269
|
+
lines.push(chalk.cyan.bold(' Notes'));
|
|
270
|
+
for (const n of notes) {
|
|
271
|
+
lines.push(` ${chalk.dim(n.created_at)} ${n.author ? chalk.dim(`(${n.author}) `) : ''}${n.body}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
lines.push('');
|
|
276
|
+
return lines;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── render: timeline (ASCII Gantt) ──────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
function barColor(status) {
|
|
282
|
+
if (status === 'COMPLETE') return chalk.dim;
|
|
283
|
+
if (status === 'BLOCKED') return chalk.red;
|
|
284
|
+
if (status === 'IN_PROGRESS') return chalk.yellow;
|
|
285
|
+
if (status === 'DECISION_NEEDED') return chalk.magenta;
|
|
286
|
+
return chalk.white;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function barChar(status) {
|
|
290
|
+
return status === 'BLOCKED' ? '▒' : '█';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function weekHeader(totalWeeks) {
|
|
294
|
+
const cells = [];
|
|
295
|
+
for (let w = 1; w <= totalWeeks; w++) cells.push(pad(`W${w}`, WEEK_WIDTH));
|
|
296
|
+
return ' '.repeat(ID_WIDTH + 1 + NAME_WIDTH + 1) + cells.join('');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function renderTimelineRow(stream, totalWeeks) {
|
|
300
|
+
const totalCols = totalWeeks * WEEK_WIDTH;
|
|
301
|
+
const startCol = Math.max(0, Math.round((stream.start_offset_days / 7) * WEEK_WIDTH));
|
|
302
|
+
const rawEndCol = Math.round((stream.end_offset_days / 7) * WEEK_WIDTH);
|
|
303
|
+
const endCol = Math.min(totalCols, Math.max(startCol + 1, rawEndCol));
|
|
304
|
+
const width = Math.max(1, endCol - startCol);
|
|
305
|
+
|
|
306
|
+
const ch = barChar(stream.status);
|
|
307
|
+
const color = barColor(stream.status);
|
|
308
|
+
const bar = stream.on_critical_path ? color.bold(ch.repeat(width)) : color(ch.repeat(width));
|
|
309
|
+
|
|
310
|
+
const idStr = chalk.cyan(pad(stream.id, ID_WIDTH));
|
|
311
|
+
const nameStr = pad(truncate(stream.name, NAME_WIDTH - 1), NAME_WIDTH);
|
|
312
|
+
|
|
313
|
+
let suffix = '';
|
|
314
|
+
if (stream.error) {
|
|
315
|
+
suffix = ' ' + chalk.red.bold('cyclic dependency');
|
|
316
|
+
} else if (stream.status === 'BLOCKED' && stream.blocked_by && stream.blocked_by.length) {
|
|
317
|
+
suffix = ' ' + chalk.red(`blocked by ${stream.blocked_by.join(', ')}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return `${idStr} ${nameStr} ${' '.repeat(startCol)}${bar}${suffix}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* timeline: array of stream schedule entries (getTimeline() domain fn shape).
|
|
325
|
+
* opts.weeks: force the number of week columns (default: derived from the
|
|
326
|
+
* longest schedule, minimum 1). opts.title: optional heading (roadmap id).
|
|
327
|
+
*/
|
|
328
|
+
function renderTimeline(timeline, opts = {}) {
|
|
329
|
+
const lines = [];
|
|
330
|
+
if (opts.title) {
|
|
331
|
+
lines.push(chalk.cyan.bold(` ${opts.title}`));
|
|
332
|
+
lines.push('');
|
|
333
|
+
}
|
|
334
|
+
if (!timeline || timeline.length === 0) {
|
|
335
|
+
lines.push(chalk.dim(' (no streams to schedule)'));
|
|
336
|
+
return lines;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const maxEnd = timeline.reduce((m, s) => Math.max(m, s.end_offset_days || 0), 0);
|
|
340
|
+
const totalWeeks = opts.weeks || Math.max(1, Math.ceil(maxEnd / 7));
|
|
341
|
+
|
|
342
|
+
lines.push(chalk.dim(weekHeader(totalWeeks)));
|
|
343
|
+
|
|
344
|
+
const sorted = [...timeline].sort((a, b) => {
|
|
345
|
+
const laneDiff = (a.lane ?? 0) - (b.lane ?? 0);
|
|
346
|
+
if (laneDiff !== 0) return laneDiff;
|
|
347
|
+
const startDiff = (a.start_offset_days ?? 0) - (b.start_offset_days ?? 0);
|
|
348
|
+
if (startDiff !== 0) return startDiff;
|
|
349
|
+
return a.id.localeCompare(b.id);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
for (const s of sorted) lines.push(renderTimelineRow(s, totalWeeks));
|
|
353
|
+
|
|
354
|
+
lines.push('');
|
|
355
|
+
lines.push(
|
|
356
|
+
chalk.dim(' Legend: ') +
|
|
357
|
+
chalk.cyan.bold('███') +
|
|
358
|
+
chalk.dim(' critical path ') +
|
|
359
|
+
chalk.yellow('███') +
|
|
360
|
+
chalk.dim(' in progress ') +
|
|
361
|
+
chalk.dim('███') +
|
|
362
|
+
chalk.dim(' complete ') +
|
|
363
|
+
chalk.red('▒▒▒') +
|
|
364
|
+
chalk.dim(' blocked')
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
return lines;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── render: stats / completed ────────────────────────────────────────────
|
|
371
|
+
// Fed by GET /roadmaps/:id/export (the same STRATEGIC_ROADMAP.json-shaped
|
|
372
|
+
// doc the retired roadmap-cli used to read from disk). Adapted from the
|
|
373
|
+
// standalone quoxplan CLI's lib/render/stats.js and completed.js, rebuilt
|
|
374
|
+
// on chalk per this file's house convention.
|
|
375
|
+
|
|
376
|
+
function progressBar(done, total) {
|
|
377
|
+
if (total <= 0) return '';
|
|
378
|
+
const width = 20;
|
|
379
|
+
const filled = Math.round((done / total) * width);
|
|
380
|
+
const pct = Math.round((done / total) * 100);
|
|
381
|
+
const color = pct >= 80 ? chalk.green : pct >= 40 ? chalk.yellow : chalk.dim;
|
|
382
|
+
return color('█'.repeat(filled)) + chalk.dim('░'.repeat(width - filled)) + chalk.dim(` ${pct}%`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function renderStats(doc) {
|
|
386
|
+
const streams = doc.streams || [];
|
|
387
|
+
const completed = doc.completed || [];
|
|
388
|
+
|
|
389
|
+
const inProgress = streams.filter((s) => s.status === 'IN_PROGRESS').length;
|
|
390
|
+
const notStarted = streams.filter((s) => s.status === 'NOT_STARTED').length;
|
|
391
|
+
const blocked = streams.filter((s) => s.status === 'BLOCKED').length;
|
|
392
|
+
const decisions = streams.filter((s) => s.status === 'DECISION_NEEDED').length;
|
|
393
|
+
|
|
394
|
+
const totalPhases = streams.reduce((a, s) => a + (s.phases_total || 0), 0);
|
|
395
|
+
const donePhases = streams.reduce((a, s) => a + (s.phases_done || 0), 0);
|
|
396
|
+
const totalTests = completed.reduce((a, s) => a + (s.tests || 0), 0);
|
|
397
|
+
|
|
398
|
+
const lines = [];
|
|
399
|
+
lines.push('');
|
|
400
|
+
lines.push(chalk.cyan.bold(' ROADMAP STATISTICS') + ' ' + chalk.dim(doc.updated || ''));
|
|
401
|
+
lines.push(' ' + '-'.repeat(50));
|
|
402
|
+
lines.push(` ${chalk.green('✓ Completed')} ${completed.length} streams`);
|
|
403
|
+
lines.push(` ${chalk.yellow('● In progress')} ${inProgress} streams`);
|
|
404
|
+
lines.push(` ${chalk.dim('○ Not started')} ${notStarted} streams`);
|
|
405
|
+
if (blocked) lines.push(` ${chalk.red('✗ Blocked')} ${blocked} streams`);
|
|
406
|
+
if (decisions) lines.push(` ${chalk.magenta('? Needs decision')} ${decisions} streams`);
|
|
407
|
+
lines.push('');
|
|
408
|
+
lines.push(
|
|
409
|
+
` Phase progress ${progressBar(donePhases, totalPhases || 1)} ${chalk.dim(`(${donePhases}/${totalPhases})`)}`
|
|
410
|
+
);
|
|
411
|
+
lines.push(` Tests shipped ${totalTests} ${chalk.dim('(completed streams)')}`);
|
|
412
|
+
lines.push('');
|
|
413
|
+
return lines;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function renderCompletedList(completed) {
|
|
417
|
+
const lines = [];
|
|
418
|
+
const list = completed || [];
|
|
419
|
+
lines.push('');
|
|
420
|
+
lines.push(chalk.cyan.bold(` COMPLETED STREAMS (${list.length})`));
|
|
421
|
+
lines.push(' ' + '-'.repeat(60));
|
|
422
|
+
if (list.length === 0) {
|
|
423
|
+
lines.push(chalk.dim(' (none yet)'));
|
|
424
|
+
lines.push('');
|
|
425
|
+
return lines;
|
|
426
|
+
}
|
|
427
|
+
const sorted = [...list].sort((a, b) => String(b.completed).localeCompare(String(a.completed)));
|
|
428
|
+
for (const s of sorted) {
|
|
429
|
+
const tests = s.tests ? chalk.dim(` (${s.tests} tests)`) : '';
|
|
430
|
+
const completedDate = s.completed || chalk.dim('unknown date');
|
|
431
|
+
lines.push(` ${chalk.green('[DONE]')} ${pad(s.id, 12)} ${pad(s.name, 35)} ${completedDate}${tests}`);
|
|
432
|
+
}
|
|
433
|
+
lines.push('');
|
|
434
|
+
return lines;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Name the active org from what the CLI profile already stores (no network),
|
|
441
|
+
* so an empty or 404 answer reads as "wrong org?" and not as data loss
|
|
442
|
+
* (quox#751). Honours a --org override.
|
|
443
|
+
*/
|
|
444
|
+
function describeActiveOrg(auth, program) {
|
|
445
|
+
let stored = null;
|
|
446
|
+
try { stored = typeof auth.getTokens === 'function' ? auth.getTokens() : null; } catch { stored = null; }
|
|
447
|
+
let override = null;
|
|
448
|
+
try { override = program?.opts?.().org || null; } catch { override = null; }
|
|
449
|
+
const id = override || stored?.org_id || null;
|
|
450
|
+
const name = override ? null : (stored?.org_name || null);
|
|
451
|
+
const slug = override ? null : (stored?.org_slug || null);
|
|
452
|
+
const ident = slug || id;
|
|
453
|
+
if (name && ident && name !== ident) return `${name} (${ident})`;
|
|
454
|
+
return name || ident || 'unknown';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Default roadmap = first from GET /roadmaps (server orders by name). */
|
|
458
|
+
async function resolveRoadmapId(client, explicit, orgLabel) {
|
|
459
|
+
if (explicit) return explicit;
|
|
460
|
+
const ep = resolveEndpoint('quoxplan.roadmaps.list');
|
|
461
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
462
|
+
const roadmaps = data.roadmaps || [];
|
|
463
|
+
if (roadmaps.length === 0) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`No roadmaps in org ${orgLabel || 'unknown'}. Roadmaps may exist in another org you belong to; `
|
|
466
|
+
+ 'switch with `quox org switch <org>`.'
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return roadmaps[0].id;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function printJson(data) {
|
|
473
|
+
console.log(JSON.stringify(data, null, 2));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isJsonOutput(program) {
|
|
477
|
+
return program.opts().output === 'json';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// `quox plan update` field coercion — mirrors the standalone quoxplan CLI's
|
|
481
|
+
// update.js: numeric fields parse as Number, array fields accept either a
|
|
482
|
+
// JSON array literal or a comma-separated list.
|
|
483
|
+
const UPDATE_NUM_FIELDS = ['tier', 'effort_days'];
|
|
484
|
+
const UPDATE_ARRAY_FIELDS = ['tags', 'depends_on'];
|
|
485
|
+
|
|
486
|
+
function coerceUpdateValue(field, value) {
|
|
487
|
+
if (UPDATE_NUM_FIELDS.includes(field)) return Number(value);
|
|
488
|
+
if (UPDATE_ARRAY_FIELDS.includes(field)) {
|
|
489
|
+
try {
|
|
490
|
+
return JSON.parse(value);
|
|
491
|
+
} catch {
|
|
492
|
+
return value.split(',').map((v) => v.trim());
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ── registration ─────────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
export function registerPlanCommands(program, ctx) {
|
|
501
|
+
const { client, auth } = ctx;
|
|
502
|
+
|
|
503
|
+
const plan = program
|
|
504
|
+
.command('plan')
|
|
505
|
+
.description('QuoxPlan: roadmaps, streams, phases, plans, timeline (governed proxy to the QuoxPlan server)');
|
|
506
|
+
|
|
507
|
+
// ── roadmaps ─────────────────────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
plan
|
|
510
|
+
.command('roadmaps')
|
|
511
|
+
.description('List roadmaps with status counts')
|
|
512
|
+
.action(async () => {
|
|
513
|
+
auth.requireAuth();
|
|
514
|
+
const ep = resolveEndpoint('quoxplan.roadmaps.list');
|
|
515
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
516
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
517
|
+
console.log(renderRoadmapsList(data.roadmaps || []).join('\n'));
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// ── roadmap (create) ─────────────────────────────────────────────────
|
|
521
|
+
// Roadmap-create primitive (ONBOARD step 0). The plural `plan roadmaps`
|
|
522
|
+
// above lists; this singular `plan roadmap create` makes one. Reused by the
|
|
523
|
+
// `quox onboard` wizard to give a fresh tenant its own roadmap, but a
|
|
524
|
+
// standalone verb in its own right so nothing has to shell out to onboard.
|
|
525
|
+
const roadmap = plan
|
|
526
|
+
.command('roadmap')
|
|
527
|
+
.description('Roadmap-level operations (create a roadmap)');
|
|
528
|
+
|
|
529
|
+
roadmap
|
|
530
|
+
.command('create')
|
|
531
|
+
.description('Create a roadmap (POST to the governed QuoxPlan proxy)')
|
|
532
|
+
// NOT requiredOption: --org shadows the root program's global --org <slug>
|
|
533
|
+
// (bin/quox.js). Commander's mandatory-option check runs BEFORE the
|
|
534
|
+
// mergeShadowedGlobals preAction hook, so a requiredOption here fires
|
|
535
|
+
// "required option '--org' not specified" even when the user supplied it.
|
|
536
|
+
// Same defect as quox#457 on fleet rehome-agent; the action validates it
|
|
537
|
+
// below instead. See lib/core/global-flags.js.
|
|
538
|
+
.option('--org <orgId>', 'org that owns the roadmap')
|
|
539
|
+
.requiredOption('--id <roadmapId>', 'roadmap id (stable, e.g. the tenant slug)')
|
|
540
|
+
.requiredOption('--title <title>', 'human-readable roadmap title')
|
|
541
|
+
.option('--description <text>', 'roadmap description')
|
|
542
|
+
.action(async (options) => {
|
|
543
|
+
auth.requireAuth();
|
|
544
|
+
// --org can arrive on the subcommand (merged by the preAction hook) or,
|
|
545
|
+
// when shadowed, on the root. Read both before deciding it is missing.
|
|
546
|
+
const orgId = String(options.org || program.opts().org || '').trim();
|
|
547
|
+
if (!orgId) {
|
|
548
|
+
throw new Error(
|
|
549
|
+
'Missing --org <orgId>. Usage: quox plan roadmap create --org <orgId> --id <roadmapId> --title <title>'
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const body = {
|
|
553
|
+
org_id: orgId,
|
|
554
|
+
id: options.id,
|
|
555
|
+
name: options.title,
|
|
556
|
+
actor: 'quox-cli',
|
|
557
|
+
};
|
|
558
|
+
if (options.description) body.description = options.description;
|
|
559
|
+
const ep = resolveEndpoint('quoxplan.roadmaps.create');
|
|
560
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
561
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
562
|
+
const created = data.roadmap || data;
|
|
563
|
+
printSuccess(`Created roadmap ${created.id || options.id} for org ${orgId}`);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// ── search ───────────────────────────────────────────────────────────
|
|
567
|
+
|
|
568
|
+
plan
|
|
569
|
+
.command('search <text...>')
|
|
570
|
+
.description('Search streams by text (id, name, notes, plan, phase titles)')
|
|
571
|
+
.option('--roadmap <id>', 'limit to one roadmap')
|
|
572
|
+
.option('--status <status>', `limit to one status (${VALID_STATUSES.join(', ')})`)
|
|
573
|
+
.action(async (textParts, options) => {
|
|
574
|
+
auth.requireAuth();
|
|
575
|
+
if (options.status && !VALID_STATUSES.includes(options.status)) {
|
|
576
|
+
throw new Error(`invalid --status "${options.status}" (expected ${VALID_STATUSES.join(', ')})`);
|
|
577
|
+
}
|
|
578
|
+
const query = { q: textParts.join(' ') };
|
|
579
|
+
if (options.roadmap) query.roadmap = options.roadmap;
|
|
580
|
+
if (options.status) query.status = options.status;
|
|
581
|
+
const ep = resolveEndpoint('quoxplan.search');
|
|
582
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
583
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
584
|
+
console.log(renderSearchResults(data).join('\n'));
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// ── list ─────────────────────────────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
plan
|
|
590
|
+
.command('list [roadmap]')
|
|
591
|
+
.description('Tier-grouped stream list (default roadmap: first by name)')
|
|
592
|
+
.option('--lens <lens>', 'focus | tier | all', 'tier')
|
|
593
|
+
.action(async (roadmapArg, options) => {
|
|
594
|
+
auth.requireAuth();
|
|
595
|
+
if (!VALID_LENSES.includes(options.lens)) {
|
|
596
|
+
throw new Error(`invalid --lens "${options.lens}" (expected focus, tier, or all)`);
|
|
597
|
+
}
|
|
598
|
+
const roadmapId = await resolveRoadmapId(client, roadmapArg, describeActiveOrg(auth, program));
|
|
599
|
+
const ep = resolveEndpoint('quoxplan.roadmap.streams.list', { roadmapId });
|
|
600
|
+
const data = await client.get(ep.path, { service: ep.service, query: { lens: options.lens } });
|
|
601
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
602
|
+
console.log(`\n ${roadmapId} (lens: ${options.lens})`);
|
|
603
|
+
console.log(renderStreamList(data, { lens: options.lens }).join('\n'));
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// ── focus ────────────────────────────────────────────────────────────
|
|
607
|
+
|
|
608
|
+
plan
|
|
609
|
+
.command('focus [roadmap]')
|
|
610
|
+
.description('The focus lens: what matters now (P0, blocked, decision needed, in-flight, stale)')
|
|
611
|
+
.action(async (roadmapArg) => {
|
|
612
|
+
auth.requireAuth();
|
|
613
|
+
const roadmapId = await resolveRoadmapId(client, roadmapArg, describeActiveOrg(auth, program));
|
|
614
|
+
const ep = resolveEndpoint('quoxplan.roadmap.streams.list', { roadmapId });
|
|
615
|
+
const data = await client.get(ep.path, { service: ep.service, query: { lens: 'focus' } });
|
|
616
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
617
|
+
console.log(`\n ${roadmapId} (focus)`);
|
|
618
|
+
console.log(renderStreamList(data, { lens: 'focus' }).join('\n'));
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// ── show ─────────────────────────────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
plan
|
|
624
|
+
.command('show <streamId>')
|
|
625
|
+
.description('Full stream detail: header, phases checklist, plan, notes timeline, deps')
|
|
626
|
+
.action(async (streamId) => {
|
|
627
|
+
auth.requireAuth();
|
|
628
|
+
const ep = resolveEndpoint('quoxplan.stream.get', { id: streamId });
|
|
629
|
+
let detail;
|
|
630
|
+
try {
|
|
631
|
+
detail = await client.get(ep.path, { service: ep.service });
|
|
632
|
+
} catch (err) {
|
|
633
|
+
if (err instanceof QuoxApiError && err.status === 404) {
|
|
634
|
+
throw new QuoxApiError(404, err.code, `Stream ${streamId} not found in org ${describeActiveOrg(auth, program)}. It may exist in another org you belong to; switch with \`quox org switch <org>\`.`, err.detail, err.body);
|
|
635
|
+
}
|
|
636
|
+
throw err;
|
|
637
|
+
}
|
|
638
|
+
if (isJsonOutput(program)) return printJson(detail);
|
|
639
|
+
console.log(renderStreamDetail(detail).join('\n'));
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// ── timeline ─────────────────────────────────────────────────────────
|
|
643
|
+
|
|
644
|
+
plan
|
|
645
|
+
.command('timeline [roadmap]')
|
|
646
|
+
.description('ASCII Gantt: dependency-sequenced schedule, critical path bold, blocked edges marked')
|
|
647
|
+
.option('--weeks <n>', 'force the number of week columns shown', (v) => parseInt(v, 10))
|
|
648
|
+
.action(async (roadmapArg, options) => {
|
|
649
|
+
auth.requireAuth();
|
|
650
|
+
const roadmapId = await resolveRoadmapId(client, roadmapArg, describeActiveOrg(auth, program));
|
|
651
|
+
const ep = resolveEndpoint('quoxplan.roadmap.timeline.get', { roadmapId });
|
|
652
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
653
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
654
|
+
console.log(renderTimeline(data.timeline, { weeks: options.weeks, title: roadmapId }).join('\n'));
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// ── status ───────────────────────────────────────────────────────────
|
|
658
|
+
|
|
659
|
+
plan
|
|
660
|
+
.command('status <id> <status>')
|
|
661
|
+
.description('PATCH a stream status (COMPLETE requires --confirm-owner; never auto-confirmed)')
|
|
662
|
+
.option('--confirm-owner', 'confirm an owner-gated transition to COMPLETE')
|
|
663
|
+
.action(async (id, statusArg, options) => {
|
|
664
|
+
auth.requireAuth();
|
|
665
|
+
const status = statusArg.toUpperCase();
|
|
666
|
+
if (!VALID_STATUSES.includes(status)) {
|
|
667
|
+
throw new Error(`invalid status "${statusArg}" (expected one of ${VALID_STATUSES.join(', ')})`);
|
|
668
|
+
}
|
|
669
|
+
const body = { status, actor: 'quox-cli' };
|
|
670
|
+
// No auto-confirm: confirm_owner is only ever sent when the caller
|
|
671
|
+
// explicitly passed --confirm-owner. The server rejects COMPLETE
|
|
672
|
+
// without it (403) and that error is surfaced verbatim, not retried.
|
|
673
|
+
if (options.confirmOwner) body.confirm_owner = true;
|
|
674
|
+
const ep = resolveEndpoint('quoxplan.stream.update', { id });
|
|
675
|
+
const data = await client.patch(ep.path, { service: ep.service, body });
|
|
676
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
677
|
+
if (data.stream.status === 'COMPLETE') {
|
|
678
|
+
printEvent(glyphLine('quoxplan', 'stream', { result: celebrate(`◆ ${id} COMPLETE ◆ every phase done`) }));
|
|
679
|
+
} else {
|
|
680
|
+
printEvent(glyphLine('quoxplan', 'status', { result: `${id} -> ${data.stream.status}` }));
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// ── note ─────────────────────────────────────────────────────────────
|
|
685
|
+
|
|
686
|
+
plan
|
|
687
|
+
.command('note <id> <text...>')
|
|
688
|
+
.description("Append a note to a stream's timeline (append-only, never replaced)")
|
|
689
|
+
.action(async (id, textParts) => {
|
|
690
|
+
auth.requireAuth();
|
|
691
|
+
const body = textParts.join(' ');
|
|
692
|
+
if (!body.trim()) throw new Error('note text is required');
|
|
693
|
+
const ep = resolveEndpoint('quoxplan.stream.notes.create', { id });
|
|
694
|
+
const data = await client.post(ep.path, { service: ep.service, body: { body, actor: 'quox-cli' } });
|
|
695
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
696
|
+
printEvent(glyphLine('quoxplan', 'note appended', { result: `${id} #${data.id}`, status: 'ok' }));
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// ── phase ────────────────────────────────────────────────────────────
|
|
700
|
+
|
|
701
|
+
plan
|
|
702
|
+
.command('phase <streamId> <verb> [rest...]')
|
|
703
|
+
.description('Phase ops: "tick <seq>", "untick <seq>", "add <title>", "rename <seq> <title>", "remove <seq>"')
|
|
704
|
+
.action(async (streamId, verb, rest) => {
|
|
705
|
+
auth.requireAuth();
|
|
706
|
+
|
|
707
|
+
if (verb === 'tick' || verb === 'untick' || verb === 'rename' || verb === 'remove') {
|
|
708
|
+
const seq = parseInt(rest[0], 10);
|
|
709
|
+
if (!Number.isInteger(seq)) {
|
|
710
|
+
throw new Error(`phase ${verb} requires a numeric <seq>, got "${rest[0]}"`);
|
|
711
|
+
}
|
|
712
|
+
const newTitle = rest.slice(1).join(' ').trim();
|
|
713
|
+
if (verb === 'rename' && !newTitle) throw new Error('phase rename requires a <title>');
|
|
714
|
+
// Resolve seq -> phaseId via stream detail (phases are addressed by
|
|
715
|
+
// numeric id server-side, not by seq).
|
|
716
|
+
const detailEp = resolveEndpoint('quoxplan.stream.get', { id: streamId });
|
|
717
|
+
const detail = await client.get(detailEp.path, { service: detailEp.service });
|
|
718
|
+
const phase = (detail.phases || []).find((p) => p.seq === seq);
|
|
719
|
+
if (!phase) throw new Error(`no phase with seq ${seq} on ${streamId}`);
|
|
720
|
+
|
|
721
|
+
if (verb === 'rename') {
|
|
722
|
+
const ep = resolveEndpoint('quoxplan.phase.update', { phaseId: phase.id });
|
|
723
|
+
const data = await client.patch(ep.path, { service: ep.service, body: { title: newTitle, actor: 'quox-cli' } });
|
|
724
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
725
|
+
printSuccess(`${streamId}: phase ${seq} "${phase.title}" renamed to "${newTitle}"`);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (verb === 'remove') {
|
|
730
|
+
const ep = resolveEndpoint('quoxplan.phase.delete', { phaseId: phase.id });
|
|
731
|
+
const data = await client.delete(ep.path, { service: ep.service, body: { actor: 'quox-cli' } });
|
|
732
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
733
|
+
printSuccess(`${streamId}: phase ${seq} "${phase.title}" removed (remaining phases renumbered)`);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const status = verb === 'tick' ? 'DONE' : 'TODO';
|
|
738
|
+
const ep = resolveEndpoint('quoxplan.phase.update', { phaseId: phase.id });
|
|
739
|
+
const data = await client.patch(ep.path, { service: ep.service, body: { status, actor: 'quox-cli' } });
|
|
740
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
741
|
+
printSuccess(`${streamId}: phase ${seq} "${phase.title}" -> ${status}`);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (verb === 'add') {
|
|
746
|
+
const title = rest.join(' ');
|
|
747
|
+
if (!title) throw new Error('phase add requires a <title>');
|
|
748
|
+
const ep = resolveEndpoint('quoxplan.stream.phases.create', { id: streamId });
|
|
749
|
+
const data = await client.post(ep.path, { service: ep.service, body: { title, actor: 'quox-cli' } });
|
|
750
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
751
|
+
const phases = data.phases || [];
|
|
752
|
+
const added = phases[phases.length - 1];
|
|
753
|
+
printSuccess(`${streamId}: phase "${title}" added (seq ${added ? added.seq : '?'})`);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
throw new Error(`unknown phase verb "${verb}" (expected tick, untick, add, rename, or remove)`);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// ── plan (doc) ───────────────────────────────────────────────────────
|
|
761
|
+
|
|
762
|
+
plan
|
|
763
|
+
.command('plan <id>')
|
|
764
|
+
.description('Print raw plan markdown; --edit opens $EDITOR (PUT on save if changed); --history lists versions')
|
|
765
|
+
.option('--edit', 'open $EDITOR on the plan body, PUT on save if changed')
|
|
766
|
+
.option('--history', 'list plan versions instead of printing the body')
|
|
767
|
+
.action(async (id, options) => {
|
|
768
|
+
auth.requireAuth();
|
|
769
|
+
|
|
770
|
+
if (options.history) {
|
|
771
|
+
const ep = resolveEndpoint('quoxplan.stream.plan.versions.list', { id });
|
|
772
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
773
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
774
|
+
const versions = data.versions || [];
|
|
775
|
+
if (versions.length === 0) {
|
|
776
|
+
console.log(chalk.dim(' (no plan versions yet)'));
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
for (const v of versions) {
|
|
780
|
+
console.log(` v${v.version} ${v.updated_at} ${v.updated_by || 'unknown'} (${v.length} bytes)`);
|
|
781
|
+
}
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const detailEp = resolveEndpoint('quoxplan.stream.get', { id });
|
|
786
|
+
const detail = await client.get(detailEp.path, { service: detailEp.service });
|
|
787
|
+
const currentBody = (detail.plan && detail.plan.body) || '';
|
|
788
|
+
|
|
789
|
+
if (options.edit) {
|
|
790
|
+
const dir = mkdtempSync(join(tmpdir(), 'quox-plan-'));
|
|
791
|
+
const file = join(dir, `${id}.md`);
|
|
792
|
+
writeFileSync(file, currentBody, 'utf8');
|
|
793
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
794
|
+
const result = spawnSync(editor, [file], { stdio: 'inherit' });
|
|
795
|
+
if (result.error) throw result.error;
|
|
796
|
+
const newBody = readFileSync(file, 'utf8');
|
|
797
|
+
try {
|
|
798
|
+
unlinkSync(file);
|
|
799
|
+
} catch {
|
|
800
|
+
/* best-effort cleanup */
|
|
801
|
+
}
|
|
802
|
+
if (newBody === currentBody) {
|
|
803
|
+
console.log(chalk.dim(' No changes.'));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const putEp = resolveEndpoint('quoxplan.stream.plan.put', { id });
|
|
807
|
+
const data = await client.put(putEp.path, { service: putEp.service, body: { body: newBody, actor: 'quox-cli' } });
|
|
808
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
809
|
+
printSuccess(`${id}: plan updated to v${data.version}`);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (isJsonOutput(program)) return printJson(detail.plan);
|
|
814
|
+
if (!currentBody) {
|
|
815
|
+
console.log(chalk.dim(' (no plan yet)'));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
console.log(renderMarkdownLite(currentBody));
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// ── move ─────────────────────────────────────────────────────────────
|
|
822
|
+
|
|
823
|
+
plan
|
|
824
|
+
.command('move <roadmap> <id> <position>')
|
|
825
|
+
.description('Reprioritise a top-level stream to a 1-based position')
|
|
826
|
+
.action(async (roadmapId, id, positionArg) => {
|
|
827
|
+
auth.requireAuth();
|
|
828
|
+
const position = parseInt(positionArg, 10);
|
|
829
|
+
if (!Number.isInteger(position) || position < 1) {
|
|
830
|
+
throw new Error('position must be a positive integer');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const listEp = resolveEndpoint('quoxplan.roadmap.streams.list', { roadmapId });
|
|
834
|
+
const listData = await client.get(listEp.path, { service: listEp.service, query: { lens: 'all' } });
|
|
835
|
+
const order = (listData.streams || []).map((s) => s.id);
|
|
836
|
+
const idx = order.indexOf(id);
|
|
837
|
+
if (idx === -1) {
|
|
838
|
+
throw new Error(`"${id}" is not a top-level stream on roadmap "${roadmapId}"`);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
order.splice(idx, 1);
|
|
842
|
+
const insertAt = Math.min(position - 1, order.length);
|
|
843
|
+
order.splice(insertAt, 0, id);
|
|
844
|
+
|
|
845
|
+
const ep = resolveEndpoint('quoxplan.roadmap.reprioritise', { roadmapId });
|
|
846
|
+
const data = await client.post(ep.path, { service: ep.service, body: { order, actor: 'quox-cli' } });
|
|
847
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
848
|
+
printSuccess(`${id}: moved to position #${insertAt + 1} on ${roadmapId}`);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// ── add ──────────────────────────────────────────────────────────────
|
|
852
|
+
|
|
853
|
+
plan
|
|
854
|
+
.command('add <json>')
|
|
855
|
+
.description('Create a new stream from a JSON object (id, name required)')
|
|
856
|
+
.option('--roadmap <roadmap>', 'roadmap id (default: first roadmap)')
|
|
857
|
+
.action(async (json, options) => {
|
|
858
|
+
auth.requireAuth();
|
|
859
|
+
let body;
|
|
860
|
+
try {
|
|
861
|
+
body = JSON.parse(json);
|
|
862
|
+
} catch (err) {
|
|
863
|
+
throw new Error(`invalid JSON: ${err.message}`);
|
|
864
|
+
}
|
|
865
|
+
body.actor = body.actor || 'quox-cli';
|
|
866
|
+
const roadmapId = await resolveRoadmapId(client, options.roadmap, describeActiveOrg(auth, program));
|
|
867
|
+
const ep = resolveEndpoint('quoxplan.roadmap.streams.create', { roadmapId });
|
|
868
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
869
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
870
|
+
printSuccess(`Added stream ${data.stream.id} to ${roadmapId} at priority #${data.stream.priority}`);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// ── update ───────────────────────────────────────────────────────────
|
|
874
|
+
|
|
875
|
+
plan
|
|
876
|
+
.command('update <id> <field> <value>')
|
|
877
|
+
.description('Update a stream field (notes append; priority use `move` instead)')
|
|
878
|
+
.action(async (id, field, value) => {
|
|
879
|
+
auth.requireAuth();
|
|
880
|
+
if (field === 'priority') {
|
|
881
|
+
throw new Error('priority is not directly settable; use `quox plan move <roadmap> <id> <position>` instead');
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (field === 'notes') {
|
|
885
|
+
console.error(
|
|
886
|
+
chalk.yellow(
|
|
887
|
+
' Warning: notes are append-only in QuoxPlan (a timeline, not a single field) — appending, not replacing.'
|
|
888
|
+
)
|
|
889
|
+
);
|
|
890
|
+
const ep = resolveEndpoint('quoxplan.stream.notes.create', { id });
|
|
891
|
+
const data = await client.post(ep.path, { service: ep.service, body: { body: value, actor: 'quox-cli' } });
|
|
892
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
893
|
+
printEvent(glyphLine('quoxplan', 'note appended', { result: `${id} #${data.id}`, status: 'ok' }));
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const body = { [field]: coerceUpdateValue(field, value), actor: 'quox-cli' };
|
|
898
|
+
const ep = resolveEndpoint('quoxplan.stream.update', { id });
|
|
899
|
+
const data = await client.patch(ep.path, { service: ep.service, body });
|
|
900
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
901
|
+
printSuccess(`${id}.${field} = ${JSON.stringify(data.stream[field])}`);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// ── rename ───────────────────────────────────────────────────────────
|
|
905
|
+
|
|
906
|
+
plan
|
|
907
|
+
.command('rename <oldId> <newId>')
|
|
908
|
+
.description('Rename a stream id, cascading to depends_on/parent_id references')
|
|
909
|
+
.action(async (oldId, newId) => {
|
|
910
|
+
auth.requireAuth();
|
|
911
|
+
const ep = resolveEndpoint('quoxplan.stream.rename', { id: oldId });
|
|
912
|
+
const data = await client.post(ep.path, { service: ep.service, body: { new_id: newId, actor: 'quox-cli' } });
|
|
913
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
914
|
+
printSuccess(`Renamed ${oldId} -> ${data.stream.id}`);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// ── stats ────────────────────────────────────────────────────────────
|
|
918
|
+
|
|
919
|
+
plan
|
|
920
|
+
.command('stats [roadmap]')
|
|
921
|
+
.description('Roadmap statistics (streams, phase progress, tests shipped)')
|
|
922
|
+
.action(async (roadmapArg) => {
|
|
923
|
+
auth.requireAuth();
|
|
924
|
+
const roadmapId = await resolveRoadmapId(client, roadmapArg, describeActiveOrg(auth, program));
|
|
925
|
+
const ep = resolveEndpoint('quoxplan.roadmap.export.get', { roadmapId });
|
|
926
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
927
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
928
|
+
console.log(renderStats(data).join('\n'));
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// ── completed ────────────────────────────────────────────────────────
|
|
932
|
+
|
|
933
|
+
plan
|
|
934
|
+
.command('completed [roadmap]')
|
|
935
|
+
.description('List completed streams')
|
|
936
|
+
.action(async (roadmapArg) => {
|
|
937
|
+
auth.requireAuth();
|
|
938
|
+
const roadmapId = await resolveRoadmapId(client, roadmapArg, describeActiveOrg(auth, program));
|
|
939
|
+
const ep = resolveEndpoint('quoxplan.roadmap.export.get', { roadmapId });
|
|
940
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
941
|
+
if (isJsonOutput(program)) return printJson(data.completed);
|
|
942
|
+
console.log(renderCompletedList(data.completed).join('\n'));
|
|
943
|
+
});
|
|
944
|
+
}
|