@geraldmaron/construct 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -18
- package/bin/construct +392 -91
- package/bin/construct-postinstall.mjs +30 -2
- package/examples/distribution/README.md +42 -0
- package/examples/distribution/manifest.json +53 -0
- package/examples/distribution/sources/adr.md +84 -0
- package/examples/distribution/sources/deck-one-pager.md +65 -0
- package/examples/distribution/sources/prd-platform.md +161 -0
- package/examples/distribution/sources/research-brief.md +80 -0
- package/examples/distribution/sources/rfc-platform.md +82 -0
- package/examples/distribution/sources/runbook.md +103 -0
- package/examples/distribution/sources/strategy.md +88 -0
- package/lib/adapters-sync.mjs +67 -0
- package/lib/artifact-gate-notice.mjs +38 -0
- package/lib/artifact-manifest.mjs +112 -0
- package/lib/artifact-release-gate.mjs +171 -0
- package/lib/artifact-reviewers.mjs +68 -0
- package/lib/artifact-type-from-path.mjs +79 -0
- package/lib/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +77 -10
- package/lib/auto-docs.mjs +87 -18
- package/lib/beads/drift.mjs +27 -6
- package/lib/beads-client.mjs +49 -121
- package/lib/beads-optimistic.mjs +5 -12
- package/lib/brand-fonts.mjs +93 -0
- package/lib/brand-prose.mjs +214 -0
- package/lib/brand-tokens.mjs +92 -0
- package/lib/bridges/copilot-proxy.mjs +13 -26
- package/lib/capability-ledger.mjs +156 -0
- package/lib/certification/artifact-fixtures.mjs +132 -0
- package/lib/certification/artifact-gates.mjs +63 -0
- package/lib/certification/artifact-provenance.mjs +97 -0
- package/lib/certification/canonical-scenarios.mjs +78 -0
- package/lib/certification/cli.mjs +191 -0
- package/lib/certification/dashboard-api.mjs +71 -0
- package/lib/certification/demo-parity.mjs +116 -0
- package/lib/certification/document-io-fixtures.mjs +246 -0
- package/lib/certification/document-workflow.mjs +97 -0
- package/lib/certification/eval-bridge.mjs +77 -0
- package/lib/certification/model-routing.mjs +149 -0
- package/lib/certification/prompt-budget.mjs +119 -0
- package/lib/certification/rc-gate.mjs +206 -0
- package/lib/certification/real-llm-scenarios.mjs +303 -0
- package/lib/certification/role-cards.mjs +113 -0
- package/lib/certification/role-overlays.mjs +117 -0
- package/lib/certification/run.mjs +122 -0
- package/lib/certification/runner.mjs +323 -0
- package/lib/certification/scenarios.mjs +51 -0
- package/lib/certification/skill-inventory.mjs +289 -0
- package/lib/certification/skill-scenarios.mjs +147 -0
- package/lib/certification/specialist-contracts.mjs +85 -0
- package/lib/certification/specialist-scenarios.mjs +175 -0
- package/lib/certification/stale-impact.mjs +146 -0
- package/lib/certification/status.mjs +252 -0
- package/lib/certification/store.mjs +77 -0
- package/lib/chat/cli.mjs +333 -0
- package/lib/chat/command-suggest.mjs +161 -0
- package/lib/chat/commands.mjs +215 -0
- package/lib/chat/config.mjs +142 -0
- package/lib/chat/context-compactor.mjs +250 -0
- package/lib/chat/context-continuation.mjs +253 -0
- package/lib/chat/continuation-source.mjs +58 -0
- package/lib/chat/demo-guide.mjs +61 -0
- package/lib/chat/design-tokens.mjs +91 -0
- package/lib/chat/desktop-binary.mjs +79 -0
- package/lib/chat/desktop-build.mjs +130 -0
- package/lib/chat/desktop-launcher.mjs +133 -0
- package/lib/chat/evidence.mjs +145 -0
- package/lib/chat/export.mjs +74 -0
- package/lib/chat/harness/driver.mjs +91 -0
- package/lib/chat/list-picker.mjs +112 -0
- package/lib/chat/model-picker.mjs +356 -0
- package/lib/chat/openrouter-fallback.mjs +151 -0
- package/lib/chat/permission-prompt.mjs +33 -0
- package/lib/chat/picker-catalog.mjs +45 -0
- package/lib/chat/policy-telemetry.mjs +34 -0
- package/lib/chat/present.mjs +245 -0
- package/lib/chat/session-context.mjs +39 -0
- package/lib/chat/session-persist.mjs +73 -0
- package/lib/chat/session-restore.mjs +71 -0
- package/lib/chat/session-settings.mjs +53 -0
- package/lib/chat/system-prompt.mjs +52 -0
- package/lib/chat/transparency.mjs +93 -0
- package/lib/chat/tui/color-scheme.mjs +42 -0
- package/lib/chat/tui/markdown.mjs +123 -0
- package/lib/chat/tui/presentation.mjs +100 -0
- package/lib/chat/tui/render.mjs +500 -0
- package/lib/chat/tui/turn-block.mjs +284 -0
- package/lib/chat/tui/turn-present.mjs +18 -0
- package/lib/chat/tui/turn-state.mjs +88 -0
- package/lib/chat/tui/usage.mjs +122 -0
- package/lib/chat/web-commands.mjs +146 -0
- package/lib/chat/web-launcher.mjs +63 -0
- package/lib/chat/web-picker-keys.mjs +46 -0
- package/lib/chat/web-session.mjs +159 -0
- package/lib/cli-commands.mjs +232 -11
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/schema.mjs +36 -5
- package/lib/contract-schemas/decision.json +50 -0
- package/lib/contract-schemas/implementation.json +51 -0
- package/lib/contract-schemas/review-report.json +32 -0
- package/lib/contract-schemas/test-report.json +43 -0
- package/lib/contracts/construct-handoff.mjs +60 -0
- package/lib/contracts/validate.mjs +32 -3
- package/lib/contracts/violation-log.mjs +58 -5
- package/lib/dashboard-demo.mjs +71 -0
- package/lib/dashboard-static.mjs +19 -5
- package/lib/decisions/registry.mjs +5 -3
- package/lib/deck-export-pptx.mjs +1152 -0
- package/lib/demo-recording.mjs +142 -0
- package/lib/demo-script.mjs +114 -0
- package/lib/demo-surface.mjs +249 -0
- package/lib/demo.mjs +703 -0
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +302 -0
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +4 -2
- package/lib/doctor/project-adapters.mjs +44 -0
- package/lib/doctor/watchers/cx-budget.mjs +98 -0
- package/lib/doctor/watchers/graph-staleness.mjs +52 -0
- package/lib/doctor/watchers/process-pressure.mjs +4 -3
- package/lib/doctor/watchers/service-health.mjs +1 -1
- package/lib/document-export.mjs +360 -36
- package/lib/document-extract.mjs +88 -0
- package/lib/document-ingest.mjs +32 -18
- package/lib/embed/cli.mjs +2 -2
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/semantic.mjs +5 -2
- package/lib/embed/worker.mjs +2 -2
- package/lib/embedded-contract/model-resolve.mjs +10 -9
- package/lib/env-config.mjs +26 -0
- package/lib/evals/dataset.mjs +137 -0
- package/lib/evals/gates.mjs +175 -0
- package/lib/graph/build-co-change.mjs +54 -0
- package/lib/graph/build-from-registry.mjs +136 -0
- package/lib/graph/build-import-graph.mjs +153 -0
- package/lib/graph/cli.mjs +110 -0
- package/lib/graph/impact-cli.mjs +89 -0
- package/lib/graph/impact.mjs +108 -0
- package/lib/graph/staleness.mjs +44 -0
- package/lib/graph/store.mjs +172 -0
- package/lib/handoffs/cleanup.mjs +1 -1
- package/lib/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- package/lib/hooks/audit-trail.mjs +14 -28
- package/lib/hooks/brand-prose-lint.mjs +38 -0
- package/lib/hooks/graph-impact-advisory.mjs +62 -0
- package/lib/hooks/session-optimize.mjs +84 -207
- package/lib/hooks/session-start.mjs +4 -5
- package/lib/host-disposition.mjs +7 -0
- package/lib/improvement/cli.mjs +189 -0
- package/lib/improvement/controller.mjs +137 -0
- package/lib/improvement/proposal.mjs +120 -0
- package/lib/improvement/specialist-loop.mjs +192 -0
- package/lib/improvement/store.mjs +89 -0
- package/lib/improvement/surface.mjs +219 -0
- package/lib/ingest-tooling.mjs +97 -0
- package/lib/init/doc-lanes.mjs +165 -0
- package/lib/init-docs.mjs +31 -178
- package/lib/init-unified.mjs +27 -97
- package/lib/init-update-guide.mjs +102 -0
- package/lib/init-update.mjs +32 -1
- package/lib/init.mjs +8 -0
- package/lib/install/desktop-binary-download.mjs +85 -0
- package/lib/install/legacy-global-cleanup.mjs +189 -0
- package/lib/intake/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- package/lib/intake/queue.mjs +2 -1
- package/lib/intake/session-prelude.mjs +90 -1
- package/lib/libreoffice-export.mjs +97 -0
- package/lib/logging/rotate.mjs +9 -0
- package/lib/maintenance/docker-reclaim.mjs +206 -0
- package/lib/mcp/external-schema-cost.mjs +74 -0
- package/lib/mcp/server.mjs +84 -9
- package/lib/mcp/stdio-mcp-probe.mjs +188 -0
- package/lib/mcp/tool-budget.mjs +55 -4
- package/lib/mcp/tools/document.mjs +18 -4
- package/lib/mcp/tools/skills.mjs +32 -3
- package/lib/mcp/tools/workflow.mjs +13 -1
- package/lib/model-free-selector.mjs +18 -0
- package/lib/model-registry.mjs +13 -229
- package/lib/model-router.mjs +425 -93
- package/lib/models/behavior-matrix.mjs +289 -0
- package/lib/models/catalog.mjs +209 -0
- package/lib/models/execution-capability-profile.mjs +196 -0
- package/lib/models/execution-policy.mjs +307 -0
- package/lib/models/provider-poll.mjs +383 -0
- package/lib/npm-spawn-env.mjs +17 -0
- package/lib/ollama/installed-models.mjs +129 -0
- package/lib/oracle/actions.mjs +187 -0
- package/lib/oracle/artifact-gate.mjs +99 -0
- package/lib/oracle/cli.mjs +204 -0
- package/lib/oracle/daemon-entry.mjs +14 -0
- package/lib/oracle/dispatch.mjs +81 -0
- package/lib/oracle/execute.mjs +143 -0
- package/lib/oracle/gaps.mjs +76 -0
- package/lib/oracle/index.mjs +84 -0
- package/lib/oracle/issues.mjs +164 -0
- package/lib/oracle/org-graph.mjs +170 -0
- package/lib/oracle/policy.mjs +80 -0
- package/lib/oracle/read-model.mjs +398 -0
- package/lib/oracle/reconcile.mjs +191 -0
- package/lib/oracle/routing.mjs +89 -0
- package/lib/oracle/synthesize.mjs +384 -0
- package/lib/oracle/verdicts.mjs +51 -0
- package/lib/orchestration/worker.mjs +88 -27
- package/lib/orchestration-policy.mjs +50 -0
- package/lib/parity.mjs +96 -2
- package/lib/persona-sections.mjs +66 -0
- package/lib/playwright-demo.mjs +252 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +9 -4
- package/lib/prompt-validation-contract.mjs +24 -0
- package/lib/provider-capabilities.js +57 -11
- package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
- package/lib/providers/contract/adapters/git/index.mjs +115 -0
- package/lib/providers/contract/adapters/github/index.mjs +166 -0
- package/lib/providers/contract/adapters/jira/index.mjs +187 -0
- package/lib/providers/contract/adapters/slack/index.mjs +175 -0
- package/lib/providers/contract/contract-tests.mjs +57 -0
- package/lib/providers/contract/errors.mjs +48 -0
- package/lib/providers/contract/interface.mjs +50 -0
- package/lib/providers/contract/registry.mjs +102 -0
- package/lib/providers/copilot-auth.mjs +297 -0
- package/lib/providers/credential-bootstrap.mjs +178 -0
- package/lib/providers/credential-catalog.mjs +46 -0
- package/lib/providers/credential-sources.mjs +63 -0
- package/lib/providers/creds.mjs +5 -2
- package/lib/providers/op-run.mjs +59 -0
- package/lib/providers/secret-resolver.mjs +159 -0
- package/lib/publish-template.mjs +163 -0
- package/lib/publish-tooling.mjs +119 -0
- package/lib/publish.mjs +305 -0
- package/lib/registry/cli.mjs +82 -0
- package/lib/registry/consolidation.mjs +147 -0
- package/lib/registry/generate-docs.mjs +75 -0
- package/lib/registry/skill-verification.mjs +57 -0
- package/lib/registry/surface-map.mjs +76 -0
- package/lib/registry/validate.mjs +135 -0
- package/lib/resources/budget.mjs +82 -0
- package/lib/resources/process-budget.mjs +45 -0
- package/lib/rules-delivery.mjs +9 -2
- package/lib/rules-read.mjs +26 -0
- package/lib/runtime-env.mjs +23 -0
- package/lib/runtime-pressure.mjs +51 -7
- package/lib/schema-infer.mjs +13 -2
- package/lib/server/chat-loop.mjs +622 -0
- package/lib/server/demo-preview.mjs +63 -0
- package/lib/server/index.mjs +229 -8
- package/lib/server/langfuse-login.mjs +3 -3
- package/lib/service-manager.mjs +61 -9
- package/lib/session-store.mjs +1 -1
- package/lib/setup.mjs +102 -2
- package/lib/specialists/prompt-schema.mjs +19 -10
- package/lib/specialists/roster.mjs +43 -0
- package/lib/specialists/scaffold.mjs +56 -0
- package/lib/storage/backend.mjs +6 -6
- package/lib/storage/embeddings-local.mjs +6 -3
- package/lib/storage/file-lock.mjs +18 -11
- package/lib/storage/hybrid-query.mjs +7 -4
- package/lib/storage/state-source.mjs +5 -5
- package/lib/storage/sync.mjs +2 -3
- package/lib/telemetry/rule-calls.mjs +52 -0
- package/lib/template-registry.mjs +2 -2
- package/lib/templates/visual-requirements.mjs +27 -51
- package/lib/test-corpus-inventory.mjs +313 -0
- package/lib/uninstall/uninstall.mjs +19 -7
- package/lib/update.mjs +12 -0
- package/lib/upgrade.mjs +14 -0
- package/lib/wireframe.mjs +20 -14
- package/lib/worker/run.mjs +17 -4
- package/lib/worker/trace.mjs +11 -3
- package/package.json +29 -13
- package/personas/construct.md +13 -13
- package/platforms/claude/settings.template.json +36 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/release-gates.md +4 -3
- package/scripts/sync-specialists.mjs +187 -60
- package/skills/devops/data-engineering.md +1 -1
- package/skills/docs/adr-workflow.md +1 -0
- package/skills/docs/backlog-proposal-workflow.md +1 -0
- package/skills/docs/codebase-research-workflow.md +40 -0
- package/skills/docs/customer-profile-workflow.md +1 -0
- package/skills/docs/document-ingest-workflow.md +1 -0
- package/skills/docs/evidence-ingest-workflow.md +1 -0
- package/skills/docs/init-docs.md +2 -2
- package/skills/docs/init-project.md +8 -3
- package/skills/docs/prd-workflow.md +24 -1
- package/skills/docs/prfaq-workflow.md +1 -0
- package/skills/docs/product-intelligence-workflow.md +1 -0
- package/skills/docs/product-signal-workflow.md +1 -0
- package/skills/docs/research-workflow.md +54 -37
- package/skills/docs/runbook-workflow.md +1 -0
- package/skills/docs/strategy-workflow.md +1 -0
- package/skills/docs/user-research-workflow.md +40 -0
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/architect.md +5 -0
- package/skills/roles/operator.docs.md +4 -0
- package/skills/routing.md +4 -2
- package/specialists/artifact-manifest.json +480 -0
- package/specialists/artifact-manifest.schema.json +53 -0
- package/specialists/audit-enrichments.json +454 -0
- package/specialists/contracts.json +37 -25
- package/specialists/prompts/_shared/validation-contract.md +26 -0
- package/specialists/prompts/cx-accessibility.md +21 -2
- package/specialists/prompts/cx-ai-engineer.md +24 -1
- package/specialists/prompts/cx-architect.md +4 -1
- package/specialists/prompts/cx-business-strategist.md +23 -2
- package/specialists/prompts/cx-data-analyst.md +22 -1
- package/specialists/prompts/cx-data-engineer.md +23 -2
- package/specialists/prompts/cx-debugger.md +21 -2
- package/specialists/prompts/cx-designer.md +26 -3
- package/specialists/prompts/cx-devil-advocate.md +19 -2
- package/specialists/prompts/cx-docs-keeper.md +28 -1
- package/specialists/prompts/cx-engineer.md +22 -1
- package/specialists/prompts/cx-evaluator.md +18 -1
- package/specialists/prompts/cx-explorer.md +21 -2
- package/specialists/prompts/cx-legal-compliance.md +21 -2
- package/specialists/prompts/cx-operations.md +21 -2
- package/specialists/prompts/cx-oracle.md +94 -0
- package/specialists/prompts/cx-orchestrator.md +20 -1
- package/specialists/prompts/cx-platform-engineer.md +25 -2
- package/specialists/prompts/cx-product-manager.md +32 -1
- package/specialists/prompts/cx-qa.md +24 -2
- package/specialists/prompts/cx-rd-lead.md +23 -2
- package/specialists/prompts/cx-release-manager.md +23 -2
- package/specialists/prompts/cx-researcher.md +22 -2
- package/specialists/prompts/cx-reviewer.md +18 -1
- package/specialists/prompts/cx-security.md +22 -2
- package/specialists/prompts/cx-sre.md +30 -3
- package/specialists/prompts/cx-test-automation.md +7 -1
- package/specialists/prompts/cx-trace-reviewer.md +22 -3
- package/specialists/prompts/cx-ux-researcher.md +21 -2
- package/specialists/registry.json +52 -173
- package/specialists/tone-profiles.json +42 -0
- package/templates/demos/playwright/demo-recording.config.mjs +47 -0
- package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
- package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
- package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
- package/templates/demos/tapes/resource-guard-rails.tape +49 -0
- package/templates/demos/vhs/construct-cockpit.json +24 -0
- package/templates/distribution/construct-brand.typ +446 -0
- package/templates/distribution/construct-decision.typ +38 -0
- package/templates/distribution/construct-deck.html +95 -0
- package/templates/distribution/construct-pdf.typ +38 -0
- package/templates/distribution/construct-prd.typ +38 -0
- package/templates/distribution/construct-research.typ +38 -0
- package/templates/distribution/construct-web.html +92 -0
- package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
- package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
- package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
- package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/README.md +36 -0
- package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
- package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
- package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
- package/templates/distribution/icons/activity.svg +15 -0
- package/templates/distribution/icons/alert-triangle.svg +17 -0
- package/templates/distribution/icons/book-open.svg +16 -0
- package/templates/distribution/icons/bot.svg +20 -0
- package/templates/distribution/icons/brain.svg +22 -0
- package/templates/distribution/icons/circle-check.svg +16 -0
- package/templates/distribution/icons/clipboard-check.svg +17 -0
- package/templates/distribution/icons/cpu.svg +28 -0
- package/templates/distribution/icons/database.svg +17 -0
- package/templates/distribution/icons/eye.svg +16 -0
- package/templates/distribution/icons/file-text.svg +19 -0
- package/templates/distribution/icons/gauge.svg +16 -0
- package/templates/distribution/icons/git-branch.svg +17 -0
- package/templates/distribution/icons/key.svg +17 -0
- package/templates/distribution/icons/layers.svg +17 -0
- package/templates/distribution/icons/list-checks.svg +19 -0
- package/templates/distribution/icons/lock.svg +16 -0
- package/templates/distribution/icons/message-square.svg +15 -0
- package/templates/distribution/icons/network.svg +19 -0
- package/templates/distribution/icons/route.svg +17 -0
- package/templates/distribution/icons/scale.svg +19 -0
- package/templates/distribution/icons/search.svg +16 -0
- package/templates/distribution/icons/send.svg +16 -0
- package/templates/distribution/icons/server.svg +18 -0
- package/templates/distribution/icons/shield-check.svg +16 -0
- package/templates/distribution/icons/users.svg +18 -0
- package/templates/distribution/icons/webhook.svg +17 -0
- package/templates/distribution/icons/workflow.svg +17 -0
- package/templates/distribution/icons/wrench.svg +15 -0
- package/templates/distribution/run.mjs +18 -1
- package/templates/docs/adr.md +7 -0
- package/templates/docs/construct_guide.md +10 -10
- package/templates/docs/customer-profile.md +4 -0
- package/templates/docs/one-pager.md +4 -0
- package/templates/docs/postmortem.md +31 -0
- package/templates/docs/prd-platform.md +19 -0
- package/templates/docs/prd.md +15 -0
- package/templates/docs/prfaq.md +7 -0
- package/templates/docs/qa-strategy.md +103 -0
- package/templates/docs/research-brief.md +8 -0
- package/templates/docs/rfc-platform.md +12 -0
- package/templates/docs/test-plan.md +7 -0
- package/lib/ingest/chunker.mjs +0 -94
- package/lib/ingest/pipeline.mjs +0 -53
- package/lib/ingest/store.mjs +0 -82
- package/lib/mode-commands.mjs +0 -122
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/profiles/validate-custom.mjs +0 -114
- package/lib/services/telemetry-backend.mjs +0 -177
- package/lib/storage/fusion.mjs +0 -95
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* providers/confluence/index.mjs — Confluence provider.
|
|
3
|
+
*
|
|
4
|
+
* Transport: Confluence REST API v2 (direct fetch, no SDK).
|
|
5
|
+
* Auth: API token via CONFLUENCE_TOKEN + CONFLUENCE_EMAIL + CONFLUENCE_URL,
|
|
6
|
+
* or passed directly in config.
|
|
7
|
+
*
|
|
8
|
+
* Capabilities: read, write, search
|
|
9
|
+
*
|
|
10
|
+
* read refs:
|
|
11
|
+
* "page:<id>" → single page by ID
|
|
12
|
+
* "space:<key>:pages" → pages in a space
|
|
13
|
+
* "space:<key>:pages:<n>" → first n pages in a space
|
|
14
|
+
* "spaces" → list all accessible spaces
|
|
15
|
+
*
|
|
16
|
+
* write items:
|
|
17
|
+
* { type: 'page', spaceId, title, body, parentId? } → create page
|
|
18
|
+
* { type: 'page-update', pageId, title, body, version } → update page
|
|
19
|
+
* { type: 'comment', pageId, body } → add footer comment
|
|
20
|
+
*
|
|
21
|
+
* search: CQL query string (e.g. "space = DEV AND title ~ 'RFC'")
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { AuthError, NotFoundError, RateLimitError } from '../../errors.mjs';
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
name: 'confluence',
|
|
28
|
+
capabilities: ['read', 'write', 'search'],
|
|
29
|
+
|
|
30
|
+
_baseUrl: null,
|
|
31
|
+
_auth: null,
|
|
32
|
+
|
|
33
|
+
async init(config = {}) {
|
|
34
|
+
const url = config.baseUrl ?? process.env.CONFLUENCE_URL;
|
|
35
|
+
const email = config.email ?? process.env.CONFLUENCE_EMAIL;
|
|
36
|
+
const token = config.token ?? process.env.CONFLUENCE_TOKEN;
|
|
37
|
+
|
|
38
|
+
if (!url || !email || !token) {
|
|
39
|
+
throw new AuthError(
|
|
40
|
+
'Confluence provider requires CONFLUENCE_URL, CONFLUENCE_EMAIL, and CONFLUENCE_TOKEN',
|
|
41
|
+
{ provider: 'confluence' },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._baseUrl = url.replace(/\/$/, '');
|
|
46
|
+
this._auth = `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`;
|
|
47
|
+
|
|
48
|
+
const res = await this._fetch('/wiki/rest/api/user/current');
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new AuthError(`Confluence auth failed: ${res.status} ${res.statusText}`, { provider: 'confluence' });
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async _fetch(path, { method = 'GET', body } = {}) {
|
|
55
|
+
const url = `${this._baseUrl}${path}`;
|
|
56
|
+
const opts = {
|
|
57
|
+
method,
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: this._auth,
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
Accept: 'application/json',
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
if (body) opts.body = JSON.stringify(body);
|
|
65
|
+
const res = await fetch(url, opts);
|
|
66
|
+
if (res.status === 429) {
|
|
67
|
+
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '60', 10);
|
|
68
|
+
throw new RateLimitError('Confluence rate limit hit', { provider: 'confluence', retryAfter });
|
|
69
|
+
}
|
|
70
|
+
return res;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async read(ref, _opts = {}) {
|
|
74
|
+
if (ref === 'spaces') {
|
|
75
|
+
const res = await this._fetch('/wiki/api/v2/spaces?limit=50');
|
|
76
|
+
if (!res.ok) throw new Error(`Confluence error: ${res.status}`);
|
|
77
|
+
const data = await res.json();
|
|
78
|
+
return (data.results ?? []).map((s) => ({
|
|
79
|
+
type: 'space',
|
|
80
|
+
id: s.id,
|
|
81
|
+
key: s.key,
|
|
82
|
+
name: s.name,
|
|
83
|
+
homepageId: s.homepageId,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (ref.startsWith('page:')) {
|
|
88
|
+
const pageId = ref.slice(5);
|
|
89
|
+
const res = await this._fetch(`/wiki/api/v2/pages/${pageId}?body-format=storage`);
|
|
90
|
+
if (res.status === 404) throw new NotFoundError(`Page not found: ${pageId}`, { provider: 'confluence' });
|
|
91
|
+
if (!res.ok) throw new Error(`Confluence error: ${res.status}`);
|
|
92
|
+
return [normalizePage(await res.json())];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const spaceMatch = ref.match(/^space:([^:]+):pages(?::(\d+))?$/);
|
|
96
|
+
if (spaceMatch) {
|
|
97
|
+
const spaceKey = spaceMatch[1];
|
|
98
|
+
const limit = parseInt(spaceMatch[2] ?? '25', 10);
|
|
99
|
+
// Look up space id from key first
|
|
100
|
+
const spacesRes = await this._fetch(`/wiki/api/v2/spaces?keys=${spaceKey}&limit=1`);
|
|
101
|
+
const spacesData = await spacesRes.json();
|
|
102
|
+
const space = spacesData.results?.[0];
|
|
103
|
+
if (!space) throw new NotFoundError(`Space not found: ${spaceKey}`, { provider: 'confluence' });
|
|
104
|
+
const res = await this._fetch(`/wiki/api/v2/spaces/${space.id}/pages?limit=${limit}&body-format=storage`);
|
|
105
|
+
if (!res.ok) throw new Error(`Confluence error: ${res.status}`);
|
|
106
|
+
const data = await res.json();
|
|
107
|
+
return (data.results ?? []).map(normalizePage);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
throw new NotFoundError(`Unknown Confluence read ref: "${ref}"`, { provider: 'confluence' });
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async write(item) {
|
|
114
|
+
if (item.type === 'page') {
|
|
115
|
+
const body = {
|
|
116
|
+
spaceId: item.spaceId,
|
|
117
|
+
title: item.title,
|
|
118
|
+
parentId: item.parentId ?? undefined,
|
|
119
|
+
body: { representation: 'storage', value: item.body },
|
|
120
|
+
};
|
|
121
|
+
const res = await this._fetch('/wiki/api/v2/pages', { method: 'POST', body });
|
|
122
|
+
if (!res.ok) throw new Error(`Confluence create page failed: ${res.status}`);
|
|
123
|
+
const data = await res.json();
|
|
124
|
+
return { type: 'page-created', id: data.id, title: data.title };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (item.type === 'page-update') {
|
|
128
|
+
const body = {
|
|
129
|
+
id: item.pageId,
|
|
130
|
+
title: item.title,
|
|
131
|
+
version: { number: item.version },
|
|
132
|
+
body: { representation: 'storage', value: item.body },
|
|
133
|
+
};
|
|
134
|
+
const res = await this._fetch(`/wiki/api/v2/pages/${item.pageId}`, { method: 'PUT', body });
|
|
135
|
+
if (!res.ok) throw new Error(`Confluence update page failed: ${res.status}`);
|
|
136
|
+
return { type: 'page-updated', id: item.pageId };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (item.type === 'comment') {
|
|
140
|
+
const body = {
|
|
141
|
+
pageId: item.pageId,
|
|
142
|
+
body: { representation: 'storage', value: item.body },
|
|
143
|
+
};
|
|
144
|
+
const res = await this._fetch('/wiki/api/v2/footer-comments', { method: 'POST', body });
|
|
145
|
+
if (!res.ok) throw new Error(`Confluence create comment failed: ${res.status}`);
|
|
146
|
+
const data = await res.json();
|
|
147
|
+
return { type: 'comment-created', id: data.id };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
throw new Error(`Unknown Confluence write item type: "${item.type}"`);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
async search(query, opts = {}) {
|
|
154
|
+
const limit = opts.limit ?? 25;
|
|
155
|
+
const cql = encodeURIComponent(query);
|
|
156
|
+
const res = await this._fetch(`/wiki/rest/api/content/search?cql=${cql}&limit=${limit}`);
|
|
157
|
+
if (!res.ok) throw new Error(`Confluence search failed: ${res.status}`);
|
|
158
|
+
const data = await res.json();
|
|
159
|
+
return (data.results ?? []).map((r) => ({
|
|
160
|
+
type: 'page',
|
|
161
|
+
id: r.id,
|
|
162
|
+
title: r.title,
|
|
163
|
+
spaceKey: r.space?.key,
|
|
164
|
+
url: r._links?.webui ?? null,
|
|
165
|
+
}));
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
function normalizePage(raw) {
|
|
170
|
+
return {
|
|
171
|
+
type: 'page',
|
|
172
|
+
id: raw.id,
|
|
173
|
+
title: raw.title,
|
|
174
|
+
spaceId: raw.spaceId,
|
|
175
|
+
parentId: raw.parentId ?? null,
|
|
176
|
+
version: raw.version?.number ?? null,
|
|
177
|
+
body: raw.body?.storage?.value ?? null,
|
|
178
|
+
createdAt: raw.createdAt ?? null,
|
|
179
|
+
url: raw._links?.webui ?? null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* providers/git/index.mjs — git repo provider.
|
|
3
|
+
*
|
|
4
|
+
* Transport: git CLI (via child_process). No external deps.
|
|
5
|
+
*
|
|
6
|
+
* Capabilities: read, search
|
|
7
|
+
*
|
|
8
|
+
* read refs:
|
|
9
|
+
* "commits" → recent commits (default 20)
|
|
10
|
+
* "commits:<n>" → last n commits
|
|
11
|
+
* "branches" → all branch names
|
|
12
|
+
* "file:<path>" → file contents at HEAD
|
|
13
|
+
* "status" → working tree status
|
|
14
|
+
* "log:<path>" → commit log for a specific path
|
|
15
|
+
*
|
|
16
|
+
* search: grep across tracked files (git grep)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { AuthError, NotFoundError } from '../../errors.mjs';
|
|
22
|
+
|
|
23
|
+
function git(args, cwd) {
|
|
24
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
25
|
+
if (result.error) throw new Error(`git spawn error: ${result.error.message}`);
|
|
26
|
+
return { stdout: result.stdout ?? '', stderr: result.stderr ?? '', status: result.status };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
name: 'git',
|
|
31
|
+
capabilities: ['read', 'search'],
|
|
32
|
+
|
|
33
|
+
_cwd: null,
|
|
34
|
+
|
|
35
|
+
async init(config = {}) {
|
|
36
|
+
const cwd = config.cwd ?? process.cwd();
|
|
37
|
+
// Verify this is actually a git repo
|
|
38
|
+
const check = git(['rev-parse', '--git-dir'], cwd);
|
|
39
|
+
if (check.status !== 0) {
|
|
40
|
+
throw new AuthError(`Not a git repository: ${cwd}`, { provider: 'git' });
|
|
41
|
+
}
|
|
42
|
+
this._cwd = cwd;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async read(ref, _opts = {}) {
|
|
46
|
+
const cwd = this._cwd ?? process.cwd();
|
|
47
|
+
|
|
48
|
+
if (ref === 'branches') {
|
|
49
|
+
const r = git(['branch', '-a', '--format=%(refname:short)'], cwd);
|
|
50
|
+
return r.stdout.trim().split('\n').filter(Boolean).map((name) => ({ type: 'branch', name }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (ref === 'status') {
|
|
54
|
+
const r = git(['status', '--porcelain=v1'], cwd);
|
|
55
|
+
return r.stdout.trim().split('\n').filter(Boolean).map((line) => ({
|
|
56
|
+
type: 'status-entry',
|
|
57
|
+
code: line.slice(0, 2).trim(),
|
|
58
|
+
path: line.slice(3),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (ref.startsWith('commits') || ref === 'commits') {
|
|
63
|
+
const n = ref.includes(':') ? parseInt(ref.split(':')[1], 10) : 20;
|
|
64
|
+
const r = git(['log', `--max-count=${n}`, '--pretty=format:%H\t%an\t%ae\t%ai\t%s'], cwd);
|
|
65
|
+
return r.stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
66
|
+
const [hash, author, email, date, ...subjectParts] = line.split('\t');
|
|
67
|
+
return { type: 'commit', hash, author, email, date, subject: subjectParts.join('\t') };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (ref.startsWith('file:')) {
|
|
72
|
+
const filePath = ref.slice(5);
|
|
73
|
+
const r = git(['show', `HEAD:${filePath}`], cwd);
|
|
74
|
+
if (r.status !== 0) {
|
|
75
|
+
throw new NotFoundError(`File not found in HEAD: ${filePath}`, { provider: 'git' });
|
|
76
|
+
}
|
|
77
|
+
return [{ type: 'file', path: filePath, content: r.stdout }];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (ref.startsWith('log:')) {
|
|
81
|
+
const filePath = ref.slice(4);
|
|
82
|
+
const r = git(['log', '--follow', '--pretty=format:%H\t%an\t%ai\t%s', '--', filePath], cwd);
|
|
83
|
+
return r.stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
84
|
+
const [hash, author, date, ...subjectParts] = line.split('\t');
|
|
85
|
+
return { type: 'commit', hash, author, date, subject: subjectParts.join('\t'), path: filePath };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new NotFoundError(`Unknown git read ref: "${ref}"`, { provider: 'git' });
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async search(query, opts = {}) {
|
|
93
|
+
const cwd = this._cwd ?? process.cwd();
|
|
94
|
+
const args = ['grep', '--line-number', '-I'];
|
|
95
|
+
if (opts.ignoreCase) args.push('-i');
|
|
96
|
+
args.push(query);
|
|
97
|
+
if (opts.paths) args.push('--', ...opts.paths);
|
|
98
|
+
const r = git(args, cwd);
|
|
99
|
+
if (r.status === 1 && r.stdout === '') return []; // no matches
|
|
100
|
+
if (r.status !== 0 && r.status !== 1) {
|
|
101
|
+
throw new Error(`git grep failed: ${r.stderr}`);
|
|
102
|
+
}
|
|
103
|
+
return r.stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
104
|
+
const colonIdx = line.indexOf(':');
|
|
105
|
+
const rest = line.slice(colonIdx + 1);
|
|
106
|
+
const lineNumIdx = rest.indexOf(':');
|
|
107
|
+
return {
|
|
108
|
+
type: 'grep-match',
|
|
109
|
+
file: line.slice(0, colonIdx),
|
|
110
|
+
lineNumber: parseInt(rest.slice(0, lineNumIdx), 10),
|
|
111
|
+
text: rest.slice(lineNumIdx + 1),
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* providers/github/index.mjs — GitHub provider.
|
|
3
|
+
*
|
|
4
|
+
* Transport: gh CLI (must be installed and authenticated).
|
|
5
|
+
*
|
|
6
|
+
* Capabilities: read, write, search, webhook
|
|
7
|
+
*
|
|
8
|
+
* read refs:
|
|
9
|
+
* "prs" → open pull requests
|
|
10
|
+
* "prs:all" → all PRs (open + closed)
|
|
11
|
+
* "pr:<number>" → single PR detail
|
|
12
|
+
* "issues" → open issues
|
|
13
|
+
* "issue:<number>" → single issue detail
|
|
14
|
+
* "releases" → recent releases
|
|
15
|
+
* "repo" → repo metadata
|
|
16
|
+
*
|
|
17
|
+
* write items:
|
|
18
|
+
* { type: 'pr', title, body, head, base } → create PR
|
|
19
|
+
* { type: 'issue', title, body, labels? } → create issue
|
|
20
|
+
* { type: 'comment', issue_number, body } → comment on issue/PR
|
|
21
|
+
* { type: 'pr-merge', number, merge_method? } → merge a PR
|
|
22
|
+
*
|
|
23
|
+
* search: gh search issues/prs/code
|
|
24
|
+
* { scope: 'issues'|'prs'|'code', q: 'query string' }
|
|
25
|
+
*
|
|
26
|
+
* webhook: process GitHub webhook event objects
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import { AuthError, NotFoundError } from '../../errors.mjs';
|
|
31
|
+
|
|
32
|
+
function gh(args, { json = true, input } = {}) {
|
|
33
|
+
const fullArgs = json ? [...args, '--json', ...ghJsonFields(args)] : args;
|
|
34
|
+
const result = spawnSync('gh', fullArgs, {
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
input,
|
|
37
|
+
env: process.env,
|
|
38
|
+
});
|
|
39
|
+
if (result.error) throw new Error(`gh spawn error: ${result.error.message}`);
|
|
40
|
+
if (result.status !== 0) {
|
|
41
|
+
const msg = result.stderr?.trim() || `gh exited with status ${result.status}`;
|
|
42
|
+
if (msg.includes('authentication') || msg.includes('401')) {
|
|
43
|
+
throw new AuthError(msg, { provider: 'github' });
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`gh error: ${msg}`);
|
|
46
|
+
}
|
|
47
|
+
if (json && result.stdout) {
|
|
48
|
+
try { return JSON.parse(result.stdout); } catch { return result.stdout; }
|
|
49
|
+
}
|
|
50
|
+
return result.stdout;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// gh requires --json to list specific fields; default to a safe broad set
|
|
54
|
+
function ghJsonFields(args) {
|
|
55
|
+
const cmd = args[0];
|
|
56
|
+
if (cmd === 'pr') return ['number,title,state,url,body,headRefName,baseRefName,author,createdAt,mergeable,labels'];
|
|
57
|
+
if (cmd === 'issue') return ['number,title,state,url,body,author,createdAt,labels,assignees'];
|
|
58
|
+
if (cmd === 'release') return ['tagName,name,publishedAt,url,body'];
|
|
59
|
+
if (cmd === 'repo') return ['name,description,url,defaultBranchRef,isPrivate,stargazerCount'];
|
|
60
|
+
return [''];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default {
|
|
64
|
+
name: 'github',
|
|
65
|
+
capabilities: ['read', 'write', 'search', 'webhook'],
|
|
66
|
+
|
|
67
|
+
_repo: null,
|
|
68
|
+
|
|
69
|
+
async init(config = {}) {
|
|
70
|
+
this._repo = config.repo ?? null; // optional "owner/repo" override
|
|
71
|
+
// Verify gh is available and authenticated
|
|
72
|
+
const check = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', env: process.env });
|
|
73
|
+
if (check.status !== 0) {
|
|
74
|
+
throw new AuthError('gh CLI not authenticated. Run: gh auth login', { provider: 'github' });
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async read(ref, _opts = {}) {
|
|
79
|
+
const repoFlag = this._repo ? ['-R', this._repo] : [];
|
|
80
|
+
|
|
81
|
+
if (ref === 'prs' || ref === 'prs:all') {
|
|
82
|
+
const stateFlag = ref === 'prs:all' ? ['--state', 'all'] : ['--state', 'open'];
|
|
83
|
+
return gh(['pr', 'list', ...repoFlag, ...stateFlag, '--limit', '50']);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (ref.startsWith('pr:')) {
|
|
87
|
+
const num = ref.slice(3);
|
|
88
|
+
return [gh(['pr', 'view', num, ...repoFlag])];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (ref === 'issues') {
|
|
92
|
+
return gh(['issue', 'list', ...repoFlag, '--state', 'open', '--limit', '50']);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (ref.startsWith('issue:')) {
|
|
96
|
+
const num = ref.slice(6);
|
|
97
|
+
return [gh(['issue', 'view', num, ...repoFlag])];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (ref === 'releases') {
|
|
101
|
+
return gh(['release', 'list', ...repoFlag, '--limit', '20']);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (ref === 'repo') {
|
|
105
|
+
return [gh(['repo', 'view', ...repoFlag])];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new NotFoundError(`Unknown GitHub read ref: "${ref}"`, { provider: 'github' });
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async write(item) {
|
|
112
|
+
const repoFlag = this._repo ? ['-R', this._repo] : [];
|
|
113
|
+
|
|
114
|
+
if (item.type === 'issue') {
|
|
115
|
+
const args = ['issue', 'create', ...repoFlag, '--title', item.title, '--body', item.body ?? ''];
|
|
116
|
+
if (item.labels?.length) args.push('--label', item.labels.join(','));
|
|
117
|
+
const out = spawnSync('gh', args, { encoding: 'utf8', env: process.env });
|
|
118
|
+
return { type: 'issue-created', url: out.stdout.trim() };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (item.type === 'pr') {
|
|
122
|
+
const args = ['pr', 'create', ...repoFlag,
|
|
123
|
+
'--title', item.title,
|
|
124
|
+
'--body', item.body ?? '',
|
|
125
|
+
'--head', item.head,
|
|
126
|
+
'--base', item.base ?? 'main',
|
|
127
|
+
];
|
|
128
|
+
if (item.draft) args.push('--draft');
|
|
129
|
+
const out = spawnSync('gh', args, { encoding: 'utf8', env: process.env });
|
|
130
|
+
return { type: 'pr-created', url: out.stdout.trim() };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (item.type === 'comment') {
|
|
134
|
+
const args = ['issue', 'comment', String(item.issue_number), ...repoFlag, '--body', item.body];
|
|
135
|
+
spawnSync('gh', args, { encoding: 'utf8', env: process.env });
|
|
136
|
+
return { type: 'comment-created', issue_number: item.issue_number };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (item.type === 'pr-merge') {
|
|
140
|
+
const method = item.merge_method ?? 'squash';
|
|
141
|
+
spawnSync('gh', ['pr', 'merge', String(item.number), ...repoFlag, `--${method}`], {
|
|
142
|
+
encoding: 'utf8',
|
|
143
|
+
env: process.env,
|
|
144
|
+
});
|
|
145
|
+
return { type: 'pr-merged', number: item.number };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new Error(`Unknown GitHub write item type: "${item.type}"`);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
async search(query, opts = {}) {
|
|
152
|
+
const scope = opts.scope ?? 'issues';
|
|
153
|
+
const repoFlag = this._repo ? ['--repo', this._repo] : [];
|
|
154
|
+
const args = ['search', scope, query, ...repoFlag, '--limit', '30', '--json',
|
|
155
|
+
scope === 'code' ? 'path,repository,url,textMatches' : 'number,title,state,url,repository',
|
|
156
|
+
];
|
|
157
|
+
const results = gh(args, { json: false });
|
|
158
|
+
try { return JSON.parse(results); } catch { return []; }
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async webhook(event) {
|
|
162
|
+
// Normalize inbound GitHub webhook event to a common shape for core to consume
|
|
163
|
+
const type = event?.action ? `${event.type ?? 'unknown'}.${event.action}` : (event?.type ?? 'unknown');
|
|
164
|
+
return { provider: 'github', type, raw: event };
|
|
165
|
+
},
|
|
166
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* providers/jira/index.mjs — Jira provider.
|
|
3
|
+
*
|
|
4
|
+
* Transport: Jira REST API v3 (direct fetch, no SDK).
|
|
5
|
+
* Auth: API token via JIRA_TOKEN + JIRA_EMAIL + JIRA_URL env vars,
|
|
6
|
+
* or passed directly in config.
|
|
7
|
+
*
|
|
8
|
+
* Capabilities: read, write, search, webhook
|
|
9
|
+
*
|
|
10
|
+
* read refs:
|
|
11
|
+
* "issue:<KEY>" → single issue (e.g. "issue:PROJ-123")
|
|
12
|
+
* "project:<KEY>" → project metadata
|
|
13
|
+
* "board:<id>:sprint" → active sprint issues for a board
|
|
14
|
+
* "search:<JQL>" → JQL query (shorthand; prefer search() for full control)
|
|
15
|
+
*
|
|
16
|
+
* write items:
|
|
17
|
+
* { type: 'issue', project, issueType, summary, description?, assignee?, labels? }
|
|
18
|
+
* { type: 'comment', issueKey, body }
|
|
19
|
+
* { type: 'transition', issueKey, transitionId }
|
|
20
|
+
* { type: 'update', issueKey, fields }
|
|
21
|
+
*
|
|
22
|
+
* search: JQL string (e.g. "project = PROJ AND status = 'In Progress'")
|
|
23
|
+
*
|
|
24
|
+
* webhook: process Jira webhook event objects
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { AuthError, NotFoundError, RateLimitError } from '../../errors.mjs';
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
name: 'jira',
|
|
31
|
+
capabilities: ['read', 'write', 'search', 'webhook'],
|
|
32
|
+
|
|
33
|
+
_baseUrl: null,
|
|
34
|
+
_auth: null,
|
|
35
|
+
|
|
36
|
+
async init(config = {}) {
|
|
37
|
+
const url = config.baseUrl ?? process.env.JIRA_URL;
|
|
38
|
+
const email = config.email ?? process.env.JIRA_EMAIL;
|
|
39
|
+
const token = config.token ?? process.env.JIRA_TOKEN;
|
|
40
|
+
|
|
41
|
+
if (!url || !email || !token) {
|
|
42
|
+
throw new AuthError(
|
|
43
|
+
'Jira provider requires JIRA_URL, JIRA_EMAIL, and JIRA_TOKEN (or config.baseUrl/email/token)',
|
|
44
|
+
{ provider: 'jira' },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this._baseUrl = url.replace(/\/$/, '');
|
|
49
|
+
this._auth = `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`;
|
|
50
|
+
|
|
51
|
+
// Verify credentials
|
|
52
|
+
const res = await this._fetch('/rest/api/3/myself');
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new AuthError(`Jira auth failed: ${res.status} ${res.statusText}`, { provider: 'jira' });
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async _fetch(path, { method = 'GET', body } = {}) {
|
|
59
|
+
const url = `${this._baseUrl}${path}`;
|
|
60
|
+
const opts = {
|
|
61
|
+
method,
|
|
62
|
+
headers: {
|
|
63
|
+
Authorization: this._auth,
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
Accept: 'application/json',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
if (body) opts.body = JSON.stringify(body);
|
|
69
|
+
const res = await fetch(url, opts);
|
|
70
|
+
if (res.status === 429) {
|
|
71
|
+
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '60', 10);
|
|
72
|
+
throw new RateLimitError('Jira rate limit hit', { provider: 'jira', retryAfter });
|
|
73
|
+
}
|
|
74
|
+
return res;
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async read(ref, _opts = {}) {
|
|
78
|
+
if (ref.startsWith('issue:')) {
|
|
79
|
+
const key = ref.slice(6);
|
|
80
|
+
const res = await this._fetch(`/rest/api/3/issue/${key}`);
|
|
81
|
+
if (res.status === 404) throw new NotFoundError(`Issue not found: ${key}`, { provider: 'jira' });
|
|
82
|
+
const data = await res.json();
|
|
83
|
+
return [normalizeIssue(data)];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (ref.startsWith('project:')) {
|
|
87
|
+
const key = ref.slice(8);
|
|
88
|
+
const res = await this._fetch(`/rest/api/3/project/${key}`);
|
|
89
|
+
if (res.status === 404) throw new NotFoundError(`Project not found: ${key}`, { provider: 'jira' });
|
|
90
|
+
return [await res.json()];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (ref.startsWith('search:')) {
|
|
94
|
+
const jql = ref.slice(7);
|
|
95
|
+
return this.search(jql);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw new NotFoundError(`Unknown Jira read ref: "${ref}"`, { provider: 'jira' });
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async write(item) {
|
|
102
|
+
if (item.type === 'issue') {
|
|
103
|
+
const body = {
|
|
104
|
+
fields: {
|
|
105
|
+
project: { key: item.project },
|
|
106
|
+
issuetype: { name: item.issueType ?? 'Task' },
|
|
107
|
+
summary: item.summary,
|
|
108
|
+
...(item.description ? { description: adfDoc(item.description) } : {}),
|
|
109
|
+
...(item.assignee ? { assignee: { accountId: item.assignee } } : {}),
|
|
110
|
+
...(item.labels ? { labels: item.labels } : {}),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
const res = await this._fetch('/rest/api/3/issue', { method: 'POST', body });
|
|
114
|
+
const data = await res.json();
|
|
115
|
+
return { type: 'issue-created', key: data.key, id: data.id };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (item.type === 'comment') {
|
|
119
|
+
const body = { body: adfDoc(item.body) };
|
|
120
|
+
const res = await this._fetch(`/rest/api/3/issue/${item.issueKey}/comment`, { method: 'POST', body });
|
|
121
|
+
const data = await res.json();
|
|
122
|
+
return { type: 'comment-created', id: data.id, issueKey: item.issueKey };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (item.type === 'transition') {
|
|
126
|
+
await this._fetch(`/rest/api/3/issue/${item.issueKey}/transitions`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
body: { transition: { id: String(item.transitionId) } },
|
|
129
|
+
});
|
|
130
|
+
return { type: 'transitioned', issueKey: item.issueKey, transitionId: item.transitionId };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (item.type === 'update') {
|
|
134
|
+
await this._fetch(`/rest/api/3/issue/${item.issueKey}`, {
|
|
135
|
+
method: 'PUT',
|
|
136
|
+
body: { fields: item.fields },
|
|
137
|
+
});
|
|
138
|
+
return { type: 'updated', issueKey: item.issueKey };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new Error(`Unknown Jira write item type: "${item.type}"`);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async search(query, opts = {}) {
|
|
145
|
+
const body = {
|
|
146
|
+
jql: query,
|
|
147
|
+
maxResults: opts.maxResults ?? 50,
|
|
148
|
+
fields: opts.fields ?? ['summary', 'status', 'assignee', 'priority', 'issuetype', 'created', 'updated'],
|
|
149
|
+
};
|
|
150
|
+
const res = await this._fetch('/rest/api/3/search', { method: 'POST', body });
|
|
151
|
+
const data = await res.json();
|
|
152
|
+
return (data.issues ?? []).map(normalizeIssue);
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
async webhook(event) {
|
|
156
|
+
const type = event?.webhookEvent ?? 'unknown';
|
|
157
|
+
return { provider: 'jira', type, raw: event };
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
function normalizeIssue(raw) {
|
|
164
|
+
const f = raw.fields ?? {};
|
|
165
|
+
return {
|
|
166
|
+
type: 'issue',
|
|
167
|
+
key: raw.key,
|
|
168
|
+
id: raw.id,
|
|
169
|
+
summary: f.summary,
|
|
170
|
+
status: f.status?.name,
|
|
171
|
+
assignee: f.assignee?.displayName ?? null,
|
|
172
|
+
priority: f.priority?.name ?? null,
|
|
173
|
+
issueType: f.issuetype?.name,
|
|
174
|
+
created: f.created,
|
|
175
|
+
updated: f.updated,
|
|
176
|
+
url: raw.self?.replace(/\/rest\/api\/.*/, `/browse/${raw.key}`) ?? null,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Wrap plain text in minimal ADF document for Jira REST API v3 */
|
|
181
|
+
function adfDoc(text) {
|
|
182
|
+
return {
|
|
183
|
+
type: 'doc',
|
|
184
|
+
version: 1,
|
|
185
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: String(text) }] }],
|
|
186
|
+
};
|
|
187
|
+
}
|