quiver-cli 0.7.0 → 1.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 (200) hide show
  1. package/README.md +110 -45
  2. package/bin/quiver-cli.mjs +3 -1
  3. package/dist/cli.js +733 -499
  4. package/package.json +2 -2
  5. package/template/.agents/AGENTS.md +4 -3
  6. package/template/.agents/config.json +7 -0
  7. package/template/.agents/plugins/opencode/rtk.ts +34 -0
  8. package/template/.agents/skills/agent-browser/SKILL.md +1 -0
  9. package/template/.agents/skills/apps/skybridge/SKILL.md +4 -0
  10. package/template/.agents/skills/design/impeccable/SKILL.md +36 -118
  11. package/template/.agents/skills/design/impeccable/reference/adapt.md +1 -0
  12. package/template/.agents/skills/design/impeccable/reference/adapt.native.md +58 -0
  13. package/template/.agents/skills/design/impeccable/reference/android.md +40 -0
  14. package/template/.agents/skills/design/impeccable/reference/animate.md +73 -188
  15. package/template/.agents/skills/design/impeccable/reference/audit.md +12 -9
  16. package/template/.agents/skills/design/impeccable/reference/audit.native.md +139 -0
  17. package/template/.agents/skills/design/impeccable/reference/bolder.md +19 -101
  18. package/template/.agents/skills/design/impeccable/reference/clarify.md +59 -253
  19. package/template/.agents/skills/design/impeccable/reference/colorize.md +51 -222
  20. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +42 -0
  21. package/template/.agents/skills/design/impeccable/reference/craft.md +3 -121
  22. package/template/.agents/skills/design/impeccable/reference/critique.md +44 -23
  23. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +90 -0
  24. package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +24 -0
  25. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +37 -0
  26. package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  27. package/template/.agents/skills/design/impeccable/reference/delight.md +47 -279
  28. package/template/.agents/skills/design/impeccable/reference/distill.md +2 -2
  29. package/template/.agents/skills/design/impeccable/reference/doctor.md +53 -0
  30. package/template/.agents/skills/design/impeccable/reference/document.md +60 -73
  31. package/template/.agents/skills/design/impeccable/reference/harden.md +1 -12
  32. package/template/.agents/skills/design/impeccable/reference/hooks.md +20 -5
  33. package/template/.agents/skills/design/impeccable/reference/init.md +72 -119
  34. package/template/.agents/skills/design/impeccable/reference/ios.md +45 -0
  35. package/template/.agents/skills/design/impeccable/reference/layout.md +54 -131
  36. package/template/.agents/skills/design/impeccable/reference/live-setup.md +102 -0
  37. package/template/.agents/skills/design/impeccable/reference/live.md +116 -511
  38. package/template/.agents/skills/design/impeccable/reference/new-work.md +105 -0
  39. package/template/.agents/skills/design/impeccable/reference/{product.md → operate.md} +6 -5
  40. package/template/.agents/skills/design/impeccable/reference/optimize.md +4 -4
  41. package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -4
  42. package/template/.agents/skills/design/impeccable/reference/polish.md +68 -212
  43. package/template/.agents/skills/design/impeccable/reference/quieter.md +3 -3
  44. package/template/.agents/skills/design/impeccable/reference/routing.md +18 -0
  45. package/template/.agents/skills/design/impeccable/reference/shape.md +38 -144
  46. package/template/.agents/skills/design/impeccable/reference/typeset.md +51 -250
  47. package/template/.agents/skills/design/impeccable/reference/visualize.md +47 -0
  48. package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +1 -1
  49. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +558 -0
  50. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +119 -10
  51. package/template/.agents/skills/design/impeccable/scripts/context.mjs +534 -45
  52. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +18 -47
  53. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
  54. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +174 -26
  55. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +233 -0
  56. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +3348 -203
  57. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
  58. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +297 -97
  59. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +187 -16
  60. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +41 -11
  61. package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +7 -1
  62. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +16 -2
  63. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +207 -38
  64. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +3082 -173
  65. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +11 -0
  66. package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +30 -0
  67. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +336 -0
  68. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +133 -0
  69. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +240 -0
  70. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +98 -18
  71. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +46 -6
  72. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +518 -50
  73. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +25 -8
  74. package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  75. package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +200 -0
  76. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +357 -0
  77. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +27 -7
  78. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +17 -8
  79. package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +5 -0
  80. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +362 -0
  81. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +457 -0
  82. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  83. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +457 -0
  84. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  85. package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +33 -0
  86. package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +146 -0
  87. package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +617 -0
  88. package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +60 -0
  89. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +7 -1
  90. package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +10 -1
  91. package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +67 -5
  92. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +47 -0
  93. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
  94. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +143 -0
  95. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +197 -0
  96. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
  97. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
  98. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
  99. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
  100. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
  101. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
  102. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
  103. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
  104. package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +149 -0
  105. package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +142 -0
  106. package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +14 -0
  107. package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +508 -0
  108. package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +324 -50
  109. package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +105 -0
  110. package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +105 -0
  111. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +961 -0
  112. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +588 -72
  113. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +59 -17
  114. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  115. package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +135 -0
  116. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +210 -68
  117. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1562 -223
  118. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
  119. package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +33 -1
  120. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +175 -255
  121. package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +26 -6
  122. package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
  123. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +61 -16
  124. package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +39 -10
  125. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +573 -47
  126. package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +17 -7
  127. package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +124 -91
  128. package/template/.agents/skills/design/impeccable/scripts/live.mjs +88 -26
  129. package/template/.agents/skills/design/impeccable/scripts/palette.mjs +76 -81
  130. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +18 -11
  131. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +932 -0
  132. package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +74 -0
  133. package/template/.agents/skills/design/shadcn/SKILL.md +46 -11
  134. package/template/.agents/skills/design/shadcn/cli.md +49 -16
  135. package/template/.agents/skills/design/shadcn/customization.md +14 -7
  136. package/template/.agents/skills/design/shadcn/evals/evals.json +30 -0
  137. package/template/.agents/skills/design/shadcn/mcp.md +27 -16
  138. package/template/.agents/skills/design/shadcn/registry.md +277 -0
  139. package/template/.agents/skills/design/shadcn/rules/chat.md +224 -0
  140. package/template/.agents/skills/design/shadcn/rules/composition.md +20 -2
  141. package/template/.agents/skills/design/shadcn/rules/styling.md +23 -0
  142. package/template/.agents/skills/find-skills/SKILL.md +2 -3
  143. package/template/.agents/skills/hono/SKILL.md +579 -0
  144. package/template/.agents/skills/integrations/langfuse/SKILL.md +10 -6
  145. package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +41 -0
  146. package/template/.agents/skills/integrations/langfuse/references/cli.md +8 -0
  147. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +15 -27
  148. package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +39 -52
  149. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +5 -2
  150. package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +35 -0
  151. package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +41 -196
  152. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +3 -0
  153. package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +76 -0
  154. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -0
  155. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +73 -0
  156. package/template/.agents/skills/supabase/CHANGELOG.md +71 -0
  157. package/template/.agents/skills/supabase/SKILL.md +145 -0
  158. package/template/.agents/skills/supabase/assets/feedback-issue-template.md +17 -0
  159. package/template/.agents/skills/supabase/references/skill-feedback.md +17 -0
  160. package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
  161. package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
  162. package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
  163. package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
  164. package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +34 -0
  165. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
  166. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
  167. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
  168. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
  169. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
  170. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
  171. package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
  172. package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
  173. package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
  174. package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
  175. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
  176. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
  177. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
  178. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
  179. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
  180. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
  181. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
  182. package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
  183. package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
  184. package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
  185. package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
  186. package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
  187. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
  188. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
  189. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
  190. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
  191. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
  192. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
  193. package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
  194. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
  195. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
  196. package/template/.agents/upstreams.json +34 -12
  197. package/template/.agents/skills/design/impeccable/reference/brand.md +0 -108
  198. package/template/.agents/skills/design/impeccable/reference/codex.md +0 -105
  199. package/template/.agents/skills/design/impeccable/reference/interaction-design.md +0 -189
  200. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -175
@@ -29,6 +29,9 @@ const ROLLBACK_EXTENSIONS = new Set([
29
29
  '.astro',
30
30
  '.cjs',
31
31
  '.css',
32
+ '.eex',
33
+ '.ex',
34
+ '.heex',
32
35
  '.htm',
33
36
  '.html',
34
37
  '.js',
@@ -3,8 +3,12 @@
3
3
  * Canonical durable completion acknowledgement for Impeccable live sessions.
4
4
  */
5
5
 
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
6
8
  import { createLiveSessionStore } from './live/session-store.mjs';
7
9
  import { readLiveServerInfo } from './lib/impeccable-paths.mjs';
10
+ import { enterLiveRoot } from './live/roots.mjs';
11
+ import { verifyAcceptedFile } from './live/accept-verify.mjs';
8
12
 
9
13
  function parseArgs(argv) {
10
14
  const out = { status: 'complete' };
@@ -15,6 +19,7 @@ function parseArgs(argv) {
15
19
  else if (arg === '--discarded' || arg === '--discard') out.status = 'discarded';
16
20
  else if (arg === '--error') { out.status = 'agent_error'; out.message = argv[++i] || 'unknown error'; }
17
21
  else if (arg.startsWith('--error=')) { out.status = 'agent_error'; out.message = arg.slice('--error='.length); }
22
+ else if (arg === '--force') out.force = true;
18
23
  else if (arg === '--help' || arg === '-h') out.help = true;
19
24
  }
20
25
  return out;
@@ -23,10 +28,36 @@ function parseArgs(argv) {
23
28
  export async function completeCli() {
24
29
  const args = parseArgs(process.argv.slice(2));
25
30
  if (args.help || !args.id) {
26
- console.log(`Usage: node live-complete.mjs --id SESSION_ID [--discarded|--error MESSAGE]\n\nAppend the final durable session acknowledgement. Use after accept/discard cleanup is verified.`);
31
+ console.log(`Usage: node live-complete.mjs --id SESSION_ID [--discarded|--error MESSAGE] [--force]\n\nAppend the final durable session acknowledgement. Use after accept/discard cleanup is verified.\nCompletion is refused while the session's source file still carries live-mode leftovers\n(markers, data-p-* attributes, unbaked --p-* vars); fix the file or pass --force.`);
27
32
  process.exit(args.help ? 0 : 1);
28
33
  }
29
34
 
35
+ // The carbonize contract used to be prose; this makes it mechanical. A
36
+ // "complete" while the source still carries live plumbing is how markers
37
+ // and dead param branches accumulated across sessions.
38
+ if (args.status === 'complete' && !args.force) {
39
+ const store = createLiveSessionStore({ cwd: process.cwd(), sessionId: args.id });
40
+ const snapshot = store.getSnapshot(args.id, { includeCompleted: true });
41
+ const sourceFile = snapshot?.sourceFile;
42
+ const absSource = sourceFile ? path.resolve(process.cwd(), sourceFile) : null;
43
+ const relSource = absSource ? path.relative(process.cwd(), absSource) : null;
44
+ const insideProject = relSource !== null && relSource !== '' && !relSource.startsWith('..') && !path.isAbsolute(relSource);
45
+ if (insideProject && !relSource.startsWith('node_modules' + path.sep) && !relSource.startsWith('node_modules/')) {
46
+ const verify = verifyAcceptedFile(fs, absSource);
47
+ if (!verify.clean) {
48
+ console.log(JSON.stringify({
49
+ ok: false,
50
+ error: 'source_dirty',
51
+ id: args.id,
52
+ file: sourceFile,
53
+ findings: verify.findings,
54
+ hint: 'The accepted source still carries live-mode leftovers. Finish the carbonize cleanup (bake params, remove markers and data-p-* attributes), then run live-complete again. Use --force only if a finding is a false positive.',
55
+ }, null, 2));
56
+ process.exit(1);
57
+ }
58
+ }
59
+ }
60
+
30
61
  const serverInfo = readServerInfo();
31
62
  const serverResult = serverInfo ? await completeThroughServer(serverInfo, args) : null;
32
63
  if (serverResult?.ok) {
@@ -71,5 +102,6 @@ async function completeThroughServer(info, args) {
71
102
 
72
103
  const _running = process.argv[1];
73
104
  if (_running?.endsWith('live-complete.mjs') || _running?.endsWith('live-complete.mjs/')) {
105
+ enterLiveRoot();
74
106
  completeCli();
75
107
  }
@@ -7,10 +7,20 @@
7
7
  * every subsequent run, this script handles insert/remove deterministically
8
8
  * with zero LLM involvement.
9
9
  *
10
+ * Framework knowledge lives in `live/frameworks/` — detection order, adapters,
11
+ * the generic tag strategy, and the per-extension authoring traits live-wrap
12
+ * reads. This file is the CLI around it: resolve config, resolve the
13
+ * framework, heal orphaned artifacts, apply or remove, record the journal.
14
+ *
10
15
  * Usage:
11
- * node live-inject.mjs --port PORT # Insert the live script tag
12
- * node live-inject.mjs --remove # Remove the live script tag
13
- * node live-inject.mjs --check # Check whether live config exists
16
+ * node live-inject.mjs --port PORT [--token TOKEN] # Insert the live script tag
17
+ * node live-inject.mjs --remove # Remove the live script tag
18
+ * node live-inject.mjs --check # Check whether live config exists
19
+ *
20
+ * When --token is supplied, it is appended to the /live.js src as `?token=...`
21
+ * so the server's token-gated /live.js handler will serve the bundle. Omitting
22
+ * the token yields a bare `/live.js` src (legacy behavior; the server returns
23
+ * 401 for it under the current gate).
14
24
  */
15
25
 
16
26
  import fs from 'node:fs';
@@ -18,15 +28,36 @@ import path from 'node:path';
18
28
  import { fileURLToPath } from 'node:url';
19
29
  import { resolveLiveConfigPath } from './lib/impeccable-paths.mjs';
20
30
  import {
21
- applySvelteKitLiveAdapter,
22
- detectSvelteKitProject,
23
- removeSvelteKitLiveAdapter,
24
- } from './live/sveltekit-adapter.mjs';
31
+ describeInjectArtifacts,
32
+ frameworkIgnorePatterns,
33
+ resolveFramework,
34
+ resolveSourceTraits,
35
+ } from './live/frameworks/index.mjs';
36
+ import {
37
+ clearInjectJournal,
38
+ healInjectJournal,
39
+ recordInjection,
40
+ } from './live/frameworks/journal.mjs';
41
+ import {
42
+ buildTagBlock,
43
+ insertTag,
44
+ patchCspMeta,
45
+ removeTag,
46
+ revertCspMeta,
47
+ } from './live/frameworks/tag-strategy.mjs';
48
+ import { buildLiveScriptSrc } from './live/frameworks/script-src.mjs';
49
+ import { enterLiveRoot } from './live/roots.mjs';
25
50
 
26
51
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
- const CONFIG_PATH = resolveLiveConfigPath({ cwd: process.cwd(), scriptsDir: __dirname });
28
- const MARKER_OPEN_TEXT = 'impeccable-live-start';
29
- const MARKER_CLOSE_TEXT = 'impeccable-live-end';
52
+ // Resolved lazily so the enterLiveRoot() chdir in the CLI guard below takes
53
+ // effect first; module scope runs before the guard.
54
+ let CONFIG_PATH_CACHED = null;
55
+ function CONFIG_PATH_GET() {
56
+ if (!CONFIG_PATH_CACHED) {
57
+ CONFIG_PATH_CACHED = resolveLiveConfigPath({ cwd: process.cwd(), scriptsDir: __dirname });
58
+ }
59
+ return CONFIG_PATH_CACHED;
60
+ }
30
61
  const IGNORE_MARKER_OPEN = '# impeccable-live-ignore-start';
31
62
  const IGNORE_MARKER_CLOSE = '# impeccable-live-ignore-end';
32
63
 
@@ -35,9 +66,15 @@ export const LIVE_IGNORE_PATTERNS = Object.freeze([
35
66
  '.impeccable/hook.pending.json',
36
67
  '.impeccable/config.local.json',
37
68
  '.impeccable/live/server.json',
69
+ '.impeccable/live/roots.json',
70
+ '.impeccable/live/app-root.json',
71
+ '.impeccable/live/inject-journal.json',
38
72
  '.impeccable/live/sessions/',
39
73
  '.impeccable/live/previews/',
40
74
  '.impeccable/live/annotations/',
75
+ '.impeccable/live/artifacts/',
76
+ '.impeccable/live/accept-receipts/',
77
+ '.impeccable/live/locks/',
41
78
  '.impeccable/live/cache/',
42
79
  '.impeccable/live/manual-edit-apply-transaction.json',
43
80
  '.impeccable/live/manual-edit-events.jsonl',
@@ -46,10 +83,15 @@ export const LIVE_IGNORE_PATTERNS = Object.freeze([
46
83
  '.impeccable/live/deferred-svelte-component-accepts.json',
47
84
  '.impeccable-live.json',
48
85
  '.impeccable-live/',
86
+ 'app/.impeccable-live/',
87
+ 'src/.impeccable-live/',
49
88
  'node_modules/.impeccable-live/',
50
89
  'src/lib/impeccable/ImpeccableLiveRoot.svelte',
51
90
  'src/lib/impeccable/__runtime.js',
52
91
  'src/lib/impeccable/[0-9a-f]*/',
92
+ 'plugins/impeccable-live.client.ts',
93
+ 'app/plugins/impeccable-live.client.ts',
94
+ 'src/plugins/impeccable-live.client.ts',
53
95
  ]);
54
96
 
55
97
  /**
@@ -82,46 +124,61 @@ Output (JSON):
82
124
  }
83
125
 
84
126
  if (args.includes('--check')) {
85
- if (!fs.existsSync(CONFIG_PATH)) {
86
- console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
127
+ // Deliberately read-only: --check runs from status paths and must never
128
+ // mutate the tree. Journal reconciliation happens on the inject run.
129
+ if (!fs.existsSync(CONFIG_PATH_GET())) {
130
+ console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH_GET() }));
87
131
  process.exit(0);
88
132
  }
89
133
  let cfg;
90
134
  try {
91
- cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
135
+ cfg = JSON.parse(fs.readFileSync(CONFIG_PATH_GET(), 'utf-8'));
92
136
  } catch (err) {
93
- console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message, path: CONFIG_PATH }));
137
+ console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message, path: CONFIG_PATH_GET() }));
94
138
  return;
95
139
  }
96
140
  try {
97
141
  validateConfig(cfg);
98
142
  } catch (err) {
99
- console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message, path: CONFIG_PATH }));
143
+ console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message, path: CONFIG_PATH_GET() }));
100
144
  return;
101
145
  }
102
- console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
146
+ console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH_GET() }));
103
147
  return;
104
148
  }
105
149
 
106
150
  // Load config
107
- if (!fs.existsSync(CONFIG_PATH)) {
108
- console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
151
+ if (!fs.existsSync(CONFIG_PATH_GET())) {
152
+ console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH_GET() }));
109
153
  process.exit(1);
110
154
  }
111
- const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
155
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH_GET(), 'utf-8'));
112
156
  validateConfig(config);
113
157
 
114
- const resolvedFiles = resolveFiles(process.cwd(), config);
115
- const svelteKit = detectSvelteKitProject(process.cwd(), config);
158
+ const cwd = process.cwd();
159
+ const resolvedFiles = resolveFiles(cwd, config);
160
+ const resolved = resolveFramework(cwd, config);
161
+ const isAdapter = resolved?.framework.inject.kind === 'adapter';
116
162
 
117
163
  if (args.includes('--remove')) {
118
- if (svelteKit) {
119
- const adapterResult = removeSvelteKitLiveAdapter({ cwd: process.cwd(), config });
120
- console.log(JSON.stringify({ ok: true, adapter: 'sveltekit', results: [adapterResult] }));
164
+ if (isAdapter) {
165
+ const adapterResult = resolved.framework.inject.remove({ cwd, config, project: resolved.project });
166
+ const ok = !(adapterResult && adapterResult.error);
167
+ // Anything the adapter could not reach (its detection may have shifted
168
+ // since the session started) is still on the journal.
169
+ const { healed } = healInjectJournal(cwd);
170
+ clearInjectJournal(cwd);
171
+ console.log(JSON.stringify({
172
+ ok,
173
+ adapter: resolved.framework.name,
174
+ results: [adapterResult],
175
+ healed: healed.length ? healed : undefined,
176
+ }));
177
+ if (!ok) process.exitCode = 1;
121
178
  return;
122
179
  }
123
180
  const results = resolvedFiles.map((relFile) => {
124
- const absFile = path.resolve(process.cwd(), relFile);
181
+ const absFile = path.resolve(cwd, relFile);
125
182
  if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
126
183
  const content = fs.readFileSync(absFile, 'utf-8');
127
184
  const detagged = removeTag(content, config.commentSyntax);
@@ -134,7 +191,9 @@ Output (JSON):
134
191
  cspReverted: updated !== detagged,
135
192
  };
136
193
  });
137
- console.log(JSON.stringify({ ok: true, results }));
194
+ const { healed } = healInjectJournal(cwd);
195
+ clearInjectJournal(cwd);
196
+ console.log(JSON.stringify({ ok: true, results, healed: healed.length ? healed : undefined }));
138
197
  return;
139
198
  }
140
199
 
@@ -145,20 +204,69 @@ Output (JSON):
145
204
  console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
146
205
  process.exit(1);
147
206
  }
148
- const gitIgnore = ensureLiveGitIgnores(process.cwd());
207
+ // Optional server token: appended to the /live.js src so the token-gated
208
+ // /live.js handler authorizes the browser fetch. `live.mjs` always passes
209
+ // it; a manual `--port`-only invocation reads the running helper's token
210
+ // from server.json instead of writing an unauthenticated URL that 401s.
211
+ const tokenIdx = args.indexOf('--token');
212
+ let token = tokenIdx !== -1 ? args[tokenIdx + 1] : undefined;
213
+ if (!token) {
214
+ try {
215
+ const info = JSON.parse(fs.readFileSync(path.join(cwd, '.impeccable', 'live', 'server.json'), 'utf-8'));
216
+ // A record for a DIFFERENT port is a stale or foreign helper; its token
217
+ // would 401 just the same, so only adopt a matching one.
218
+ if (info?.token && Number(info.port) === port) token = info.token;
219
+ } catch { /* no running helper recorded; keep legacy tokenless behavior */ }
220
+ }
149
221
 
150
- if (svelteKit) {
151
- const adapterResult = applySvelteKitLiveAdapter({ cwd: process.cwd(), port, config });
152
- console.log(JSON.stringify({ ok: true, port, adapter: 'sveltekit', gitIgnore, results: [adapterResult] }));
222
+ // Reconcile before writing anything. Artifacts this run is about to own are
223
+ // kept (so a repeat inject stays byte-idempotent); artifacts left behind by
224
+ // a session that never got to stop are healed.
225
+ const plannedArtifacts = describeInjectArtifacts(resolved, { cwd, files: resolvedFiles });
226
+ const { healed } = healInjectJournal(cwd, { keep: plannedArtifacts.map((a) => a.path) });
227
+
228
+ const gitIgnore = ensureLiveGitIgnores(cwd, frameworkIgnorePatterns(resolved));
229
+ // In a nested-app repo the roots pointer lives at the REPO root, outside the
230
+ // reach of the appRoot-relative ignore block above; give that directory its
231
+ // own local excludes so the pointer (absolute host paths) never gets staged.
232
+ try {
233
+ const rootsManifest = JSON.parse(fs.readFileSync(path.join(cwd, '.impeccable', 'live', 'roots.json'), 'utf-8'));
234
+ if (rootsManifest?.repoRoot && path.resolve(rootsManifest.repoRoot) !== path.resolve(cwd)) {
235
+ ensureLiveGitIgnores(rootsManifest.repoRoot);
236
+ }
237
+ } catch { /* no manifest: single-root project */ }
238
+
239
+ if (isAdapter) {
240
+ const adapterResult = resolved.framework.inject.apply({
241
+ cwd,
242
+ port,
243
+ token,
244
+ config,
245
+ project: resolved.project,
246
+ });
247
+ const ok = !(adapterResult && adapterResult.error);
248
+ if (ok) recordInjection(cwd, { framework: resolved.framework.name, port, artifacts: plannedArtifacts });
249
+ console.log(JSON.stringify({
250
+ ok,
251
+ port,
252
+ adapter: resolved.framework.name,
253
+ gitIgnore,
254
+ results: [adapterResult],
255
+ healed: healed.length ? healed : undefined,
256
+ }));
257
+ if (!ok) process.exitCode = 1;
153
258
  return;
154
259
  }
155
260
 
156
261
  const results = resolvedFiles.map((relFile) => {
157
- const absFile = path.resolve(process.cwd(), relFile);
262
+ const absFile = path.resolve(cwd, relFile);
158
263
  if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
159
264
  const content = fs.readFileSync(absFile, 'utf-8');
160
265
  const withoutOld = revertCspMeta(removeTag(content, config.commentSyntax));
161
- const withTag = insertTag(withoutOld, config, port, relFile);
266
+ // Per-file, not per-project: a Vite app can hold an .astro partial, and a
267
+ // framework project's entry template is often plain HTML.
268
+ const scriptAttrs = resolveSourceTraits(relFile).injectScriptAttrs;
269
+ const withTag = insertTag(withoutOld, config, port, token, scriptAttrs);
162
270
  if (withTag === withoutOld) {
163
271
  return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
164
272
  }
@@ -171,16 +279,28 @@ Output (JSON):
171
279
  };
172
280
  });
173
281
  const anyInserted = results.some((r) => r.inserted);
174
- console.log(JSON.stringify({ ok: anyInserted, port, gitIgnore, results }));
282
+ const writtenFiles = new Set(results.filter((r) => r.inserted).map((r) => r.file));
283
+ recordInjection(cwd, {
284
+ framework: resolved?.framework.name,
285
+ port,
286
+ artifacts: plannedArtifacts.filter((a) => writtenFiles.has(a.path)),
287
+ });
288
+ console.log(JSON.stringify({
289
+ ok: anyInserted,
290
+ port,
291
+ gitIgnore,
292
+ results,
293
+ healed: healed.length ? healed : undefined,
294
+ }));
175
295
  if (!anyInserted) process.exit(1);
176
296
  }
177
297
 
178
- export function ensureLiveGitIgnores(cwd = process.cwd()) {
298
+ export function ensureLiveGitIgnores(cwd = process.cwd(), extraPatterns = []) {
179
299
  const target = resolveIgnoreTarget(cwd);
180
300
  const existing = fs.existsSync(target.path) ? fs.readFileSync(target.path, 'utf-8') : '';
181
301
  const block = [
182
302
  IGNORE_MARKER_OPEN,
183
- ...LIVE_IGNORE_PATTERNS,
303
+ ...new Set([...LIVE_IGNORE_PATTERNS, ...extraPatterns]),
184
304
  IGNORE_MARKER_CLOSE,
185
305
  ].join('\n');
186
306
  const markerRe = new RegExp(`${escapeRegExp(IGNORE_MARKER_OPEN)}[\\s\\S]*?${escapeRegExp(IGNORE_MARKER_CLOSE)}`);
@@ -202,7 +322,7 @@ export function ensureLiveGitIgnores(cwd = process.cwd()) {
202
322
  file: path.relative(cwd, target.path).split(path.sep).join('/'),
203
323
  mode: target.mode,
204
324
  changed: updated !== existing,
205
- patterns: [...LIVE_IGNORE_PATTERNS],
325
+ patterns: [...new Set([...LIVE_IGNORE_PATTERNS, ...extraPatterns])],
206
326
  };
207
327
  }
208
328
 
@@ -353,231 +473,31 @@ function validateConfig(cfg) {
353
473
  }
354
474
  }
355
475
 
356
- function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
357
- function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
358
-
359
- function buildTagBlock(syntax, port, filePath) {
360
- const open = commentOpen(syntax);
361
- const close = commentClose(syntax);
362
- // Astro processes <script> tags by default and rewrites src to its own
363
- // bundled URL. is:inline opts out so the literal external src survives.
364
- const isAstro = typeof filePath === 'string' && filePath.endsWith('.astro');
365
- const scriptAttrs = isAstro ? 'is:inline ' : '';
366
- return (
367
- open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
368
- '<script ' + scriptAttrs + 'src="http://localhost:' + port + '/live.js"></script>\n' +
369
- open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
370
- );
371
- }
372
-
373
- function detectLineEnding(content) {
374
- if (content.includes('\r\n')) return '\r\n';
375
- if (content.includes('\r')) return '\r';
376
- return '\n';
377
- }
378
-
379
- function normalizeLineEndings(content, lineEnding) {
380
- return lineEnding === '\n' ? content : content.replace(/\n/g, lineEnding);
381
- }
382
-
383
- function readLineEndingAt(content, index) {
384
- if (content[index] === '\r' && content[index + 1] === '\n') return '\r\n';
385
- if (content[index] === '\n') return '\n';
386
- if (content[index] === '\r') return '\r';
387
- return '';
388
- }
389
-
390
- function insertTag(content, config, port, filePath) {
391
- const lineEnding = detectLineEnding(content);
392
- const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding);
393
- // insertBefore: match the LAST occurrence. Anchors like `</body>` naturally
394
- // belong at the end, and the same literal can appear earlier in code blocks
395
- // within rendered documentation pages.
396
- if (config.insertBefore) {
397
- const idx = content.lastIndexOf(config.insertBefore);
398
- if (idx === -1) return content;
399
- return content.slice(0, idx) + block + content.slice(idx);
400
- }
401
- // insertAfter: match the FIRST occurrence — typical anchors like `<head>` or
402
- // `<body>` open near the top of the document.
403
- const idx = content.indexOf(config.insertAfter);
404
- if (idx === -1) return content;
405
- const after = idx + config.insertAfter.length;
406
- // Preserve an existing trailing newline if the anchor already has one.
407
- // Slice the remainder from the original anchor offset, not prefix.length:
408
- // in the no-newline case prefix is one char longer than the anchor (the
409
- // appended '\n'), so slicing by prefix.length would drop the first real
410
- // character after the anchor (#227).
411
- const existingNewline = readLineEndingAt(content, after);
412
- const prefix = content.slice(0, after) + (existingNewline || lineEnding);
413
- const rest = content.slice(after + existingNewline.length);
414
- return prefix + block + rest;
415
- }
416
-
417
- /**
418
- * Remove the live script block. Matches either HTML or JSX comment markers
419
- * regardless of config (so stale tags from a wrong config can still be cleaned).
420
- *
421
- * Indent-preserving: captures any whitespace immediately preceding the opener
422
- * marker and re-emits it in place of the removed block. `insertTag` inserted
423
- * the block *after* the original line's indent and *before* the anchor (e.g.
424
- * `</body>`), which moved the indent onto the opener line and left the anchor
425
- * unindented. Replacing the whole block (plus its trailing newline) with just
426
- * the captured indent hands the indent back to the anchor that follows.
427
- */
428
- function removeTag(content, _syntax) {
429
- const patterns = [
430
- /([ \t]*)<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->([ \t]*(?:\r\n|\n|\r|$)?)/,
431
- /([ \t]*)\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}([ \t]*(?:\r\n|\n|\r|$)?)/,
432
- ];
433
- for (const pat of patterns) {
434
- let changed = false;
435
- let next = content;
436
- do {
437
- content = next;
438
- next = content.replace(pat, (_match, leadingIndent, trailing = '') => {
439
- if (/[\r\n]/.test(trailing)) return leadingIndent;
440
- return leadingIndent || trailing || '';
441
- });
442
- if (next !== content) changed = true;
443
- } while (next !== content);
444
- if (changed) return next;
445
- }
446
- return content;
447
- }
448
-
449
- // ---------------------------------------------------------------------------
450
- // Content-Security-Policy meta-tag patcher
451
- //
452
- // When the user's HTML carries `<meta http-equiv="Content-Security-Policy">`,
453
- // the cross-origin load of /live.js (and the SSE/POST connection back to
454
- // localhost:PORT) is blocked unless the CSP explicitly allows that origin.
455
- //
456
- // On insert: append `http://localhost:PORT` to `script-src` and `connect-src`,
457
- // and stash the original `content` value in a `data-impeccable-csp-original`
458
- // attribute (base64) so revert is exact.
459
- //
460
- // On remove: detect the marker attribute, decode it, restore the original
461
- // content value verbatim, drop the marker.
462
- //
463
- // Header-based CSP (Next.js headers, Nuxt routeRules, SvelteKit kit.csp,
464
- // shared helpers) is NOT patched here — those need framework-specific config
465
- // edits and are handled via the existing detect-csp.mjs reference output.
466
- // Only the in-source meta-tag form gets the auto-patch.
467
- // ---------------------------------------------------------------------------
468
-
469
- const CSP_MARKER_ATTR = 'data-impeccable-csp-original';
470
-
471
- function findCspMetaTags(content) {
472
- const out = [];
473
- const tagRe = /<meta\s+([^>]*?)\/?>/gis;
474
- let m;
475
- while ((m = tagRe.exec(content)) !== null) {
476
- const attrs = m[1];
477
- if (!/(http-equiv|httpEquiv)\s*=\s*(['"])Content-Security-Policy\2/i.test(attrs)) continue;
478
- out.push({ start: m.index, end: m.index + m[0].length, full: m[0], attrs });
479
- }
480
- return out;
481
- }
482
-
483
- function getAttr(attrs, name) {
484
- const re = new RegExp(`\\b${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, 'i');
485
- const m = attrs.match(re);
486
- return m ? { quote: m[1], value: m[2], full: m[0] } : null;
487
- }
488
-
489
- function appendOriginToDirective(csp, directive, origin) {
490
- const re = new RegExp(`(^|;)(\\s*)(${directive})\\s+([^;]*)`, 'i');
491
- const m = csp.match(re);
492
- if (m) {
493
- const tokens = m[4].trim().split(/\s+/);
494
- if (tokens.includes(origin)) return csp;
495
- return csp.replace(re, `${m[1]}${m[2]}${m[3]} ${[...tokens, origin].join(' ')}`);
496
- }
497
- // Directive missing — add it. Use 'self' + origin so we don't inadvertently
498
- // narrow the policy compared to the default-src fallback (most users with
499
- // an explicit CSP have 'self' there).
500
- return csp.trim().replace(/;?\s*$/, '') + `; ${directive} 'self' ${origin}`;
501
- }
502
-
503
- export function patchCspMeta(content, port) {
504
- const tags = findCspMetaTags(content);
505
- if (tags.length === 0) return content;
506
- const origin = `http://localhost:${port}`;
507
-
508
- // Walk last-to-first so prior splices don't invalidate later indices.
509
- let result = content;
510
- for (let i = tags.length - 1; i >= 0; i--) {
511
- const tag = tags[i];
512
- const attrs = tag.attrs;
513
- if (getAttr(attrs, CSP_MARKER_ATTR)) continue; // already patched
514
- const contentAttr = getAttr(attrs, 'content');
515
- if (!contentAttr) continue;
516
-
517
- const original = contentAttr.value;
518
- let patched = original;
519
- patched = appendOriginToDirective(patched, 'script-src', origin);
520
- patched = appendOriginToDirective(patched, 'connect-src', origin);
521
- // The shader overlay during 'generating' creates a screenshot via
522
- // URL.createObjectURL, producing a `blob:` URL — img-src 'self' rejects
523
- // those. Add `blob:` so the overlay doesn't throw a CSP violation.
524
- patched = appendOriginToDirective(patched, 'img-src', 'blob:');
525
- if (patched === original) continue;
526
-
527
- const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
528
- const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
529
- // The tagRe captures any whitespace between the last attribute and the
530
- // closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
531
- // a replace would land it BEFORE that trailing space, leaving a double
532
- // space inside attrs and clobbering the space before `/>`. Split off
533
- // the trailing whitespace, splice the marker into the attribute body,
534
- // and re-append the original trailing whitespace so a self-closing
535
- // `<meta … />` round-trips byte-for-byte.
536
- const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
537
- const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
538
- const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
539
- const newTag = tag.full.replace(attrs, newAttrs);
540
-
541
- result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
542
- }
543
- return result;
544
- }
545
-
546
- export function revertCspMeta(content) {
547
- const tags = findCspMetaTags(content);
548
- if (tags.length === 0) return content;
549
-
550
- let result = content;
551
- for (let i = tags.length - 1; i >= 0; i--) {
552
- const tag = tags[i];
553
- const origAttr = getAttr(tag.attrs, CSP_MARKER_ATTR);
554
- if (!origAttr) continue;
555
- const contentAttr = getAttr(tag.attrs, 'content');
556
- if (!contentAttr) continue;
557
-
558
- let originalValue;
559
- try { originalValue = Buffer.from(origAttr.value, 'base64').toString('utf-8'); }
560
- catch { continue; }
561
-
562
- const newContentAttr = `content=${contentAttr.quote}${originalValue}${contentAttr.quote}`;
563
- let newAttrs = tag.attrs.replace(contentAttr.full, newContentAttr);
564
- // Drop the marker attribute and any single space immediately preceding it.
565
- newAttrs = newAttrs.replace(new RegExp(`\\s*${origAttr.full}`), '');
566
- const newTag = tag.full.replace(tag.attrs, newAttrs);
567
-
568
- result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
569
- }
570
- return result;
571
- }
572
-
573
476
  // ---------------------------------------------------------------------------
574
477
  // Auto-execute
575
478
  // ---------------------------------------------------------------------------
576
479
 
577
480
  const _running = process.argv[1];
578
481
  if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
482
+ enterLiveRoot();
579
483
  injectCli();
580
484
  }
581
485
 
582
- export { insertTag, removeTag, validateConfig, buildTagBlock };
583
- // patchCspMeta + revertCspMeta are exported above where they're defined.
486
+ // Re-exported so long-standing importers (live.mjs, the adapter modules, the
487
+ // test suites) keep their entry points while the implementations live in
488
+ // live/frameworks/.
489
+ export {
490
+ buildLiveScriptSrc,
491
+ buildTagBlock,
492
+ insertTag,
493
+ patchCspMeta,
494
+ removeTag,
495
+ revertCspMeta,
496
+ validateConfig,
497
+ };
498
+ export {
499
+ applyNuxtLiveAdapter,
500
+ buildNuxtPlugin,
501
+ detectNuxtProject,
502
+ removeNuxtLiveAdapter,
503
+ } from './live/frameworks/nuxt.mjs';