@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.
Files changed (408) hide show
  1. package/README.md +29 -19
  2. package/bin/construct +311 -81
  3. package/bin/construct-postinstall.mjs +30 -2
  4. package/examples/distribution/README.md +42 -0
  5. package/examples/distribution/manifest.json +53 -0
  6. package/examples/distribution/sources/adr.md +84 -0
  7. package/examples/distribution/sources/deck-one-pager.md +65 -0
  8. package/examples/distribution/sources/prd-platform.md +161 -0
  9. package/examples/distribution/sources/research-brief.md +80 -0
  10. package/examples/distribution/sources/rfc-platform.md +82 -0
  11. package/examples/distribution/sources/runbook.md +103 -0
  12. package/examples/distribution/sources/strategy.md +88 -0
  13. package/lib/adapters-sync.mjs +67 -0
  14. package/lib/artifact-gate-notice.mjs +38 -0
  15. package/lib/artifact-manifest.mjs +112 -0
  16. package/lib/artifact-release-gate.mjs +171 -0
  17. package/lib/artifact-reviewers.mjs +68 -0
  18. package/lib/artifact-type-from-path.mjs +79 -0
  19. package/lib/audit-skills.mjs +4 -0
  20. package/lib/audit-specialists.mjs +285 -0
  21. package/lib/audit-trail.mjs +1 -1
  22. package/lib/auto-docs.mjs +87 -18
  23. package/lib/beads/drift.mjs +27 -6
  24. package/lib/beads-client.mjs +49 -121
  25. package/lib/beads-optimistic.mjs +5 -12
  26. package/lib/brand-fonts.mjs +93 -0
  27. package/lib/brand-prose.mjs +214 -0
  28. package/lib/brand-tokens.mjs +92 -0
  29. package/lib/bridges/copilot-proxy.mjs +13 -26
  30. package/lib/capability-ledger.mjs +156 -0
  31. package/lib/certification/artifact-fixtures.mjs +132 -0
  32. package/lib/certification/artifact-gates.mjs +63 -0
  33. package/lib/certification/artifact-provenance.mjs +97 -0
  34. package/lib/certification/canonical-scenarios.mjs +78 -0
  35. package/lib/certification/cli.mjs +191 -0
  36. package/lib/certification/dashboard-api.mjs +71 -0
  37. package/lib/certification/demo-parity.mjs +116 -0
  38. package/lib/certification/document-io-fixtures.mjs +246 -0
  39. package/lib/certification/document-workflow.mjs +97 -0
  40. package/lib/certification/eval-bridge.mjs +77 -0
  41. package/lib/certification/model-routing.mjs +149 -0
  42. package/lib/certification/prompt-budget.mjs +119 -0
  43. package/lib/certification/rc-gate.mjs +206 -0
  44. package/lib/certification/real-llm-scenarios.mjs +303 -0
  45. package/lib/certification/role-cards.mjs +113 -0
  46. package/lib/certification/role-overlays.mjs +117 -0
  47. package/lib/certification/run.mjs +122 -0
  48. package/lib/certification/runner.mjs +323 -0
  49. package/lib/certification/scenarios.mjs +51 -0
  50. package/lib/certification/skill-inventory.mjs +289 -0
  51. package/lib/certification/skill-scenarios.mjs +147 -0
  52. package/lib/certification/specialist-contracts.mjs +85 -0
  53. package/lib/certification/specialist-scenarios.mjs +175 -0
  54. package/lib/certification/stale-impact.mjs +146 -0
  55. package/lib/certification/status.mjs +252 -0
  56. package/lib/certification/store.mjs +77 -0
  57. package/lib/chat/cli.mjs +333 -0
  58. package/lib/chat/command-suggest.mjs +161 -0
  59. package/lib/chat/commands.mjs +215 -0
  60. package/lib/chat/config.mjs +142 -0
  61. package/lib/chat/context-compactor.mjs +250 -0
  62. package/lib/chat/context-continuation.mjs +253 -0
  63. package/lib/chat/continuation-source.mjs +58 -0
  64. package/lib/chat/demo-guide.mjs +61 -0
  65. package/lib/chat/design-tokens.mjs +91 -0
  66. package/lib/chat/desktop-binary.mjs +79 -0
  67. package/lib/chat/desktop-build.mjs +130 -0
  68. package/lib/chat/desktop-launcher.mjs +133 -0
  69. package/lib/chat/evidence.mjs +145 -0
  70. package/lib/chat/export.mjs +74 -0
  71. package/lib/chat/harness/driver.mjs +91 -0
  72. package/lib/chat/list-picker.mjs +112 -0
  73. package/lib/chat/model-picker.mjs +356 -0
  74. package/lib/chat/openrouter-fallback.mjs +151 -0
  75. package/lib/chat/permission-prompt.mjs +33 -0
  76. package/lib/chat/picker-catalog.mjs +45 -0
  77. package/lib/chat/policy-telemetry.mjs +34 -0
  78. package/lib/chat/present.mjs +245 -0
  79. package/lib/chat/session-context.mjs +39 -0
  80. package/lib/chat/session-persist.mjs +73 -0
  81. package/lib/chat/session-restore.mjs +71 -0
  82. package/lib/chat/session-settings.mjs +53 -0
  83. package/lib/chat/system-prompt.mjs +52 -0
  84. package/lib/chat/transparency.mjs +93 -0
  85. package/lib/chat/tui/color-scheme.mjs +42 -0
  86. package/lib/chat/tui/markdown.mjs +123 -0
  87. package/lib/chat/tui/presentation.mjs +100 -0
  88. package/lib/chat/tui/render.mjs +500 -0
  89. package/lib/chat/tui/turn-block.mjs +284 -0
  90. package/lib/chat/tui/turn-present.mjs +18 -0
  91. package/lib/chat/tui/turn-state.mjs +88 -0
  92. package/lib/chat/tui/usage.mjs +122 -0
  93. package/lib/chat/web-commands.mjs +146 -0
  94. package/lib/chat/web-launcher.mjs +63 -0
  95. package/lib/chat/web-picker-keys.mjs +46 -0
  96. package/lib/chat/web-session.mjs +159 -0
  97. package/lib/cli-commands.mjs +129 -15
  98. package/lib/comment-lint.mjs +6 -4
  99. package/lib/config/schema.mjs +36 -5
  100. package/lib/contract-schemas/decision.json +50 -0
  101. package/lib/contract-schemas/implementation.json +51 -0
  102. package/lib/contract-schemas/review-report.json +32 -0
  103. package/lib/contract-schemas/test-report.json +43 -0
  104. package/lib/contracts/construct-handoff.mjs +60 -0
  105. package/lib/contracts/validate.mjs +32 -3
  106. package/lib/contracts/violation-log.mjs +58 -5
  107. package/lib/dashboard-demo.mjs +71 -0
  108. package/lib/dashboard-static.mjs +19 -5
  109. package/lib/decisions/registry.mjs +5 -3
  110. package/lib/deck-export-pptx.mjs +1152 -0
  111. package/lib/demo-recording.mjs +142 -0
  112. package/lib/demo-script.mjs +114 -0
  113. package/lib/demo-surface.mjs +249 -0
  114. package/lib/demo.mjs +598 -140
  115. package/lib/diagram-export.mjs +192 -0
  116. package/lib/diagram.mjs +13 -11
  117. package/lib/docs-verify.mjs +25 -7
  118. package/lib/doctor/index.mjs +3 -1
  119. package/lib/doctor/project-adapters.mjs +44 -0
  120. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  121. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  122. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  123. package/lib/document-export.mjs +360 -36
  124. package/lib/document-extract.mjs +44 -4
  125. package/lib/document-ingest.mjs +29 -20
  126. package/lib/embed/cli.mjs +2 -2
  127. package/lib/embed/daemon.mjs +18 -46
  128. package/lib/embed/worker.mjs +2 -2
  129. package/lib/embedded-contract/model-resolve.mjs +10 -9
  130. package/lib/env-config.mjs +26 -0
  131. package/lib/evals/dataset.mjs +137 -0
  132. package/lib/evals/gates.mjs +175 -0
  133. package/lib/graph/build-co-change.mjs +54 -0
  134. package/lib/graph/build-from-registry.mjs +136 -0
  135. package/lib/graph/build-import-graph.mjs +153 -0
  136. package/lib/graph/cli.mjs +110 -0
  137. package/lib/graph/impact-cli.mjs +89 -0
  138. package/lib/graph/impact.mjs +108 -0
  139. package/lib/graph/staleness.mjs +44 -0
  140. package/lib/graph/store.mjs +172 -0
  141. package/lib/health-check.mjs +90 -76
  142. package/lib/hooks/artifact-release-gate.mjs +43 -0
  143. package/lib/hooks/brand-prose-lint.mjs +38 -0
  144. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  145. package/lib/hooks/session-optimize.mjs +84 -207
  146. package/lib/hooks/session-start.mjs +4 -5
  147. package/lib/host-disposition.mjs +7 -0
  148. package/lib/improvement/cli.mjs +189 -0
  149. package/lib/improvement/controller.mjs +137 -0
  150. package/lib/improvement/proposal.mjs +120 -0
  151. package/lib/improvement/specialist-loop.mjs +192 -0
  152. package/lib/improvement/store.mjs +89 -0
  153. package/lib/improvement/surface.mjs +219 -0
  154. package/lib/ingest-tooling.mjs +97 -0
  155. package/lib/init/doc-lanes.mjs +165 -0
  156. package/lib/init-docs.mjs +31 -178
  157. package/lib/init-unified.mjs +27 -97
  158. package/lib/init-update-guide.mjs +102 -0
  159. package/lib/init-update.mjs +32 -1
  160. package/lib/init.mjs +8 -0
  161. package/lib/install/desktop-binary-download.mjs +85 -0
  162. package/lib/install/legacy-global-cleanup.mjs +189 -0
  163. package/lib/intake/daemon.mjs +2 -0
  164. package/lib/intake/git-queue.mjs +9 -8
  165. package/lib/intake/queue.mjs +2 -1
  166. package/lib/intake/session-prelude.mjs +90 -1
  167. package/lib/libreoffice-export.mjs +97 -0
  168. package/lib/logging/rotate.mjs +9 -0
  169. package/lib/maintenance/docker-reclaim.mjs +206 -0
  170. package/lib/mcp/external-schema-cost.mjs +74 -0
  171. package/lib/mcp/server.mjs +84 -9
  172. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  173. package/lib/mcp/tool-budget.mjs +55 -4
  174. package/lib/mcp/tools/document.mjs +18 -4
  175. package/lib/mcp/tools/skills.mjs +32 -3
  176. package/lib/mcp/tools/workflow.mjs +13 -1
  177. package/lib/model-free-selector.mjs +18 -0
  178. package/lib/model-registry.mjs +13 -229
  179. package/lib/model-router.mjs +373 -92
  180. package/lib/models/behavior-matrix.mjs +289 -0
  181. package/lib/models/catalog.mjs +209 -0
  182. package/lib/models/execution-capability-profile.mjs +196 -0
  183. package/lib/models/execution-policy.mjs +307 -0
  184. package/lib/models/provider-poll.mjs +383 -0
  185. package/lib/npm-spawn-env.mjs +17 -0
  186. package/lib/ollama/installed-models.mjs +129 -0
  187. package/lib/oracle/actions.mjs +187 -0
  188. package/lib/oracle/artifact-gate.mjs +99 -0
  189. package/lib/oracle/cli.mjs +204 -0
  190. package/lib/oracle/daemon-entry.mjs +14 -0
  191. package/lib/oracle/dispatch.mjs +81 -0
  192. package/lib/oracle/execute.mjs +143 -0
  193. package/lib/oracle/gaps.mjs +76 -0
  194. package/lib/oracle/index.mjs +84 -0
  195. package/lib/oracle/issues.mjs +164 -0
  196. package/lib/oracle/org-graph.mjs +170 -0
  197. package/lib/oracle/policy.mjs +80 -0
  198. package/lib/oracle/read-model.mjs +398 -0
  199. package/lib/oracle/reconcile.mjs +191 -0
  200. package/lib/oracle/routing.mjs +89 -0
  201. package/lib/oracle/synthesize.mjs +384 -0
  202. package/lib/oracle/verdicts.mjs +51 -0
  203. package/lib/orchestration/worker.mjs +88 -27
  204. package/lib/orchestration-policy.mjs +50 -0
  205. package/lib/parity.mjs +96 -2
  206. package/lib/playwright-demo.mjs +252 -0
  207. package/lib/project-init-shared.mjs +4 -1
  208. package/lib/prompt-composer.js +7 -3
  209. package/lib/prompt-validation-contract.mjs +24 -0
  210. package/lib/provider-capabilities.js +57 -11
  211. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  212. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  213. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  214. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  215. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  216. package/lib/providers/contract/contract-tests.mjs +57 -0
  217. package/lib/providers/contract/errors.mjs +48 -0
  218. package/lib/providers/contract/interface.mjs +50 -0
  219. package/lib/providers/contract/registry.mjs +102 -0
  220. package/lib/providers/copilot-auth.mjs +297 -0
  221. package/lib/providers/credential-bootstrap.mjs +178 -0
  222. package/lib/providers/credential-catalog.mjs +46 -0
  223. package/lib/providers/credential-sources.mjs +63 -0
  224. package/lib/providers/creds.mjs +5 -2
  225. package/lib/providers/op-run.mjs +59 -0
  226. package/lib/providers/secret-resolver.mjs +159 -0
  227. package/lib/publish-template.mjs +163 -0
  228. package/lib/publish-tooling.mjs +119 -0
  229. package/lib/publish.mjs +305 -0
  230. package/lib/registry/cli.mjs +82 -0
  231. package/lib/registry/consolidation.mjs +147 -0
  232. package/lib/registry/generate-docs.mjs +75 -0
  233. package/lib/registry/skill-verification.mjs +57 -0
  234. package/lib/registry/surface-map.mjs +76 -0
  235. package/lib/registry/validate.mjs +135 -0
  236. package/lib/resources/budget.mjs +82 -0
  237. package/lib/resources/process-budget.mjs +45 -0
  238. package/lib/rules-delivery.mjs +9 -2
  239. package/lib/rules-read.mjs +26 -0
  240. package/lib/runtime-env.mjs +23 -0
  241. package/lib/runtime-pressure.mjs +51 -7
  242. package/lib/schema-infer.mjs +13 -2
  243. package/lib/server/chat-loop.mjs +622 -0
  244. package/lib/server/demo-preview.mjs +63 -0
  245. package/lib/server/index.mjs +228 -7
  246. package/lib/service-manager.mjs +60 -8
  247. package/lib/setup.mjs +70 -7
  248. package/lib/specialists/prompt-schema.mjs +19 -10
  249. package/lib/specialists/roster.mjs +43 -0
  250. package/lib/specialists/scaffold.mjs +56 -0
  251. package/lib/storage/backend.mjs +6 -6
  252. package/lib/storage/hybrid-query.mjs +7 -4
  253. package/lib/storage/state-source.mjs +5 -5
  254. package/lib/storage/sync.mjs +2 -3
  255. package/lib/telemetry/rule-calls.mjs +52 -0
  256. package/lib/template-registry.mjs +2 -2
  257. package/lib/templates/visual-requirements.mjs +27 -51
  258. package/lib/test-corpus-inventory.mjs +313 -0
  259. package/lib/uninstall/uninstall.mjs +19 -7
  260. package/lib/update.mjs +12 -0
  261. package/lib/upgrade.mjs +14 -0
  262. package/lib/wireframe.mjs +20 -14
  263. package/lib/worker/run.mjs +17 -4
  264. package/lib/worker/trace.mjs +11 -3
  265. package/package.json +28 -14
  266. package/personas/construct.md +2 -2
  267. package/platforms/claude/settings.template.json +36 -0
  268. package/rules/common/patterns.md +1 -1
  269. package/rules/common/release-gates.md +4 -3
  270. package/scripts/sync-specialists.mjs +41 -28
  271. package/skills/devops/data-engineering.md +1 -1
  272. package/skills/docs/adr-workflow.md +1 -0
  273. package/skills/docs/backlog-proposal-workflow.md +1 -0
  274. package/skills/docs/codebase-research-workflow.md +40 -0
  275. package/skills/docs/customer-profile-workflow.md +1 -0
  276. package/skills/docs/document-ingest-workflow.md +1 -0
  277. package/skills/docs/evidence-ingest-workflow.md +1 -0
  278. package/skills/docs/init-docs.md +2 -2
  279. package/skills/docs/init-project.md +7 -2
  280. package/skills/docs/prd-workflow.md +24 -1
  281. package/skills/docs/prfaq-workflow.md +1 -0
  282. package/skills/docs/product-intelligence-workflow.md +1 -0
  283. package/skills/docs/product-signal-workflow.md +1 -0
  284. package/skills/docs/research-workflow.md +54 -37
  285. package/skills/docs/runbook-workflow.md +1 -0
  286. package/skills/docs/strategy-workflow.md +1 -0
  287. package/skills/docs/user-research-workflow.md +40 -0
  288. package/skills/operating/orchestration-reference.md +1 -1
  289. package/skills/roles/architect.md +5 -0
  290. package/skills/roles/operator.docs.md +4 -0
  291. package/skills/routing.md +4 -2
  292. package/specialists/artifact-manifest.json +480 -0
  293. package/specialists/artifact-manifest.schema.json +53 -0
  294. package/specialists/audit-enrichments.json +454 -0
  295. package/specialists/contracts.json +37 -25
  296. package/specialists/prompts/_shared/validation-contract.md +26 -0
  297. package/specialists/prompts/cx-accessibility.md +21 -2
  298. package/specialists/prompts/cx-ai-engineer.md +24 -1
  299. package/specialists/prompts/cx-architect.md +4 -1
  300. package/specialists/prompts/cx-business-strategist.md +23 -2
  301. package/specialists/prompts/cx-data-analyst.md +22 -1
  302. package/specialists/prompts/cx-data-engineer.md +23 -2
  303. package/specialists/prompts/cx-debugger.md +21 -2
  304. package/specialists/prompts/cx-designer.md +26 -3
  305. package/specialists/prompts/cx-devil-advocate.md +19 -2
  306. package/specialists/prompts/cx-docs-keeper.md +28 -1
  307. package/specialists/prompts/cx-engineer.md +22 -1
  308. package/specialists/prompts/cx-evaluator.md +18 -1
  309. package/specialists/prompts/cx-explorer.md +21 -2
  310. package/specialists/prompts/cx-legal-compliance.md +21 -2
  311. package/specialists/prompts/cx-operations.md +21 -2
  312. package/specialists/prompts/cx-oracle.md +94 -0
  313. package/specialists/prompts/cx-orchestrator.md +20 -1
  314. package/specialists/prompts/cx-platform-engineer.md +25 -2
  315. package/specialists/prompts/cx-product-manager.md +32 -1
  316. package/specialists/prompts/cx-qa.md +24 -2
  317. package/specialists/prompts/cx-rd-lead.md +23 -2
  318. package/specialists/prompts/cx-release-manager.md +23 -2
  319. package/specialists/prompts/cx-researcher.md +22 -2
  320. package/specialists/prompts/cx-reviewer.md +18 -1
  321. package/specialists/prompts/cx-security.md +22 -2
  322. package/specialists/prompts/cx-sre.md +30 -3
  323. package/specialists/prompts/cx-test-automation.md +7 -1
  324. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  325. package/specialists/prompts/cx-ux-researcher.md +21 -2
  326. package/specialists/registry.json +52 -173
  327. package/specialists/tone-profiles.json +42 -0
  328. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  329. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  330. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  331. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  332. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  333. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  334. package/templates/demos/vhs/construct-cockpit.json +24 -0
  335. package/templates/distribution/construct-brand.typ +446 -0
  336. package/templates/distribution/construct-decision.typ +38 -0
  337. package/templates/distribution/construct-deck.html +95 -0
  338. package/templates/distribution/construct-pdf.typ +38 -0
  339. package/templates/distribution/construct-prd.typ +38 -0
  340. package/templates/distribution/construct-research.typ +38 -0
  341. package/templates/distribution/construct-web.html +92 -0
  342. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  343. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  344. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  345. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  346. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  347. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  348. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  349. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  350. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  351. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  352. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  353. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  354. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  355. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  356. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  357. package/templates/distribution/fonts/README.md +36 -0
  358. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  359. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  360. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  361. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  362. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  363. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  364. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  365. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  366. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  367. package/templates/distribution/icons/activity.svg +15 -0
  368. package/templates/distribution/icons/alert-triangle.svg +17 -0
  369. package/templates/distribution/icons/book-open.svg +16 -0
  370. package/templates/distribution/icons/bot.svg +20 -0
  371. package/templates/distribution/icons/brain.svg +22 -0
  372. package/templates/distribution/icons/circle-check.svg +16 -0
  373. package/templates/distribution/icons/clipboard-check.svg +17 -0
  374. package/templates/distribution/icons/cpu.svg +28 -0
  375. package/templates/distribution/icons/database.svg +17 -0
  376. package/templates/distribution/icons/eye.svg +16 -0
  377. package/templates/distribution/icons/file-text.svg +19 -0
  378. package/templates/distribution/icons/gauge.svg +16 -0
  379. package/templates/distribution/icons/git-branch.svg +17 -0
  380. package/templates/distribution/icons/key.svg +17 -0
  381. package/templates/distribution/icons/layers.svg +17 -0
  382. package/templates/distribution/icons/list-checks.svg +19 -0
  383. package/templates/distribution/icons/lock.svg +16 -0
  384. package/templates/distribution/icons/message-square.svg +15 -0
  385. package/templates/distribution/icons/network.svg +19 -0
  386. package/templates/distribution/icons/route.svg +17 -0
  387. package/templates/distribution/icons/scale.svg +19 -0
  388. package/templates/distribution/icons/search.svg +16 -0
  389. package/templates/distribution/icons/send.svg +16 -0
  390. package/templates/distribution/icons/server.svg +18 -0
  391. package/templates/distribution/icons/shield-check.svg +16 -0
  392. package/templates/distribution/icons/users.svg +18 -0
  393. package/templates/distribution/icons/webhook.svg +17 -0
  394. package/templates/distribution/icons/workflow.svg +17 -0
  395. package/templates/distribution/icons/wrench.svg +15 -0
  396. package/templates/distribution/run.mjs +18 -1
  397. package/templates/docs/adr.md +7 -0
  398. package/templates/docs/construct_guide.md +8 -8
  399. package/templates/docs/customer-profile.md +4 -0
  400. package/templates/docs/one-pager.md +4 -0
  401. package/templates/docs/postmortem.md +31 -0
  402. package/templates/docs/prd-platform.md +19 -0
  403. package/templates/docs/prd.md +15 -0
  404. package/templates/docs/prfaq.md +7 -0
  405. package/templates/docs/qa-strategy.md +103 -0
  406. package/templates/docs/research-brief.md +8 -0
  407. package/templates/docs/rfc-platform.md +12 -0
  408. 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` command, produces a reproducible terminal
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
- * Tool evaluation (2026-06), against: reproducible-from-code, headless CI
8
- * render, distribution formats, license:
9
- * - VHS (charm.sh, MIT): records a `.tape` script (declarative keystrokes +
10
- * timing) to GIF/MP4/WebM headlessly via an embedded ttyd + ffmpeg. The
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
- const FORMATS = ['gif', 'mp4', 'webm'];
34
-
35
- function timestamp() {
36
- return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
37
- }
38
-
39
- function which(bin) {
40
- const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], { encoding: 'utf8' });
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
- export function installHint() {
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
- // Built-in workflow tapes. Each is a VHS `.tape`: declarative Type/Enter/Sleep
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 TAPES = {
68
- quickstart: (out) => `# construct demo: quickstart — plan then build
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) => `# construct demo: diagram — render an architecture diagram
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 TAPE_NAMES = Object.keys(TAPES);
66
+ export const TAPE_TEMPLATE_NAMES = Object.keys(TAPE_TEMPLATES);
107
67
 
108
- export function generateTape(name, outputArtifact) {
109
- const builder = TAPES[name];
110
- if (!builder) return null;
111
- return builder(outputArtifact);
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
- // VHS reads the Output directive from the .tape, so the artifact path is baked
115
- // into the source before rendering. The render is fully headless.
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 renderWithVhs(binary, tapePath) {
118
- return spawnSync(binary, [tapePath], { encoding: 'utf8', timeout: 180_000 });
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
- // asciinema can't replay a VHS .tape, so for the fallback we extract the
122
- // `Type`/`Enter` lines into a single shell command string and capture it
123
- // non-interactively into a .cast file. The .tape source is still written so
124
- // the canonical, VHS-renderable form is preserved.
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
- console.log(`Usage: construct demo <name> [options]
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
- Produces a reproducible terminal recording of a Construct workflow from a
146
- .tape script. Renders to GIF/MP4/WebM via VHS (primary) or a .cast via
147
- asciinema (fallback). When neither recorder is installed, writes the .tape
148
- SOURCE plus an install hint and exits 0.
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
- Tapes:
151
- ${TAPE_NAMES.join('\n ')}
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
- --format <f> gif (default) | mp4 | webm (VHS only)
155
- --out <path> Output path (default: .cx/demos/<name>-<ts>.<ext>)
156
- --source-only Always write the .tape source; skip recording
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 quickstart
165
- construct demo diagram --format mp4
166
- construct demo quickstart --source-only
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 = { format: 'gif', out: null, sourceOnly: false, name: null };
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
- options.name = positional[0] || null;
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 || !options.name) {
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
- const dir = options.out ? dirname(resolve(cwd, options.out)) : join(cwd, '.cx', 'demos');
206
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
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
- const baseName = options.out
209
- ? resolve(cwd, options.out).replace(/\.[^./]+$/, '')
210
- : join(dir, `${options.name}-${timestamp()}`);
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
- // asciinema emits .cast; VHS emits the requested image format. The Output
213
- // directive baked into the tape must match the artifact VHS will write.
615
+ const demoName = options.command === 'record' ? options.name : options.name;
616
+ if (!demoName) {
617
+ printHelp();
618
+ process.exit(1);
619
+ }
214
620
 
215
- const artifactExt = recorder?.engine === 'asciinema' ? 'cast' : options.format;
216
- const artifactPath = `${baseName}.${artifactExt}`;
217
- const tapeSource = generateTape(options.name, artifactPath);
218
- const tapePath = `${baseName}.tape`;
219
- writeFileSync(tapePath, tapeSource, 'utf8');
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
- if (!recorder) {
222
- console.log(`Demo tape (${options.name}) written to:`);
223
- console.log(` ${tapePath}`);
224
- console.log(`\nNo terminal recorder found. To produce a recording, install one:`);
225
- console.log(` ${installHint()}`);
226
- console.log(`\nThen: vhs "${tapePath}"`);
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 render = recorder.engine === 'vhs'
231
- ? renderWithVhs(recorder.binary, tapePath)
232
- : renderWithAsciinema(recorder.binary, tapeSource, artifactPath);
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 (render.status === 0 && existsSync(artifactPath)) {
235
- console.log(`Demo (${options.name}) recorded via ${recorder.engine} to:`);
236
- console.log(` ${artifactPath}`);
237
- console.log(`Tape: ${tapePath}`);
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
- console.log(`Demo tape written to:`);
242
- console.log(` ${tapePath}`);
243
- console.log(`\nRecorder ${recorder.engine} failed (exit ${render.status}); tape preserved.`);
244
- if (render.stderr) console.log(` ${render.stderr.trim().split('\n').slice(0, 3).join('\n ')}`);
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
  }