quiver-cli 1.3.1 → 2.0.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 (407) hide show
  1. package/README.md +197 -239
  2. package/dist/cli.js +3163 -2040
  3. package/package.json +10 -11
  4. package/template/.agents/AGENTS.md +0 -26
  5. package/template/.agents/commands/cp.md +0 -108
  6. package/template/.agents/commands/next-setup.md +0 -1064
  7. package/template/.agents/commands/review.md +0 -68
  8. package/template/.agents/commands/terraform-readme.md +0 -38
  9. package/template/.agents/config.json +0 -80
  10. package/template/.agents/plugins/opencode/rtk.ts +0 -34
  11. package/template/.agents/skills/agent-browser/SKILL.md +0 -52
  12. package/template/.agents/skills/apps/skybridge/SKILL.md +0 -52
  13. package/template/.agents/skills/code/cleanup/SKILL.md +0 -26
  14. package/template/.agents/skills/code/improve/SKILL.md +0 -122
  15. package/template/.agents/skills/code/improve/references/audit-playbook.md +0 -130
  16. package/template/.agents/skills/code/improve/references/closing-the-loop.md +0 -96
  17. package/template/.agents/skills/code/improve/references/plan-template.md +0 -197
  18. package/template/.agents/skills/code/vercel-react-best-practices/AGENTS.md +0 -3810
  19. package/template/.agents/skills/code/vercel-react-best-practices/README.md +0 -123
  20. package/template/.agents/skills/code/vercel-react-best-practices/SKILL.md +0 -149
  21. package/template/.agents/skills/code/vercel-react-best-practices/metadata.json +0 -15
  22. package/template/.agents/skills/code/vercel-react-best-practices/rules/_sections.md +0 -46
  23. package/template/.agents/skills/code/vercel-react-best-practices/rules/_template.md +0 -28
  24. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-effect-event-deps.md +0 -56
  25. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  26. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-init-once.md +0 -42
  27. package/template/.agents/skills/code/vercel-react-best-practices/rules/advanced-use-latest.md +0 -39
  28. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-api-routes.md +0 -38
  29. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-cheap-condition-before-await.md +0 -37
  30. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-defer-await.md +0 -82
  31. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-dependencies.md +0 -51
  32. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-parallel.md +0 -28
  33. package/template/.agents/skills/code/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -99
  34. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-analyzable-paths.md +0 -63
  35. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -60
  36. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-conditional.md +0 -31
  37. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -49
  38. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  39. package/template/.agents/skills/code/vercel-react-best-practices/rules/bundle-preload.md +0 -50
  40. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-event-listeners.md +0 -74
  41. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -71
  42. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -48
  43. package/template/.agents/skills/code/vercel-react-best-practices/rules/client-swr-dedup.md +0 -56
  44. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -107
  45. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-cache-function-results.md +0 -80
  46. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-cache-property-access.md +0 -28
  47. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-cache-storage.md +0 -70
  48. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-combine-iterations.md +0 -32
  49. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-early-exit.md +0 -50
  50. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-flatmap-filter.md +0 -60
  51. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -45
  52. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-index-maps.md +0 -37
  53. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-length-check-first.md +0 -49
  54. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-min-max-loop.md +0 -82
  55. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-request-idle-callback.md +0 -105
  56. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -24
  57. package/template/.agents/skills/code/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -57
  58. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-activity.md +0 -26
  59. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  60. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -40
  61. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -38
  62. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  63. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  64. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +0 -30
  65. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-resource-hints.md +0 -85
  66. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-script-defer-async.md +0 -68
  67. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -28
  68. package/template/.agents/skills/code/vercel-react-best-practices/rules/rendering-usetransition-loading.md +0 -75
  69. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -39
  70. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-dependencies.md +0 -45
  71. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +0 -40
  72. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-derived-state.md +0 -29
  73. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -74
  74. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  75. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +0 -38
  76. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-memo.md +0 -44
  77. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +0 -45
  78. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-no-inline-components.md +0 -82
  79. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +0 -35
  80. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-split-combined-hooks.md +0 -64
  81. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-transitions.md +0 -40
  82. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-use-deferred-value.md +0 -59
  83. package/template/.agents/skills/code/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +0 -73
  84. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -73
  85. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-auth-actions.md +0 -96
  86. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-cache-lru.md +0 -41
  87. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-cache-react.md +0 -76
  88. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-dedup-props.md +0 -65
  89. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-hoist-static-io.md +0 -149
  90. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-no-shared-module-state.md +0 -50
  91. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -83
  92. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-parallel-nested-fetching.md +0 -34
  93. package/template/.agents/skills/code/vercel-react-best-practices/rules/server-serialization.md +0 -38
  94. package/template/.agents/skills/data/prisma-cli/SKILL.md +0 -265
  95. package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +0 -27
  96. package/template/.agents/skills/data/prisma-cli/references/complete.md +0 -22
  97. package/template/.agents/skills/data/prisma-cli/references/db-execute.md +0 -78
  98. package/template/.agents/skills/data/prisma-cli/references/db-pull.md +0 -185
  99. package/template/.agents/skills/data/prisma-cli/references/db-push.md +0 -150
  100. package/template/.agents/skills/data/prisma-cli/references/db-seed.md +0 -188
  101. package/template/.agents/skills/data/prisma-cli/references/debug.md +0 -46
  102. package/template/.agents/skills/data/prisma-cli/references/dev.md +0 -157
  103. package/template/.agents/skills/data/prisma-cli/references/format.md +0 -48
  104. package/template/.agents/skills/data/prisma-cli/references/generate.md +0 -173
  105. package/template/.agents/skills/data/prisma-cli/references/init.md +0 -139
  106. package/template/.agents/skills/data/prisma-cli/references/mcp.md +0 -39
  107. package/template/.agents/skills/data/prisma-cli/references/migrate-deploy.md +0 -127
  108. package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +0 -145
  109. package/template/.agents/skills/data/prisma-cli/references/migrate-diff.md +0 -89
  110. package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +0 -80
  111. package/template/.agents/skills/data/prisma-cli/references/migrate-resolve.md +0 -57
  112. package/template/.agents/skills/data/prisma-cli/references/migrate-status.md +0 -65
  113. package/template/.agents/skills/data/prisma-cli/references/studio.md +0 -137
  114. package/template/.agents/skills/data/prisma-cli/references/validate.md +0 -53
  115. package/template/.agents/skills/data/prisma-client-api/SKILL.md +0 -216
  116. package/template/.agents/skills/data/prisma-client-api/references/client-methods.md +0 -223
  117. package/template/.agents/skills/data/prisma-client-api/references/constructor.md +0 -221
  118. package/template/.agents/skills/data/prisma-client-api/references/filters.md +0 -256
  119. package/template/.agents/skills/data/prisma-client-api/references/model-queries.md +0 -281
  120. package/template/.agents/skills/data/prisma-client-api/references/query-options.md +0 -276
  121. package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +0 -198
  122. package/template/.agents/skills/data/prisma-client-api/references/relations.md +0 -308
  123. package/template/.agents/skills/data/prisma-client-api/references/transactions.md +0 -184
  124. package/template/.agents/skills/design/impeccable/SKILL.md +0 -84
  125. package/template/.agents/skills/design/impeccable/reference/adapt.md +0 -312
  126. package/template/.agents/skills/design/impeccable/reference/adapt.native.md +0 -58
  127. package/template/.agents/skills/design/impeccable/reference/android.md +0 -46
  128. package/template/.agents/skills/design/impeccable/reference/animate.md +0 -89
  129. package/template/.agents/skills/design/impeccable/reference/audit.md +0 -136
  130. package/template/.agents/skills/design/impeccable/reference/audit.native.md +0 -139
  131. package/template/.agents/skills/design/impeccable/reference/bolder.md +0 -33
  132. package/template/.agents/skills/design/impeccable/reference/clarify.md +0 -94
  133. package/template/.agents/skills/design/impeccable/reference/colorize.md +0 -86
  134. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +0 -44
  135. package/template/.agents/skills/design/impeccable/reference/craft.md +0 -5
  136. package/template/.agents/skills/design/impeccable/reference/critique.md +0 -806
  137. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +0 -37
  138. package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +0 -24
  139. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +0 -38
  140. package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +0 -92
  141. package/template/.agents/skills/design/impeccable/reference/delight.md +0 -70
  142. package/template/.agents/skills/design/impeccable/reference/distill.md +0 -111
  143. package/template/.agents/skills/design/impeccable/reference/doctor.md +0 -54
  144. package/template/.agents/skills/design/impeccable/reference/document.md +0 -416
  145. package/template/.agents/skills/design/impeccable/reference/extract.md +0 -69
  146. package/template/.agents/skills/design/impeccable/reference/harden.md +0 -336
  147. package/template/.agents/skills/design/impeccable/reference/hooks.md +0 -111
  148. package/template/.agents/skills/design/impeccable/reference/init.md +0 -131
  149. package/template/.agents/skills/design/impeccable/reference/ios.md +0 -51
  150. package/template/.agents/skills/design/impeccable/reference/layout.md +0 -84
  151. package/template/.agents/skills/design/impeccable/reference/live-setup.md +0 -102
  152. package/template/.agents/skills/design/impeccable/reference/live.md +0 -323
  153. package/template/.agents/skills/design/impeccable/reference/new-work.md +0 -145
  154. package/template/.agents/skills/design/impeccable/reference/onboard.md +0 -234
  155. package/template/.agents/skills/design/impeccable/reference/operate.md +0 -61
  156. package/template/.agents/skills/design/impeccable/reference/optimize.md +0 -258
  157. package/template/.agents/skills/design/impeccable/reference/overdrive.md +0 -127
  158. package/template/.agents/skills/design/impeccable/reference/polish.md +0 -105
  159. package/template/.agents/skills/design/impeccable/reference/quieter.md +0 -99
  160. package/template/.agents/skills/design/impeccable/reference/routing.md +0 -18
  161. package/template/.agents/skills/design/impeccable/reference/shape.md +0 -59
  162. package/template/.agents/skills/design/impeccable/reference/typeset.md +0 -80
  163. package/template/.agents/skills/design/impeccable/reference/visualize.md +0 -46
  164. package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +0 -1022
  165. package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +0 -94
  166. package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +0 -391
  167. package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +0 -513
  168. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +0 -814
  169. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +0 -325
  170. package/template/.agents/skills/design/impeccable/scripts/context.mjs +0 -1565
  171. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +0 -473
  172. package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +0 -121
  173. package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +0 -1
  174. package/template/.agents/skills/design/impeccable/scripts/detect-csp.mjs +0 -198
  175. package/template/.agents/skills/design/impeccable/scripts/detect.mjs +0 -30
  176. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +0 -2204
  177. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +0 -432
  178. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +0 -1311
  179. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +0 -9043
  180. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +0 -51
  181. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +0 -434
  182. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +0 -1319
  183. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +0 -1241
  184. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +0 -288
  185. package/template/.agents/skills/design/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +0 -189
  186. package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +0 -18
  187. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +0 -213
  188. package/template/.agents/skills/design/impeccable/scripts/detector/profile/profiler.mjs +0 -166
  189. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +0 -635
  190. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +0 -5681
  191. package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +0 -596
  192. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +0 -127
  193. package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +0 -30
  194. package/template/.agents/skills/design/impeccable/scripts/detector/shared/inline-ignores.mjs +0 -148
  195. package/template/.agents/skills/design/impeccable/scripts/detector/shared/page.mjs +0 -7
  196. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +0 -329
  197. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +0 -166
  198. package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +0 -457
  199. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +0 -447
  200. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +0 -819
  201. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +0 -538
  202. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +0 -2490
  203. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +0 -79
  204. package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +0 -93
  205. package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +0 -200
  206. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +0 -396
  207. package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +0 -880
  208. package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +0 -564
  209. package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +0 -130
  210. package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +0 -246
  211. package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +0 -306
  212. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +0 -640
  213. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +0 -137
  214. package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +0 -72
  215. package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +0 -37
  216. package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +0 -26
  217. package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +0 -281
  218. package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +0 -5
  219. package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +0 -194
  220. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +0 -369
  221. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +0 -485
  222. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +0 -169
  223. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +0 -533
  224. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +0 -149
  225. package/template/.agents/skills/design/impeccable/scripts/lib/target-args.mjs +0 -42
  226. package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +0 -33
  227. package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +0 -146
  228. package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +0 -617
  229. package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +0 -60
  230. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +0 -84
  231. package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +0 -28
  232. package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +0 -199
  233. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +0 -47
  234. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +0 -73
  235. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +0 -143
  236. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +0 -197
  237. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +0 -49
  238. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +0 -161
  239. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +0 -17
  240. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +0 -26
  241. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +0 -71
  242. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +0 -247
  243. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +0 -70
  244. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +0 -42
  245. package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +0 -149
  246. package/template/.agents/skills/design/impeccable/scripts/live/insert-ui.mjs +0 -458
  247. package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +0 -142
  248. package/template/.agents/skills/design/impeccable/scripts/live/manual-apply.mjs +0 -939
  249. package/template/.agents/skills/design/impeccable/scripts/live/manual-edit-routes.mjs +0 -357
  250. package/template/.agents/skills/design/impeccable/scripts/live/manual-edits-buffer.mjs +0 -152
  251. package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +0 -14
  252. package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +0 -139
  253. package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +0 -508
  254. package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +0 -563
  255. package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +0 -105
  256. package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +0 -105
  257. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +0 -969
  258. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +0 -1366
  259. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +0 -304
  260. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +0 -259
  261. package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +0 -75
  262. package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +0 -171
  263. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +0 -938
  264. package/template/.agents/skills/design/impeccable/scripts/live-browser-dom.js +0 -146
  265. package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +0 -242
  266. package/template/.agents/skills/design/impeccable/scripts/live-browser-session.js +0 -123
  267. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +0 -12551
  268. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +0 -1200
  269. package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +0 -107
  270. package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +0 -800
  271. package/template/.agents/skills/design/impeccable/scripts/live-discard-manual-edits.mjs +0 -51
  272. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +0 -463
  273. package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +0 -292
  274. package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +0 -368
  275. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +0 -430
  276. package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +0 -123
  277. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +0 -1698
  278. package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +0 -71
  279. package/template/.agents/skills/design/impeccable/scripts/live-target.mjs +0 -30
  280. package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +0 -927
  281. package/template/.agents/skills/design/impeccable/scripts/live.mjs +0 -334
  282. package/template/.agents/skills/design/impeccable/scripts/modern-screenshot.umd.js +0 -14
  283. package/template/.agents/skills/design/impeccable/scripts/palette.mjs +0 -628
  284. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +0 -224
  285. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +0 -1783
  286. package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +0 -74
  287. package/template/.agents/skills/design/shadcn/SKILL.md +0 -277
  288. package/template/.agents/skills/design/shadcn/agents/openai.yml +0 -5
  289. package/template/.agents/skills/design/shadcn/assets/shadcn-small.png +0 -0
  290. package/template/.agents/skills/design/shadcn/assets/shadcn.png +0 -0
  291. package/template/.agents/skills/design/shadcn/cli.md +0 -290
  292. package/template/.agents/skills/design/shadcn/customization.md +0 -209
  293. package/template/.agents/skills/design/shadcn/evals/evals.json +0 -77
  294. package/template/.agents/skills/design/shadcn/mcp.md +0 -105
  295. package/template/.agents/skills/design/shadcn/registry.md +0 -277
  296. package/template/.agents/skills/design/shadcn/rules/base-vs-radix.md +0 -306
  297. package/template/.agents/skills/design/shadcn/rules/chat.md +0 -250
  298. package/template/.agents/skills/design/shadcn/rules/composition.md +0 -213
  299. package/template/.agents/skills/design/shadcn/rules/forms.md +0 -192
  300. package/template/.agents/skills/design/shadcn/rules/icons.md +0 -101
  301. package/template/.agents/skills/design/shadcn/rules/styling.md +0 -185
  302. package/template/.agents/skills/find-skills/SKILL.md +0 -141
  303. package/template/.agents/skills/hono/SKILL.md +0 -593
  304. package/template/.agents/skills/integrations/langfuse/SKILL.md +0 -150
  305. package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +0 -41
  306. package/template/.agents/skills/integrations/langfuse/references/cli.md +0 -60
  307. package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +0 -35
  308. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +0 -87
  309. package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +0 -121
  310. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +0 -293
  311. package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +0 -35
  312. package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +0 -79
  313. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -34
  314. package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +0 -65
  315. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +0 -35
  316. package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +0 -76
  317. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +0 -66
  318. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +0 -75
  319. package/template/.agents/skills/integrations/posthog/SKILL.md +0 -102
  320. package/template/.agents/skills/integrations/posthog/references/error-tracking-alerts.md +0 -63
  321. package/template/.agents/skills/integrations/posthog/references/error-tracking-assigning-issues.md +0 -77
  322. package/template/.agents/skills/integrations/posthog/references/error-tracking-fingerprints.md +0 -57
  323. package/template/.agents/skills/integrations/posthog/references/error-tracking-monitoring.md +0 -140
  324. package/template/.agents/skills/integrations/posthog/references/error-tracking-nextjs.md +0 -490
  325. package/template/.agents/skills/integrations/posthog/references/error-tracking-source-maps.md +0 -45
  326. package/template/.agents/skills/integrations/posthog/references/feature-flags-best-practices.md +0 -139
  327. package/template/.agents/skills/integrations/posthog/references/feature-flags-react.md +0 -302
  328. package/template/.agents/skills/integrations/posthog/references/identify-users.md +0 -202
  329. package/template/.agents/skills/integrations/posthog/references/integration-example.md +0 -706
  330. package/template/.agents/skills/integrations/posthog/references/integration-nextjs.md +0 -385
  331. package/template/.agents/skills/integrations/posthog/references/integration-step-1-begin.md +0 -43
  332. package/template/.agents/skills/integrations/posthog/references/integration-step-2-edit.md +0 -37
  333. package/template/.agents/skills/integrations/posthog/references/integration-step-3-revise.md +0 -22
  334. package/template/.agents/skills/integrations/posthog/references/integration-step-4-conclude.md +0 -38
  335. package/template/.agents/skills/integrations/posthog/references/llm-analytics-anthropic.md +0 -200
  336. package/template/.agents/skills/integrations/posthog/references/llm-analytics-basics.md +0 -62
  337. package/template/.agents/skills/integrations/posthog/references/llm-analytics-costs.md +0 -197
  338. package/template/.agents/skills/integrations/posthog/references/llm-analytics-manual-capture.md +0 -397
  339. package/template/.agents/skills/integrations/posthog/references/llm-analytics-traces.md +0 -98
  340. package/template/.agents/skills/integrations/posthog/references/llm-analytics-vercel-ai.md +0 -120
  341. package/template/.agents/skills/repo/repo-ci/SKILL.md +0 -265
  342. package/template/.agents/skills/repo/repo-init-next-js/SKILL.md +0 -129
  343. package/template/.agents/skills/repo/repo-init-next-js/references/file-contents.md +0 -800
  344. package/template/.agents/skills/repo/repo-init-next-js/scripts/setup.sh +0 -47
  345. package/template/.agents/skills/repo/repo-init-node/SKILL.md +0 -196
  346. package/template/.agents/skills/skill-creator/LICENSE.txt +0 -202
  347. package/template/.agents/skills/skill-creator/SKILL.md +0 -485
  348. package/template/.agents/skills/skill-creator/agents/analyzer.md +0 -274
  349. package/template/.agents/skills/skill-creator/agents/comparator.md +0 -202
  350. package/template/.agents/skills/skill-creator/agents/grader.md +0 -223
  351. package/template/.agents/skills/skill-creator/assets/eval_review.html +0 -146
  352. package/template/.agents/skills/skill-creator/eval-viewer/generate_review.py +0 -471
  353. package/template/.agents/skills/skill-creator/eval-viewer/viewer.html +0 -1325
  354. package/template/.agents/skills/skill-creator/references/schemas.md +0 -430
  355. package/template/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  356. package/template/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
  357. package/template/.agents/skills/skill-creator/scripts/generate_report.py +0 -326
  358. package/template/.agents/skills/skill-creator/scripts/improve_description.py +0 -247
  359. package/template/.agents/skills/skill-creator/scripts/package_skill.py +0 -136
  360. package/template/.agents/skills/skill-creator/scripts/quick_validate.py +0 -103
  361. package/template/.agents/skills/skill-creator/scripts/run_eval.py +0 -310
  362. package/template/.agents/skills/skill-creator/scripts/run_loop.py +0 -328
  363. package/template/.agents/skills/skill-creator/scripts/utils.py +0 -47
  364. package/template/.agents/skills/supabase/CHANGELOG.md +0 -78
  365. package/template/.agents/skills/supabase/SKILL.md +0 -149
  366. package/template/.agents/skills/supabase/assets/feedback-issue-template.md +0 -17
  367. package/template/.agents/skills/supabase/references/skill-feedback.md +0 -17
  368. package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +0 -73
  369. package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +0 -64
  370. package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +0 -170
  371. package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +0 -39
  372. package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +0 -34
  373. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +0 -55
  374. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +0 -49
  375. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +0 -46
  376. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +0 -44
  377. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +0 -41
  378. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +0 -46
  379. package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +0 -54
  380. package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +0 -53
  381. package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +0 -50
  382. package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +0 -50
  383. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +0 -56
  384. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +0 -68
  385. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +0 -50
  386. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +0 -54
  387. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +0 -45
  388. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +0 -55
  389. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +0 -55
  390. package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +0 -44
  391. package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +0 -40
  392. package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +0 -48
  393. package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +0 -43
  394. package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +0 -45
  395. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +0 -80
  396. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +0 -46
  397. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +0 -59
  398. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +0 -55
  399. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +0 -55
  400. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +0 -61
  401. package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +0 -54
  402. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +0 -50
  403. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +0 -63
  404. package/template/.agents/skills/writing/humanizer/LICENSE +0 -21
  405. package/template/.agents/skills/writing/humanizer/README.md +0 -209
  406. package/template/.agents/skills/writing/humanizer/SKILL.md +0 -456
  407. package/template/.agents/upstreams.json +0 -115
@@ -1,1698 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Live variant mode server (self-contained, zero dependencies).
4
- *
5
- * Serves the browser script (/live.js), the detection overlay (/detect.js),
6
- * uses Server-Sent Events (SSE) for server→browser push, and HTTP POST for
7
- * browser→server events. Agent communicates via HTTP long-poll (/poll).
8
- *
9
- * Usage:
10
- * node <scripts_path>/live-server.mjs # start
11
- * node <scripts_path>/live-server.mjs stop # stop + remove injected live.js tag
12
- * node <scripts_path>/live-server.mjs stop --keep-inject # stop only
13
- * node <scripts_path>/live-server.mjs --help
14
- */
15
-
16
- import http from 'node:http';
17
- import { randomUUID } from 'node:crypto';
18
- import { spawn, execFileSync } from 'node:child_process';
19
- import fs from 'node:fs';
20
- import path from 'node:path';
21
- import net from 'node:net';
22
- import { fileURLToPath } from 'node:url';
23
- import { parseDesignMd } from './lib/design-parser.mjs';
24
- import { loadContext } from './context.mjs';
25
- import {
26
- assembleLiveBrowserScript,
27
- assertLiveBrowserScriptParts,
28
- readLiveBrowserScriptParts,
29
- resolveLiveBrowserScriptParts,
30
- } from './live/browser-script-parts.mjs';
31
- import { createLiveSessionStore, GENERATION_FENCED_PHASES } from './live/session-store.mjs';
32
- import { runGenerationPreflight } from './live/generation-preflight.mjs';
33
- import { validateEvent } from './live/event-validation.mjs';
34
- import { selectAvailablePendingEvent } from './live/poll-lanes.mjs';
35
- import { createManualEditRoutes } from './live/manual-edit-routes.mjs';
36
- import {
37
- LIVE_COMMANDS,
38
- VARIANT_PROGRESS_CHECKPOINT_REASONS as VARIANT_PROGRESS_CHECKPOINT_REASON_LIST,
39
- } from './live/vocabulary.mjs';
40
- import {
41
- getDesignSidecarPath,
42
- getLiveDir,
43
- getLiveAnnotationsDir,
44
- IMPECCABLE_COMMAND_PREFIX,
45
- readLiveServerInfo,
46
- removeLiveServerInfo,
47
- resolveDesignSidecarPath,
48
- writeLiveServerInfo,
49
- } from './lib/impeccable-paths.mjs';
50
- import { countByPage as countPendingByPage } from './live/manual-edits-buffer.mjs';
51
- import { collectProjectDetectorIgnores } from './live/project-ignores.mjs';
52
- import {
53
- createManualApplyController,
54
- summarizeManualApplyFailures,
55
- } from './live/manual-apply.mjs';
56
- import {
57
- applyDeferredSvelteComponentAccepts,
58
- bumpSvelteComponentPreviewRevision,
59
- compileCheckVariants,
60
- removeAllSvelteComponentSessions,
61
- sweepInactiveSvelteComponentSessions,
62
- } from './live/svelte-component.mjs';
63
- import { enterLiveRoot } from './live/roots.mjs';
64
-
65
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
66
- // Anchor the whole process on the live roots manifest before anything derives
67
- // a path from cwd. A server started from the wrong directory re-roots itself
68
- // onto the appRoot the boot decided on instead of minting a second project.
69
- const LIVE_ROOTS = enterLiveRoot(process.cwd());
70
-
71
- // PRODUCT.md / DESIGN.md context, resolved lazily and per request so a server
72
- // that outlives an `impeccable document` run (or a context file created after
73
- // boot) reports current truth instead of a boot-time snapshot. The roots
74
- // manifest wins when the ambient resolution misses (nested app inheriting
75
- // repo-level context files).
76
- function resolveProjectContext() {
77
- const ctx = loadContext(process.cwd());
78
- const designPath = ctx.designPath
79
- ? path.resolve(process.cwd(), ctx.designPath)
80
- : (LIVE_ROOTS?.designPath && fs.existsSync(LIVE_ROOTS.designPath) ? LIVE_ROOTS.designPath : null);
81
- const hasProduct = ctx.hasProduct
82
- || !!(LIVE_ROOTS?.productPath && fs.existsSync(LIVE_ROOTS.productPath));
83
- return {
84
- ...ctx,
85
- hasProduct,
86
- hasDesign: !!designPath,
87
- resolvedDesignPath: designPath,
88
- contextDir: ctx.contextDir || LIVE_ROOTS?.contextRoot || process.cwd(),
89
- designContextDir: ctx.designContextDir
90
- || (designPath ? path.dirname(designPath) : null),
91
- };
92
- }
93
- const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
94
- const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
95
-
96
- // The browser events allowed to mint a NEW session journal. `generate` starts
97
- // a variant session at Go; `steer` mints its own request id. Every other
98
- // id-carrying event must land on an existing session (see the unknown_session
99
- // gate in the /events handler).
100
- const SESSION_CREATING_EVENT_TYPES = new Set(['generate', 'steer']);
101
- // The browser checkpoints for several unrelated reasons (see checkpointPayload
102
- // in live-browser.js). Only these two report that variant availability changed,
103
- // and only they may drive variant_progress / the *_reviewable phases.
104
- const VARIANT_PROGRESS_CHECKPOINT_REASONS = new Set(VARIANT_PROGRESS_CHECKPOINT_REASON_LIST);
105
-
106
- // ---------------------------------------------------------------------------
107
- // Port detection
108
- // ---------------------------------------------------------------------------
109
-
110
- async function findOpenPort(start = 8400) {
111
- return new Promise((resolve) => {
112
- const srv = net.createServer();
113
- srv.listen(start, '127.0.0.1', () => {
114
- const port = srv.address().port;
115
- srv.close(() => resolve(port));
116
- });
117
- srv.on('error', () => resolve(findOpenPort(start + 1)));
118
- });
119
- }
120
-
121
- // ---------------------------------------------------------------------------
122
- // Session state
123
- // ---------------------------------------------------------------------------
124
-
125
- const state = {
126
- token: null,
127
- port: null,
128
- sseClients: new Set(), // SSE response objects (server→browser push)
129
- pendingEvents: [], // browser events waiting for agent ack ({ event, leaseUntil })
130
- pendingPolls: [], // agent poll callbacks waiting for browser events
131
- nextEventSeq: 1,
132
- lastAgentPollingBroadcast: null,
133
- exitTimer: null,
134
- sessionDir: null, // per-session tmp dir for annotation screenshots
135
- sessionStore: null,
136
- leaseTimer: null,
137
- manualEditActivity: null,
138
- nextManualEditSeq: 1,
139
- // Deferreds for in-flight chat-routed Apply events. Keyed by event id; each
140
- // entry is resolved when the chat agent POSTs an ack carrying the batch
141
- // result, or rejected when the hard timeout fires.
142
- pendingApplyDeferreds: new Map(),
143
- // Updated whenever a /poll long-poll request arrives or is resolved with an
144
- // event. Used to detect "a chat agent is likely attached" without requiring
145
- // a poll to be parked at the exact moment we dispatch.
146
- lastPollAt: 0,
147
- timedOutApplyIds: new Map(),
148
- };
149
-
150
- const CHAT_POLL_FRESHNESS_MS = 60_000;
151
- const POLL_LEASE_EXPIRY_TIMER_GRACE_MS = 2;
152
- const DEBUG_MANUAL_EDIT_EVENTS = /^(1|true|yes)$/i.test(process.env.IMPECCABLE_LIVE_DEBUG_EVENTS || '');
153
-
154
- const manualApply = createManualApplyController({
155
- pendingEvents: state.pendingEvents,
156
- pendingApplyDeferreds: state.pendingApplyDeferreds,
157
- timedOutApplyIds: state.timedOutApplyIds,
158
- enqueueEvent,
159
- acknowledgePendingEvent,
160
- flushPendingPolls,
161
- recordManualEditActivity,
162
- cwd: () => process.cwd(),
163
- });
164
-
165
- const manualEditRoutes = createManualEditRoutes({
166
- getToken: () => state.token,
167
- manualApply,
168
- recordManualEditActivity,
169
- getManualEditStatus,
170
- chatAgentLikelyActive,
171
- cwd: () => process.cwd(),
172
- env: () => process.env,
173
- });
174
-
175
- function chatAgentLikelyActive() {
176
- if (state.pendingPolls.length > 0) return true;
177
- if (!state.lastPollAt) return false;
178
- return Date.now() - state.lastPollAt < CHAT_POLL_FRESHNESS_MS;
179
- }
180
-
181
- // Cap per-annotation upload size. A full 1920×1080 PNG is typically <1 MB;
182
- // cap at 10 MB to guard against runaway writes from a misbehaving client.
183
- const MAX_ANNOTATION_BYTES = 10 * 1024 * 1024;
184
-
185
- const POLLER_OWNED_EVENT_FIELDS = ['_instructions', '_completionAck', '_acceptResult'];
186
-
187
- function stripPollerOwnedEventFields(event) {
188
- if (!event || typeof event !== 'object') return;
189
- for (const key of POLLER_OWNED_EVENT_FIELDS) delete event[key];
190
- }
191
-
192
- function enqueueEvent(event) {
193
- if (!event) return;
194
- stripPollerOwnedEventFields(event);
195
- // Dedupe by (session, type), except mount failures, which are per-variant:
196
- // variant 2 failing must not be swallowed because variant 1's failure is
197
- // still queued.
198
- const duplicate = event.id && state.pendingEvents.some((entry) => (
199
- entry.event?.id === event.id
200
- && entry.event?.type === event.type
201
- && (event.type !== 'variant_mount_failed' || entry.event?.variant === event.variant)
202
- ));
203
- if (duplicate) return;
204
- state.pendingEvents.push({ event, leaseUntil: 0, seq: state.nextEventSeq++ });
205
- flushPendingPolls();
206
- }
207
-
208
- function restorePendingEventsFromStore() {
209
- if (!state.sessionStore) return;
210
- for (const snapshot of state.sessionStore.listActiveSessions()) {
211
- if (snapshot.pendingEvent) enqueueEvent(snapshot.pendingEvent);
212
- }
213
- }
214
-
215
- function findAvailablePendingEvent(now = Date.now(), types = null) {
216
- return selectAvailablePendingEvent(state.pendingEvents, { now, types });
217
- }
218
-
219
- async function leaseEvent(entry, leaseMs) {
220
- // Claim the entry before awaiting anything. prepareGenerateEventForLease
221
- // yields to the event loop, and selectAvailablePendingEvent only skips
222
- // entries whose lease is in the future — an unclaimed entry would be handed
223
- // to a second poll in that window and generated twice.
224
- entry.leaseUntil = Date.now() + leaseMs;
225
- await prepareGenerateEventForLease(entry);
226
- if (!entry.event?.id) {
227
- const idx = state.pendingEvents.indexOf(entry);
228
- if (idx !== -1) state.pendingEvents.splice(idx, 1);
229
- return entry.event;
230
- }
231
- // Re-stamp so the lease window starts when the agent actually receives the
232
- // work, not when scaffolding began.
233
- entry.leaseUntil = Date.now() + leaseMs;
234
- recordGenerateDelivery(entry);
235
- scheduleLeaseFlush();
236
- broadcastAgentPollingIfChanged();
237
- return entry.event;
238
- }
239
-
240
- function recordGenerateDelivery(entry) {
241
- const event = entry?.event;
242
- if (!event || event.type !== 'generate' || event.generationReadyAt) return;
243
- const at = Date.now();
244
- entry.event = { ...event, generationReadyAt: at };
245
- state.sessionStore?.appendEvent(entry.event);
246
- recordAgentPhase(event.id, 'generation_ready', { at });
247
- }
248
-
249
- async function prepareGenerateEventForLease(entry) {
250
- const event = entry?.event;
251
- if (!event || event.type !== 'generate' || event.scaffoldAttempted) return;
252
-
253
- recordAgentPhase(event.id, 'picked_up');
254
- recordAgentPhase(event.id, 'scaffolding');
255
- const result = await runGenerationPreflight(event, {
256
- cwd: process.cwd(),
257
- scriptsDir: __dirname,
258
- });
259
- entry.event = {
260
- ...event,
261
- scaffoldAttempted: true,
262
- scaffoldDurationMs: result.durationMs ?? null,
263
- ...(result.ok ? { scaffold: result.scaffold } : { scaffoldError: result.error || result.reason }),
264
- };
265
- state.sessionStore?.appendEvent(entry.event);
266
- recordAgentPhase(event.id, result.ok ? 'source_ready' : 'scaffold_fallback', {
267
- durationMs: result.durationMs ?? null,
268
- previewMode: result.scaffold?.previewMode || 'source',
269
- });
270
- }
271
-
272
- function recordAgentPhase(id, phase, details = {}) {
273
- if (!id) return;
274
- const event = {
275
- type: 'agent_phase',
276
- id,
277
- phase,
278
- at: Date.now(),
279
- ...details,
280
- };
281
- state.sessionStore?.appendEvent(event);
282
- broadcast(event);
283
- }
284
-
285
- /**
286
- * Detect a browser that missed the generation `done` broadcast.
287
- *
288
- * The preflight no longer writes the scaffold into source for source-preview
289
- * targets (the agent writes wrapper + variants in one atomic edit), so the old
290
- * scaffold-write full-reload that opened the "stranded at 0/N" race is gone.
291
- * This recovery stays as defense in depth: any framework reload that drops the
292
- * agent's variant write + `done` while the browser is mid-reload leaves the new
293
- * page in GENERATING at 0/N. That resumed page always checkpoints
294
- * (`browser_resumed`), so a checkpoint claiming "still generating, variants
295
- * missing" for a session whose generation already completed is direct
296
- * evidence of the miss. Rebuild the `done` payload from the snapshot so the
297
- * caller can re-broadcast it; the browser's done handler is idempotent and
298
- * falls back to injecting variants from source.
299
- *
300
- * Keys on the store's monotone `generationCompletedAt`, not `phase` — the
301
- * behind checkpoint itself regresses `phase` to `generating`, and a browser
302
- * that misses the redelivered `done` too (another reload) must still trigger
303
- * redelivery from its next checkpoint.
304
- */
305
- function detectMissedGenerationCompletion(event) {
306
- if (!event?.id || event.type !== 'checkpoint') return null;
307
- if (event.phase !== 'generating') return null;
308
- if (!variantCountLooksBehind(event.arrivedVariants, event.expectedVariants)) return null;
309
- if (!state.sessionStore) return null;
310
- let snapshot = null;
311
- try {
312
- snapshot = state.sessionStore.getSnapshot(event.id);
313
- } catch {
314
- return null;
315
- }
316
- return missedCompletionFromSnapshot(snapshot);
317
- }
318
-
319
- function variantCountLooksBehind(arrivedValue, expectedValue) {
320
- const arrived = Number(arrivedValue) || 0;
321
- const expected = Number(expectedValue) || 0;
322
- return arrived <= 0 || (expected > 0 && arrived < expected);
323
- }
324
-
325
- function missedCompletionFromSnapshot(snapshot) {
326
- if (!snapshot?.id || !snapshot.generationCompletedAt) return null;
327
- if (snapshot.generationCanceled) return null;
328
- // Accept/discard already underway: the browser is no longer waiting on
329
- // generation, and a late `done` there would collide with teardown.
330
- if (GENERATION_FENCED_PHASES.has(snapshot.phase)) return null;
331
- const file = snapshot.sourceFile || snapshot.previewFile;
332
- if (!file) return null;
333
- return {
334
- type: 'done',
335
- id: snapshot.id,
336
- file,
337
- sourceFile: snapshot.sourceFile || undefined,
338
- previewFile: snapshot.previewFile || undefined,
339
- previewMode: snapshot.previewMode || undefined,
340
- redelivered: true,
341
- };
342
- }
343
-
344
- function recordGenerationCheckpoint(event) {
345
- if (!event?.id || event.type !== 'checkpoint') return;
346
- if (generationIsFenced(event.id)) return;
347
- // Only checkpoints that report a change in variant availability are
348
- // generation progress. The browser also checkpoints for durability on Tune
349
- // slider drags, resumes, and anchor recovery; treating those as progress
350
- // echoed `variant_progress` straight back to the browser that sent it, which
351
- // remounts the component preview mid-drag (reverting the user's live param
352
- // edit and detaching the popover's element), and permanently latched the
353
- // *_reviewable phases from the wrong trigger, corrupting generation timings.
354
- if (!VARIANT_PROGRESS_CHECKPOINT_REASONS.has(event.reason)) return;
355
- const arrived = Number(event.arrivedVariants) || 0;
356
- const expected = Number(event.expectedVariants) || 0;
357
- if (arrived <= 0 || expected <= 0) return;
358
- const previewMode = event.previewMode || 'source';
359
- const previewFile = event.previewFile || event.file;
360
- if (previewFile) {
361
- broadcast({
362
- type: 'variant_progress',
363
- id: event.id,
364
- file: previewFile,
365
- sourceFile: event.sourceFile || (previewMode === 'source' ? previewFile : undefined),
366
- previewFile,
367
- previewMode,
368
- arrivedVariants: arrived,
369
- expectedVariants: expected,
370
- publicationKind: event.publicationKind || 'variants',
371
- });
372
- }
373
- const details = {
374
- arrivedVariants: arrived,
375
- expectedVariants: expected,
376
- checkpointReason: event.reason || null,
377
- };
378
- const at = Date.now();
379
- if (!generationPhaseAlreadyRecorded(event.id, 'first_reviewable')) {
380
- recordAgentPhase(event.id, 'first_reviewable', { ...details, at });
381
- }
382
- if (arrived >= 2 && expected >= 3 && !generationPhaseAlreadyRecorded(event.id, 'second_reviewable')) {
383
- recordAgentPhase(event.id, 'second_reviewable', { ...details, at });
384
- }
385
- if (arrived >= expected && !generationPhaseAlreadyRecorded(event.id, 'all_variants_ready')) {
386
- recordAgentPhase(event.id, 'all_variants_ready', { ...details, at });
387
- }
388
- }
389
-
390
- function generationIsFenced(id) {
391
- if (!state.sessionStore || !id) return false;
392
- try {
393
- const snapshot = state.sessionStore.getSnapshot(id, { includeCompleted: true });
394
- return snapshot?.generationCanceled === true;
395
- } catch {
396
- return false;
397
- }
398
- }
399
-
400
- function generationPhaseAlreadyRecorded(id, phase) {
401
- if (!state.sessionStore) return false;
402
- try {
403
- const snapshot = state.sessionStore.getSnapshot(id, { includeCompleted: true });
404
- return !!snapshot?.generationTimings?.[phase];
405
- } catch {
406
- return false;
407
- }
408
- }
409
-
410
- function acknowledgePendingEvent(id, sourceEventType) {
411
- if (!id) return false;
412
- const idx = state.pendingEvents.findIndex((entry) => (
413
- entry.event?.id === id
414
- && (!sourceEventType || entry.event?.type === sourceEventType)
415
- ));
416
- if (idx === -1) return false;
417
- const acknowledged = state.pendingEvents[idx].event;
418
- state.pendingEvents.splice(idx, 1);
419
- scheduleLeaseFlush();
420
- broadcastAgentPollingIfChanged();
421
- return acknowledged;
422
- }
423
-
424
- function releasePendingEvent(id, sourceEventType) {
425
- const entry = state.pendingEvents.find((item) => (
426
- item.event?.id === id
427
- && (!sourceEventType || item.event?.type === sourceEventType)
428
- ));
429
- if (!entry) return null;
430
- entry.leaseUntil = 0;
431
- scheduleLeaseFlush();
432
- return entry.event;
433
- }
434
-
435
- function retirePendingGeneration(id) {
436
- if (!id) return 0;
437
- let retired = 0;
438
- for (let index = state.pendingEvents.length - 1; index >= 0; index -= 1) {
439
- const event = state.pendingEvents[index]?.event;
440
- if (event?.id !== id || event.type !== 'generate') continue;
441
- state.pendingEvents.splice(index, 1);
442
- retired += 1;
443
- }
444
- if (retired > 0) {
445
- scheduleLeaseFlush();
446
- broadcastAgentPollingIfChanged();
447
- }
448
- return retired;
449
- }
450
-
451
- function findPendingEventById(id, sourceEventType) {
452
- if (!id) return null;
453
- const entry = state.pendingEvents.find((item) => (
454
- item.event?.id === id
455
- && (!sourceEventType || item.event?.type === sourceEventType)
456
- ));
457
- return entry?.event || null;
458
- }
459
-
460
- function summarizePendingEventForStatus(entry) {
461
- const event = entry.event || {};
462
- const summary = {
463
- id: event.id,
464
- type: event.type,
465
- leased: isLeased(entry),
466
- leaseUntil: entry.leaseUntil || null,
467
- };
468
- if (event.type === 'manual_edit_apply') {
469
- summary.pageUrl = event.pageUrl || null;
470
- summary.chunk = event.chunk || null;
471
- summary.repair = event.repair || null;
472
- summary.evidencePath = event.evidencePath || null;
473
- summary.agentAction = event.agentAction || manualApply.buildAgentAction(event);
474
- summary.manualApplySummary = manualApply.summarizeEvent(event, manualApply.getDeferred(event.id)?.batch || event.batch);
475
- }
476
- return summary;
477
- }
478
-
479
- function summarizeActiveSessionForClient(snapshot = {}) {
480
- return {
481
- id: snapshot.id,
482
- phase: snapshot.phase,
483
- pageUrl: snapshot.pageUrl ?? null,
484
- sourceFile: snapshot.sourceFile ?? null,
485
- previewFile: snapshot.previewFile ?? null,
486
- previewMode: snapshot.previewMode ?? null,
487
- expectedVariants: snapshot.expectedVariants ?? 0,
488
- arrivedVariants: snapshot.arrivedVariants ?? 0,
489
- visibleVariant: snapshot.visibleVariant ?? null,
490
- checkpointRevision: snapshot.checkpointRevision ?? 0,
491
- browserCheckpointRevision: snapshot.browserCheckpointRevision ?? snapshot.checkpointRevision ?? 0,
492
- publicationCheckpointRevision: snapshot.publicationCheckpointRevision ?? 0,
493
- paramValues: snapshot.paramValues || {},
494
- generationPhase: snapshot.generationPhase ?? null,
495
- generationCompletedAt: snapshot.generationCompletedAt ?? null,
496
- generationCanceled: snapshot.generationCanceled === true,
497
- cancelReason: snapshot.cancelReason ?? null,
498
- // Render truth, so a browser with no localStorage can rehydrate to the
499
- // same comparison the server already knows about.
500
- mountedVariants: Array.isArray(snapshot.mountedVariants) ? snapshot.mountedVariants : [],
501
- mountFailures: Array.isArray(snapshot.mountFailures) ? snapshot.mountFailures : [],
502
- renderState: snapshot.renderState ?? null,
503
- };
504
- }
505
-
506
- function activeSessionSummaries() {
507
- if (!state.sessionStore) return [];
508
- return state.sessionStore.listActiveSessions().map((snapshot) => summarizeActiveSessionForClient(snapshot));
509
- }
510
-
511
- function cancelQueuedAnonymousExitEvents() {
512
- let removed = 0;
513
- for (let i = state.pendingEvents.length - 1; i >= 0; i -= 1) {
514
- const event = state.pendingEvents[i]?.event;
515
- if (event?.type !== 'exit' || event.id) continue;
516
- state.pendingEvents.splice(i, 1);
517
- removed += 1;
518
- }
519
- if (removed > 0) {
520
- scheduleLeaseFlush();
521
- broadcastAgentPollingIfChanged();
522
- }
523
- return removed;
524
- }
525
-
526
- function scheduleLeaseFlush() {
527
- if (state.leaseTimer) {
528
- clearTimeout(state.leaseTimer);
529
- state.leaseTimer = null;
530
- }
531
- const now = Date.now();
532
- const nextLeaseUntil = state.pendingEvents
533
- .map((entry) => entry.leaseUntil || 0)
534
- .filter((leaseUntil) => leaseUntil > now)
535
- .sort((a, b) => a - b)[0];
536
- if (!nextLeaseUntil) return;
537
- state.leaseTimer = setTimeout(() => {
538
- state.leaseTimer = null;
539
- flushPendingPolls();
540
- broadcastAgentPollingIfChanged();
541
- }, Math.max(0, nextLeaseUntil - now + POLL_LEASE_EXPIRY_TIMER_GRACE_MS));
542
- }
543
-
544
- function flushPendingPolls() {
545
- let changed = false;
546
- while (state.pendingPolls.length > 0) {
547
- let pollIndex = -1;
548
- let entry = null;
549
- for (let index = 0; index < state.pendingPolls.length; index += 1) {
550
- const candidate = findAvailablePendingEvent(Date.now(), state.pendingPolls[index].types);
551
- if (!candidate) continue;
552
- pollIndex = index;
553
- entry = candidate;
554
- break;
555
- }
556
- if (!entry) {
557
- scheduleLeaseFlush();
558
- broadcastAgentPollingIfChanged();
559
- return;
560
- }
561
- const [poll] = state.pendingPolls.splice(pollIndex, 1);
562
- // leaseEvent is async (it may scaffold source), but it claims the entry
563
- // synchronously, so the next loop iteration will not re-select it. Resolve
564
- // the poll when the lease settles rather than awaiting here, so one slow
565
- // scaffold never delays the other parked polls. On the exceptional failure
566
- // path, answer `timeout` so the agent re-polls; the claim stays until the
567
- // lease expires, which keeps a deterministic failure from hot-looping.
568
- leaseEvent(entry, poll.leaseMs).then(poll.resolve, (error) => {
569
- console.error('[live] lease failed for ' + (entry.event?.id || 'unknown') + ': ' + (error?.message || error));
570
- poll.resolve({ type: 'timeout' });
571
- });
572
- changed = true;
573
- }
574
- scheduleLeaseFlush();
575
- if (changed) broadcastAgentPollingIfChanged();
576
- }
577
-
578
- function isLeased(entry) {
579
- return !!(entry?.leaseUntil && entry.leaseUntil > Date.now());
580
- }
581
-
582
- function agentPollingConnected() {
583
- // A leased event only proves that a poll returned once. The foreground task
584
- // may have ended immediately afterward, so only an actively waiting poll is
585
- // evidence that steering can wake the task right now.
586
- return state.pendingPolls.length > 0;
587
- }
588
-
589
- function broadcastAgentPollingIfChanged() {
590
- const connected = agentPollingConnected();
591
- if (state.lastAgentPollingBroadcast === connected) return;
592
- state.lastAgentPollingBroadcast = connected;
593
- broadcast({ type: 'agent_polling', connected });
594
- }
595
-
596
- /** Push a message to all connected SSE clients. */
597
- function broadcast(msg) {
598
- const data = 'data: ' + JSON.stringify(msg) + '\n\n';
599
- for (const res of state.sseClients) {
600
- try { res.write(data); } catch { /* client gone */ }
601
- }
602
- }
603
-
604
- function recordManualEditActivity(type, details = {}) {
605
- const entry = {
606
- seq: state.nextManualEditSeq++,
607
- type,
608
- ts: new Date().toISOString(),
609
- ...details,
610
- };
611
- state.manualEditActivity = entry;
612
- if (DEBUG_MANUAL_EDIT_EVENTS) {
613
- try {
614
- const filePath = path.join(getLiveDir(process.cwd()), 'manual-edit-events.jsonl');
615
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
616
- fs.appendFileSync(filePath, JSON.stringify(entry) + '\n');
617
- } catch {
618
- /* diagnostics are best-effort; never block live mode on observability */
619
- }
620
- }
621
- broadcast(entry);
622
- return entry;
623
- }
624
-
625
- function getManualEditStatus() {
626
- try {
627
- const { totalCount, perPage } = countPendingByPage(process.cwd());
628
- return { totalCount, perPage, lastActivity: state.manualEditActivity };
629
- } catch (err) {
630
- return {
631
- totalCount: null,
632
- perPage: {},
633
- lastActivity: state.manualEditActivity,
634
- error: err.message,
635
- };
636
- }
637
- }
638
-
639
- // ---------------------------------------------------------------------------
640
- // Load scripts
641
- // ---------------------------------------------------------------------------
642
-
643
- function loadBrowserScripts() {
644
- // Detection script: prefer the skill-bundled detector, then fall back to
645
- // source/npm package locations for local development and older installs.
646
- // This one IS cached — detect.js rarely changes during a session.
647
- const detectPaths = [
648
- path.join(__dirname, 'detector', 'detect-antipatterns-browser.js'),
649
- path.join(__dirname, '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
650
- path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
651
- path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
652
- ];
653
- let detectScript = '';
654
- for (const p of detectPaths) {
655
- try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ }
656
- }
657
-
658
- // Browser script parts: DO NOT cache. Return paths so the /live.js handler
659
- // can re-read every part on each request. Editing browser code during
660
- // iteration should land on the next tab reload, not require a server restart.
661
- const liveScriptParts = resolveLiveBrowserScriptParts(__dirname);
662
- try {
663
- assertLiveBrowserScriptParts(liveScriptParts);
664
- } catch (err) {
665
- process.stderr.write('Error: ' + err.message + '\n');
666
- process.exit(1);
667
- }
668
-
669
- return { detectScript, liveScriptParts };
670
- }
671
-
672
- function hasProjectContext() {
673
- // PRODUCT.md carries brand voice / anti-references — that's what determines
674
- // whether variants are brand-aware. DESIGN.md (visual tokens) is a separate
675
- // concern, surfaced by the design panel's own empty state.
676
- return !!resolveProjectContext().hasProduct;
677
- }
678
-
679
- function statOrNull(filePath) {
680
- try { return fs.statSync(filePath); } catch { return null; }
681
- }
682
-
683
- // Strict loopback-origin test for CORS. Parses the Origin as a URL (never a
684
- // substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com`
685
- // fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback.
686
- function isLoopbackOrigin(origin) {
687
- if (typeof origin !== 'string' || origin.length === 0) return false;
688
- let parsed;
689
- try { parsed = new URL(origin); } catch { return false; }
690
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false;
691
- const host = parsed.hostname.toLowerCase();
692
- return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
693
- }
694
-
695
- // HTTP request handler
696
- // ---------------------------------------------------------------------------
697
-
698
- function createRequestHandler({ detectScript, liveScriptParts }) {
699
- return (req, res) => {
700
- const url = new URL(req.url, `http://localhost:${state.port}`);
701
- // Token-or-loopback CORS. Reflect the caller's Origin when it is a
702
- // loopback origin OR the request carries the valid session token, always
703
- // paired with `Vary: Origin` so an intermediary cache never serves a
704
- // response authorized for one origin to another. A remote page (e.g.
705
- // https://evil.example probing the port from a tab open on the same
706
- // machine) has no token and gets no Access-Control-Allow-Origin, so its
707
- // JS-initiated fetch cannot read any response. The token branch exists for
708
- // dev servers on non-localhost loopback aliases (ddev's *.ddev.site,
709
- // Valet's *.test, hosts-file entries): the injected classic <script src>
710
- // delivers the token to the page regardless of origin, every overlay
711
- // request carries it in the query string (preflights included, since
712
- // OPTIONS hits the same URL), and a token bearer is already fully
713
- // authorized on every route — the token is the security boundary, not the
714
- // origin. Requests with no Origin header (script tags, curl, the agent's
715
- // own fetches) are not subject to CORS and keep working; no ACAO header
716
- // is needed for them.
717
- const origin = req.headers.origin;
718
- if (origin && (isLoopbackOrigin(origin) || url.searchParams.get('token') === state.token)) {
719
- res.setHeader('Access-Control-Allow-Origin', origin);
720
- res.setHeader('Vary', 'Origin');
721
- }
722
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
723
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
724
- if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
725
-
726
- const p = url.pathname;
727
-
728
- // --- Scripts ---
729
- if (p === '/live.js') {
730
- // Token-gated: the script body embeds state.token, which unlocks every
731
- // token-guarded route. Serving it unauthenticated let any local page read
732
- // the token and drive the session. The injected <script src> carries
733
- // `?token=...` (see live-inject.mjs). A missing/wrong token → 401.
734
- if (url.searchParams.get('token') !== state.token) {
735
- res.writeHead(401, { 'Content-Type': 'text/plain' });
736
- res.end('Unauthorized');
737
- return;
738
- }
739
- // Re-read from disk each request so edits to live-browser.js land on
740
- // the next tab reload. No-store headers prevent browser caching across
741
- // sessions — during iteration, a cached old script silently breaks
742
- // every subsequent session.
743
- let parts;
744
- try {
745
- parts = readLiveBrowserScriptParts(liveScriptParts);
746
- } catch (err) {
747
- res.writeHead(500, { 'Content-Type': 'text/plain' });
748
- res.end('Error reading live browser scripts: ' + err.message);
749
- return;
750
- }
751
- const body = assembleLiveBrowserScript({
752
- token: state.token,
753
- port: state.port,
754
- vocabulary: LIVE_COMMANDS,
755
- commandPrefix: IMPECCABLE_COMMAND_PREFIX,
756
- appRoot: process.cwd(),
757
- parts,
758
- // Read per request rather than cached, so editing the config and
759
- // reloading the tab is enough to pick up a new waiver. Config comes
760
- // from every root the session spans (appRoot, contextRoot, repoRoot):
761
- // in a monorepo the hook and the CLI key it at the repo root, which
762
- // is not the appRoot this process chdir'd onto.
763
- projectIgnores: collectProjectDetectorIgnores({
764
- appRoot: process.cwd(),
765
- contextRoot: LIVE_ROOTS?.contextRoot,
766
- repoRoot: LIVE_ROOTS?.repoRoot,
767
- scriptsDir: __dirname,
768
- }),
769
- });
770
- res.writeHead(200, {
771
- 'Content-Type': 'application/javascript; charset=utf-8',
772
- 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
773
- 'Pragma': 'no-cache',
774
- });
775
- res.end(body);
776
- return;
777
- }
778
- if (p === '/detect.js' || p === '/') {
779
- if (!detectScript) { res.writeHead(404); res.end('Not available'); return; }
780
- res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
781
- res.end(detectScript);
782
- return;
783
- }
784
-
785
- // --- Vendored modern-screenshot (UMD build) ---
786
- // Lazy-loaded by live.js when the user clicks Go; exposes
787
- // window.modernScreenshot.domToBlob(...) for capture.
788
- if (p === '/modern-screenshot.js') {
789
- const vendorPath = path.join(__dirname, 'modern-screenshot.umd.js');
790
- try {
791
- res.writeHead(200, {
792
- 'Content-Type': 'application/javascript',
793
- 'Cache-Control': 'public, max-age=31536000, immutable',
794
- });
795
- res.end(fs.readFileSync(vendorPath));
796
- } catch {
797
- res.writeHead(404); res.end('Vendor script not found');
798
- }
799
- return;
800
- }
801
-
802
- // --- Annotation upload (browser → server, raw PNG body) ---
803
- // Client generates the eventId, POSTs the PNG, then POSTs the generate
804
- // event with screenshotPath already set. Keeps bytes out of the SSE/poll
805
- // bridge and preserves the "one shot from the user's POV" UX.
806
- if (p === '/annotation' && req.method === 'POST') {
807
- const token = url.searchParams.get('token');
808
- if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
809
- const eventId = url.searchParams.get('eventId');
810
- if (!eventId || !/^[A-Za-z0-9_-]{1,64}$/.test(eventId)) {
811
- res.writeHead(400, { 'Content-Type': 'application/json' });
812
- res.end(JSON.stringify({ error: 'Invalid eventId' }));
813
- return;
814
- }
815
- if ((req.headers['content-type'] || '').toLowerCase() !== 'image/png') {
816
- res.writeHead(415, { 'Content-Type': 'application/json' });
817
- res.end(JSON.stringify({ error: 'Content-Type must be image/png' }));
818
- return;
819
- }
820
- if (!state.sessionDir) {
821
- res.writeHead(500, { 'Content-Type': 'application/json' });
822
- res.end(JSON.stringify({ error: 'Session dir unavailable' }));
823
- return;
824
- }
825
- const chunks = [];
826
- let total = 0;
827
- let aborted = false;
828
- req.on('data', (c) => {
829
- if (aborted) return;
830
- total += c.length;
831
- if (total > MAX_ANNOTATION_BYTES) {
832
- aborted = true;
833
- res.writeHead(413, { 'Content-Type': 'application/json' });
834
- res.end(JSON.stringify({ error: 'Payload too large' }));
835
- req.destroy();
836
- return;
837
- }
838
- chunks.push(c);
839
- });
840
- req.on('end', () => {
841
- if (aborted) return;
842
- const absPath = path.join(state.sessionDir, eventId + '.png');
843
- try {
844
- fs.writeFileSync(absPath, Buffer.concat(chunks));
845
- } catch (err) {
846
- res.writeHead(500, { 'Content-Type': 'application/json' });
847
- res.end(JSON.stringify({ error: 'Write failed: ' + err.message }));
848
- return;
849
- }
850
- res.writeHead(200, { 'Content-Type': 'application/json' });
851
- res.end(JSON.stringify({ ok: true, path: absPath }));
852
- });
853
- req.on('error', () => {
854
- if (!aborted) {
855
- res.writeHead(500, { 'Content-Type': 'application/json' });
856
- res.end(JSON.stringify({ error: 'Upload failed' }));
857
- }
858
- });
859
- return;
860
- }
861
-
862
- // --- Health ---
863
- if (p === '/status') {
864
- const token = url.searchParams.get('token');
865
- if (token !== state.token) { res.writeHead(401, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
866
- const sessions = activeSessionSummaries();
867
- res.writeHead(200, { 'Content-Type': 'application/json' });
868
- res.end(JSON.stringify({
869
- status: 'ok',
870
- port: state.port,
871
- connectedClients: state.sseClients.size,
872
- pendingEvents: state.pendingEvents.map((entry) => summarizePendingEventForStatus(entry)),
873
- agentPolling: agentPollingConnected(),
874
- activeSessions: sessions,
875
- manualEdits: getManualEditStatus(),
876
- }));
877
- return;
878
- }
879
-
880
- if (p === '/health') {
881
- res.writeHead(200, { 'Content-Type': 'application/json' });
882
- res.end(JSON.stringify({
883
- status: 'ok', port: state.port, mode: 'variant',
884
- hasProjectContext: hasProjectContext(),
885
- connectedClients: state.sseClients.size,
886
- }));
887
- return;
888
- }
889
-
890
- // --- Design system (unified v2 response) + raw ---
891
- // /design-system.json returns both parsed DESIGN.md and .impeccable/design.json
892
- // sidecar when present. Panel merges them:
893
- // { present, parsed, sidecar, hasMd, hasSidecar,
894
- // mdNewerThanJson, parseError?, sidecarError? }
895
- // - parsed: output of parseDesignMd (frontmatter
896
- // + the canonical sections) when DESIGN.md exists.
897
- // - sidecar: .impeccable/design.json contents when present.
898
- // Expected shape: schemaVersion 2, carrying
899
- // extensions + components + narrative.
900
- // /design-system/raw returns DESIGN.md markdown verbatim
901
- if (p === '/design-system.json' || p === '/design-system/raw') {
902
- const token = url.searchParams.get('token');
903
- if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
904
-
905
- const projectContext = resolveProjectContext();
906
- const mdPath = projectContext.resolvedDesignPath;
907
- const jsonPath = resolveDesignSidecarPath(process.cwd(), projectContext.designContextDir || projectContext.contextDir) || getDesignSidecarPath(process.cwd());
908
- const mdStat = statOrNull(mdPath);
909
- const jsonStat = statOrNull(jsonPath);
910
-
911
- if (p === '/design-system/raw') {
912
- if (!mdStat) { res.writeHead(404); res.end('Not found'); return; }
913
- res.writeHead(200, { 'Content-Type': 'text/markdown; charset=utf-8' });
914
- res.end(fs.readFileSync(mdPath, 'utf-8'));
915
- return;
916
- }
917
-
918
- if (!mdStat && !jsonStat) {
919
- res.writeHead(404, { 'Content-Type': 'application/json' });
920
- res.end(JSON.stringify({ present: false }));
921
- return;
922
- }
923
-
924
- const response = {
925
- present: true,
926
- hasMd: !!mdStat,
927
- hasSidecar: !!jsonStat,
928
- mdNewerThanJson: !!(mdStat && jsonStat && mdStat.mtimeMs > jsonStat.mtimeMs + 1000),
929
- };
930
-
931
- if (mdStat) {
932
- try {
933
- response.parsed = parseDesignMd(fs.readFileSync(mdPath, 'utf-8'));
934
- } catch (err) {
935
- response.parseError = err.message;
936
- }
937
- }
938
-
939
- if (jsonStat) {
940
- try {
941
- response.sidecar = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
942
- } catch (err) {
943
- response.sidecarError = 'Failed to parse .impeccable/design.json: ' + err.message;
944
- }
945
- }
946
-
947
- res.writeHead(200, { 'Content-Type': 'application/json' });
948
- res.end(JSON.stringify(response));
949
- return;
950
- }
951
-
952
- // --- Source file (no-HMR fallback) ---
953
- if (p === '/source') {
954
- const token = url.searchParams.get('token');
955
- if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
956
- const filePath = url.searchParams.get('path');
957
- if (!filePath || filePath.includes('..')) { res.writeHead(400); res.end('Bad path'); return; }
958
- const absPath = path.resolve(process.cwd(), filePath);
959
- let realRoot, realTarget;
960
- try {
961
- realRoot = fs.realpathSync(process.cwd());
962
- realTarget = fs.realpathSync(absPath);
963
- } catch {
964
- res.writeHead(404); res.end('File not found'); return;
965
- }
966
- // Confine to the project root after symlink resolution. A bare
967
- // `startsWith(cwd)` string check lets a sibling dir whose name extends the
968
- // root name (projeto -> projeto-backup) slip through; compare on the
969
- // relative path instead (same pattern as sessionFileMetadataFromPollReply
970
- // below). An empty rel means the request resolved to the root directory
971
- // itself, which this file route never serves.
972
- const rel = path.relative(realRoot, realTarget);
973
- if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) { res.writeHead(403); res.end('Forbidden'); return; }
974
- let content;
975
- try { content = fs.readFileSync(realTarget, 'utf-8'); }
976
- catch { res.writeHead(404); res.end('File not found'); return; }
977
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
978
- res.end(content);
979
- return;
980
- }
981
-
982
- // --- SSE: server→browser push (replaces WebSocket) ---
983
- if (p === '/events' && req.method === 'GET') {
984
- const token = url.searchParams.get('token');
985
- if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
986
- clearTimeout(state.exitTimer);
987
- state.exitTimer = null;
988
- cancelQueuedAnonymousExitEvents();
989
- res.writeHead(200, {
990
- 'Content-Type': 'text/event-stream',
991
- 'Cache-Control': 'no-cache',
992
- 'Connection': 'keep-alive',
993
- });
994
- res.write('data: ' + JSON.stringify({
995
- type: 'connected',
996
- hasProjectContext: hasProjectContext(),
997
- agentPolling: agentPollingConnected(),
998
- activeSessions: activeSessionSummaries(),
999
- }) + '\n\n');
1000
-
1001
- state.sseClients.add(res);
1002
-
1003
- // Keepalive: SSE comment every 30s prevents silent connection drops.
1004
- const heartbeat = setInterval(() => {
1005
- try { res.write(': keepalive\n\n'); } catch { clearInterval(heartbeat); }
1006
- }, SSE_HEARTBEAT_INTERVAL);
1007
-
1008
- req.on('close', () => {
1009
- clearInterval(heartbeat);
1010
- state.sseClients.delete(res);
1011
- if (state.sseClients.size === 0) {
1012
- clearTimeout(state.exitTimer);
1013
- state.exitTimer = setTimeout(() => {
1014
- if (state.sseClients.size === 0) enqueueEvent({ type: 'exit' });
1015
- }, 8000);
1016
- }
1017
- });
1018
- return;
1019
- }
1020
-
1021
- if (manualEditRoutes(req, res, url)) return;
1022
-
1023
- // --- Browser→server events (replaces WebSocket messages) ---
1024
- if (p === '/events' && req.method === 'POST') {
1025
- let body = '';
1026
- req.on('data', (c) => { body += c; });
1027
- req.on('end', () => {
1028
- let msg;
1029
- try { msg = JSON.parse(body); } catch {
1030
- res.writeHead(400, { 'Content-Type': 'application/json' });
1031
- res.end(JSON.stringify({ error: 'Invalid JSON' }));
1032
- return;
1033
- }
1034
- if (msg.token !== state.token) {
1035
- res.writeHead(401, { 'Content-Type': 'application/json' });
1036
- res.end(JSON.stringify({ error: 'Unauthorized' }));
1037
- return;
1038
- }
1039
- // Defense in depth: manual copy edits must use the staged stash/apply
1040
- // endpoints. The direct Save event path is disabled in the browser.
1041
- if (msg.type === 'manual_edits') {
1042
- res.writeHead(400, { 'Content-Type': 'application/json' });
1043
- res.end(JSON.stringify({ error: 'manual_edits must POST to /manual-edit-stash, not /events' }));
1044
- return;
1045
- }
1046
- if (msg.type === 'manual_edit_apply') {
1047
- res.writeHead(400, { 'Content-Type': 'application/json' });
1048
- res.end(JSON.stringify({ error: 'manual_edit_apply is disabled; use /manual-edit-stash then /manual-edit-commit' }));
1049
- return;
1050
- }
1051
- const error = validateEvent(msg);
1052
- if (error) {
1053
- res.writeHead(400, { 'Content-Type': 'application/json' });
1054
- res.end(JSON.stringify({ error }));
1055
- return;
1056
- }
1057
- stripPollerOwnedEventFields(msg);
1058
- if (msg.type === 'agent_phase') {
1059
- recordAgentPhase(msg.id, msg.phase, {
1060
- ...(Number.isFinite(msg.durationMs) ? { durationMs: msg.durationMs } : {}),
1061
- owner: typeof msg.owner === 'string' ? msg.owner : undefined,
1062
- });
1063
- res.writeHead(200, { 'Content-Type': 'application/json' });
1064
- res.end(JSON.stringify({ ok: true }));
1065
- return;
1066
- }
1067
- // Only the events that START a session may create its journal.
1068
- // Everything else (checkpoints, mount acks, accept/discard) must
1069
- // reference a session THIS store already knows: appendEvent creates a
1070
- // journal for any id it is handed, so without this gate a browser
1071
- // resuming another project's session from per-origin storage (two
1072
- // apps sharing a localhost port) materializes a ghost session here
1073
- // that keeps reattaching after every discard.
1074
- if (msg.id && state.sessionStore
1075
- && !SESSION_CREATING_EVENT_TYPES.has(msg.type)
1076
- && !state.sessionStore.has(msg.id)) {
1077
- res.writeHead(404, { 'Content-Type': 'application/json' });
1078
- res.end(JSON.stringify({ error: 'unknown_session', id: msg.id }));
1079
- return;
1080
- }
1081
- const missedCompletion = detectMissedGenerationCompletion(msg);
1082
- if (state.sessionStore && msg.id) {
1083
- try {
1084
- state.sessionStore.appendEvent(msg);
1085
- } catch (err) {
1086
- res.writeHead(500, { 'Content-Type': 'application/json' });
1087
- res.end(JSON.stringify({ error: 'session_store_append_failed', message: err.message }));
1088
- return;
1089
- }
1090
- }
1091
- if (msg.type === 'accept' || msg.type === 'discard') {
1092
- retirePendingGeneration(msg.id);
1093
- }
1094
- recordGenerationCheckpoint(msg);
1095
- if (missedCompletion) broadcast(missedCompletion);
1096
- if (msg.type === 'exit') {
1097
- cleanupSvelteComponentSessionsBeforeExit();
1098
- }
1099
- // An ORPHANED discard is the browser reporting that the session's
1100
- // wrapper no longer exists in source (edited or regenerated away).
1101
- // There is no cleanup for an agent to perform, and asking one to run
1102
- // the normal discard flow would just fail against the missing
1103
- // scaffolding, so the server terminalizes the session itself and the
1104
- // event stays out of the poll queue.
1105
- const orphanedDiscard = msg.type === 'discard' && msg.orphaned === true;
1106
- if (orphanedDiscard && state.sessionStore && msg.id) {
1107
- try {
1108
- state.sessionStore.appendEvent({ type: 'discarded', id: msg.id, orphaned: true });
1109
- } catch { /* the discard_requested phase already left the resumable set */ }
1110
- }
1111
- // `variant_mounted` is the happy path: it is journaled above so the
1112
- // snapshot carries render truth, but there is nothing for the agent to
1113
- // do about it, so it stays out of the poll queue and off the SSE bus.
1114
- // `variant_mount_failed` is the opposite: the agent published something
1115
- // the browser could not render, and only the agent can fix it, so it
1116
- // goes to the queue as a first-class event.
1117
- if (msg.type !== 'checkpoint' && msg.type !== 'variant_mounted' && !orphanedDiscard) {
1118
- enqueueEvent(msg);
1119
- }
1120
- res.writeHead(200, { 'Content-Type': 'application/json' });
1121
- res.end(JSON.stringify({ ok: true }));
1122
- });
1123
- return;
1124
- }
1125
-
1126
- // --- Stop ---
1127
- if (p === '/stop') {
1128
- const token = url.searchParams.get('token');
1129
- if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
1130
- res.writeHead(200, { 'Content-Type': 'text/plain' });
1131
- res.end('stopping');
1132
- shutdown();
1133
- return;
1134
- }
1135
-
1136
- // --- Agent poll ---
1137
- if (p === '/poll' && req.method === 'GET') {
1138
- handlePollGet(req, res, url);
1139
- return;
1140
- }
1141
- if (p === '/poll' && req.method === 'POST') {
1142
- handlePollPost(req, res);
1143
- return;
1144
- }
1145
-
1146
- res.writeHead(404); res.end('Not found');
1147
- };
1148
- }
1149
-
1150
- // ---------------------------------------------------------------------------
1151
- // Agent poll endpoints (unchanged from WS version)
1152
- // ---------------------------------------------------------------------------
1153
-
1154
- function parsePollTypes(value) {
1155
- if (!value) return null;
1156
- const types = String(value).split(',').map((type) => type.trim()).filter(Boolean);
1157
- return types.length > 0 ? new Set(types) : null;
1158
- }
1159
-
1160
- function handlePollGet(req, res, url) {
1161
- const token = url.searchParams.get('token');
1162
- if (token !== state.token) {
1163
- res.writeHead(401, { 'Content-Type': 'application/json' });
1164
- res.end(JSON.stringify({ error: 'Unauthorized' }));
1165
- return;
1166
- }
1167
- state.lastPollAt = Date.now();
1168
- const timeout = parseInt(url.searchParams.get('timeout') || DEFAULT_POLL_TIMEOUT, 10);
1169
- const leaseMs = parseInt(url.searchParams.get('leaseMs') || '30000', 10);
1170
- const types = parsePollTypes(url.searchParams.get('types'));
1171
- const available = findAvailablePendingEvent(Date.now(), types);
1172
- if (available) {
1173
- // Do not await inline: leaseEvent may scaffold source, and this handler runs
1174
- // on the server's only thread. The client can disconnect during that window,
1175
- // so check the socket before replying.
1176
- leaseEvent(available, leaseMs).then((event) => {
1177
- if (res.writableEnded || res.destroyed) return;
1178
- res.writeHead(200, { 'Content-Type': 'application/json' });
1179
- res.end(JSON.stringify(event));
1180
- }, (error) => {
1181
- console.error('[live] lease failed for ' + (available.event?.id || 'unknown') + ': ' + (error?.message || error));
1182
- if (res.writableEnded || res.destroyed) return;
1183
- res.writeHead(200, { 'Content-Type': 'application/json' });
1184
- res.end(JSON.stringify({ type: 'timeout' }));
1185
- });
1186
- return;
1187
- }
1188
- const poll = { resolve, leaseMs, types };
1189
- const timer = setTimeout(() => {
1190
- const idx = state.pendingPolls.indexOf(poll);
1191
- if (idx !== -1) state.pendingPolls.splice(idx, 1);
1192
- broadcastAgentPollingIfChanged();
1193
- res.writeHead(200, { 'Content-Type': 'application/json' });
1194
- res.end(JSON.stringify({ type: 'timeout' }));
1195
- }, timeout);
1196
- function resolve(event) {
1197
- clearTimeout(timer);
1198
- state.lastPollAt = Date.now();
1199
- res.writeHead(200, { 'Content-Type': 'application/json' });
1200
- res.end(JSON.stringify(event));
1201
- }
1202
- state.pendingPolls.push(poll);
1203
- broadcastAgentPollingIfChanged();
1204
- scheduleLeaseFlush();
1205
- req.on('close', () => {
1206
- clearTimeout(timer);
1207
- const idx = state.pendingPolls.indexOf(poll);
1208
- if (idx !== -1) state.pendingPolls.splice(idx, 1);
1209
- broadcastAgentPollingIfChanged();
1210
- });
1211
- }
1212
-
1213
- function sessionFileMetadataFromPollReply(file) {
1214
- if (!file || typeof file !== 'string') return { file };
1215
- const normalized = file.split(path.sep).join('/');
1216
- const base = { file: normalized };
1217
- const metadataFile = normalized;
1218
- if (!metadataFile.endsWith('/manifest.json') && metadataFile !== 'manifest.json') return base;
1219
- if (!metadataFile.includes('.impeccable/live/previews/')
1220
- && !metadataFile.includes('node_modules/.impeccable-live/')
1221
- && !metadataFile.includes('src/lib/impeccable/')
1222
- && !metadataFile.includes('/.impeccable-live/')) return base;
1223
-
1224
- let full;
1225
- try {
1226
- full = path.resolve(process.cwd(), metadataFile);
1227
- const rel = path.relative(process.cwd(), full);
1228
- if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return base;
1229
- } catch {
1230
- return base;
1231
- }
1232
-
1233
- try {
1234
- const manifest = JSON.parse(fs.readFileSync(full, 'utf-8'));
1235
- if (manifest?.previewMode !== 'svelte-component'
1236
- || !manifest.sourceFile) return base;
1237
- return {
1238
- file: String(manifest.sourceFile).split(path.sep).join('/'),
1239
- sourceFile: String(manifest.sourceFile).split(path.sep).join('/'),
1240
- previewFile: normalized,
1241
- previewMode: manifest.previewMode,
1242
- };
1243
- } catch {
1244
- return base;
1245
- }
1246
- }
1247
-
1248
- function inferSourceEventType(msg = {}, pendingEvents = state.pendingEvents) {
1249
- const entriesForId = pendingEvents.filter((entry) => entry.event?.id === msg.id);
1250
- const pendingTypes = new Set(entriesForId.map((entry) => entry.event?.type));
1251
- if (msg.type === 'discarded' || msg.type === 'discard') return 'discard';
1252
- if (msg.type === 'complete') {
1253
- if (pendingTypes.has('carbonize_cleanup')) return 'carbonize_cleanup';
1254
- return pendingTypes.has('accept') ? 'accept' : (pendingTypes.has('generate') ? 'generate' : undefined);
1255
- }
1256
- if (msg.type === 'steer_done') return 'steer';
1257
- // `agent_done` can be the automatic acknowledgement for a carbonize Accept.
1258
- // New pollers send sourceEventType explicitly; default to generate only for
1259
- // older callers so a late worker cannot acknowledge a queued Accept.
1260
- if (msg.type === 'agent_done' || msg.type === 'done') {
1261
- // A `done` reply to a mount failure is the republish that unblocks the
1262
- // browser. Without this the ack would look for a `generate` that was
1263
- // already retired, the mount-failure event would stay queued, and the next
1264
- // poll would hand the same failure back to the agent forever.
1265
- if (!pendingTypes.has('generate') && pendingTypes.has('variant_mount_failed')) return 'variant_mount_failed';
1266
- return 'generate';
1267
- }
1268
- // `error` is reference/live.md's documented failure reply, and parseReplyArgs
1269
- // never sets sourceEventType on it (the poller is a fresh process that cannot
1270
- // know what it leased). Returning undefined here makes acknowledgePendingEvent
1271
- // match *any* event for this id: a stale generate worker's failure silently
1272
- // consumed the user's queued Accept, which was then never delivered to any
1273
- // agent and left the browser in SAVING forever. Attribute the failure to the
1274
- // event this agent actually holds a lease on, and otherwise to `generate` —
1275
- // never to a wildcard. If that generate was already retired by an Accept, the
1276
- // ack simply finds no match, which is the correct outcome for a stale reply.
1277
- if (msg.type === 'error') {
1278
- return entriesForId.find(isLeased)?.event?.type || 'generate';
1279
- }
1280
- return undefined;
1281
- }
1282
-
1283
- function handlePollPost(req, res) {
1284
- let body = '';
1285
- req.on('data', (c) => { body += c; });
1286
- req.on('end', () => {
1287
- let msg;
1288
- try { msg = JSON.parse(body); } catch {
1289
- res.writeHead(400, { 'Content-Type': 'application/json' });
1290
- res.end(JSON.stringify({ error: 'Invalid JSON' }));
1291
- return;
1292
- }
1293
- if (msg.token !== state.token) {
1294
- res.writeHead(401, { 'Content-Type': 'application/json' });
1295
- res.end(JSON.stringify({ error: 'Unauthorized' }));
1296
- return;
1297
- }
1298
- const pendingApplyDeferred = manualApply.getDeferred(msg.id);
1299
- if (pendingApplyDeferred) {
1300
- const validation = manualApply.validateResultMessage(msg, pendingApplyDeferred);
1301
- if (!validation.ok) {
1302
- recordManualEditActivity('manual_edit_apply_reply_invalid', {
1303
- id: msg.id,
1304
- pageUrl: pendingApplyDeferred.pageUrl,
1305
- chunk: pendingApplyDeferred.event?.chunk || null,
1306
- repair: pendingApplyDeferred.event?.repair || null,
1307
- reason: validation.body?.reason || validation.body?.error || 'invalid_manual_apply_result',
1308
- status: msg.data?.status || null,
1309
- });
1310
- res.writeHead(400, { 'Content-Type': 'application/json' });
1311
- res.end(JSON.stringify(validation.body));
1312
- return;
1313
- }
1314
- recordManualEditActivity('manual_edit_apply_reply_received', {
1315
- id: msg.id,
1316
- pageUrl: pendingApplyDeferred.pageUrl,
1317
- chunk: pendingApplyDeferred.event?.chunk || null,
1318
- repair: pendingApplyDeferred.event?.repair || null,
1319
- status: validation.result.status,
1320
- appliedCount: validation.result.appliedEntryIds.length,
1321
- failed: summarizeManualApplyFailures(validation.result.failed),
1322
- fileCount: validation.result.files.length,
1323
- noteCount: validation.result.notes.length,
1324
- });
1325
- manualApply.resolveDeferred(msg.id, validation.result);
1326
- acknowledgePendingEvent(msg.id);
1327
- flushPendingPolls();
1328
- res.writeHead(200, { 'Content-Type': 'application/json' });
1329
- res.end(JSON.stringify({ ok: true }));
1330
- return;
1331
- }
1332
- if (manualApply.hasTimedOutId(msg.id)) {
1333
- const rollback = manualApply.rollbackTimedOutReply(msg);
1334
- recordManualEditActivity('manual_edit_apply_stale_reply_rejected', {
1335
- id: msg.id,
1336
- rolledBackFileCount: rollback.rolledBackFiles?.length || 0,
1337
- rollbackFailureCount: rollback.rollbackFailures?.length || 0,
1338
- });
1339
- res.writeHead(409, { 'Content-Type': 'application/json' });
1340
- res.end(JSON.stringify({ error: 'stale_manual_edit_apply_reply', ...rollback }));
1341
- return;
1342
- }
1343
- const sourceEventType = msg.sourceEventType || inferSourceEventType(msg);
1344
- if (msg.type === 'retry') {
1345
- const releasedEvent = releasePendingEvent(msg.id, sourceEventType);
1346
- if (!releasedEvent) {
1347
- res.writeHead(msg.id ? 404 : 400, { 'Content-Type': 'application/json' });
1348
- res.end(JSON.stringify({
1349
- error: msg.id ? 'unknown_poll_retry_id' : 'missing_poll_retry_id',
1350
- id: msg.id,
1351
- }));
1352
- return;
1353
- }
1354
- flushPendingPolls();
1355
- res.writeHead(200, { 'Content-Type': 'application/json' });
1356
- res.end(JSON.stringify({ ok: true, released: true }));
1357
- return;
1358
- }
1359
- const pendingEventBeforeAck = findPendingEventById(msg.id, sourceEventType);
1360
- if (pendingEventBeforeAck?.type === 'steer' && msg.type === 'steer_done'
1361
- && !msg.file && !(typeof msg.message === 'string' && msg.message.trim())) {
1362
- res.writeHead(400, { 'Content-Type': 'application/json' });
1363
- res.end(JSON.stringify({
1364
- error: 'steer_done_requires_file_or_message',
1365
- hint: 'Reply with --file after writing source, or include a message explaining an intentional no-op.',
1366
- }));
1367
- return;
1368
- }
1369
- const acknowledgedEvent = acknowledgePendingEvent(msg.id, sourceEventType);
1370
- let skipJournalReply = false;
1371
- let existingSession = null;
1372
- if (!acknowledgedEvent && state.sessionStore && msg.id) {
1373
- try {
1374
- existingSession = state.sessionStore.getSnapshot(msg.id, { includeCompleted: true });
1375
- if (!existingSession?.updatedAt) existingSession = null;
1376
- skipJournalReply = existingSession?.phase === 'completed' || existingSession?.phase === 'discarded';
1377
- } catch { /* fall through and record the reply normally */ }
1378
- }
1379
- if (!acknowledgedEvent && !existingSession) {
1380
- recordManualEditActivity('manual_edit_poll_reply_unknown', {
1381
- id: msg.id || null,
1382
- type: msg.type || null,
1383
- });
1384
- res.writeHead(msg.id ? 404 : 400, { 'Content-Type': 'application/json' });
1385
- res.end(JSON.stringify({
1386
- error: msg.id ? 'unknown_poll_reply_id' : 'missing_poll_reply_id',
1387
- id: msg.id,
1388
- }));
1389
- return;
1390
- }
1391
- const replyFileMeta = sessionFileMetadataFromPollReply(msg.file);
1392
- // A publish (done reply carrying a component manifest) snapshots the
1393
- // variant files into a fresh revision dir before the browser is told:
1394
- // the import path changes every publish, so no transform cache can pin a
1395
- // stale compile of a republished module (node_modules is unwatched).
1396
- // Broken variants are bounced HERE, before the browser imports anything:
1397
- // a compile error that reaches the page is a red overlay in the user's
1398
- // face; bounced at publish it is a private fix with file and line.
1399
- if (replyFileMeta.previewMode === 'svelte-component'
1400
- && msg.id
1401
- && (msg.type === 'done' || !msg.type)) {
1402
- let compileCheck = { ok: true, failures: [] };
1403
- try { compileCheck = compileCheckVariants(msg.id, process.cwd()); } catch { /* best-effort */ }
1404
- if (!compileCheck.ok) {
1405
- res.writeHead(422, { 'Content-Type': 'application/json' });
1406
- res.end(JSON.stringify({
1407
- error: 'variant_compile_failed',
1408
- id: msg.id,
1409
- failures: compileCheck.failures,
1410
- _instructions: 'The publish was NOT delivered: the listed variant file(s) do not compile, so the browser never saw them. Fix each failure at the given file and line (the most common cause is a second top-level <style> element; Svelte allows exactly one, so merge all rules into the existing block), then send the same --reply done again.',
1411
- }));
1412
- return;
1413
- }
1414
- try { bumpSvelteComponentPreviewRevision(msg.id, process.cwd()); } catch { /* best-effort */ }
1415
- }
1416
- if (state.sessionStore && msg.id && !skipJournalReply) {
1417
- try {
1418
- const eventType = msg.type === 'steer_done'
1419
- ? 'steer_done'
1420
- : msg.type === 'discard' || msg.type === 'discarded'
1421
- ? 'discarded'
1422
- : msg.type === 'complete'
1423
- ? 'complete'
1424
- : msg.type === 'error'
1425
- ? 'agent_error'
1426
- : 'agent_done';
1427
- state.sessionStore.appendEvent({
1428
- type: eventType,
1429
- id: msg.id,
1430
- file: replyFileMeta.file,
1431
- sourceFile: replyFileMeta.sourceFile,
1432
- previewFile: replyFileMeta.previewFile,
1433
- previewMode: replyFileMeta.previewMode,
1434
- message: msg.message,
1435
- sourceEventType: acknowledgedEvent?.type,
1436
- carbonize: msg.data?.carbonize === true,
1437
- });
1438
- } catch { /* keep reply path best-effort; browser still needs SSE */ }
1439
- }
1440
- flushPendingPolls();
1441
- // Forward the reply to the browser via SSE
1442
- broadcast({
1443
- type: msg.type || 'done',
1444
- id: msg.id,
1445
- message: msg.message,
1446
- file: msg.file,
1447
- sourceFile: replyFileMeta.sourceFile,
1448
- previewFile: replyFileMeta.previewFile,
1449
- previewMode: replyFileMeta.previewMode,
1450
- data: msg.data,
1451
- });
1452
- res.writeHead(200, { 'Content-Type': 'application/json' });
1453
- res.end(JSON.stringify({ ok: true }));
1454
- });
1455
- }
1456
-
1457
- // ---------------------------------------------------------------------------
1458
- // Lifecycle
1459
- // ---------------------------------------------------------------------------
1460
-
1461
- let httpServer = null;
1462
-
1463
- function shutdown() {
1464
- cleanupSvelteComponentSessionsBeforeExit();
1465
- removeLiveServerInfo(process.cwd());
1466
- if (state.leaseTimer) clearTimeout(state.leaseTimer);
1467
- state.leaseTimer = null;
1468
- if (state.sessionDir) {
1469
- try { fs.rmSync(state.sessionDir, { recursive: true, force: true }); } catch {}
1470
- }
1471
- for (const res of state.sseClients) { try { res.end(); } catch {} }
1472
- state.sseClients.clear();
1473
- for (const poll of state.pendingPolls) poll.resolve({ type: 'exit' });
1474
- state.pendingPolls.length = 0;
1475
- if (httpServer) httpServer.close();
1476
- process.exit(0);
1477
- }
1478
-
1479
- function cleanupSvelteComponentSessionsBeforeExit() {
1480
- try {
1481
- removeAllSvelteComponentSessions(process.cwd());
1482
- } catch (err) {
1483
- console.warn('[impeccable] Svelte component session cleanup failed:', err.message);
1484
- }
1485
- }
1486
-
1487
- /**
1488
- * A previous run that died without its shutdown hook leaves preview component
1489
- * dirs behind. Drop the ones whose session the store no longer considers
1490
- * active; anything still active is mid-generation and must survive a restart.
1491
- */
1492
- function sweepOrphanSvelteComponentSessionsOnStartup() {
1493
- try {
1494
- const activeIds = (state.sessionStore?.listActiveSessions() || [])
1495
- .map((snapshot) => snapshot?.id)
1496
- .filter(Boolean);
1497
- const result = sweepInactiveSvelteComponentSessions(activeIds, process.cwd());
1498
- if (result.removed.length > 0 || result.removedRoot) {
1499
- console.log('[impeccable] swept orphaned Svelte component sessions:', JSON.stringify(result));
1500
- }
1501
- } catch (err) {
1502
- console.warn('[impeccable] Svelte component session sweep failed:', err.message);
1503
- }
1504
- }
1505
-
1506
- // Accept receipts are a short-lived idempotency record for a single accept.
1507
- // Nothing reads one after the session that wrote it is gone, so they only need
1508
- // to outlive a crash-and-retry window.
1509
- const ACCEPT_RECEIPT_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
1510
-
1511
- function sweepStaleAcceptReceiptsOnStartup() {
1512
- try {
1513
- const dir = path.join(getLiveDir(process.cwd()), 'accept-receipts');
1514
- if (!fs.existsSync(dir)) return;
1515
- const cutoff = Date.now() - ACCEPT_RECEIPT_MAX_AGE_MS;
1516
- let removed = 0;
1517
- for (const name of fs.readdirSync(dir)) {
1518
- if (!name.endsWith('.json') && !name.endsWith('.tmp')) continue;
1519
- const file = path.join(dir, name);
1520
- try {
1521
- if (fs.statSync(file).mtimeMs >= cutoff) continue;
1522
- fs.rmSync(file, { force: true });
1523
- removed++;
1524
- } catch { /* non-fatal */ }
1525
- }
1526
- if (removed > 0) console.log(`[impeccable] removed ${removed} accept receipt(s) older than 14 days`);
1527
- } catch (err) {
1528
- console.warn('[impeccable] accept receipt retention sweep failed:', err.message);
1529
- }
1530
- }
1531
-
1532
- function applyLegacyDeferredAcceptsOnStartup() {
1533
- try {
1534
- const result = applyDeferredSvelteComponentAccepts(process.cwd());
1535
- if (result.applied > 0 || result.failed > 0) {
1536
- console.log('[impeccable] applied legacy deferred Svelte component accepts:', JSON.stringify(result));
1537
- }
1538
- } catch (err) {
1539
- console.warn('[impeccable] legacy deferred Svelte component accept apply failed:', err.message);
1540
- }
1541
- }
1542
-
1543
- // ---------------------------------------------------------------------------
1544
- // Main
1545
- // ---------------------------------------------------------------------------
1546
-
1547
- const args = process.argv.slice(2);
1548
-
1549
- if (args.includes('--help') || args.includes('-h')) {
1550
- console.log(`Usage: node live-server.mjs [options]
1551
-
1552
- Start the live variant mode server (zero dependencies).
1553
-
1554
- Commands:
1555
- (default) Start the server (foreground)
1556
- stop Stop the server and remove the injected live.js script tag
1557
- stop --keep-inject Stop the server only (leave the script tag in the HTML entry)
1558
-
1559
- Options:
1560
- --background Start detached, print connection JSON to stdout, then exit
1561
- --port=PORT Use a specific port (default: auto-detect starting at 8400)
1562
- --keep-inject Only with stop: skip live-inject.mjs --remove
1563
- --help Show this help
1564
-
1565
- Endpoints:
1566
- /live.js Browser script (element picker + variant cycling)
1567
- /detect.js Detection overlay (backwards compatible)
1568
- /modern-screenshot.js Vendored modern-screenshot UMD build (lazy-loaded by live.js)
1569
- /annotation POST raw image/png to stage a variant screenshot
1570
- /events SSE stream (server→browser) + POST (browser→server)
1571
- /poll Long-poll for agent CLI
1572
- /manual-edit-stash Stage browser copy edits
1573
- /manual-edit-commit Apply staged browser copy edits
1574
- /manual-edit-discard Discard staged browser copy edits
1575
- /source Raw source file reader (no-HMR fallback)
1576
- /status Durable recovery status (token-protected)
1577
- /health Health check`);
1578
- process.exit(0);
1579
- }
1580
-
1581
- if (args.includes('stop')) {
1582
- const keepInject = args.includes('--keep-inject');
1583
- try {
1584
- const { info } = readLiveServerInfo(process.cwd()) || {};
1585
- const res = await fetch(`http://localhost:${info.port}/stop?token=${info.token}`);
1586
- if (res.ok) console.log(`Stopped live server on port ${info.port}.`);
1587
- } catch {
1588
- console.log('No running live server found.');
1589
- }
1590
- if (!keepInject) {
1591
- const injectPath = path.join(__dirname, 'live-inject.mjs');
1592
- try {
1593
- const out = execFileSync(process.execPath, [injectPath, '--remove'], {
1594
- encoding: 'utf-8',
1595
- cwd: process.cwd(),
1596
- });
1597
- const line = out.trim().split('\n').filter(Boolean).pop();
1598
- if (line) {
1599
- try {
1600
- const j = JSON.parse(line);
1601
- if (j.removed === true) {
1602
- console.log(`Removed live script tag from ${j.file}.`);
1603
- }
1604
- } catch {
1605
- /* ignore non-JSON lines */
1606
- }
1607
- }
1608
- } catch (err) {
1609
- const detail = err.stderr?.toString?.().trim?.()
1610
- || err.stdout?.toString?.().trim?.()
1611
- || err.message
1612
- || String(err);
1613
- console.warn(`Note: could not remove live script tag (${detail.split('\n')[0]})`);
1614
- }
1615
- }
1616
- process.exit(0);
1617
- }
1618
-
1619
- // --background: spawn a detached child server, wait for it to be ready,
1620
- // print the connection JSON, then exit. This keeps the startup command
1621
- // simple (no shell backgrounding or chained commands).
1622
- if (args.includes('--background')) {
1623
- const childArgs = args.filter(a => a !== '--background');
1624
- const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...childArgs], {
1625
- detached: true,
1626
- stdio: 'ignore',
1627
- cwd: process.cwd(),
1628
- });
1629
- child.unref();
1630
-
1631
- // Poll for the PID file (the child writes it once the HTTP server is listening).
1632
- const deadline = Date.now() + 10_000;
1633
- while (Date.now() < deadline) {
1634
- try {
1635
- const { info } = readLiveServerInfo(process.cwd()) || {};
1636
- if (info.pid !== process.pid) {
1637
- // Output JSON so the agent can read port + token from stdout.
1638
- console.log(JSON.stringify(info));
1639
- process.exit(0);
1640
- }
1641
- } catch { /* not ready yet */ }
1642
- // The detached child is typically listening in 35-45ms. A 200ms polling
1643
- // floor dominated configured cold Live startup; poll cheaply and return
1644
- // as soon as the child has written its ready record.
1645
- await new Promise(r => setTimeout(r, 5));
1646
- }
1647
- console.error('Timed out waiting for live server to start.');
1648
- process.exit(1);
1649
- }
1650
-
1651
- // Check for existing session
1652
- const existingRecord = readLiveServerInfo(process.cwd());
1653
- if (existingRecord?.info) {
1654
- const existing = existingRecord.info;
1655
- try {
1656
- process.kill(existing.pid, 0);
1657
- console.error(`Live server already running on port ${existing.port} (pid ${existing.pid}).`);
1658
- console.error('Stop it first with: node ' + path.basename(fileURLToPath(import.meta.url)) + ' stop');
1659
- process.exit(1);
1660
- } catch {
1661
- try { fs.unlinkSync(existingRecord.path); } catch {}
1662
- }
1663
- }
1664
-
1665
- state.token = randomUUID();
1666
- state.sessionStore = createLiveSessionStore({ cwd: process.cwd() });
1667
- manualApply.rollbackTransaction({
1668
- reason: 'manual_edit_server_start_recovered_abandoned_transaction',
1669
- });
1670
- applyLegacyDeferredAcceptsOnStartup();
1671
- sweepOrphanSvelteComponentSessionsOnStartup();
1672
- sweepStaleAcceptReceiptsOnStartup();
1673
- restorePendingEventsFromStore();
1674
- manualApply.pruneStaleEvidence();
1675
- const portArg = args.find(a => a.startsWith('--port='));
1676
- state.port = portArg ? parseInt(portArg.split('=')[1], 10) : await findOpenPort();
1677
- // Annotation screenshots live in the project root so the agent's Read tool
1678
- // doesn't trip a per-file permission prompt. Sessioned by token so concurrent
1679
- // projects (or quick restarts) don't collide.
1680
- const annotRoot = getLiveAnnotationsDir(process.cwd());
1681
- fs.mkdirSync(annotRoot, { recursive: true });
1682
- state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-'));
1683
-
1684
- const { detectScript, liveScriptParts } = loadBrowserScripts();
1685
- httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts }));
1686
-
1687
- httpServer.listen(state.port, '127.0.0.1', () => {
1688
- writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token });
1689
- const url = `http://localhost:${state.port}`;
1690
- console.log(`\nImpeccable live server running on ${url}`);
1691
- console.log(`Token: ${state.token}\n`);
1692
- console.log(`Script: ${url}/live.js`);
1693
- console.log('Inject: managed by live-inject.mjs; Astro source tags use is:inline automatically.');
1694
- console.log(`Stop: node ${path.basename(fileURLToPath(import.meta.url))} stop`);
1695
- });
1696
-
1697
- process.on('SIGINT', shutdown);
1698
- process.on('SIGTERM', shutdown);