@geraldmaron/construct 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -18
- package/bin/construct +392 -91
- package/bin/construct-postinstall.mjs +30 -2
- package/examples/distribution/README.md +42 -0
- package/examples/distribution/manifest.json +53 -0
- package/examples/distribution/sources/adr.md +84 -0
- package/examples/distribution/sources/deck-one-pager.md +65 -0
- package/examples/distribution/sources/prd-platform.md +161 -0
- package/examples/distribution/sources/research-brief.md +80 -0
- package/examples/distribution/sources/rfc-platform.md +82 -0
- package/examples/distribution/sources/runbook.md +103 -0
- package/examples/distribution/sources/strategy.md +88 -0
- package/lib/adapters-sync.mjs +67 -0
- package/lib/artifact-gate-notice.mjs +38 -0
- package/lib/artifact-manifest.mjs +112 -0
- package/lib/artifact-release-gate.mjs +171 -0
- package/lib/artifact-reviewers.mjs +68 -0
- package/lib/artifact-type-from-path.mjs +79 -0
- package/lib/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +77 -10
- package/lib/auto-docs.mjs +87 -18
- package/lib/beads/drift.mjs +27 -6
- package/lib/beads-client.mjs +49 -121
- package/lib/beads-optimistic.mjs +5 -12
- package/lib/brand-fonts.mjs +93 -0
- package/lib/brand-prose.mjs +214 -0
- package/lib/brand-tokens.mjs +92 -0
- package/lib/bridges/copilot-proxy.mjs +13 -26
- package/lib/capability-ledger.mjs +156 -0
- package/lib/certification/artifact-fixtures.mjs +132 -0
- package/lib/certification/artifact-gates.mjs +63 -0
- package/lib/certification/artifact-provenance.mjs +97 -0
- package/lib/certification/canonical-scenarios.mjs +78 -0
- package/lib/certification/cli.mjs +191 -0
- package/lib/certification/dashboard-api.mjs +71 -0
- package/lib/certification/demo-parity.mjs +116 -0
- package/lib/certification/document-io-fixtures.mjs +246 -0
- package/lib/certification/document-workflow.mjs +97 -0
- package/lib/certification/eval-bridge.mjs +77 -0
- package/lib/certification/model-routing.mjs +149 -0
- package/lib/certification/prompt-budget.mjs +119 -0
- package/lib/certification/rc-gate.mjs +206 -0
- package/lib/certification/real-llm-scenarios.mjs +303 -0
- package/lib/certification/role-cards.mjs +113 -0
- package/lib/certification/role-overlays.mjs +117 -0
- package/lib/certification/run.mjs +122 -0
- package/lib/certification/runner.mjs +323 -0
- package/lib/certification/scenarios.mjs +51 -0
- package/lib/certification/skill-inventory.mjs +289 -0
- package/lib/certification/skill-scenarios.mjs +147 -0
- package/lib/certification/specialist-contracts.mjs +85 -0
- package/lib/certification/specialist-scenarios.mjs +175 -0
- package/lib/certification/stale-impact.mjs +146 -0
- package/lib/certification/status.mjs +252 -0
- package/lib/certification/store.mjs +77 -0
- package/lib/chat/cli.mjs +333 -0
- package/lib/chat/command-suggest.mjs +161 -0
- package/lib/chat/commands.mjs +215 -0
- package/lib/chat/config.mjs +142 -0
- package/lib/chat/context-compactor.mjs +250 -0
- package/lib/chat/context-continuation.mjs +253 -0
- package/lib/chat/continuation-source.mjs +58 -0
- package/lib/chat/demo-guide.mjs +61 -0
- package/lib/chat/design-tokens.mjs +91 -0
- package/lib/chat/desktop-binary.mjs +79 -0
- package/lib/chat/desktop-build.mjs +130 -0
- package/lib/chat/desktop-launcher.mjs +133 -0
- package/lib/chat/evidence.mjs +145 -0
- package/lib/chat/export.mjs +74 -0
- package/lib/chat/harness/driver.mjs +91 -0
- package/lib/chat/list-picker.mjs +112 -0
- package/lib/chat/model-picker.mjs +356 -0
- package/lib/chat/openrouter-fallback.mjs +151 -0
- package/lib/chat/permission-prompt.mjs +33 -0
- package/lib/chat/picker-catalog.mjs +45 -0
- package/lib/chat/policy-telemetry.mjs +34 -0
- package/lib/chat/present.mjs +245 -0
- package/lib/chat/session-context.mjs +39 -0
- package/lib/chat/session-persist.mjs +73 -0
- package/lib/chat/session-restore.mjs +71 -0
- package/lib/chat/session-settings.mjs +53 -0
- package/lib/chat/system-prompt.mjs +52 -0
- package/lib/chat/transparency.mjs +93 -0
- package/lib/chat/tui/color-scheme.mjs +42 -0
- package/lib/chat/tui/markdown.mjs +123 -0
- package/lib/chat/tui/presentation.mjs +100 -0
- package/lib/chat/tui/render.mjs +500 -0
- package/lib/chat/tui/turn-block.mjs +284 -0
- package/lib/chat/tui/turn-present.mjs +18 -0
- package/lib/chat/tui/turn-state.mjs +88 -0
- package/lib/chat/tui/usage.mjs +122 -0
- package/lib/chat/web-commands.mjs +146 -0
- package/lib/chat/web-launcher.mjs +63 -0
- package/lib/chat/web-picker-keys.mjs +46 -0
- package/lib/chat/web-session.mjs +159 -0
- package/lib/cli-commands.mjs +232 -11
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/schema.mjs +36 -5
- package/lib/contract-schemas/decision.json +50 -0
- package/lib/contract-schemas/implementation.json +51 -0
- package/lib/contract-schemas/review-report.json +32 -0
- package/lib/contract-schemas/test-report.json +43 -0
- package/lib/contracts/construct-handoff.mjs +60 -0
- package/lib/contracts/validate.mjs +32 -3
- package/lib/contracts/violation-log.mjs +58 -5
- package/lib/dashboard-demo.mjs +71 -0
- package/lib/dashboard-static.mjs +19 -5
- package/lib/decisions/registry.mjs +5 -3
- package/lib/deck-export-pptx.mjs +1152 -0
- package/lib/demo-recording.mjs +142 -0
- package/lib/demo-script.mjs +114 -0
- package/lib/demo-surface.mjs +249 -0
- package/lib/demo.mjs +703 -0
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +302 -0
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +4 -2
- package/lib/doctor/project-adapters.mjs +44 -0
- package/lib/doctor/watchers/cx-budget.mjs +98 -0
- package/lib/doctor/watchers/graph-staleness.mjs +52 -0
- package/lib/doctor/watchers/process-pressure.mjs +4 -3
- package/lib/doctor/watchers/service-health.mjs +1 -1
- package/lib/document-export.mjs +360 -36
- package/lib/document-extract.mjs +88 -0
- package/lib/document-ingest.mjs +32 -18
- package/lib/embed/cli.mjs +2 -2
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/semantic.mjs +5 -2
- package/lib/embed/worker.mjs +2 -2
- package/lib/embedded-contract/model-resolve.mjs +10 -9
- package/lib/env-config.mjs +26 -0
- package/lib/evals/dataset.mjs +137 -0
- package/lib/evals/gates.mjs +175 -0
- package/lib/graph/build-co-change.mjs +54 -0
- package/lib/graph/build-from-registry.mjs +136 -0
- package/lib/graph/build-import-graph.mjs +153 -0
- package/lib/graph/cli.mjs +110 -0
- package/lib/graph/impact-cli.mjs +89 -0
- package/lib/graph/impact.mjs +108 -0
- package/lib/graph/staleness.mjs +44 -0
- package/lib/graph/store.mjs +172 -0
- package/lib/handoffs/cleanup.mjs +1 -1
- package/lib/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- package/lib/hooks/audit-trail.mjs +14 -28
- package/lib/hooks/brand-prose-lint.mjs +38 -0
- package/lib/hooks/graph-impact-advisory.mjs +62 -0
- package/lib/hooks/session-optimize.mjs +84 -207
- package/lib/hooks/session-start.mjs +4 -5
- package/lib/host-disposition.mjs +7 -0
- package/lib/improvement/cli.mjs +189 -0
- package/lib/improvement/controller.mjs +137 -0
- package/lib/improvement/proposal.mjs +120 -0
- package/lib/improvement/specialist-loop.mjs +192 -0
- package/lib/improvement/store.mjs +89 -0
- package/lib/improvement/surface.mjs +219 -0
- package/lib/ingest-tooling.mjs +97 -0
- package/lib/init/doc-lanes.mjs +165 -0
- package/lib/init-docs.mjs +31 -178
- package/lib/init-unified.mjs +27 -97
- package/lib/init-update-guide.mjs +102 -0
- package/lib/init-update.mjs +32 -1
- package/lib/init.mjs +8 -0
- package/lib/install/desktop-binary-download.mjs +85 -0
- package/lib/install/legacy-global-cleanup.mjs +189 -0
- package/lib/intake/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- package/lib/intake/queue.mjs +2 -1
- package/lib/intake/session-prelude.mjs +90 -1
- package/lib/libreoffice-export.mjs +97 -0
- package/lib/logging/rotate.mjs +9 -0
- package/lib/maintenance/docker-reclaim.mjs +206 -0
- package/lib/mcp/external-schema-cost.mjs +74 -0
- package/lib/mcp/server.mjs +84 -9
- package/lib/mcp/stdio-mcp-probe.mjs +188 -0
- package/lib/mcp/tool-budget.mjs +55 -4
- package/lib/mcp/tools/document.mjs +18 -4
- package/lib/mcp/tools/skills.mjs +32 -3
- package/lib/mcp/tools/workflow.mjs +13 -1
- package/lib/model-free-selector.mjs +18 -0
- package/lib/model-registry.mjs +13 -229
- package/lib/model-router.mjs +425 -93
- package/lib/models/behavior-matrix.mjs +289 -0
- package/lib/models/catalog.mjs +209 -0
- package/lib/models/execution-capability-profile.mjs +196 -0
- package/lib/models/execution-policy.mjs +307 -0
- package/lib/models/provider-poll.mjs +383 -0
- package/lib/npm-spawn-env.mjs +17 -0
- package/lib/ollama/installed-models.mjs +129 -0
- package/lib/oracle/actions.mjs +187 -0
- package/lib/oracle/artifact-gate.mjs +99 -0
- package/lib/oracle/cli.mjs +204 -0
- package/lib/oracle/daemon-entry.mjs +14 -0
- package/lib/oracle/dispatch.mjs +81 -0
- package/lib/oracle/execute.mjs +143 -0
- package/lib/oracle/gaps.mjs +76 -0
- package/lib/oracle/index.mjs +84 -0
- package/lib/oracle/issues.mjs +164 -0
- package/lib/oracle/org-graph.mjs +170 -0
- package/lib/oracle/policy.mjs +80 -0
- package/lib/oracle/read-model.mjs +398 -0
- package/lib/oracle/reconcile.mjs +191 -0
- package/lib/oracle/routing.mjs +89 -0
- package/lib/oracle/synthesize.mjs +384 -0
- package/lib/oracle/verdicts.mjs +51 -0
- package/lib/orchestration/worker.mjs +88 -27
- package/lib/orchestration-policy.mjs +50 -0
- package/lib/parity.mjs +96 -2
- package/lib/persona-sections.mjs +66 -0
- package/lib/playwright-demo.mjs +252 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +9 -4
- package/lib/prompt-validation-contract.mjs +24 -0
- package/lib/provider-capabilities.js +57 -11
- package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
- package/lib/providers/contract/adapters/git/index.mjs +115 -0
- package/lib/providers/contract/adapters/github/index.mjs +166 -0
- package/lib/providers/contract/adapters/jira/index.mjs +187 -0
- package/lib/providers/contract/adapters/slack/index.mjs +175 -0
- package/lib/providers/contract/contract-tests.mjs +57 -0
- package/lib/providers/contract/errors.mjs +48 -0
- package/lib/providers/contract/interface.mjs +50 -0
- package/lib/providers/contract/registry.mjs +102 -0
- package/lib/providers/copilot-auth.mjs +297 -0
- package/lib/providers/credential-bootstrap.mjs +178 -0
- package/lib/providers/credential-catalog.mjs +46 -0
- package/lib/providers/credential-sources.mjs +63 -0
- package/lib/providers/creds.mjs +5 -2
- package/lib/providers/op-run.mjs +59 -0
- package/lib/providers/secret-resolver.mjs +159 -0
- package/lib/publish-template.mjs +163 -0
- package/lib/publish-tooling.mjs +119 -0
- package/lib/publish.mjs +305 -0
- package/lib/registry/cli.mjs +82 -0
- package/lib/registry/consolidation.mjs +147 -0
- package/lib/registry/generate-docs.mjs +75 -0
- package/lib/registry/skill-verification.mjs +57 -0
- package/lib/registry/surface-map.mjs +76 -0
- package/lib/registry/validate.mjs +135 -0
- package/lib/resources/budget.mjs +82 -0
- package/lib/resources/process-budget.mjs +45 -0
- package/lib/rules-delivery.mjs +9 -2
- package/lib/rules-read.mjs +26 -0
- package/lib/runtime-env.mjs +23 -0
- package/lib/runtime-pressure.mjs +51 -7
- package/lib/schema-infer.mjs +13 -2
- package/lib/server/chat-loop.mjs +622 -0
- package/lib/server/demo-preview.mjs +63 -0
- package/lib/server/index.mjs +229 -8
- package/lib/server/langfuse-login.mjs +3 -3
- package/lib/service-manager.mjs +61 -9
- package/lib/session-store.mjs +1 -1
- package/lib/setup.mjs +102 -2
- package/lib/specialists/prompt-schema.mjs +19 -10
- package/lib/specialists/roster.mjs +43 -0
- package/lib/specialists/scaffold.mjs +56 -0
- package/lib/storage/backend.mjs +6 -6
- package/lib/storage/embeddings-local.mjs +6 -3
- package/lib/storage/file-lock.mjs +18 -11
- package/lib/storage/hybrid-query.mjs +7 -4
- package/lib/storage/state-source.mjs +5 -5
- package/lib/storage/sync.mjs +2 -3
- package/lib/telemetry/rule-calls.mjs +52 -0
- package/lib/template-registry.mjs +2 -2
- package/lib/templates/visual-requirements.mjs +27 -51
- package/lib/test-corpus-inventory.mjs +313 -0
- package/lib/uninstall/uninstall.mjs +19 -7
- package/lib/update.mjs +12 -0
- package/lib/upgrade.mjs +14 -0
- package/lib/wireframe.mjs +20 -14
- package/lib/worker/run.mjs +17 -4
- package/lib/worker/trace.mjs +11 -3
- package/package.json +29 -13
- package/personas/construct.md +13 -13
- package/platforms/claude/settings.template.json +36 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/release-gates.md +4 -3
- package/scripts/sync-specialists.mjs +187 -60
- package/skills/devops/data-engineering.md +1 -1
- package/skills/docs/adr-workflow.md +1 -0
- package/skills/docs/backlog-proposal-workflow.md +1 -0
- package/skills/docs/codebase-research-workflow.md +40 -0
- package/skills/docs/customer-profile-workflow.md +1 -0
- package/skills/docs/document-ingest-workflow.md +1 -0
- package/skills/docs/evidence-ingest-workflow.md +1 -0
- package/skills/docs/init-docs.md +2 -2
- package/skills/docs/init-project.md +8 -3
- package/skills/docs/prd-workflow.md +24 -1
- package/skills/docs/prfaq-workflow.md +1 -0
- package/skills/docs/product-intelligence-workflow.md +1 -0
- package/skills/docs/product-signal-workflow.md +1 -0
- package/skills/docs/research-workflow.md +54 -37
- package/skills/docs/runbook-workflow.md +1 -0
- package/skills/docs/strategy-workflow.md +1 -0
- package/skills/docs/user-research-workflow.md +40 -0
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/architect.md +5 -0
- package/skills/roles/operator.docs.md +4 -0
- package/skills/routing.md +4 -2
- package/specialists/artifact-manifest.json +480 -0
- package/specialists/artifact-manifest.schema.json +53 -0
- package/specialists/audit-enrichments.json +454 -0
- package/specialists/contracts.json +37 -25
- package/specialists/prompts/_shared/validation-contract.md +26 -0
- package/specialists/prompts/cx-accessibility.md +21 -2
- package/specialists/prompts/cx-ai-engineer.md +24 -1
- package/specialists/prompts/cx-architect.md +4 -1
- package/specialists/prompts/cx-business-strategist.md +23 -2
- package/specialists/prompts/cx-data-analyst.md +22 -1
- package/specialists/prompts/cx-data-engineer.md +23 -2
- package/specialists/prompts/cx-debugger.md +21 -2
- package/specialists/prompts/cx-designer.md +26 -3
- package/specialists/prompts/cx-devil-advocate.md +19 -2
- package/specialists/prompts/cx-docs-keeper.md +28 -1
- package/specialists/prompts/cx-engineer.md +22 -1
- package/specialists/prompts/cx-evaluator.md +18 -1
- package/specialists/prompts/cx-explorer.md +21 -2
- package/specialists/prompts/cx-legal-compliance.md +21 -2
- package/specialists/prompts/cx-operations.md +21 -2
- package/specialists/prompts/cx-oracle.md +94 -0
- package/specialists/prompts/cx-orchestrator.md +20 -1
- package/specialists/prompts/cx-platform-engineer.md +25 -2
- package/specialists/prompts/cx-product-manager.md +32 -1
- package/specialists/prompts/cx-qa.md +24 -2
- package/specialists/prompts/cx-rd-lead.md +23 -2
- package/specialists/prompts/cx-release-manager.md +23 -2
- package/specialists/prompts/cx-researcher.md +22 -2
- package/specialists/prompts/cx-reviewer.md +18 -1
- package/specialists/prompts/cx-security.md +22 -2
- package/specialists/prompts/cx-sre.md +30 -3
- package/specialists/prompts/cx-test-automation.md +7 -1
- package/specialists/prompts/cx-trace-reviewer.md +22 -3
- package/specialists/prompts/cx-ux-researcher.md +21 -2
- package/specialists/registry.json +52 -173
- package/specialists/tone-profiles.json +42 -0
- package/templates/demos/playwright/demo-recording.config.mjs +47 -0
- package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
- package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
- package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
- package/templates/demos/tapes/resource-guard-rails.tape +49 -0
- package/templates/demos/vhs/construct-cockpit.json +24 -0
- package/templates/distribution/construct-brand.typ +446 -0
- package/templates/distribution/construct-decision.typ +38 -0
- package/templates/distribution/construct-deck.html +95 -0
- package/templates/distribution/construct-pdf.typ +38 -0
- package/templates/distribution/construct-prd.typ +38 -0
- package/templates/distribution/construct-research.typ +38 -0
- package/templates/distribution/construct-web.html +92 -0
- package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
- package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
- package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
- package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/README.md +36 -0
- package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
- package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
- package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
- package/templates/distribution/icons/activity.svg +15 -0
- package/templates/distribution/icons/alert-triangle.svg +17 -0
- package/templates/distribution/icons/book-open.svg +16 -0
- package/templates/distribution/icons/bot.svg +20 -0
- package/templates/distribution/icons/brain.svg +22 -0
- package/templates/distribution/icons/circle-check.svg +16 -0
- package/templates/distribution/icons/clipboard-check.svg +17 -0
- package/templates/distribution/icons/cpu.svg +28 -0
- package/templates/distribution/icons/database.svg +17 -0
- package/templates/distribution/icons/eye.svg +16 -0
- package/templates/distribution/icons/file-text.svg +19 -0
- package/templates/distribution/icons/gauge.svg +16 -0
- package/templates/distribution/icons/git-branch.svg +17 -0
- package/templates/distribution/icons/key.svg +17 -0
- package/templates/distribution/icons/layers.svg +17 -0
- package/templates/distribution/icons/list-checks.svg +19 -0
- package/templates/distribution/icons/lock.svg +16 -0
- package/templates/distribution/icons/message-square.svg +15 -0
- package/templates/distribution/icons/network.svg +19 -0
- package/templates/distribution/icons/route.svg +17 -0
- package/templates/distribution/icons/scale.svg +19 -0
- package/templates/distribution/icons/search.svg +16 -0
- package/templates/distribution/icons/send.svg +16 -0
- package/templates/distribution/icons/server.svg +18 -0
- package/templates/distribution/icons/shield-check.svg +16 -0
- package/templates/distribution/icons/users.svg +18 -0
- package/templates/distribution/icons/webhook.svg +17 -0
- package/templates/distribution/icons/workflow.svg +17 -0
- package/templates/distribution/icons/wrench.svg +15 -0
- package/templates/distribution/run.mjs +18 -1
- package/templates/docs/adr.md +7 -0
- package/templates/docs/construct_guide.md +10 -10
- package/templates/docs/customer-profile.md +4 -0
- package/templates/docs/one-pager.md +4 -0
- package/templates/docs/postmortem.md +31 -0
- package/templates/docs/prd-platform.md +19 -0
- package/templates/docs/prd.md +15 -0
- package/templates/docs/prfaq.md +7 -0
- package/templates/docs/qa-strategy.md +103 -0
- package/templates/docs/research-brief.md +8 -0
- package/templates/docs/rfc-platform.md +12 -0
- package/templates/docs/test-plan.md +7 -0
- package/lib/ingest/chunker.mjs +0 -94
- package/lib/ingest/pipeline.mjs +0 -53
- package/lib/ingest/store.mjs +0 -82
- package/lib/mode-commands.mjs +0 -122
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/profiles/validate-custom.mjs +0 -114
- package/lib/services/telemetry-backend.mjs +0 -177
- package/lib/storage/fusion.mjs +0 -95
package/lib/demo.mjs
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/demo.mjs — `construct demo` terminal recordings via VHS `.tape` files.
|
|
3
|
+
*
|
|
4
|
+
* Shipped tapes live in templates/demos/tapes/; project overrides in
|
|
5
|
+
* .cx/demos/tapes/. Built-in scaffolds via `construct demo init --from=`.
|
|
6
|
+
* Default run mode launches construct chat with a demo script; use `record` or
|
|
7
|
+
* --surface=tape for VHS. Dashboard demos delegate to lib/dashboard-demo.mjs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { join, resolve, dirname, basename, relative } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { listDemoScripts } from './demo-script.mjs';
|
|
15
|
+
import { DEMO_SURFACES } from './demo-surface.mjs';
|
|
16
|
+
import { listDemoRecordings, loadDemoRecording } from './demo-recording.mjs';
|
|
17
|
+
import { recordPlaywrightDemo } from './playwright-demo.mjs';
|
|
18
|
+
|
|
19
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
|
|
21
|
+
const FORMATS = ['gif', 'mp4', 'webm'];
|
|
22
|
+
|
|
23
|
+
const TAPE_TEMPLATES = {
|
|
24
|
+
quickstart: (out) => `Require node
|
|
25
|
+
Require vhs
|
|
26
|
+
# construct demo: quickstart — plan then build
|
|
27
|
+
Output ${out}
|
|
28
|
+
Set FontSize 18
|
|
29
|
+
Set Width 1100
|
|
30
|
+
Set Height 600
|
|
31
|
+
Set Theme "Dracula"
|
|
32
|
+
Set TypingSpeed 60ms
|
|
33
|
+
|
|
34
|
+
Type "node bin/construct plan 'add a rate limiter to the API'"
|
|
35
|
+
Sleep 500ms
|
|
36
|
+
Enter
|
|
37
|
+
Sleep 2s
|
|
38
|
+
|
|
39
|
+
Type "node bin/construct build"
|
|
40
|
+
Sleep 500ms
|
|
41
|
+
Enter
|
|
42
|
+
Sleep 2s
|
|
43
|
+
|
|
44
|
+
Type "node bin/construct ship status"
|
|
45
|
+
Sleep 500ms
|
|
46
|
+
Enter
|
|
47
|
+
Sleep 2s
|
|
48
|
+
`,
|
|
49
|
+
diagram: (out) => `Require node
|
|
50
|
+
Require vhs
|
|
51
|
+
# construct demo: diagram — render an architecture diagram
|
|
52
|
+
Output ${out}
|
|
53
|
+
Set FontSize 18
|
|
54
|
+
Set Width 1100
|
|
55
|
+
Set Height 600
|
|
56
|
+
Set Theme "Dracula"
|
|
57
|
+
Set TypingSpeed 60ms
|
|
58
|
+
|
|
59
|
+
Type "node bin/construct diagram 'web app: client -> api -> db'"
|
|
60
|
+
Sleep 500ms
|
|
61
|
+
Enter
|
|
62
|
+
Sleep 2s
|
|
63
|
+
`,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const TAPE_TEMPLATE_NAMES = Object.keys(TAPE_TEMPLATES);
|
|
67
|
+
|
|
68
|
+
export const RECORDING_INIT_TEMPLATES = ['quickstart', 'diagram', 'nextjs-static', 'external-url', 'construct-cockpit'];
|
|
69
|
+
|
|
70
|
+
function recordingsDir(cwd) {
|
|
71
|
+
return join(cwd, '.cx', 'demos', 'recordings');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function specsDir(cwd) {
|
|
75
|
+
return join(cwd, '.cx', 'demos', 'specs');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function projectRecordingPath(cwd, name) {
|
|
79
|
+
return join(recordingsDir(cwd), `${name}.json`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function copyScaffoldFile(src, dest) {
|
|
83
|
+
if (!existsSync(src)) return { ok: false, message: `Missing scaffold: ${src}` };
|
|
84
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
85
|
+
writeFileSync(dest, readFileSync(src, 'utf8'), 'utf8');
|
|
86
|
+
return { ok: true, dest };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function initNextjsStaticRecording(name, { cwd, port = 3456 } = {}) {
|
|
90
|
+
const recDir = recordingsDir(cwd);
|
|
91
|
+
const specDir = specsDir(cwd);
|
|
92
|
+
const recPath = projectRecordingPath(cwd, name);
|
|
93
|
+
if (existsSync(recPath)) return { ok: false, message: `Recording already exists: ${recPath}` };
|
|
94
|
+
|
|
95
|
+
mkdirSync(recDir, { recursive: true });
|
|
96
|
+
mkdirSync(join(specDir, '_helpers'), { recursive: true });
|
|
97
|
+
|
|
98
|
+
const helperSrc = join(REPO_ROOT, 'templates', 'demos', 'specs', '_helpers', 'scroll-artifact.ts');
|
|
99
|
+
copyScaffoldFile(helperSrc, join(specDir, '_helpers', 'scroll-artifact.ts'));
|
|
100
|
+
|
|
101
|
+
const specRel = `.cx/demos/specs/${name}.spec.ts`;
|
|
102
|
+
const specPath = join(cwd, specRel);
|
|
103
|
+
writeFileSync(specPath, `/**
|
|
104
|
+
* ${name}.spec.ts — static site demo recording (scaffold).
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
import { test, expect } from '@playwright/test';
|
|
108
|
+
import { scrollPage } from './_helpers/scroll-artifact.ts';
|
|
109
|
+
|
|
110
|
+
test('${name} — home tour', async ({ page }) => {
|
|
111
|
+
test.setTimeout(120_000);
|
|
112
|
+
await page.goto('/');
|
|
113
|
+
await expect(page.locator('body')).toBeVisible({ timeout: 30_000 });
|
|
114
|
+
await scrollPage(page);
|
|
115
|
+
});
|
|
116
|
+
`, 'utf8');
|
|
117
|
+
|
|
118
|
+
const configDest = join(cwd, '.cx', 'demos', 'playwright.config.mjs');
|
|
119
|
+
if (!existsSync(configDest)) {
|
|
120
|
+
copyScaffoldFile(
|
|
121
|
+
join(REPO_ROOT, 'templates', 'demos', 'playwright', 'demo-recording.config.mjs'),
|
|
122
|
+
configDest,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const recording = {
|
|
127
|
+
name,
|
|
128
|
+
title: `${name} site tour`,
|
|
129
|
+
engine: 'playwright',
|
|
130
|
+
workspace: '.',
|
|
131
|
+
spec: specRel,
|
|
132
|
+
playwrightConfig: '.cx/demos/playwright.config.mjs',
|
|
133
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
134
|
+
project: 'demo-recording',
|
|
135
|
+
webServer: {
|
|
136
|
+
command: `npx serve out -l ${port}`,
|
|
137
|
+
url: `http://127.0.0.1:${port}`,
|
|
138
|
+
cwd: '.',
|
|
139
|
+
timeout: 120_000,
|
|
140
|
+
},
|
|
141
|
+
artifactReveal: {
|
|
142
|
+
mode: 'sameOrigin',
|
|
143
|
+
staticDir: 'out',
|
|
144
|
+
file: 'index.html',
|
|
145
|
+
scroll: true,
|
|
146
|
+
},
|
|
147
|
+
output: { format: 'mp4', path: `.cx/demos/${name}.mp4` },
|
|
148
|
+
};
|
|
149
|
+
writeFileSync(recPath, `${JSON.stringify(recording, null, 2)}\n`, 'utf8');
|
|
150
|
+
return { ok: true, recordingPath: recPath, specPath, message: `Scaffolded Playwright recording ${recPath}` };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function initExternalUrlRecording(name, { cwd, baseUrl = 'https://example.com' } = {}) {
|
|
154
|
+
const recDir = recordingsDir(cwd);
|
|
155
|
+
const specDir = specsDir(cwd);
|
|
156
|
+
const recPath = projectRecordingPath(cwd, name);
|
|
157
|
+
if (existsSync(recPath)) return { ok: false, message: `Recording already exists: ${recPath}` };
|
|
158
|
+
|
|
159
|
+
mkdirSync(recDir, { recursive: true });
|
|
160
|
+
mkdirSync(specDir, { recursive: true });
|
|
161
|
+
|
|
162
|
+
const specRel = `.cx/demos/specs/${name}.spec.ts`;
|
|
163
|
+
writeFileSync(join(cwd, specRel), `/**
|
|
164
|
+
* ${name}.spec.ts — external URL demo recording (scaffold).
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
import { test, expect } from '@playwright/test';
|
|
168
|
+
import { scrollPage } from './_helpers/scroll-artifact.ts';
|
|
169
|
+
|
|
170
|
+
test('${name} — external site tour', async ({ page }) => {
|
|
171
|
+
test.setTimeout(120_000);
|
|
172
|
+
await page.goto('/');
|
|
173
|
+
await expect(page.locator('body')).toBeVisible({ timeout: 30_000 });
|
|
174
|
+
await scrollPage(page, { steps: 6 });
|
|
175
|
+
});
|
|
176
|
+
`, 'utf8');
|
|
177
|
+
|
|
178
|
+
mkdirSync(join(specDir, '_helpers'), { recursive: true });
|
|
179
|
+
copyScaffoldFile(
|
|
180
|
+
join(REPO_ROOT, 'templates', 'demos', 'specs', '_helpers', 'scroll-artifact.ts'),
|
|
181
|
+
join(specDir, '_helpers', 'scroll-artifact.ts'),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const configDest = join(cwd, '.cx', 'demos', 'playwright.config.mjs');
|
|
185
|
+
if (!existsSync(configDest)) {
|
|
186
|
+
copyScaffoldFile(
|
|
187
|
+
join(REPO_ROOT, 'templates', 'demos', 'playwright', 'demo-recording.config.mjs'),
|
|
188
|
+
configDest,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const recording = {
|
|
193
|
+
name,
|
|
194
|
+
title: `${name} external tour`,
|
|
195
|
+
engine: 'playwright',
|
|
196
|
+
workspace: '.',
|
|
197
|
+
spec: specRel,
|
|
198
|
+
playwrightConfig: '.cx/demos/playwright.config.mjs',
|
|
199
|
+
baseUrl,
|
|
200
|
+
skipWebServer: true,
|
|
201
|
+
project: 'demo-recording',
|
|
202
|
+
output: { format: 'mp4', path: `.cx/demos/${name}.mp4` },
|
|
203
|
+
};
|
|
204
|
+
writeFileSync(recPath, `${JSON.stringify(recording, null, 2)}\n`, 'utf8');
|
|
205
|
+
return { ok: true, recordingPath: recPath, message: `Scaffolded external URL recording ${recPath}` };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function initConstructCockpitRecording(name, { cwd, repoRoot = REPO_ROOT } = {}) {
|
|
209
|
+
const recPath = projectRecordingPath(cwd, name);
|
|
210
|
+
if (existsSync(recPath)) return { ok: false, message: `Recording already exists: ${recPath}` };
|
|
211
|
+
|
|
212
|
+
const reference = loadDemoRecording('agentic-platforms-prd', { cwd: repoRoot, repoRoot });
|
|
213
|
+
if (!reference) {
|
|
214
|
+
return { ok: false, message: 'Shipped agentic-platforms-prd recording not found in templates/demos/recordings/' };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
mkdirSync(recordingsDir(cwd), { recursive: true });
|
|
218
|
+
const recording = {
|
|
219
|
+
...reference,
|
|
220
|
+
name,
|
|
221
|
+
title: reference.title,
|
|
222
|
+
output: { format: 'mp4', path: `.cx/demos/dashboard/${name}.mp4` },
|
|
223
|
+
_reference: 'agentic-platforms-prd',
|
|
224
|
+
};
|
|
225
|
+
delete recording.sourcePath;
|
|
226
|
+
delete recording._legacy;
|
|
227
|
+
writeFileSync(recPath, `${JSON.stringify(recording, null, 2)}\n`, 'utf8');
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
recordingPath: recPath,
|
|
231
|
+
message: `Scaffolded Construct cockpit recording ${recPath} (references apps/dashboard spec)`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function initProjectRecording(name, { cwd, from = 'nextjs-static', repoRoot = REPO_ROOT } = {}) {
|
|
236
|
+
if (from === 'nextjs-static') return initNextjsStaticRecording(name, { cwd });
|
|
237
|
+
if (from === 'external-url') return initExternalUrlRecording(name, { cwd });
|
|
238
|
+
if (from === 'construct-cockpit') return initConstructCockpitRecording(name, { cwd, repoRoot });
|
|
239
|
+
if (TAPE_TEMPLATES[from]) return initProjectTape(name, { cwd, from });
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
message: `Unknown template: ${from}. Valid: ${RECORDING_INIT_TEMPLATES.join(', ')}`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function timestamp() {
|
|
247
|
+
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function which(bin) {
|
|
251
|
+
const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], { encoding: 'utf8' });
|
|
252
|
+
if (result.status !== 0) return null;
|
|
253
|
+
return result.stdout.trim().split('\n')[0] || null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function locateRecorder() {
|
|
257
|
+
const vhs = which('vhs');
|
|
258
|
+
if (vhs) return { engine: 'vhs', binary: vhs };
|
|
259
|
+
const asciinema = which('asciinema');
|
|
260
|
+
if (asciinema) return { engine: 'asciinema', binary: asciinema };
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function installHint() {
|
|
265
|
+
if (process.platform === 'darwin') return 'brew install vhs (or: brew install asciinema)';
|
|
266
|
+
if (process.platform === 'linux') return 'See https://github.com/charmbracelet/vhs#installation (or: pip install asciinema)';
|
|
267
|
+
return 'See https://github.com/charmbracelet/vhs#installation';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function tapesDir(cwd) {
|
|
271
|
+
return join(cwd, '.cx', 'demos', 'tapes');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function tapeSearchDirs({ cwd = process.cwd(), repoRoot = REPO_ROOT } = {}) {
|
|
275
|
+
return [
|
|
276
|
+
tapesDir(cwd),
|
|
277
|
+
join(repoRoot, 'templates', 'demos', 'tapes'),
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function resolveTapeFile(name, { cwd = process.cwd(), repoRoot = REPO_ROOT } = {}) {
|
|
282
|
+
for (const dir of tapeSearchDirs({ cwd, repoRoot })) {
|
|
283
|
+
const candidate = join(dir, `${name}.tape`);
|
|
284
|
+
if (existsSync(candidate)) return candidate;
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function projectTapePath(cwd, name) {
|
|
290
|
+
return join(tapesDir(cwd), `${name}.tape`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function listProjectTapes({ cwd = process.cwd(), repoRoot = REPO_ROOT } = {}) {
|
|
294
|
+
const seen = new Set();
|
|
295
|
+
const out = [];
|
|
296
|
+
for (const dir of tapeSearchDirs({ cwd, repoRoot })) {
|
|
297
|
+
if (!existsSync(dir)) continue;
|
|
298
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith('.tape')).sort()) {
|
|
299
|
+
const name = basename(file, '.tape');
|
|
300
|
+
if (seen.has(name)) continue;
|
|
301
|
+
seen.add(name);
|
|
302
|
+
out.push(name);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return out.sort();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function resolveTapeSource(name, { cwd, repoRoot = REPO_ROOT, artifactPath } = {}) {
|
|
309
|
+
if (name.startsWith('dashboard:')) {
|
|
310
|
+
return { kind: 'dashboard', name: name.slice('dashboard:'.length) };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const tapePath = resolveTapeFile(name, { cwd, repoRoot });
|
|
314
|
+
if (tapePath) {
|
|
315
|
+
let source = readFileSync(tapePath, 'utf8');
|
|
316
|
+
if (artifactPath && /^Output\s+/m.test(source)) {
|
|
317
|
+
source = source.replace(/^Output\s+.+$/m, `Output ${artifactPath}`);
|
|
318
|
+
}
|
|
319
|
+
const shippedDir = join(resolve(repoRoot), 'templates', 'demos', 'tapes');
|
|
320
|
+
const from = tapePath.startsWith(shippedDir) ? 'shipped' : 'project';
|
|
321
|
+
return { kind: 'terminal', name, source, tapePath, from };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const builder = TAPE_TEMPLATES[name];
|
|
325
|
+
if (builder) {
|
|
326
|
+
const output = artifactPath || join(cwd, '.cx', 'demos', `${name}.mp4`);
|
|
327
|
+
return { kind: 'terminal', name, source: builder(output), from: 'template' };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function initProjectTape(name, { cwd, from = 'quickstart' } = {}) {
|
|
334
|
+
const builder = TAPE_TEMPLATES[from];
|
|
335
|
+
if (!builder) return { ok: false, message: `Unknown template: ${from}. Valid: ${TAPE_TEMPLATE_NAMES.join(', ')}` };
|
|
336
|
+
const dir = tapesDir(cwd);
|
|
337
|
+
mkdirSync(dir, { recursive: true });
|
|
338
|
+
const tapePath = projectTapePath(cwd, name);
|
|
339
|
+
if (existsSync(tapePath)) return { ok: false, message: `Tape already exists: ${tapePath}` };
|
|
340
|
+
const placeholderOut = `.cx/demos/${name}.mp4`;
|
|
341
|
+
writeFileSync(tapePath, builder(placeholderOut), 'utf8');
|
|
342
|
+
return { ok: true, tapePath, message: `Scaffolded ${tapePath}` };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function renderWithVhs(binary, tapePath, {
|
|
346
|
+
spawnSyncFn = spawnSync,
|
|
347
|
+
killFn = process.kill.bind(process),
|
|
348
|
+
platform = process.platform,
|
|
349
|
+
} = {}) {
|
|
350
|
+
// A POSIX session leader confines every recorder descendant to a group Construct
|
|
351
|
+
// owns. When VHS returns, SIGTERM reaps a lingering ttyd or browser child without
|
|
352
|
+
// targeting the caller's terminal or its unrelated browser processes.
|
|
353
|
+
const ownsProcessGroup = platform !== 'win32';
|
|
354
|
+
const result = spawnSyncFn(binary, [tapePath], {
|
|
355
|
+
encoding: 'utf8',
|
|
356
|
+
timeout: 180_000,
|
|
357
|
+
detached: ownsProcessGroup,
|
|
358
|
+
});
|
|
359
|
+
if (ownsProcessGroup && Number.isInteger(result?.pid) && result.pid > 0) {
|
|
360
|
+
try { killFn(-result.pid, 'SIGTERM'); } catch { /* recorder group is already gone */ }
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function tapeToShellCommand(tapeSource) {
|
|
366
|
+
const commands = [];
|
|
367
|
+
let pending = '';
|
|
368
|
+
for (const line of tapeSource.split('\n')) {
|
|
369
|
+
const typeMatch = line.match(/^Type\s+"(.*)"\s*$/);
|
|
370
|
+
if (typeMatch) { pending = typeMatch[1]; continue; }
|
|
371
|
+
if (/^Enter\s*$/.test(line) && pending) { commands.push(pending); pending = ''; }
|
|
372
|
+
}
|
|
373
|
+
return commands.join(' && ');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function renderWithAsciinema(binary, tapeSource, outPath) {
|
|
377
|
+
const command = tapeToShellCommand(tapeSource) || 'echo "construct demo"';
|
|
378
|
+
return spawnSync(binary, ['rec', '--overwrite', '-c', command, outPath], { encoding: 'utf8', timeout: 180_000 });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function parseTapeOutput(source) {
|
|
382
|
+
const match = source.match(/^Output\s+(.+)$/m);
|
|
383
|
+
return match ? match[1].trim() : null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function resolveOutputPath(outputLine, cwd) {
|
|
387
|
+
if (!outputLine) return null;
|
|
388
|
+
return resolve(cwd, outputLine);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function tapeOutputLine(artifactPath, cwd) {
|
|
392
|
+
const rel = relative(cwd, artifactPath);
|
|
393
|
+
return rel && !rel.startsWith('..') ? rel : artifactPath;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function runDemoRecord(name, {
|
|
397
|
+
cwd = process.cwd(),
|
|
398
|
+
repoRoot = REPO_ROOT,
|
|
399
|
+
format = 'gif',
|
|
400
|
+
out = null,
|
|
401
|
+
sourceOnly = false,
|
|
402
|
+
required = false,
|
|
403
|
+
surface = null,
|
|
404
|
+
} = {}) {
|
|
405
|
+
if (name.startsWith('dashboard:')) {
|
|
406
|
+
return { ok: false, name, message: 'Use recordDashboardDemo for dashboard: demos' };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const recording = loadDemoRecording(name, { cwd, repoRoot });
|
|
410
|
+
const usePlaywright = surface !== 'tape'
|
|
411
|
+
&& (surface === 'playwright' || (recording && recording.engine === 'playwright'));
|
|
412
|
+
if (usePlaywright && recording && !sourceOnly) {
|
|
413
|
+
const outFormat = format === 'gif' ? 'mp4' : format;
|
|
414
|
+
const dest = out ? resolve(cwd, out) : null;
|
|
415
|
+
const result = recordPlaywrightDemo(recording, {
|
|
416
|
+
cwd,
|
|
417
|
+
repoRoot,
|
|
418
|
+
outputPath: dest,
|
|
419
|
+
format: outFormat,
|
|
420
|
+
});
|
|
421
|
+
return { ...result, required, surface: 'playwright' };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (!FORMATS.includes(format)) {
|
|
425
|
+
return { ok: false, name, message: `Unknown format: ${format}` };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const dir = out ? dirname(resolve(cwd, out)) : join(cwd, '.cx', 'demos');
|
|
429
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
430
|
+
|
|
431
|
+
const resolved = resolveTapeSource(name, { cwd, repoRoot });
|
|
432
|
+
if (!resolved || resolved.kind !== 'terminal') {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
name,
|
|
436
|
+
required,
|
|
437
|
+
message: `Unknown demo: ${name}. Run \`construct demo list\` or \`construct demo init ${name}\`.`,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const recorder = sourceOnly ? null : locateRecorder();
|
|
442
|
+
const artifactExt = recorder?.engine === 'asciinema' ? 'cast' : format;
|
|
443
|
+
const tapedOutput = parseTapeOutput(resolved.source);
|
|
444
|
+
const ephemeralSource = resolved.from === 'template' || resolved.from === 'shipped';
|
|
445
|
+
|
|
446
|
+
let artifactPath;
|
|
447
|
+
if (out) {
|
|
448
|
+
const base = resolve(cwd, out).replace(/\.[^./]+$/, '');
|
|
449
|
+
artifactPath = `${base}.${artifactExt}`;
|
|
450
|
+
} else if (tapedOutput && resolved.from !== 'template') {
|
|
451
|
+
artifactPath = resolveOutputPath(tapedOutput, cwd);
|
|
452
|
+
} else {
|
|
453
|
+
const baseName = join(dir, `${name}-${timestamp()}`);
|
|
454
|
+
artifactPath = `${baseName}.${artifactExt}`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let tapePath = resolved.from === 'project' ? resolved.tapePath : join(dir, `${name}-${timestamp()}.tape`);
|
|
458
|
+
const expectedFromTape = tapedOutput ? resolveOutputPath(tapedOutput, cwd) : null;
|
|
459
|
+
const needsTempTape = ephemeralSource
|
|
460
|
+
|| out
|
|
461
|
+
|| (resolved.from === 'project' && expectedFromTape && artifactPath !== expectedFromTape);
|
|
462
|
+
|
|
463
|
+
if (needsTempTape) {
|
|
464
|
+
const source = resolved.source.replace(/^Output\s+.+$/m, `Output ${tapeOutputLine(artifactPath, cwd)}`);
|
|
465
|
+
if (resolved.from === 'project') {
|
|
466
|
+
tapePath = join(dir, `${name}-render-${timestamp()}.tape`);
|
|
467
|
+
}
|
|
468
|
+
writeFileSync(tapePath, source, 'utf8');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!recorder) {
|
|
472
|
+
if (ephemeralSource) {
|
|
473
|
+
console.log(`Demo tape (${name}) written to: ${tapePath}`);
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
ok: true,
|
|
477
|
+
name,
|
|
478
|
+
sourceOnly: true,
|
|
479
|
+
tapePath: resolved.from === 'project' ? resolved.tapePath : tapePath,
|
|
480
|
+
message: installHint(),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const render = recorder.engine === 'vhs'
|
|
485
|
+
? renderWithVhs(recorder.binary, tapePath)
|
|
486
|
+
: renderWithAsciinema(recorder.binary, resolved.source, artifactPath);
|
|
487
|
+
|
|
488
|
+
if (render.status === 0 && existsSync(artifactPath)) {
|
|
489
|
+
return { ok: true, name, tapePath, artifactPath, engine: recorder.engine };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
ok: !required,
|
|
494
|
+
name,
|
|
495
|
+
required,
|
|
496
|
+
tapePath,
|
|
497
|
+
message: `Recorder ${recorder.engine} failed (exit ${render.status})`,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function printHelp() {
|
|
502
|
+
const templates = RECORDING_INIT_TEMPLATES.join(', ');
|
|
503
|
+
const surfaces = DEMO_SURFACES.join(' | ');
|
|
504
|
+
console.log(`Usage: construct demo <command|name> [options]
|
|
505
|
+
|
|
506
|
+
Commands:
|
|
507
|
+
list List tapes, recordings, and demo scripts
|
|
508
|
+
init <name> [--from=T] Scaffold tape or Playwright recording
|
|
509
|
+
record <name> Record VHS tape or Playwright demo manifest
|
|
510
|
+
<name> Run guided demo (default: construct chat)
|
|
511
|
+
|
|
512
|
+
Default run uses construct chat with a demo script (templates/demos/scripts/).
|
|
513
|
+
Falls back to web chat, VHS tape, Playwright, dashboard, or printed steps.
|
|
514
|
+
|
|
515
|
+
Options:
|
|
516
|
+
--surface=<s> ${surfaces}
|
|
517
|
+
--model <id> Pin model for chat demo
|
|
518
|
+
--web Open web chat (/chat/) instead of terminal
|
|
519
|
+
--plain Linear chat renderer
|
|
520
|
+
--free OpenRouter free-router mode
|
|
521
|
+
--format <f> gif (default) | mp4 | webm
|
|
522
|
+
--out <path> Output path (recording)
|
|
523
|
+
--source-only Tape: write .tape only; skip recording
|
|
524
|
+
-h, --help Show this message
|
|
525
|
+
|
|
526
|
+
Examples:
|
|
527
|
+
construct demo agentic-platforms-prd
|
|
528
|
+
construct demo record agentic-platforms-prd --format mp4
|
|
529
|
+
construct demo init marketing --from=nextjs-static
|
|
530
|
+
construct demo dashboard:cockpit-tour
|
|
531
|
+
`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function parseArgs(argv) {
|
|
535
|
+
const options = {
|
|
536
|
+
format: 'gif',
|
|
537
|
+
out: null,
|
|
538
|
+
sourceOnly: false,
|
|
539
|
+
from: 'quickstart',
|
|
540
|
+
command: null,
|
|
541
|
+
name: null,
|
|
542
|
+
surface: 'chat',
|
|
543
|
+
model: null,
|
|
544
|
+
plain: false,
|
|
545
|
+
accessible: false,
|
|
546
|
+
web: false,
|
|
547
|
+
free: false,
|
|
548
|
+
};
|
|
549
|
+
const positional = [];
|
|
550
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
551
|
+
const arg = argv[i];
|
|
552
|
+
if (arg === '--help' || arg === '-h') { options.help = true; continue; }
|
|
553
|
+
if (arg === '--source-only') { options.sourceOnly = true; continue; }
|
|
554
|
+
if (arg === '--plain') { options.plain = true; continue; }
|
|
555
|
+
if (arg === '--accessible') { options.accessible = true; continue; }
|
|
556
|
+
if (arg === '--web') { options.web = true; continue; }
|
|
557
|
+
if (arg === '--free') { options.free = true; continue; }
|
|
558
|
+
if (arg === '--format') { options.format = argv[++i]; continue; }
|
|
559
|
+
if (arg.startsWith('--format=')) { options.format = arg.slice(9); continue; }
|
|
560
|
+
if (arg === '--out') { options.out = argv[++i]; continue; }
|
|
561
|
+
if (arg.startsWith('--out=')) { options.out = arg.slice(6); continue; }
|
|
562
|
+
if (arg === '--from') { options.from = argv[++i]; continue; }
|
|
563
|
+
if (arg.startsWith('--from=')) { options.from = arg.slice(7); continue; }
|
|
564
|
+
if (arg === '--surface') { options.surface = argv[++i]; continue; }
|
|
565
|
+
if (arg.startsWith('--surface=')) { options.surface = arg.slice(10); continue; }
|
|
566
|
+
if (arg === '--model') { options.model = argv[++i]; continue; }
|
|
567
|
+
if (arg.startsWith('--model=')) { options.model = arg.slice(8); continue; }
|
|
568
|
+
positional.push(arg);
|
|
569
|
+
}
|
|
570
|
+
if (positional[0] === 'list') options.command = 'list';
|
|
571
|
+
else if (positional[0] === 'init') { options.command = 'init'; options.name = positional[1]; }
|
|
572
|
+
else if (positional[0] === 'record') { options.command = 'record'; options.name = positional[1]; }
|
|
573
|
+
else options.name = positional[0] || null;
|
|
574
|
+
return options;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export async function runDemoCli(argv = [], { cwd = process.cwd(), repoRoot } = {}) {
|
|
578
|
+
const options = parseArgs(argv);
|
|
579
|
+
if (options.help) {
|
|
580
|
+
printHelp();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (options.command === 'list') {
|
|
585
|
+
const tapes = listProjectTapes({ cwd, repoRoot });
|
|
586
|
+
const scripts = listDemoScripts({ cwd, repoRoot });
|
|
587
|
+
const recordings = listDemoRecordings({ cwd, repoRoot });
|
|
588
|
+
console.log('Demo tapes (project .cx/demos/tapes/ overrides templates/demos/tapes/):');
|
|
589
|
+
if (!tapes.length) console.log(' (none — run `construct demo init <name>`)');
|
|
590
|
+
else for (const t of tapes) console.log(` ${t}`);
|
|
591
|
+
console.log('\nPlaywright recordings (.cx/demos/recordings/ overrides templates/demos/recordings/):');
|
|
592
|
+
if (!recordings.length) console.log(' (none — run `construct demo init <name> --from=nextjs-static`)');
|
|
593
|
+
else for (const r of recordings) console.log(` ${r}`);
|
|
594
|
+
console.log('\nDemo scripts (chat-guided; templates/demos/scripts/):');
|
|
595
|
+
if (!scripts.length) console.log(' (none)');
|
|
596
|
+
else for (const s of scripts) console.log(` ${s}`);
|
|
597
|
+
console.log(`\nScaffold templates: ${RECORDING_INIT_TEMPLATES.join(', ')}`);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (options.command === 'init') {
|
|
602
|
+
if (!options.name) {
|
|
603
|
+
console.error(`Usage: construct demo init <name> [--from=${RECORDING_INIT_TEMPLATES.join('|')}]`);
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
const result = initProjectRecording(options.name, { cwd, from: options.from, repoRoot });
|
|
607
|
+
if (!result.ok) {
|
|
608
|
+
console.error(result.message);
|
|
609
|
+
process.exit(1);
|
|
610
|
+
}
|
|
611
|
+
console.log(result.message);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const demoName = options.command === 'record' ? options.name : options.name;
|
|
616
|
+
if (!demoName) {
|
|
617
|
+
printHelp();
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (demoName.startsWith('dashboard:')) {
|
|
622
|
+
const { recordDashboardDemo } = await import('./dashboard-demo.mjs');
|
|
623
|
+
const result = recordDashboardDemo(demoName.slice('dashboard:'.length), { cwd, repoRoot });
|
|
624
|
+
if (!result.ok) {
|
|
625
|
+
console.error(result.message);
|
|
626
|
+
process.exit(result.missing?.length ? 2 : 1);
|
|
627
|
+
}
|
|
628
|
+
console.log(result.message);
|
|
629
|
+
for (const v of result.videos || []) console.log(` ${v}`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const preferPlaywright = options.surface === 'playwright'
|
|
634
|
+
|| (options.command === 'record' && loadDemoRecording(demoName, { cwd, repoRoot })?.engine === 'playwright');
|
|
635
|
+
const useTape = options.surface === 'tape'
|
|
636
|
+
|| (options.command === 'record' && !preferPlaywright && options.surface !== 'dashboard');
|
|
637
|
+
if (useTape || (options.command === 'record' && preferPlaywright)) {
|
|
638
|
+
const result = runDemoRecord(demoName, {
|
|
639
|
+
cwd,
|
|
640
|
+
repoRoot,
|
|
641
|
+
format: options.format,
|
|
642
|
+
out: options.out,
|
|
643
|
+
sourceOnly: options.sourceOnly,
|
|
644
|
+
surface: options.surface,
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
if (!result.ok) {
|
|
648
|
+
console.error(result.message);
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (result.artifactPath || result.outputPath) {
|
|
653
|
+
const pathOut = result.artifactPath || result.outputPath;
|
|
654
|
+
console.log(`Demo (${demoName}) recorded via ${result.engine || result.surface} to:`);
|
|
655
|
+
console.log(` ${pathOut}`);
|
|
656
|
+
if (result.tapePath) console.log(`Tape: ${result.tapePath}`);
|
|
657
|
+
} else if (result.tapePath) {
|
|
658
|
+
console.log(`Demo tape (${demoName}): ${result.tapePath}`);
|
|
659
|
+
if (result.message) console.log(`\nNo recorder: ${result.message}`);
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const { runDemoGuided } = await import('./demo-surface.mjs');
|
|
665
|
+
const result = await runDemoGuided(demoName, {
|
|
666
|
+
cwd,
|
|
667
|
+
repoRoot,
|
|
668
|
+
surface: options.surface,
|
|
669
|
+
format: options.format,
|
|
670
|
+
out: options.out,
|
|
671
|
+
sourceOnly: options.sourceOnly,
|
|
672
|
+
model: options.model,
|
|
673
|
+
plain: options.plain,
|
|
674
|
+
accessible: options.accessible,
|
|
675
|
+
web: options.web,
|
|
676
|
+
free: options.free,
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
if (!result.ok) {
|
|
680
|
+
console.error(result.message);
|
|
681
|
+
process.exit(1);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (result.notices?.length) {
|
|
685
|
+
for (const n of result.notices) console.error(`note: ${n}`);
|
|
686
|
+
}
|
|
687
|
+
if (result.surface === 'chat' || result.surface === 'web') {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (result.artifactPath) {
|
|
691
|
+
console.log(`Demo (${demoName}) [${result.surface}] recorded to:`);
|
|
692
|
+
console.log(` ${result.artifactPath}`);
|
|
693
|
+
} else if (result.message) {
|
|
694
|
+
console.log(result.message);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Legacy exports for tests
|
|
699
|
+
export const TAPE_NAMES = TAPE_TEMPLATE_NAMES;
|
|
700
|
+
export function generateTape(name, outputArtifact) {
|
|
701
|
+
const builder = TAPE_TEMPLATES[name];
|
|
702
|
+
return builder ? builder(outputArtifact) : null;
|
|
703
|
+
}
|