@geraldmaron/construct 1.1.0 → 1.2.0

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