@geraldmaron/construct 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -7
- 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 +1116 -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 +525 -38
- 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,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/session-store.mjs — Durable session index for construct.
|
|
3
|
+
*
|
|
4
|
+
* Stores *distilled* session metadata — not raw transcripts. Each session
|
|
5
|
+
* captures only what is needed to resume effectively:
|
|
6
|
+
* - summary (2-3 sentences of what happened)
|
|
7
|
+
* - decisions made
|
|
8
|
+
* - files touched with reason
|
|
9
|
+
* - open questions / blockers
|
|
10
|
+
* - task snapshot (IDs + status, not full descriptions)
|
|
11
|
+
*
|
|
12
|
+
* Storage layout:
|
|
13
|
+
* .cx/sessions/index.json — lightweight array for fast listing
|
|
14
|
+
* .cx/sessions/<id>.json — distilled session record
|
|
15
|
+
*/
|
|
16
|
+
import crypto from "node:crypto";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { cosineSimilarity, rankByBm25 } from "./storage/embeddings.mjs";
|
|
20
|
+
import { embedSync as embedText } from "./storage/embeddings-legacy.mjs";
|
|
21
|
+
import { withFileLockSync } from "./storage/file-lock.mjs";
|
|
22
|
+
|
|
23
|
+
const SESSIONS_DIR = ".cx/sessions";
|
|
24
|
+
const INDEX_FILE = "index.json";
|
|
25
|
+
const VECTORS_FILE = "vectors.json";
|
|
26
|
+
const MAX_INDEX_ENTRIES = 200;
|
|
27
|
+
const MAX_SUMMARY_LENGTH = 500;
|
|
28
|
+
const MAX_DECISIONS = 20;
|
|
29
|
+
const MAX_FILES = 50;
|
|
30
|
+
const MAX_OPEN_QUESTIONS = 10;
|
|
31
|
+
|
|
32
|
+
function ensureDir(dir) {
|
|
33
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sessionsDir(rootDir) {
|
|
37
|
+
return path.join(rootDir, SESSIONS_DIR);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function indexPath(rootDir) {
|
|
41
|
+
return path.join(sessionsDir(rootDir), INDEX_FILE);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function vectorsPath(rootDir) {
|
|
45
|
+
return path.join(sessionsDir(rootDir), VECTORS_FILE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readVectors(rootDir) {
|
|
49
|
+
const p = vectorsPath(rootDir);
|
|
50
|
+
if (!fs.existsSync(p)) return [];
|
|
51
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return []; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeVectors(rootDir, records) {
|
|
55
|
+
ensureDir(sessionsDir(rootDir));
|
|
56
|
+
const target = vectorsPath(rootDir);
|
|
57
|
+
withFileLockSync(target, () => {
|
|
58
|
+
fs.writeFileSync(target, JSON.stringify(records.slice(0, MAX_INDEX_ENTRIES), null, 2) + "\n");
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildSessionSearchText(entry) {
|
|
63
|
+
return [entry.summary, entry.project, entry.platform].filter(Boolean).join(" ");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function upsertSessionVector(rootDir, id, entry) {
|
|
67
|
+
const text = buildSessionSearchText(entry);
|
|
68
|
+
if (!text) return;
|
|
69
|
+
const embedding = embedText(text);
|
|
70
|
+
const vectors = readVectors(rootDir);
|
|
71
|
+
const idx = vectors.findIndex((v) => v.id === id);
|
|
72
|
+
const record = { id, embedding, project: entry.project || null, status: entry.status || null };
|
|
73
|
+
if (idx >= 0) vectors[idx] = record;
|
|
74
|
+
else vectors.unshift(record);
|
|
75
|
+
writeVectors(rootDir, vectors);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readIndex(rootDir) {
|
|
79
|
+
const p = indexPath(rootDir);
|
|
80
|
+
if (!fs.existsSync(p)) return [];
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function writeIndex(rootDir, entries) {
|
|
89
|
+
ensureDir(sessionsDir(rootDir));
|
|
90
|
+
const trimmed = entries.slice(0, MAX_INDEX_ENTRIES);
|
|
91
|
+
const target = indexPath(rootDir);
|
|
92
|
+
withFileLockSync(target, () => {
|
|
93
|
+
fs.writeFileSync(target, JSON.stringify(trimmed, null, 2) + "\n");
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generate a short session ID: timestamp prefix + random suffix.
|
|
99
|
+
*/
|
|
100
|
+
function generateId() {
|
|
101
|
+
const ts = Date.now().toString(36);
|
|
102
|
+
const rand = crypto.randomBytes(4).toString("hex");
|
|
103
|
+
return `${ts}-${rand}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Clamp a string to a max length, adding ellipsis if truncated.
|
|
108
|
+
*/
|
|
109
|
+
function clamp(str, max) {
|
|
110
|
+
if (!str || str.length <= max) return str || null;
|
|
111
|
+
return str.slice(0, max - 1) + "\u2026";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create a new session record and add it to the index.
|
|
116
|
+
* @param {string} rootDir — project root (where .cx/ lives).
|
|
117
|
+
* @param {object} opts — session metadata.
|
|
118
|
+
* @returns {object} — the created session record.
|
|
119
|
+
*/
|
|
120
|
+
export function createSession(rootDir, {
|
|
121
|
+
project = path.basename(rootDir),
|
|
122
|
+
platform = "unknown",
|
|
123
|
+
} = {}) {
|
|
124
|
+
const id = generateId();
|
|
125
|
+
const now = new Date().toISOString();
|
|
126
|
+
const record = {
|
|
127
|
+
id,
|
|
128
|
+
project,
|
|
129
|
+
platform,
|
|
130
|
+
startedAt: now,
|
|
131
|
+
lastActive: now,
|
|
132
|
+
status: "active",
|
|
133
|
+
summary: null,
|
|
134
|
+
decisions: [],
|
|
135
|
+
filesChanged: [],
|
|
136
|
+
openQuestions: [],
|
|
137
|
+
taskSnapshot: [],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
ensureDir(sessionsDir(rootDir));
|
|
141
|
+
fs.writeFileSync(
|
|
142
|
+
path.join(sessionsDir(rootDir), `${id}.json`),
|
|
143
|
+
JSON.stringify(record, null, 2) + "\n"
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const index = readIndex(rootDir);
|
|
147
|
+
const indexEntry = {
|
|
148
|
+
id,
|
|
149
|
+
project,
|
|
150
|
+
platform,
|
|
151
|
+
startedAt: now,
|
|
152
|
+
lastActive: now,
|
|
153
|
+
status: "active",
|
|
154
|
+
summary: null,
|
|
155
|
+
};
|
|
156
|
+
index.unshift(indexEntry);
|
|
157
|
+
writeIndex(rootDir, index);
|
|
158
|
+
upsertSessionVector(rootDir, id, indexEntry);
|
|
159
|
+
|
|
160
|
+
return record;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Update an existing session with distilled data.
|
|
165
|
+
*
|
|
166
|
+
* Accepts structured updates:
|
|
167
|
+
* summary — string (clamped to MAX_SUMMARY_LENGTH)
|
|
168
|
+
* decisions — array of strings (capped at MAX_DECISIONS)
|
|
169
|
+
* filesChanged — array of {path, reason} (capped at MAX_FILES)
|
|
170
|
+
* openQuestions — array of strings (capped at MAX_OPEN_QUESTIONS)
|
|
171
|
+
* taskSnapshot — array of {id, subject, status}
|
|
172
|
+
* status — "active" | "completed" | "closed"
|
|
173
|
+
*/
|
|
174
|
+
export function updateSession(rootDir, id, updates = {}) {
|
|
175
|
+
const filePath = path.join(sessionsDir(rootDir), `${id}.json`);
|
|
176
|
+
if (!fs.existsSync(filePath)) return null;
|
|
177
|
+
|
|
178
|
+
const record = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
179
|
+
const now = new Date().toISOString();
|
|
180
|
+
|
|
181
|
+
// Apply distilled fields with caps.
|
|
182
|
+
if (updates.summary != null) {
|
|
183
|
+
record.summary = clamp(String(updates.summary), MAX_SUMMARY_LENGTH);
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(updates.decisions)) {
|
|
186
|
+
record.decisions = updates.decisions.slice(0, MAX_DECISIONS);
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(updates.filesChanged)) {
|
|
189
|
+
record.filesChanged = updates.filesChanged.slice(0, MAX_FILES);
|
|
190
|
+
}
|
|
191
|
+
if (Array.isArray(updates.openQuestions)) {
|
|
192
|
+
record.openQuestions = updates.openQuestions.slice(0, MAX_OPEN_QUESTIONS);
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(updates.taskSnapshot)) {
|
|
195
|
+
record.taskSnapshot = updates.taskSnapshot;
|
|
196
|
+
}
|
|
197
|
+
if (updates.status) {
|
|
198
|
+
record.status = updates.status;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
record.lastActive = now;
|
|
202
|
+
fs.writeFileSync(filePath, JSON.stringify(record, null, 2) + "\n");
|
|
203
|
+
|
|
204
|
+
// Update index entry (lightweight fields only).
|
|
205
|
+
const index = readIndex(rootDir);
|
|
206
|
+
const entry = index.find((e) => e.id === id);
|
|
207
|
+
if (entry) {
|
|
208
|
+
entry.lastActive = now;
|
|
209
|
+
if (updates.status) entry.status = updates.status;
|
|
210
|
+
if (updates.summary != null) entry.summary = clamp(String(updates.summary), MAX_SUMMARY_LENGTH);
|
|
211
|
+
writeIndex(rootDir, index);
|
|
212
|
+
upsertSessionVector(rootDir, id, entry);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return record;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Load a full session record by ID.
|
|
220
|
+
*/
|
|
221
|
+
export function loadSession(rootDir, id) {
|
|
222
|
+
const filePath = path.join(sessionsDir(rootDir), `${id}.json`);
|
|
223
|
+
if (!fs.existsSync(filePath)) return null;
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* List sessions from the index.
|
|
233
|
+
* @param {string} rootDir
|
|
234
|
+
* @param {object} opts — filters: status, limit, project.
|
|
235
|
+
*/
|
|
236
|
+
export function listSessions(rootDir, { status = null, limit = 20, project = null } = {}) {
|
|
237
|
+
let entries = readIndex(rootDir);
|
|
238
|
+
if (status) entries = entries.filter((e) => e.status === status);
|
|
239
|
+
if (project) entries = entries.filter((e) => e.project === project);
|
|
240
|
+
return entries.slice(0, limit);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Search sessions using a hybrid cosine + BM25 scorer.
|
|
245
|
+
* Falls back to substring matching when no embeddings exist.
|
|
246
|
+
*/
|
|
247
|
+
export function searchSessions(rootDir, query) {
|
|
248
|
+
if (!query) return [];
|
|
249
|
+
const lower = String(query).toLowerCase();
|
|
250
|
+
|
|
251
|
+
const vectors = readVectors(rootDir);
|
|
252
|
+
if (vectors.length === 0) {
|
|
253
|
+
// No vectors yet — plain substring fallback
|
|
254
|
+
return readIndex(rootDir).filter((e) =>
|
|
255
|
+
(e.summary && e.summary.toLowerCase().includes(lower)) ||
|
|
256
|
+
(e.project && e.project.toLowerCase().includes(lower))
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const queryEmbedding = embedText(String(query));
|
|
261
|
+
const cosineScored = vectors
|
|
262
|
+
.map((v) => ({ id: v.id, score: cosineSimilarity(queryEmbedding, v.embedding || []) }))
|
|
263
|
+
.filter(({ score }) => score > 0.05);
|
|
264
|
+
|
|
265
|
+
const index = readIndex(rootDir);
|
|
266
|
+
const candidateIds = new Set(cosineScored.map((v) => v.id));
|
|
267
|
+
// Also include substring matches as BM25 candidates
|
|
268
|
+
for (const entry of index) {
|
|
269
|
+
if ((entry.summary && entry.summary.toLowerCase().includes(lower)) ||
|
|
270
|
+
(entry.project && entry.project.toLowerCase().includes(lower))) {
|
|
271
|
+
candidateIds.add(entry.id);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const candidates = [...candidateIds]
|
|
276
|
+
.map((id) => index.find((e) => e.id === id))
|
|
277
|
+
.filter(Boolean)
|
|
278
|
+
.map((e) => ({ ...e, text: buildSessionSearchText(e) }));
|
|
279
|
+
|
|
280
|
+
const bm25Scored = rankByBm25(candidates, query, { limit: 20 });
|
|
281
|
+
|
|
282
|
+
const scoreMap = new Map();
|
|
283
|
+
for (const { id, score } of cosineScored) scoreMap.set(id, score);
|
|
284
|
+
for (const item of bm25Scored) {
|
|
285
|
+
const prev = scoreMap.get(item.id) || 0;
|
|
286
|
+
const bm25Max = bm25Scored[0]?.score || 1;
|
|
287
|
+
scoreMap.set(item.id, Math.max(prev, Math.min(item.score / bm25Max, 1)));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return [...scoreMap.entries()]
|
|
291
|
+
.sort((a, b) => b[1] - a[1])
|
|
292
|
+
.map(([id]) => index.find((e) => e.id === id))
|
|
293
|
+
.filter(Boolean);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get the most recent active or completed session for a project.
|
|
298
|
+
* Returns the full distilled record for resume context.
|
|
299
|
+
*/
|
|
300
|
+
export function lastSession(rootDir, project = null) {
|
|
301
|
+
const entries = readIndex(rootDir);
|
|
302
|
+
const match = project
|
|
303
|
+
? entries.find((e) => e.project === project)
|
|
304
|
+
: entries[0];
|
|
305
|
+
if (!match) return null;
|
|
306
|
+
return loadSession(rootDir, match.id);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Build a compact resume prompt from a session record.
|
|
311
|
+
* Returns a string suitable for injection into a session-start hook.
|
|
312
|
+
*/
|
|
313
|
+
export function buildResumeContext(session) {
|
|
314
|
+
if (!session) return "";
|
|
315
|
+
const parts = [];
|
|
316
|
+
|
|
317
|
+
if (session.summary) {
|
|
318
|
+
parts.push(`## What was in progress\n${session.summary}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (session.decisions?.length) {
|
|
322
|
+
parts.push(`## Key decisions\n${session.decisions.map((d) => `- ${d}`).join("\n")}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (session.filesChanged?.length) {
|
|
326
|
+
const fileList = session.filesChanged
|
|
327
|
+
.slice(0, 15)
|
|
328
|
+
.map((f) => `- \`${f.path}\` — ${f.reason || "modified"}`)
|
|
329
|
+
.join("\n");
|
|
330
|
+
const extra = session.filesChanged.length > 15
|
|
331
|
+
? `\n- ...and ${session.filesChanged.length - 15} more`
|
|
332
|
+
: "";
|
|
333
|
+
parts.push(`## Files changed\n${fileList}${extra}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (session.openQuestions?.length) {
|
|
337
|
+
parts.push(`## Open questions\n${session.openQuestions.map((q) => `- ${q}`).join("\n")}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (session.taskSnapshot?.length) {
|
|
341
|
+
const tasks = session.taskSnapshot
|
|
342
|
+
.map((t) => `- [${t.status}] ${t.subject}`)
|
|
343
|
+
.join("\n");
|
|
344
|
+
parts.push(`## Task state\n${tasks}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return parts.join("\n\n");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Close all active sessions (called on construct down).
|
|
352
|
+
*/
|
|
353
|
+
export function closeAllSessions(rootDir) {
|
|
354
|
+
const index = readIndex(rootDir);
|
|
355
|
+
let closed = 0;
|
|
356
|
+
for (const entry of index) {
|
|
357
|
+
if (entry.status === "active") {
|
|
358
|
+
entry.status = "closed";
|
|
359
|
+
entry.lastActive = new Date().toISOString();
|
|
360
|
+
const filePath = path.join(sessionsDir(rootDir), `${entry.id}.json`);
|
|
361
|
+
if (fs.existsSync(filePath)) {
|
|
362
|
+
try {
|
|
363
|
+
const record = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
364
|
+
record.status = "closed";
|
|
365
|
+
record.lastActive = entry.lastActive;
|
|
366
|
+
fs.writeFileSync(filePath, JSON.stringify(record, null, 2) + "\n");
|
|
367
|
+
} catch { /* best effort */ }
|
|
368
|
+
}
|
|
369
|
+
closed++;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (closed > 0) writeIndex(rootDir, index);
|
|
373
|
+
return closed;
|
|
374
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/setup-prompts.mjs — yes/no consent helper for `construct init`.
|
|
3
|
+
*
|
|
4
|
+
* Centralises the "should we install this service?" decision so Postgres,
|
|
5
|
+
* telemetry, and any future service share the same prompt + consent-caching
|
|
6
|
+
* behavior. Pattern matches Supabase CLI / Hasura / Husky: interactive
|
|
7
|
+
* default-yes prompt, persisted answer so re-runs are silent.
|
|
8
|
+
*
|
|
9
|
+
* Decision tree:
|
|
10
|
+
* 1. alreadyConfigured → false (user has an external service, leave alone)
|
|
11
|
+
* 2. consent cached → return cached value silently
|
|
12
|
+
* 3. isYes flag → true, cache as yes
|
|
13
|
+
* 4. non-TTY → false (CI / scripts — caller can use --yes to opt in)
|
|
14
|
+
* 5. interactive → prompt [Y/n], cache the answer
|
|
15
|
+
*
|
|
16
|
+
* Consent is stored as BOOTSTRAP_<NAME>=yes|no in ~/.construct/config.env
|
|
17
|
+
* so subsequent runs of `construct init` don't re-prompt for the same
|
|
18
|
+
* services. Pass `force = true` to skip the cache and re-prompt.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import readline from 'node:readline';
|
|
22
|
+
|
|
23
|
+
import { parseEnvFile, writeEnvValues } from './env-config.mjs';
|
|
24
|
+
|
|
25
|
+
export async function consentToInstall({
|
|
26
|
+
name,
|
|
27
|
+
isYes = false,
|
|
28
|
+
force = false,
|
|
29
|
+
alreadyConfigured = false,
|
|
30
|
+
alreadyConfiguredNote = '',
|
|
31
|
+
envPath,
|
|
32
|
+
defaultYes = true,
|
|
33
|
+
readlineModule = readline,
|
|
34
|
+
stdin = process.stdin,
|
|
35
|
+
stdout = process.stdout,
|
|
36
|
+
} = {}) {
|
|
37
|
+
if (!name) throw new Error('consentToInstall: name is required');
|
|
38
|
+
if (!envPath) throw new Error('consentToInstall: envPath is required');
|
|
39
|
+
|
|
40
|
+
if (alreadyConfigured) {
|
|
41
|
+
return { decision: false, source: 'pre-configured', note: alreadyConfiguredNote || `${name} already configured` };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const consentKey = consentKeyFor(name);
|
|
45
|
+
const env = parseEnvFile(envPath);
|
|
46
|
+
const cached = env[consentKey];
|
|
47
|
+
|
|
48
|
+
if (!force && cached) {
|
|
49
|
+
if (cached === 'yes' || cached === 'true' || cached === '1') {
|
|
50
|
+
return { decision: true, source: 'cached', note: `using cached BOOTSTRAP_${name.toUpperCase()}=yes` };
|
|
51
|
+
}
|
|
52
|
+
if (cached === 'no' || cached === 'false' || cached === '0' || cached === 'never') {
|
|
53
|
+
return { decision: false, source: 'cached', note: `using cached BOOTSTRAP_${name.toUpperCase()}=${cached}` };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isYes) {
|
|
58
|
+
writeEnvValues(envPath, { [consentKey]: 'yes' });
|
|
59
|
+
return { decision: true, source: 'flag', note: '--yes flag accepted defaults' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!stdin.isTTY) {
|
|
63
|
+
return { decision: false, source: 'non-tty', note: 'non-interactive shell — re-run with --yes to opt into local services' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const answer = await promptYesNo({
|
|
67
|
+
question: `Install local ${name} via Docker?`,
|
|
68
|
+
defaultYes,
|
|
69
|
+
readlineModule,
|
|
70
|
+
stdin,
|
|
71
|
+
stdout,
|
|
72
|
+
});
|
|
73
|
+
writeEnvValues(envPath, { [consentKey]: answer ? 'yes' : 'no' });
|
|
74
|
+
return { decision: answer, source: 'prompt', note: answer ? 'user accepted' : 'user declined' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function consentKeyFor(name) {
|
|
78
|
+
return `BOOTSTRAP_${String(name).toUpperCase().replace(/[^A-Z0-9]+/g, '_')}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function promptYesNo({ question, defaultYes, readlineModule, stdin, stdout }) {
|
|
82
|
+
const suffix = defaultYes ? '[Y/n]' : '[y/N]';
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const rl = readlineModule.createInterface({ input: stdin, output: stdout });
|
|
85
|
+
rl.question(`${question} ${suffix} `, (raw) => {
|
|
86
|
+
rl.close();
|
|
87
|
+
const trimmed = String(raw || '').trim().toLowerCase();
|
|
88
|
+
if (!trimmed) return resolve(defaultYes);
|
|
89
|
+
if (['y', 'yes'].includes(trimmed)) return resolve(true);
|
|
90
|
+
if (['n', 'no'].includes(trimmed)) return resolve(false);
|
|
91
|
+
resolve(defaultYes);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const _consentKeyFor = consentKeyFor;
|