@geraldmaron/construct 1.0.0 → 1.0.2
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/.env.example +9 -6
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1000 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +523 -36
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/integrations/intake-integrations.mjs — External system integrations
|
|
3
|
+
* for intake packets and artifacts.
|
|
4
|
+
*
|
|
5
|
+
* Creates GitHub Issues, Jira tickets, and Confluence pages from intake
|
|
6
|
+
* packets and Construct artifacts. Each integration is optional and
|
|
7
|
+
* configured via env vars or embed.yaml.
|
|
8
|
+
*
|
|
9
|
+
* Integrations:
|
|
10
|
+
* - GitHub Issues: requires GITHUB_TOKEN + GITHUB_REPO (owner/repo)
|
|
11
|
+
* - Jira: requires JIRA_HOST + JIRA_USER + JIRA_API_TOKEN + JIRA_PROJECT
|
|
12
|
+
* - Confluence: requires CONFLUENCE_HOST + CONFLUENCE_USER + CONFLUENCE_API_TOKEN + CONFLUENCE_SPACE
|
|
13
|
+
*
|
|
14
|
+
* All operations return { ok, externalUrl, externalId, error? }.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
|
|
22
|
+
// ── Multi-method auth helpers ────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve GitHub credentials using multiple methods (in priority order):
|
|
26
|
+
* 1. `gh` CLI (already authenticated via gh auth)
|
|
27
|
+
* 2. GITHUB_TOKEN env var / .env / config.env / shell rc
|
|
28
|
+
* 3. 1Password op:// refs in shell rc files
|
|
29
|
+
*
|
|
30
|
+
* Returns { token, repo, method } or { token: null }.
|
|
31
|
+
*/
|
|
32
|
+
function resolveGitHubAuth(homeDir) {
|
|
33
|
+
// Method 1: gh CLI
|
|
34
|
+
try {
|
|
35
|
+
const status = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 5000 });
|
|
36
|
+
if (status.status === 0) {
|
|
37
|
+
// gh is authenticated — resolve the repo from env or gh itself
|
|
38
|
+
let repo = process.env.GITHUB_REPO || resolveCredentialSync('GITHUB_REPO', homeDir);
|
|
39
|
+
if (!repo) {
|
|
40
|
+
// Try to get the default remote from gh
|
|
41
|
+
const remote = spawnSync('gh', ['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner'], { encoding: 'utf8', timeout: 5000 });
|
|
42
|
+
if (remote.status === 0 && remote.stdout?.trim()) {
|
|
43
|
+
repo = remote.stdout.trim();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { token: 'gh-cli', repo, method: 'gh' };
|
|
47
|
+
}
|
|
48
|
+
} catch { /* gh not available */ }
|
|
49
|
+
|
|
50
|
+
// Method 2: Env vars (includes .env, config.env, shell rc via resolveCredentialSync)
|
|
51
|
+
const envToken = resolveCredentialSync('GITHUB_TOKEN', homeDir) || resolveCredentialSync('GH_TOKEN', homeDir);
|
|
52
|
+
const envRepo = process.env.GITHUB_REPO || resolveCredentialSync('GITHUB_REPO', homeDir);
|
|
53
|
+
if (envToken && envRepo) {
|
|
54
|
+
return { token: envToken, repo: envRepo, method: 'env' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Token found but no repo
|
|
58
|
+
if (envToken) {
|
|
59
|
+
return { token: envToken, repo: null, method: 'env' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { token: null, repo: null, method: null };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve Jira credentials (env → .env → config.env → shell rc → 1Password).
|
|
67
|
+
*/
|
|
68
|
+
function resolveJiraAuth(homeDir) {
|
|
69
|
+
return {
|
|
70
|
+
host: resolveCredentialSync('JIRA_HOST', homeDir) || process.env.JIRA_HOST,
|
|
71
|
+
email: resolveCredentialSync('JIRA_USER', homeDir) || process.env.JIRA_USER,
|
|
72
|
+
token: resolveCredentialSync('JIRA_API_TOKEN', homeDir),
|
|
73
|
+
project: resolveCredentialSync('JIRA_PROJECT', homeDir) || process.env.JIRA_PROJECT,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve Confluence credentials (env → .env → config.env → shell rc → 1Password).
|
|
79
|
+
*/
|
|
80
|
+
function resolveConfluenceAuth(homeDir) {
|
|
81
|
+
return {
|
|
82
|
+
host: resolveCredentialSync('CONFLUENCE_HOST', homeDir) || process.env.CONFLUENCE_HOST,
|
|
83
|
+
email: resolveCredentialSync('CONFLUENCE_USER', homeDir) || process.env.CONFLUENCE_USER,
|
|
84
|
+
token: resolveCredentialSync('CONFLUENCE_API_TOKEN', homeDir),
|
|
85
|
+
space: resolveCredentialSync('CONFLUENCE_SPACE', homeDir) || process.env.CONFLUENCE_SPACE,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── GitHub Issues ────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a GitHub issue from an intake packet.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} packet - Intake packet with triage, excerpt
|
|
95
|
+
* @param {object} [opts]
|
|
96
|
+
* @param {string} [opts.repo] - owner/repo (default: GITHUB_REPO env)
|
|
97
|
+
* @param {string} [opts.token] - GitHub PAT (default: GITHUB_TOKEN env)
|
|
98
|
+
* @param {string} [opts.apiUrl] - GitHub API URL (default: api.github.com)
|
|
99
|
+
* @param {string} [opts.host] - GitHub host (default: github.com)
|
|
100
|
+
* @param {object} [opts.fetchImpl]
|
|
101
|
+
* @returns {Promise<{ ok: boolean, externalUrl: string|null, externalId: string|null, error?: string }>}
|
|
102
|
+
*/
|
|
103
|
+
export async function createGitHubIssue(packet, { repo, token, apiUrl, host, fetchImpl = globalThis.fetch } = {}) {
|
|
104
|
+
const hd = homedir();
|
|
105
|
+
const auth = resolveGitHubAuth(hd);
|
|
106
|
+
const resolvedRepo = repo || auth.repo || process.env.GITHUB_REPO;
|
|
107
|
+
const resolvedToken = token || (auth.method === 'env' ? auth.token : null);
|
|
108
|
+
const useGhCli = auth.method === 'gh';
|
|
109
|
+
const resolvedApi = apiUrl || process.env.GITHUB_API_URL || 'https://api.github.com';
|
|
110
|
+
|
|
111
|
+
if (!resolvedRepo) return { ok: false, externalUrl: null, externalId: null, error: 'No repo found. Set GITHUB_REPO or run `gh repo view` to detect.' };
|
|
112
|
+
if (!resolvedToken && !useGhCli) return { ok: false, externalUrl: null, externalId: null, error: 'No GitHub auth found. Try: gh auth login, or set GITHUB_TOKEN.' };
|
|
113
|
+
|
|
114
|
+
const t = packet?.triage || {};
|
|
115
|
+
const title = buildGitHubTitle(packet);
|
|
116
|
+
const body = buildGitHubBody(packet);
|
|
117
|
+
const labels = buildLabels(t);
|
|
118
|
+
|
|
119
|
+
// Method 1: gh CLI (no token needed, labels skipped to avoid missing-label errors)
|
|
120
|
+
if (useGhCli) {
|
|
121
|
+
try {
|
|
122
|
+
const result = spawnSync('gh', [
|
|
123
|
+
'issue', 'create',
|
|
124
|
+
'--repo', resolvedRepo,
|
|
125
|
+
'--title', title,
|
|
126
|
+
'--body', body,
|
|
127
|
+
], { encoding: 'utf8', timeout: 15000 });
|
|
128
|
+
|
|
129
|
+
if (result.status === 0) {
|
|
130
|
+
const url = result.stdout?.trim();
|
|
131
|
+
const number = url?.split('/').pop();
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
externalUrl: url,
|
|
135
|
+
externalId: number || url,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// gh failed — fall through to API method
|
|
139
|
+
const stderr = (result.stderr || '').slice(0, 200);
|
|
140
|
+
if (stderr) process.stderr.write(`[integrations] gh issue create failed: ${stderr}\n`);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
process.stderr.write(`[integrations] gh CLI error: ${err.message}\n`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Method 2: REST API with token
|
|
147
|
+
if (resolvedToken) {
|
|
148
|
+
try {
|
|
149
|
+
const res = await fetchImpl(`${resolvedApi}/repos/${resolvedRepo}/issues`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${resolvedToken}`,
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
Accept: 'application/vnd.github.v3+json',
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify({ title, body, labels }),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const detail = await res.text().catch(() => '');
|
|
161
|
+
return { ok: false, externalUrl: null, externalId: null, error: `GitHub API ${res.status}: ${detail.slice(0, 200)}` };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const json = await res.json();
|
|
165
|
+
return { ok: true, externalUrl: json.html_url, externalId: String(json.number) };
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return { ok: false, externalUrl: null, externalId: null, error: err.message };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { ok: false, externalUrl: null, externalId: null, error: 'All auth methods exhausted' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildGitHubTitle(packet) {
|
|
175
|
+
const t = packet?.triage || {};
|
|
176
|
+
const prefix = t.intakeType ? `[${t.intakeType}] ` : '';
|
|
177
|
+
const source = packet?.intake?.sourcePath?.split('/')?.pop() || 'intake signal';
|
|
178
|
+
const excerpt = packet?.excerpt?.slice(0, 80)?.replace(/\n/g, ' ')?.trim() || '';
|
|
179
|
+
return `${prefix}${excerpt || source}`.slice(0, 256);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildGitHubBody(packet) {
|
|
183
|
+
const lines = [];
|
|
184
|
+
lines.push('<!-- Auto-created from Construct intake -->');
|
|
185
|
+
lines.push('');
|
|
186
|
+
if (packet?.id) lines.push(`**Intake ID:** \`${packet.id}\``);
|
|
187
|
+
if (packet?.intake?.sourcePath) lines.push(`**Source:** \`${packet.intake.sourcePath}\``);
|
|
188
|
+
lines.push('');
|
|
189
|
+
|
|
190
|
+
const t = packet?.triage || {};
|
|
191
|
+
if (t.intakeType) lines.push(`**Type:** ${t.intakeType}`);
|
|
192
|
+
if (t.rdStage) lines.push(`**Stage:** ${t.rdStage}`);
|
|
193
|
+
if (t.primaryOwner) lines.push(`**Owner:** ${t.primaryOwner}`);
|
|
194
|
+
if (t.recommendedAction) lines.push(`**Recommended action:** ${t.recommendedAction}`);
|
|
195
|
+
if (t.risk) lines.push(`**Risk:** ${t.risk}`);
|
|
196
|
+
lines.push('');
|
|
197
|
+
|
|
198
|
+
if (packet?.excerpt) {
|
|
199
|
+
lines.push('## Signal content');
|
|
200
|
+
lines.push('');
|
|
201
|
+
lines.push(packet.excerpt.slice(0, 3000));
|
|
202
|
+
lines.push('');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (packet?.related?.length) {
|
|
206
|
+
lines.push('## Related artifacts');
|
|
207
|
+
for (const r of packet.related) {
|
|
208
|
+
lines.push(`- ${r.path || r.title}${r.score ? ` (score: ${r.score.toFixed(2)})` : ''}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildLabels(t) {
|
|
216
|
+
const labels = ['construct-intake'];
|
|
217
|
+
if (t.intakeType) labels.push(t.intakeType);
|
|
218
|
+
if (t.risk) labels.push(`risk-${t.risk}`);
|
|
219
|
+
return labels;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Jira ─────────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create a Jira ticket from an intake packet.
|
|
226
|
+
*
|
|
227
|
+
* @param {object} packet - Intake packet with triage, excerpt
|
|
228
|
+
* @param {object} [opts]
|
|
229
|
+
* @param {string} [opts.host] - Jira host (default: JIRA_HOST env)
|
|
230
|
+
* @param {string} [opts.email] - Jira user email (default: JIRA_USER env)
|
|
231
|
+
* @param {string} [opts.token] - Jira API token (default: JIRA_API_TOKEN env)
|
|
232
|
+
* @param {string} [opts.project] - Jira project key (default: JIRA_PROJECT env)
|
|
233
|
+
* @param {string} [opts.issueType] - Issue type (default: Task)
|
|
234
|
+
* @param {object} [opts.fetchImpl]
|
|
235
|
+
* @returns {Promise<{ ok: boolean, externalUrl: string|null, externalId: string|null, error?: string }>}
|
|
236
|
+
*/
|
|
237
|
+
export async function createJiraTicket(packet, { host, email, token, project, issueType = 'Task', fetchImpl = globalThis.fetch } = {}) {
|
|
238
|
+
const hd = homedir();
|
|
239
|
+
const auth = resolveJiraAuth(hd);
|
|
240
|
+
const resolvedHost = host || auth.host || process.env.JIRA_HOST;
|
|
241
|
+
const resolvedEmail = email || auth.email || process.env.JIRA_USER;
|
|
242
|
+
const resolvedToken = token || auth.token || process.env.JIRA_API_TOKEN;
|
|
243
|
+
const resolvedProject = project || auth.project || process.env.JIRA_PROJECT;
|
|
244
|
+
|
|
245
|
+
if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_HOST not set (e.g. https://your-domain.atlassian.net). Check ~/.construct/config.env or shell rc.' };
|
|
246
|
+
if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_USER and JIRA_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
|
|
247
|
+
if (!resolvedProject) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_PROJECT not set (e.g. PROJ)' };
|
|
248
|
+
|
|
249
|
+
const t = packet?.triage || {};
|
|
250
|
+
const jiraAuthHeader = Buffer.from(`${resolvedEmail}:${resolvedToken}`).toString('base64');
|
|
251
|
+
const summary = buildJiraSummary(packet);
|
|
252
|
+
const description = buildJiraDescription(packet);
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const res = await fetchImpl(`${resolvedHost.replace(/\/$/, '')}/rest/api/2/issue`, {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
headers: {
|
|
258
|
+
Authorization: `Basic ${jiraAuthHeader}`,
|
|
259
|
+
'Content-Type': 'application/json',
|
|
260
|
+
},
|
|
261
|
+
body: JSON.stringify({
|
|
262
|
+
fields: {
|
|
263
|
+
project: { key: resolvedProject },
|
|
264
|
+
summary: summary.slice(0, 255),
|
|
265
|
+
description,
|
|
266
|
+
issuetype: { name: issueType },
|
|
267
|
+
labels: buildLabels(t),
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (!res.ok) {
|
|
273
|
+
const detail = await res.text().catch(() => '');
|
|
274
|
+
return { ok: false, externalUrl: null, externalId: null, error: `Jira API ${res.status}: ${detail.slice(0, 200)}` };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const json = await res.json();
|
|
278
|
+
|
|
279
|
+
// We don't have the browse URL directly from the create response,
|
|
280
|
+
// so construct it from the host + issue key
|
|
281
|
+
const issueKey = json.key;
|
|
282
|
+
const browseUrl = `${resolvedHost.replace(/\/$/, '')}/browse/${issueKey}`;
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
externalUrl: browseUrl,
|
|
287
|
+
externalId: issueKey,
|
|
288
|
+
};
|
|
289
|
+
} catch (err) {
|
|
290
|
+
return { ok: false, externalUrl: null, externalId: null, error: err.message };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildJiraSummary(packet) {
|
|
295
|
+
const t = packet?.triage || {};
|
|
296
|
+
const prefix = t.intakeType ? `[${t.intakeType}] ` : '';
|
|
297
|
+
const excerpt = packet?.excerpt?.slice(0, 80)?.replace(/\n/g, ' ')?.trim() || packet?.id || 'Intake signal';
|
|
298
|
+
return `${prefix}${excerpt}`.slice(0, 255);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildJiraDescription(packet) {
|
|
302
|
+
const lines = [];
|
|
303
|
+
lines.push('h3. Auto-created from Construct intake');
|
|
304
|
+
lines.push('');
|
|
305
|
+
|
|
306
|
+
if (packet?.id) lines.push(`* Intake ID: ${packet.id}`);
|
|
307
|
+
if (packet?.intake?.sourcePath) lines.push(`* Source: ${packet.intake.sourcePath}`);
|
|
308
|
+
lines.push('');
|
|
309
|
+
|
|
310
|
+
const t = packet?.triage || {};
|
|
311
|
+
if (t.intakeType) lines.push(`* Type: ${t.intakeType}`);
|
|
312
|
+
if (t.rdStage) lines.push(`* Stage: ${t.rdStage}`);
|
|
313
|
+
if (t.primaryOwner) lines.push(`* Owner: ${t.primaryOwner}`);
|
|
314
|
+
if (t.recommendedAction) lines.push(`* Recommended action: ${t.recommendedAction}`);
|
|
315
|
+
if (t.rationale) lines.push(`* Rationale: ${t.rationale}`);
|
|
316
|
+
lines.push('');
|
|
317
|
+
|
|
318
|
+
if (packet?.excerpt) {
|
|
319
|
+
lines.push('h3. Signal content');
|
|
320
|
+
lines.push('{noformat}');
|
|
321
|
+
lines.push(packet.excerpt.slice(0, 3000));
|
|
322
|
+
lines.push('{noformat}');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (packet?.related?.length) {
|
|
326
|
+
lines.push('h3. Related artifacts');
|
|
327
|
+
for (const r of packet.related) {
|
|
328
|
+
lines.push(`* ${r.path || r.title}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return lines.join('\n');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── Confluence ───────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Publish a Construct artifact (PRD/ADR/RFC) as a Confluence page.
|
|
339
|
+
*
|
|
340
|
+
* @param {object} artifact - Artifact metadata + content
|
|
341
|
+
* @param {string} artifact.type - 'prd' | 'adr' | 'rfc'
|
|
342
|
+
* @param {number} artifact.number - Artifact sequence number
|
|
343
|
+
* @param {string} artifact.title - Artifact title
|
|
344
|
+
* @param {string} artifact.content - Full markdown content
|
|
345
|
+
* @param {string} [artifact.path] - File path for source ref
|
|
346
|
+
* @param {object} [opts]
|
|
347
|
+
* @param {string} [opts.host] - Confluence host (default: CONFLUENCE_HOST env)
|
|
348
|
+
* @param {string} [opts.email] - Confluence user (default: CONFLUENCE_USER env)
|
|
349
|
+
* @param {string} [opts.token] - Confluence API token (default: CONFLUENCE_API_TOKEN env)
|
|
350
|
+
* @param {string} [opts.space] - Confluence space key (default: CONFLUENCE_SPACE env)
|
|
351
|
+
* @param {string} [opts.parentId] - Optional parent page ID
|
|
352
|
+
* @param {object} [opts.fetchImpl]
|
|
353
|
+
* @returns {Promise<{ ok: boolean, externalUrl: string|null, externalId: string|null, error?: string }>}
|
|
354
|
+
*/
|
|
355
|
+
export async function publishArtifactToConfluence(artifact, { host, email, token, space, parentId, fetchImpl = globalThis.fetch } = {}) {
|
|
356
|
+
const hd = homedir();
|
|
357
|
+
const auth = resolveConfluenceAuth(hd);
|
|
358
|
+
const resolvedHost = host || auth.host || process.env.CONFLUENCE_HOST;
|
|
359
|
+
const resolvedEmail = email || auth.email || process.env.CONFLUENCE_USER;
|
|
360
|
+
const resolvedToken = token || auth.token || process.env.CONFLUENCE_API_TOKEN;
|
|
361
|
+
const resolvedSpace = space || auth.space || process.env.CONFLUENCE_SPACE;
|
|
362
|
+
|
|
363
|
+
if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_HOST not set (e.g. https://your-domain.atlassian.net/wiki). Check ~/.construct/config.env or shell rc.' };
|
|
364
|
+
if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_USER and CONFLUENCE_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
|
|
365
|
+
if (!resolvedSpace) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_SPACE not set (e.g. PROD)' };
|
|
366
|
+
|
|
367
|
+
const prefix = { prd: 'PRD', adr: 'ADR', rfc: 'RFC' }[artifact.type] || artifact.type.toUpperCase();
|
|
368
|
+
const pageTitle = `${prefix}-${String(artifact.number).padStart(4, '0')}: ${artifact.title}`;
|
|
369
|
+
|
|
370
|
+
// Convert basic markdown to Confluence storage format
|
|
371
|
+
const storage = markdownToConfluenceStorage(artifact.content);
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const res = await fetchImpl(`${resolvedHost.replace(/\/$/, '')}/rest/api/content`, {
|
|
375
|
+
method: 'POST',
|
|
376
|
+
headers: {
|
|
377
|
+
Authorization: `Basic ${Buffer.from(`${resolvedEmail}:${resolvedToken}`).toString('base64')}`,
|
|
378
|
+
'Content-Type': 'application/json',
|
|
379
|
+
},
|
|
380
|
+
body: JSON.stringify({
|
|
381
|
+
type: 'page',
|
|
382
|
+
title: pageTitle,
|
|
383
|
+
space: { key: resolvedSpace },
|
|
384
|
+
...(parentId ? { ancestors: [{ id: parentId }] } : {}),
|
|
385
|
+
body: {
|
|
386
|
+
storage: {
|
|
387
|
+
value: storage,
|
|
388
|
+
representation: 'storage',
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
metadata: {
|
|
392
|
+
labels: [{ prefix: 'construct', name: `artifact-${artifact.type}` }],
|
|
393
|
+
},
|
|
394
|
+
}),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (!res.ok) {
|
|
398
|
+
// Check if page already exists (409 = conflict)
|
|
399
|
+
if (res.status === 409) {
|
|
400
|
+
return await updateConfluencePage(artifact, { host: resolvedHost, email: resolvedEmail, token: resolvedToken, space: resolvedSpace, fetchImpl });
|
|
401
|
+
}
|
|
402
|
+
const detail = await res.text().catch(() => '');
|
|
403
|
+
return { ok: false, externalUrl: null, externalId: null, error: `Confluence API ${res.status}: ${detail.slice(0, 200)}` };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const json = await res.json();
|
|
407
|
+
return {
|
|
408
|
+
ok: true,
|
|
409
|
+
externalUrl: `${resolvedHost.replace(/\/$/, '')}/spaces/${resolvedSpace}/pages/${json.id}`,
|
|
410
|
+
externalId: json.id,
|
|
411
|
+
};
|
|
412
|
+
} catch (err) {
|
|
413
|
+
return { ok: false, externalUrl: null, externalId: null, error: err.message };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function updateConfluencePage(artifact, { host, email, token, space, fetchImpl }) {
|
|
418
|
+
const prefix = { prd: 'PRD', adr: 'ADR', rfc: 'RFC' }[artifact.type] || artifact.type.toUpperCase();
|
|
419
|
+
const pageTitle = `${prefix}-${String(artifact.number).padStart(4, '0')}: ${artifact.title}`;
|
|
420
|
+
const auth = Buffer.from(`${email}:${token}`).toString('base64');
|
|
421
|
+
const baseUrl = host.replace(/\/$/, '');
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
// Find existing page by title
|
|
425
|
+
const searchRes = await fetchImpl(`${baseUrl}/rest/api/content?title=${encodeURIComponent(pageTitle)}&spaceKey=${space}`, {
|
|
426
|
+
headers: { Authorization: `Basic ${auth}` },
|
|
427
|
+
});
|
|
428
|
+
if (!searchRes.ok) {
|
|
429
|
+
return { ok: false, externalUrl: null, externalId: null, error: `Confluence search failed: ${searchRes.status}` };
|
|
430
|
+
}
|
|
431
|
+
const searchJson = await searchRes.json();
|
|
432
|
+
const existing = searchJson.results?.[0];
|
|
433
|
+
if (!existing) {
|
|
434
|
+
return { ok: false, externalUrl: null, externalId: null, error: 'Page creation conflict but could not find existing page' };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Update the existing page
|
|
438
|
+
const storage = markdownToConfluenceStorage(artifact.content);
|
|
439
|
+
const updateRes = await fetchImpl(`${baseUrl}/rest/api/content/${existing.id}`, {
|
|
440
|
+
method: 'PUT',
|
|
441
|
+
headers: {
|
|
442
|
+
Authorization: `Basic ${auth}`,
|
|
443
|
+
'Content-Type': 'application/json',
|
|
444
|
+
},
|
|
445
|
+
body: JSON.stringify({
|
|
446
|
+
id: existing.id,
|
|
447
|
+
type: 'page',
|
|
448
|
+
title: pageTitle,
|
|
449
|
+
version: { number: existing.version.number + 1 },
|
|
450
|
+
body: {
|
|
451
|
+
storage: {
|
|
452
|
+
value: storage,
|
|
453
|
+
representation: 'storage',
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
}),
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (!updateRes.ok) {
|
|
460
|
+
const detail = await updateRes.text().catch(() => '');
|
|
461
|
+
return { ok: false, externalUrl: null, externalId: null, error: `Confluence update ${updateRes.status}: ${detail.slice(0, 200)}` };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const json = await updateRes.json();
|
|
465
|
+
return {
|
|
466
|
+
ok: true,
|
|
467
|
+
externalUrl: `${baseUrl}/spaces/${space}/pages/${json.id}`,
|
|
468
|
+
externalId: json.id,
|
|
469
|
+
updated: true,
|
|
470
|
+
};
|
|
471
|
+
} catch (err) {
|
|
472
|
+
return { ok: false, externalUrl: null, externalId: null, error: err.message };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Minimal markdown to Confluence storage format conversion.
|
|
478
|
+
* Handles the subset of markdown used by Construct artifacts.
|
|
479
|
+
*/
|
|
480
|
+
function markdownToConfluenceStorage(md) {
|
|
481
|
+
if (!md) return '';
|
|
482
|
+
|
|
483
|
+
let html = md
|
|
484
|
+
// Headers
|
|
485
|
+
.replace(/^###### (.+)$/gm, '<h6>$1</h6>')
|
|
486
|
+
.replace(/^##### (.+)$/gm, '<h5>$1</h5>')
|
|
487
|
+
.replace(/^#### (.+)$/gm, '<h4>$1</h4>')
|
|
488
|
+
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
489
|
+
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
490
|
+
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
491
|
+
// Bold and italic
|
|
492
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
493
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
494
|
+
// Inline code
|
|
495
|
+
.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
496
|
+
// Horizontal rule
|
|
497
|
+
.replace(/^---$/gm, '<hr/>')
|
|
498
|
+
// Tables (simplified)
|
|
499
|
+
.replace(/^\|(.+)\|$/gm, (match) => {
|
|
500
|
+
const cells = match.split('|').filter(c => c.trim()).map(c => `<td>${c.trim()}</td>`).join('');
|
|
501
|
+
// Check if this is a header row (next row has ---)
|
|
502
|
+
return `<tr>${cells}</tr>`;
|
|
503
|
+
})
|
|
504
|
+
// Unordered lists
|
|
505
|
+
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
|
506
|
+
// Paragraphs (double newlines)
|
|
507
|
+
.replace(/\n\n/g, '</p><p>')
|
|
508
|
+
// Line breaks within paragraphs
|
|
509
|
+
.replace(/\n/g, '<br/>');
|
|
510
|
+
|
|
511
|
+
// Wrap consecutive <li> in <ul>
|
|
512
|
+
html = html.replace(/(<li>.*?<\/li>)(\s*<li>.*?<\/li>)*/g, (match) => {
|
|
513
|
+
return `<ul>${match}</ul>`;
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Wrap tables
|
|
517
|
+
const tableRows = html.match(/(<tr>.*?<\/tr>\s*)+/g);
|
|
518
|
+
if (tableRows) {
|
|
519
|
+
for (const rows of tableRows) {
|
|
520
|
+
const headerClass = rows.includes('**') ? ' class="confluenceTable"' : '';
|
|
521
|
+
html = html.replace(rows, `<table${headerClass}>${rows}</table>`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Code blocks
|
|
526
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">$1</ac:parameter><ac:plain-text-body><![CDATA[$2]]></ac:plain-text-body></ac:structured-macro>');
|
|
527
|
+
|
|
528
|
+
// Front-matter (remove)
|
|
529
|
+
html = html.replace(/^---[\s\S]*?---\n*/, '');
|
|
530
|
+
|
|
531
|
+
return `<p>${html}</p>`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Update intake packet with external reference ─────────────────────────
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Write an external reference into an intake packet's queue entry.
|
|
538
|
+
* Updates the JSON file for filesystem-backed queues.
|
|
539
|
+
*
|
|
540
|
+
* @param {string} rootDir - Project root
|
|
541
|
+
* @param {string} intakeId - Intake packet ID
|
|
542
|
+
* @param {string} system - 'github' | 'jira' | 'confluence'
|
|
543
|
+
* @param {string} externalUrl - URL to the external item
|
|
544
|
+
* @param {string} externalId - External system's ID
|
|
545
|
+
*/
|
|
546
|
+
export function tagIntakeWithExternalRef(rootDir, intakeId, system, externalUrl, externalId) {
|
|
547
|
+
const pendingDir = join(rootDir, '.cx', 'intake', 'pending');
|
|
548
|
+
if (!existsSync(pendingDir)) return;
|
|
549
|
+
|
|
550
|
+
const files = readdirSync(pendingDir).filter(f => f.endsWith('.json'));
|
|
551
|
+
for (const file of files) {
|
|
552
|
+
if (!file.includes(intakeId)) continue;
|
|
553
|
+
try {
|
|
554
|
+
const filePath = join(pendingDir, file);
|
|
555
|
+
const entry = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
556
|
+
entry.externalRefs = entry.externalRefs || {};
|
|
557
|
+
entry.externalRefs[system] = { url: externalUrl, id: externalId, createdAt: new Date().toISOString() };
|
|
558
|
+
writeFileSync(filePath, JSON.stringify(entry, null, 2) + '\n');
|
|
559
|
+
} catch { /* non-fatal */ }
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Synchronous credential resolver for use in constructors and sync contexts.
|
|
565
|
+
* Mirrors the async resolveCredential above. Checks env → .env → config.env → shell rc.
|
|
566
|
+
*/
|
|
567
|
+
function resolveCredentialSync(varName, homeDir) {
|
|
568
|
+
if (process.env[varName]) return process.env[varName];
|
|
569
|
+
try {
|
|
570
|
+
const projectEnv = join(process.cwd(), '.env');
|
|
571
|
+
if (existsSync(projectEnv)) {
|
|
572
|
+
const content = readFileSync(projectEnv, 'utf8');
|
|
573
|
+
const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
|
|
574
|
+
if (m) return m[1].trim();
|
|
575
|
+
}
|
|
576
|
+
} catch { /* skip */ }
|
|
577
|
+
try {
|
|
578
|
+
const homeEnv = join(homeDir, '.env');
|
|
579
|
+
if (existsSync(homeEnv)) {
|
|
580
|
+
const content = readFileSync(homeEnv, 'utf8');
|
|
581
|
+
const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
|
|
582
|
+
if (m) return m[1].trim();
|
|
583
|
+
}
|
|
584
|
+
} catch { /* skip */ }
|
|
585
|
+
try {
|
|
586
|
+
const cfgPath = join(homeDir, '.construct', 'config.env');
|
|
587
|
+
if (existsSync(cfgPath)) {
|
|
588
|
+
const content = readFileSync(cfgPath, 'utf8');
|
|
589
|
+
const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
|
|
590
|
+
if (m) return m[1].trim();
|
|
591
|
+
}
|
|
592
|
+
} catch { /* skip */ }
|
|
593
|
+
try {
|
|
594
|
+
const shellFiles = [join(homeDir, '.zshrc'), join(homeDir, '.bashrc'), join(homeDir, '.bash_profile'), join(homeDir, '.profile')];
|
|
595
|
+
for (const rcPath of shellFiles) {
|
|
596
|
+
if (!existsSync(rcPath)) continue;
|
|
597
|
+
const content = readFileSync(rcPath, 'utf8');
|
|
598
|
+
const directRe = new RegExp(`export\\s+${varName}=["']?(.+?)["']?$`, 'm');
|
|
599
|
+
const directMatch = content.match(directRe);
|
|
600
|
+
if (directMatch) return directMatch[1].trim();
|
|
601
|
+
const opRe = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
|
|
602
|
+
const opMatch = content.match(opRe);
|
|
603
|
+
if (opMatch) {
|
|
604
|
+
try {
|
|
605
|
+
const result = spawnSync('op', ['read', opMatch[1]], { encoding: 'utf8', timeout: 5000 });
|
|
606
|
+
if (result.status === 0) return result.stdout.trim();
|
|
607
|
+
} catch { /* op read failed */ }
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
} catch { /* skip */ }
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ── Configuration detection ──────────────────────────────────────────────
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Check which integrations are configured.
|
|
618
|
+
* Scopes environment, .env files, ~/.construct/config.env, and shell rc
|
|
619
|
+
* files (including 1Password op:// refs).
|
|
620
|
+
*
|
|
621
|
+
* @param {object} [opts]
|
|
622
|
+
* @param {string} [opts.homeDir]
|
|
623
|
+
* @returns {{ github: boolean, jira: boolean, confluence: boolean, githubToken: boolean, jiraToken: boolean, confluenceToken: boolean }}
|
|
624
|
+
*/
|
|
625
|
+
export function detectIntegrationConfig({ homeDir } = {}) {
|
|
626
|
+
const hd = homeDir || homedir();
|
|
627
|
+
|
|
628
|
+
// GitHub — try gh CLI first, then env vars
|
|
629
|
+
const ghAuth = resolveGitHubAuth(hd);
|
|
630
|
+
|
|
631
|
+
// Jira
|
|
632
|
+
const jiraHost = resolveCredentialSync('JIRA_HOST', hd);
|
|
633
|
+
const jiraUser = resolveCredentialSync('JIRA_USER', hd);
|
|
634
|
+
const jiraToken = resolveCredentialSync('JIRA_API_TOKEN', hd);
|
|
635
|
+
const jiraProject = resolveCredentialSync('JIRA_PROJECT', hd);
|
|
636
|
+
|
|
637
|
+
// Confluence
|
|
638
|
+
const confHost = resolveCredentialSync('CONFLUENCE_HOST', hd);
|
|
639
|
+
const confUser = resolveCredentialSync('CONFLUENCE_USER', hd);
|
|
640
|
+
const confToken = resolveCredentialSync('CONFLUENCE_API_TOKEN', hd);
|
|
641
|
+
const confSpace = resolveCredentialSync('CONFLUENCE_SPACE', hd);
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
github: ghAuth.method === 'gh' || Boolean(ghAuth.token && ghAuth.repo),
|
|
645
|
+
githubMethod: ghAuth.method || null,
|
|
646
|
+
githubRepo: ghAuth.repo || null,
|
|
647
|
+
jira: Boolean(jiraHost && jiraUser && jiraToken && jiraProject),
|
|
648
|
+
jiraHost: jiraHost || null,
|
|
649
|
+
confluence: Boolean(confHost && confUser && confToken && confSpace),
|
|
650
|
+
confluenceSpace: confSpace || null,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Resolve a credential from env, .env files, config.env, or shell rc files.
|
|
656
|
+
* Mirrors the approach in lib/health-check.mjs.
|
|
657
|
+
*/
|
|
658
|
+
function resolveCredential(varName, homeDir) {
|
|
659
|
+
if (process.env[varName]) return process.env[varName];
|
|
660
|
+
|
|
661
|
+
// Project .env
|
|
662
|
+
try {
|
|
663
|
+
const projectEnv = join(process.cwd(), '.env');
|
|
664
|
+
if (existsSync(projectEnv)) {
|
|
665
|
+
const content = readFileSync(projectEnv, 'utf8');
|
|
666
|
+
const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
|
|
667
|
+
if (m) return m[1].trim();
|
|
668
|
+
}
|
|
669
|
+
} catch { /* skip */ }
|
|
670
|
+
|
|
671
|
+
// ~/.env
|
|
672
|
+
try {
|
|
673
|
+
const homeEnv = join(homeDir, '.env');
|
|
674
|
+
if (existsSync(homeEnv)) {
|
|
675
|
+
const content = readFileSync(homeEnv, 'utf8');
|
|
676
|
+
const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
|
|
677
|
+
if (m) return m[1].trim();
|
|
678
|
+
}
|
|
679
|
+
} catch { /* skip */ }
|
|
680
|
+
|
|
681
|
+
// ~/.construct/config.env
|
|
682
|
+
try {
|
|
683
|
+
const cfgPath = join(homeDir, '.construct', 'config.env');
|
|
684
|
+
if (existsSync(cfgPath)) {
|
|
685
|
+
const content = readFileSync(cfgPath, 'utf8');
|
|
686
|
+
const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
|
|
687
|
+
if (m) return m[1].trim();
|
|
688
|
+
}
|
|
689
|
+
} catch { /* skip */ }
|
|
690
|
+
|
|
691
|
+
// Shell rc files (including 1Password op:// refs and command substitutions)
|
|
692
|
+
try {
|
|
693
|
+
const shellFiles = [join(homeDir, '.zshrc'), join(homeDir, '.bashrc'), join(homeDir, '.bash_profile'), join(homeDir, '.profile')];
|
|
694
|
+
for (const rcPath of shellFiles) {
|
|
695
|
+
if (!existsSync(rcPath)) continue;
|
|
696
|
+
try {
|
|
697
|
+
const content = readFileSync(rcPath, 'utf8');
|
|
698
|
+
// Direct export (simple values only, not command substitutions)
|
|
699
|
+
const directRe = new RegExp(`^\\s*export\\s+${varName}="?([^"'\\$\\(]+)"?$`, 'm');
|
|
700
|
+
const directMatch = content.match(directRe);
|
|
701
|
+
if (directMatch) return directMatch[1].trim();
|
|
702
|
+
|
|
703
|
+
// op:// ref — try 1Password CLI
|
|
704
|
+
const opRe = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
|
|
705
|
+
const opMatch = content.match(opRe);
|
|
706
|
+
if (opMatch) {
|
|
707
|
+
try {
|
|
708
|
+
const result = spawnSync('op', ['read', opMatch[1]], { encoding: 'utf8', timeout: 5000 });
|
|
709
|
+
if (result.status === 0) return result.stdout.trim();
|
|
710
|
+
// Log op read failures — often the field name in the rc file
|
|
711
|
+
// doesn't match the actual 1Password item field name.
|
|
712
|
+
const errMsg = (result.stderr || '').trim().slice(0, 200);
|
|
713
|
+
if (errMsg && process.env.CONSTRUCT_DEBUG_CREDENTIALS === '1') {
|
|
714
|
+
process.stderr.write(`[credentials] op read failed for ${varName}: ${errMsg}\n`);
|
|
715
|
+
}
|
|
716
|
+
} catch { /* op read failed */ }
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// If the value uses command substitution ($(...)), try evaluating via
|
|
720
|
+
// a shell that sources the rc file. This catches any $(...) pattern
|
|
721
|
+
// including op read with different field names or other secret managers.
|
|
722
|
+
const cmdSubRe = new RegExp(`^export\\s+${varName}="?\\$\\((.+)\\)"?`, 'm');
|
|
723
|
+
const cmdMatch = content.match(cmdSubRe);
|
|
724
|
+
if (cmdMatch) {
|
|
725
|
+
try {
|
|
726
|
+
const shellResult = spawnSync('zsh', ['-c', `source "${rcPath}" 2>/dev/null; echo "\${${varName}}"`], {
|
|
727
|
+
encoding: 'utf8', timeout: 5000,
|
|
728
|
+
});
|
|
729
|
+
if (shellResult.status === 0 && shellResult.stdout?.trim()) {
|
|
730
|
+
const val = shellResult.stdout.trim().split('\n').pop()?.trim();
|
|
731
|
+
if (val && !val.includes('ERROR') && !val.includes('could not read')) return val;
|
|
732
|
+
}
|
|
733
|
+
} catch { /* shell eval failed */ }
|
|
734
|
+
}
|
|
735
|
+
} catch { /* skip unreadable */ }
|
|
736
|
+
}
|
|
737
|
+
} catch { /* skip */ }
|
|
738
|
+
|
|
739
|
+
return null;
|
|
740
|
+
}
|