@geraldmaron/construct 1.1.1 → 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 +29 -19
- package/bin/construct +311 -81
- 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 +1 -1
- 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 +129 -15
- 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 +598 -140
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +13 -11
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +3 -1
- 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/document-export.mjs +360 -36
- package/lib/document-extract.mjs +44 -4
- package/lib/document-ingest.mjs +29 -20
- package/lib/embed/cli.mjs +2 -2
- package/lib/embed/daemon.mjs +18 -46
- 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/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- 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 +373 -92
- 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/playwright-demo.mjs +252 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +7 -3
- 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 +228 -7
- package/lib/service-manager.mjs +60 -8
- package/lib/setup.mjs +70 -7
- 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/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 +28 -14
- package/personas/construct.md +2 -2
- 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 +41 -28
- 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 +7 -2
- 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 +8 -8
- 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/demo.mjs
CHANGED
|
@@ -1,71 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/demo.mjs — `construct demo`
|
|
3
|
-
* recording of a Construct workflow from a `.tape` script, rendering to
|
|
4
|
-
* GIF/MP4/WebM via VHS, degrading to asciinema, and finally to emitting the
|
|
5
|
-
* `.tape` source + install hint when no recorder is installed.
|
|
2
|
+
* lib/demo.mjs — `construct demo` terminal recordings via VHS `.tape` files.
|
|
6
3
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `.tape` is deterministic and diffable — the source of truth lives in the
|
|
12
|
-
* repo, the artifact regenerates in CI. Chosen as PRIMARY: reproducible,
|
|
13
|
-
* embeddable in README/docs, multiple output formats.
|
|
14
|
-
* - asciinema (asciinema.org, GPL-3.0): records a terminal session to a
|
|
15
|
-
* `.cast` JSON. Reproducible and lightweight but interactive-capture by
|
|
16
|
-
* nature and GIF export needs the separate `agg` tool. Chosen as FALLBACK
|
|
17
|
-
* when VHS is absent; we drive it non-interactively via `asciinema rec -c`.
|
|
18
|
-
* - Playwright (dashboard): out of scope here — it is a dev-only browser
|
|
19
|
-
* driver for the web UI, not a terminal recorder, and lives with the
|
|
20
|
-
* dashboard tests rather than this command.
|
|
21
|
-
*
|
|
22
|
-
* Degradation contract: when neither vhs nor asciinema is on PATH the command
|
|
23
|
-
* still succeeds (exit 0) by writing the `.tape` SOURCE plus an install hint.
|
|
24
|
-
* No bundled binaries, no npm dependencies; recorders are detected at runtime,
|
|
25
|
-
* mirroring lib/runtime/whisper-bootstrap.mjs.
|
|
26
|
-
*
|
|
27
|
-
* Output: .cx/demos/<name>-<ts>.<ext> (recording) and the `.tape` source.
|
|
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.
|
|
28
8
|
*/
|
|
29
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
30
|
-
import { spawnSync } from 'node:child_process';
|
|
31
|
-
import { join, resolve, dirname } from 'node:path';
|
|
32
9
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (result.status !== 0) return null;
|
|
42
|
-
return result.stdout.trim().split('\n')[0] || null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Recorder detection follows the optional-external-tool pattern: probe PATH,
|
|
46
|
-
// return a structured descriptor, never throw. VHS is preferred for its
|
|
47
|
-
// declarative `.tape` model and multi-format output; asciinema is the fallback.
|
|
48
|
-
|
|
49
|
-
export function locateRecorder() {
|
|
50
|
-
const vhs = which('vhs');
|
|
51
|
-
if (vhs) return { engine: 'vhs', binary: vhs };
|
|
52
|
-
const asciinema = which('asciinema');
|
|
53
|
-
if (asciinema) return { engine: 'asciinema', binary: asciinema };
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
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';
|
|
56
18
|
|
|
57
|
-
|
|
58
|
-
if (process.platform === 'darwin') return 'brew install vhs (or: brew install asciinema)';
|
|
59
|
-
if (process.platform === 'linux') return 'See https://github.com/charmbracelet/vhs#installation (or: pip install asciinema)';
|
|
60
|
-
return 'See https://github.com/charmbracelet/vhs#installation';
|
|
61
|
-
}
|
|
19
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
62
20
|
|
|
63
|
-
|
|
64
|
-
// directives that record a real walkthrough. Commands are illustrative and
|
|
65
|
-
// safe to replay; tapes are the source of truth, regenerated in CI.
|
|
21
|
+
const FORMATS = ['gif', 'mp4', 'webm'];
|
|
66
22
|
|
|
67
|
-
const
|
|
68
|
-
quickstart: (out) =>
|
|
23
|
+
const TAPE_TEMPLATES = {
|
|
24
|
+
quickstart: (out) => `Require node
|
|
25
|
+
Require vhs
|
|
26
|
+
# construct demo: quickstart — plan then build
|
|
69
27
|
Output ${out}
|
|
70
28
|
Set FontSize 18
|
|
71
29
|
Set Width 1100
|
|
@@ -73,22 +31,24 @@ Set Height 600
|
|
|
73
31
|
Set Theme "Dracula"
|
|
74
32
|
Set TypingSpeed 60ms
|
|
75
33
|
|
|
76
|
-
Type "construct plan 'add a rate limiter to the API'"
|
|
34
|
+
Type "node bin/construct plan 'add a rate limiter to the API'"
|
|
77
35
|
Sleep 500ms
|
|
78
36
|
Enter
|
|
79
37
|
Sleep 2s
|
|
80
38
|
|
|
81
|
-
Type "construct build"
|
|
39
|
+
Type "node bin/construct build"
|
|
82
40
|
Sleep 500ms
|
|
83
41
|
Enter
|
|
84
42
|
Sleep 2s
|
|
85
43
|
|
|
86
|
-
Type "construct ship status"
|
|
44
|
+
Type "node bin/construct ship status"
|
|
87
45
|
Sleep 500ms
|
|
88
46
|
Enter
|
|
89
47
|
Sleep 2s
|
|
90
48
|
`,
|
|
91
|
-
diagram: (out) =>
|
|
49
|
+
diagram: (out) => `Require node
|
|
50
|
+
Require vhs
|
|
51
|
+
# construct demo: diagram — render an architecture diagram
|
|
92
52
|
Output ${out}
|
|
93
53
|
Set FontSize 18
|
|
94
54
|
Set Width 1100
|
|
@@ -96,32 +56,311 @@ Set Height 600
|
|
|
96
56
|
Set Theme "Dracula"
|
|
97
57
|
Set TypingSpeed 60ms
|
|
98
58
|
|
|
99
|
-
Type "construct diagram 'web app: client -> api -> db'"
|
|
59
|
+
Type "node bin/construct diagram 'web app: client -> api -> db'"
|
|
100
60
|
Sleep 500ms
|
|
101
61
|
Enter
|
|
102
62
|
Sleep 2s
|
|
103
63
|
`,
|
|
104
64
|
};
|
|
105
65
|
|
|
106
|
-
export const
|
|
66
|
+
export const TAPE_TEMPLATE_NAMES = Object.keys(TAPE_TEMPLATES);
|
|
107
67
|
|
|
108
|
-
export
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return
|
|
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;
|
|
112
287
|
}
|
|
113
288
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
}
|
|
116
332
|
|
|
117
|
-
function
|
|
118
|
-
|
|
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}` };
|
|
119
343
|
}
|
|
120
344
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}
|
|
125
364
|
|
|
126
365
|
function tapeToShellCommand(tapeSource) {
|
|
127
366
|
const commands = [];
|
|
@@ -139,107 +378,326 @@ function renderWithAsciinema(binary, tapeSource, outPath) {
|
|
|
139
378
|
return spawnSync(binary, ['rec', '--overwrite', '-c', command, outPath], { encoding: 'utf8', timeout: 180_000 });
|
|
140
379
|
}
|
|
141
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
|
+
|
|
142
501
|
export function printHelp() {
|
|
143
|
-
|
|
502
|
+
const templates = RECORDING_INIT_TEMPLATES.join(', ');
|
|
503
|
+
const surfaces = DEMO_SURFACES.join(' | ');
|
|
504
|
+
console.log(`Usage: construct demo <command|name> [options]
|
|
144
505
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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)
|
|
149
511
|
|
|
150
|
-
|
|
151
|
-
|
|
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.
|
|
152
514
|
|
|
153
515
|
Options:
|
|
154
|
-
--
|
|
155
|
-
--
|
|
156
|
-
--
|
|
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
|
|
157
524
|
-h, --help Show this message
|
|
158
525
|
|
|
159
|
-
Recorders (detected at runtime, never bundled):
|
|
160
|
-
VHS ${process.platform === 'darwin' ? 'brew install vhs' : 'https://github.com/charmbracelet/vhs#installation'}
|
|
161
|
-
asciinema ${process.platform === 'darwin' ? 'brew install asciinema' : 'pip install asciinema'}
|
|
162
|
-
|
|
163
526
|
Examples:
|
|
164
|
-
construct demo
|
|
165
|
-
construct demo
|
|
166
|
-
construct demo
|
|
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
|
|
167
531
|
`);
|
|
168
532
|
}
|
|
169
533
|
|
|
170
534
|
function parseArgs(argv) {
|
|
171
|
-
const options = {
|
|
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
|
+
};
|
|
172
549
|
const positional = [];
|
|
173
550
|
for (let i = 0; i < argv.length; i += 1) {
|
|
174
551
|
const arg = argv[i];
|
|
175
552
|
if (arg === '--help' || arg === '-h') { options.help = true; continue; }
|
|
176
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; }
|
|
177
558
|
if (arg === '--format') { options.format = argv[++i]; continue; }
|
|
178
559
|
if (arg.startsWith('--format=')) { options.format = arg.slice(9); continue; }
|
|
179
560
|
if (arg === '--out') { options.out = argv[++i]; continue; }
|
|
180
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; }
|
|
181
568
|
positional.push(arg);
|
|
182
569
|
}
|
|
183
|
-
|
|
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;
|
|
184
574
|
return options;
|
|
185
575
|
}
|
|
186
576
|
|
|
187
|
-
export async function runDemoCli(argv = [], { cwd = process.cwd() } = {}) {
|
|
577
|
+
export async function runDemoCli(argv = [], { cwd = process.cwd(), repoRoot } = {}) {
|
|
188
578
|
const options = parseArgs(argv);
|
|
189
|
-
if (options.help
|
|
579
|
+
if (options.help) {
|
|
190
580
|
printHelp();
|
|
191
|
-
if (!options.help && !options.name) process.exit(1);
|
|
192
581
|
return;
|
|
193
582
|
}
|
|
194
|
-
if (!TAPE_NAMES.includes(options.name)) {
|
|
195
|
-
console.error(`Unknown demo: ${options.name}. Valid: ${TAPE_NAMES.join(', ')}`);
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
if (!FORMATS.includes(options.format)) {
|
|
199
|
-
console.error(`Unknown format: ${options.format}. Valid: ${FORMATS.join(', ')}`);
|
|
200
|
-
process.exit(1);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const recorder = options.sourceOnly ? null : locateRecorder();
|
|
204
583
|
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
}
|
|
207
600
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
}
|
|
211
614
|
|
|
212
|
-
|
|
213
|
-
|
|
615
|
+
const demoName = options.command === 'record' ? options.name : options.name;
|
|
616
|
+
if (!demoName) {
|
|
617
|
+
printHelp();
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
214
620
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
}
|
|
220
632
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
}
|
|
227
661
|
return;
|
|
228
662
|
}
|
|
229
663
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
}
|
|
233
683
|
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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') {
|
|
238
688
|
return;
|
|
239
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
|
+
}
|
|
240
697
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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;
|
|
245
703
|
}
|