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