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
@@ -5,7 +5,8 @@
5
5
 
6
6
  import { createLiveSessionStore } from './live/session-store.mjs';
7
7
  import { readLiveServerInfo } from './lib/impeccable-paths.mjs';
8
- import { manualApplyResumeHint } from './live-resume.mjs';
8
+ import { manualApplyResumeHint, mountFailureAction, renderSummary } from './live-resume.mjs';
9
+ import { enterLiveRoot } from './live/roots.mjs';
9
10
 
10
11
  function readServerInfo() {
11
12
  return readLiveServerInfo(process.cwd())?.info || null;
@@ -28,6 +29,8 @@ export async function statusCli() {
28
29
  const store = createLiveSessionStore({ cwd: process.cwd() });
29
30
  const activeSessions = store.listActiveSessions();
30
31
  const manualApply = findPendingManualApply(server, activeSessions);
32
+ const sessions = server?.activeSessions || activeSessions;
33
+ const renderFailure = sessions.find((session) => session?.renderState === 'failed') || null;
31
34
  const payload = {
32
35
  liveServer: server ? {
33
36
  status: server.status,
@@ -36,16 +39,22 @@ export async function statusCli() {
36
39
  agentPolling: server.agentPolling,
37
40
  pendingEvents: server.pendingEvents,
38
41
  } : null,
39
- activeSessions: server?.activeSessions || activeSessions,
40
- recoveryHint: manualApply
41
- ? manualApplyResumeHint(manualApply)
42
- : server
43
- ? 'Run live-poll.mjs to continue pending work, or live-complete.mjs --id <session> after manual cleanup.'
44
- : 'Start live-server.mjs to requeue pending durable events, then run live-poll.mjs.',
42
+ activeSessions: sessions,
43
+ render: sessions.map((session) => ({ id: session?.id ?? null, ...renderSummary(session) })),
44
+ recoveryHint: recoveryHint({ server, manualApply, renderFailure }),
45
45
  };
46
46
  console.log(JSON.stringify(payload, null, 2));
47
47
  }
48
48
 
49
+ function recoveryHint({ server, manualApply, renderFailure }) {
50
+ if (manualApply) return manualApplyResumeHint(manualApply);
51
+ if (renderFailure) return mountFailureAction(renderFailure);
52
+ if (server) {
53
+ return 'Run live-poll.mjs to continue pending work, or live-complete.mjs --id <session> after manual cleanup.';
54
+ }
55
+ return 'Start live-server.mjs to requeue pending durable events, then run live-poll.mjs.';
56
+ }
57
+
49
58
  function findPendingManualApply(server, activeSessions) {
50
59
  const fromServer = server?.pendingEvents?.find((event) => event?.type === 'manual_edit_apply');
51
60
  if (fromServer) return fromServer;
@@ -57,5 +66,6 @@ function findPendingManualApply(server, activeSessions) {
57
66
 
58
67
  const _running = process.argv[1];
59
68
  if (_running?.endsWith('live-status.mjs') || _running?.endsWith('live-status.mjs/')) {
69
+ enterLiveRoot();
60
70
  statusCli();
61
71
  }
@@ -14,14 +14,16 @@
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import { isGeneratedFile } from './lib/is-generated.mjs';
17
+ import { resolveLiveTemplateExtensions } from './lib/template-extensions.mjs';
17
18
  import { readBuffer as readManualEditsBuffer } from './live/manual-edits-buffer.mjs';
19
+ import { findSourceFile } from './live/source-search.mjs';
20
+ import { resolveSourceTraits } from './live/frameworks/index.mjs';
18
21
  import {
19
22
  buildSvelteComponentCssAuthoring,
20
23
  scaffoldSvelteComponentSession,
21
24
  shouldUseSvelteComponentInjection,
22
25
  } from './live/svelte-component.mjs';
23
-
24
- const EXTENSIONS = ['.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro'];
26
+ import { enterLiveRoot } from './live/roots.mjs';
25
27
 
26
28
  export async function wrapCli() {
27
29
  const args = process.argv.slice(2);
@@ -68,6 +70,13 @@ The agent should insert variant HTML at insertLine.`);
68
70
  const filePath = argVal(args, '--file');
69
71
  const text = argVal(args, '--text');
70
72
  const pageUrl = argVal(args, '--page-url');
73
+ // Preflight passes this for source-preview targets. It computes the scaffold
74
+ // (element location + wrapper text) but does NOT write it into source. The
75
+ // agent then writes the wrapper + all variants in one atomic edit. The
76
+ // premature server-side write full-reloaded the framework mid-generate and
77
+ // stranded the browser at 0/N (live-server.mjs missed-completion note). It is
78
+ // a no-op on the svelte-component path, which never writes the route source.
79
+ const deferSourceWrite = args.includes('--defer-source-write');
71
80
 
72
81
  if (!id) { console.error('Missing --id'); process.exit(1); }
73
82
  if (!elementId && !classes && !query) {
@@ -160,11 +169,29 @@ The agent should insert variant HTML at insertLine.`);
160
169
  if (filtered.length === 1) {
161
170
  match = filtered[0];
162
171
  } else if (filtered.length === 0) {
163
- // Source uses dynamic content (`<h1>{title}</h1>` etc.) so the
164
- // browser-side textContent doesn't appear literally in source. Fall
165
- // back to first-match rather than refusing this is the same
166
- // behavior unmodified callers see, just preserved.
167
- match = candidates[0];
172
+ const normalizedText = String(text).replace(/\s+/g, ' ').trim();
173
+ if (normalizedText.length < 8) {
174
+ // Very short labels cannot disambiguate siblings reliably. Preserve
175
+ // the legacy behavior for these low-information picker events.
176
+ match = candidates[0];
177
+ } else {
178
+ // Rendered text that is absent from every candidate usually means
179
+ // the source uses expressions or component props. Picking the first
180
+ // same-class sibling silently edits the wrong instance (observed on
181
+ // Astro result cards), so stop and surface every candidate instead.
182
+ console.error(JSON.stringify({
183
+ error: 'element_ambiguous',
184
+ fallback: 'agent-driven',
185
+ reason: 'rendered_text_not_in_source',
186
+ file: path.relative(process.cwd(), targetFile),
187
+ candidates: candidates.map((c) => ({
188
+ startLine: c.startLine + 1,
189
+ endLine: c.endLine + 1,
190
+ })),
191
+ hint: 'Rendered text does not occur in any matching source branch. The element may use dynamic props or expressions; inspect the candidates and wrap the intended instance manually.',
192
+ }));
193
+ process.exit(1);
194
+ }
168
195
  } else {
169
196
  // Multiple candidates ALSO match the text. Truly ambiguous — refuse
170
197
  // rather than pick wrong, and hand the agent the candidate locations
@@ -268,7 +295,10 @@ The agent should insert variant HTML at insertLine.`);
268
295
  .join('\n');
269
296
  const originalIndented = reindentOriginal(' ');
270
297
  const relTargetFile = path.relative(process.cwd(), targetFile).split(path.sep).join('/');
271
- const useSvelteComponent = shouldUseSvelteComponentInjection(targetFile);
298
+ // The registry says which files get component preview; the svelte-component
299
+ // module keeps the env escape hatch that turns it off.
300
+ const useSvelteComponent = resolveSourceTraits(targetFile).preview === 'component'
301
+ && shouldUseSvelteComponentInjection(targetFile);
272
302
 
273
303
  // Wrapper attributes differ by syntax. HTML allows plain string attrs;
274
304
  // JSX requires object-literal style and parses string attrs as HTML (which
@@ -288,7 +318,7 @@ The agent should insert variant HTML at insertLine.`);
288
318
  // replacement range to include the wrapper's `<div>` open / close lines
289
319
  // so the entire scaffold gets removed cleanly.
290
320
  const wrapperLines = isJsx ? [
291
- indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
321
+ indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '"' + ' ' + styleContents + '>',
292
322
  indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
293
323
  indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
294
324
  indent + ' <div data-impeccable-variant="original">',
@@ -299,7 +329,7 @@ The agent should insert variant HTML at insertLine.`);
299
329
  indent + '</div>',
300
330
  ] : [
301
331
  indent + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
302
- indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
332
+ indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '"' + ' ' + styleContents + '>',
303
333
  indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
304
334
  indent + ' <div data-impeccable-variant="original">',
305
335
  originalIndented,
@@ -315,13 +345,20 @@ The agent should insert variant HTML at insertLine.`);
315
345
  let outputEndLine = startLine + wrapperLines.length + (originalLines.length - 1);
316
346
  let insertLine;
317
347
  let svelteSession = null;
348
+ let deferredWrapper = null;
318
349
 
350
+ let sveltePreviewFallback = null;
319
351
  if (useSvelteComponent) {
320
352
  // Svelte/SvelteKit resets component-local state on markup HMR updates.
321
353
  // Keep generation source-neutral: agents write real variant components
322
354
  // under the generated componentDir, the browser mounts them into the live
323
355
  // DOM, and live-accept.mjs inlines the accepted variant back into the route.
324
- svelteSession = scaffoldSvelteComponentSession({
356
+ //
357
+ // The scaffold is AST-based and refuses markup a detached preview cannot
358
+ // support (component tags, bind:/use:, await blocks, bound nested each).
359
+ // Refusal falls back to the plain source-preview wrapper below: an
360
+ // HMR-resetting but CORRECT preview beats a detached wrong one.
361
+ const scaffolded = scaffoldSvelteComponentSession({
325
362
  id,
326
363
  count,
327
364
  sourceFile: relTargetFile,
@@ -330,10 +367,32 @@ The agent should insert variant HTML at insertLine.`);
330
367
  originalLines,
331
368
  cwd: process.cwd(),
332
369
  });
333
- outputFile = path.resolve(process.cwd(), svelteSession.manifestFile);
334
- outputStartLine = 1;
335
- outputEndLine = 1;
336
- insertLine = 1;
370
+ if (scaffolded && scaffolded.fallback === 'source-preview') {
371
+ sveltePreviewFallback = scaffolded.reason || 'unsupported markup';
372
+ } else {
373
+ svelteSession = scaffolded;
374
+ outputFile = path.resolve(process.cwd(), svelteSession.manifestFile);
375
+ outputStartLine = 1;
376
+ outputEndLine = 1;
377
+ insertLine = 1;
378
+ }
379
+ }
380
+ if (svelteSession) {
381
+ // component preview: outputs already set above
382
+ } else if (deferSourceWrite) {
383
+ // Deferred source write: compute the scaffold text but leave source
384
+ // untouched. The agent replaces the picked element's source range with
385
+ // `wrapperBlock` (variants spliced at the marker) in one edit. Writing the
386
+ // scaffold here first would reload the framework before the agent's write
387
+ // lands, and a browser caught mid-reload misses the `done` and sits at 0/N.
388
+ deferredWrapper = {
389
+ block: wrapperLines.join('\n'),
390
+ replaceStartLine: startLine + 1, // 1-indexed picked-element range the
391
+ replaceEndLine: endLine + 1, // agent's wrapper block replaces
392
+ };
393
+ // insertLine matches the final file position the wrapper occupies once the
394
+ // agent replaces the picked range, so downstream consumers stay consistent.
395
+ insertLine = startLine + 6 + (originalLines.length - 1) + 1;
337
396
  } else {
338
397
  // Replace the original element with the wrapper
339
398
  const newLines = [
@@ -355,16 +414,31 @@ The agent should insert variant HTML at insertLine.`);
355
414
 
356
415
  const outputRelFile = path.relative(process.cwd(), outputFile).split(path.sep).join('/');
357
416
 
358
- const svelteComponentAuthoring = useSvelteComponent ? buildSvelteComponentCssAuthoring(count) : null;
417
+ const componentPreviewActive = !!svelteSession;
418
+ const svelteComponentAuthoring = componentPreviewActive ? buildSvelteComponentCssAuthoring(count) : null;
419
+ const componentSession = svelteSession;
420
+ const componentPreviewMode = componentPreviewActive ? 'svelte-component' : undefined;
421
+ const previewMode = componentPreviewMode;
359
422
 
360
423
  console.log(JSON.stringify({
361
424
  file: outputRelFile,
362
- sourceFile: useSvelteComponent ? relTargetFile : undefined,
363
- previewMode: useSvelteComponent ? 'svelte-component' : undefined,
364
- componentDir: svelteSession?.componentDir,
365
- propContract: svelteSession?.propContract,
366
- sourceStartLine: useSvelteComponent ? startLine + 1 : undefined,
367
- sourceEndLine: useSvelteComponent ? endLine + 1 : undefined,
425
+ sourceFile: componentPreviewActive ? relTargetFile : undefined,
426
+ previewMode,
427
+ previewFallback: sveltePreviewFallback
428
+ ? { from: 'svelte-component', reason: sveltePreviewFallback }
429
+ : undefined,
430
+ // Deferred source write: the wrapper is NOT yet in source. The agent
431
+ // replaces [replaceStartLine, replaceEndLine] with `wrapperBlock` (variants
432
+ // spliced at the "insert below this line" marker) in one atomic edit.
433
+ sourceWritten: deferredWrapper ? false : undefined,
434
+ wrapperBlock: deferredWrapper ? deferredWrapper.block : undefined,
435
+ replaceStartLine: deferredWrapper ? deferredWrapper.replaceStartLine : undefined,
436
+ replaceEndLine: deferredWrapper ? deferredWrapper.replaceEndLine : undefined,
437
+ componentDir: componentSession?.componentDir,
438
+ propContract: componentSession?.propContract,
439
+ componentStubMarkup: componentSession?.stubMarkup,
440
+ sourceStartLine: componentPreviewActive ? startLine + 1 : undefined,
441
+ sourceEndLine: componentPreviewActive ? endLine + 1 : undefined,
368
442
  startLine: outputStartLine, // 1-indexed for the agent
369
443
  // wrapperLines is an array but one element (the original-content slot)
370
444
  // is a `\n`-joined multi-line string, so the actual file-row count is
@@ -374,10 +448,10 @@ The agent should insert variant HTML at insertLine.`);
374
448
  endLine: outputEndLine, // 1-indexed
375
449
  insertLine, // 1-indexed: where variants go
376
450
  commentSyntax: commentSyntax,
377
- styleMode: useSvelteComponent ? 'svelte-component' : styleMode.mode,
378
- styleTag: useSvelteComponent ? null : styleMode.styleTag,
379
- cssSelectorPrefixExamples: useSvelteComponent ? [] : buildCssSelectorPrefixExamples(styleMode.mode, count),
380
- cssAuthoring: useSvelteComponent ? svelteComponentAuthoring : buildCssAuthoring(styleMode, count),
451
+ styleMode: componentPreviewMode || styleMode.mode,
452
+ styleTag: componentPreviewActive ? null : styleMode.styleTag,
453
+ cssSelectorPrefixExamples: componentPreviewActive ? [] : buildCssSelectorPrefixExamples(styleMode.mode, count),
454
+ cssAuthoring: svelteComponentAuthoring || buildCssAuthoring(styleMode, count),
381
455
  originalLineCount: originalLines.length,
382
456
  }));
383
457
  }
@@ -579,27 +653,22 @@ function attrEscapeDouble(str) {
579
653
  .replace(/>/g, '&gt;');
580
654
  }
581
655
 
656
+ /**
657
+ * Comment syntax, style mode, and preview strategy all come from the framework
658
+ * registry, keyed on the target file's extension: `.jsx`/`.tsx` author JSX
659
+ * comments, `.astro` needs global-prefixed preview CSS because Astro scopes
660
+ * component styles away from the generated wrappers, `.svelte` gets component
661
+ * preview. See live/frameworks/index.mjs for why extension and not project.
662
+ */
582
663
  function detectCommentSyntax(filePath) {
583
- const ext = path.extname(filePath).toLowerCase();
584
- if (ext === '.jsx' || ext === '.tsx') {
585
- return { open: '{/*', close: '*/}' };
586
- }
587
- // HTML, Vue, Svelte, Astro all use HTML comments
588
- return { open: '<!--', close: '-->' };
664
+ return resolveSourceTraits(filePath).commentSyntax === 'jsx'
665
+ ? { open: '{/*', close: '*/}' }
666
+ : { open: '<!--', close: '-->' };
589
667
  }
590
668
 
591
669
  function detectStyleMode(filePath) {
592
- const ext = path.extname(filePath).toLowerCase();
593
- if (ext === '.astro') {
594
- return {
595
- mode: 'astro-global-prefixed',
596
- styleTag: '<style is:inline data-impeccable-css="SESSION_ID">',
597
- };
598
- }
599
- return {
600
- mode: 'scoped',
601
- styleTag: '<style data-impeccable-css="SESSION_ID">',
602
- };
670
+ const traits = resolveSourceTraits(filePath);
671
+ return { mode: traits.styleMode, styleTag: traits.styleTag };
603
672
  }
604
673
 
605
674
  function buildCssSelectorPrefixExamples(styleMode, count) {
@@ -650,56 +719,19 @@ function buildCssAuthoring(styleMode, count) {
650
719
  /**
651
720
  * Search project files for the query string (class name, ID, etc.)
652
721
  * Returns the first matching file path, or null.
722
+ *
723
+ * Only `node_modules`, `.git`, and `.impeccable` are skipped outright.
724
+ * dist/build/out are left to the isGeneratedFile guard so the
725
+ * `includeGenerated` second pass can still find the element there and report
726
+ * `generatedMatch`.
653
727
  */
654
728
  function findFileWithQuery(query, cwd, genOpts = {}) {
655
- const searchDirs = ['src', 'app', 'pages', 'components', 'public', 'views', 'templates', '.'];
656
- const seen = new Set();
657
-
658
- for (const dir of searchDirs) {
659
- const absDir = path.join(cwd, dir);
660
- if (!fs.existsSync(absDir)) continue;
661
- const result = searchDir(absDir, query, seen, 0, genOpts);
662
- if (result) return result;
663
- }
664
- return null;
665
- }
666
-
667
- function searchDir(dir, query, seen, depth, genOpts) {
668
- if (depth > 5) return null; // don't go too deep
669
- const realDir = fs.realpathSync(dir);
670
- if (seen.has(realDir)) return null;
671
- seen.add(realDir);
672
-
673
- let entries;
674
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
675
- catch { return null; }
676
-
677
- // Check files first
678
- for (const entry of entries) {
679
- if (!entry.isFile()) continue;
680
- const ext = path.extname(entry.name).toLowerCase();
681
- if (!EXTENSIONS.includes(ext)) continue;
682
-
683
- const filePath = path.join(dir, entry.name);
684
- if (!genOpts.includeGenerated && isGeneratedFile(filePath, genOpts)) continue;
685
- try {
686
- const content = fs.readFileSync(filePath, 'utf-8');
687
- if (content.includes(query)) return filePath;
688
- } catch { /* skip unreadable files */ }
689
- }
690
-
691
- // Then recurse into directories. Always skip node_modules and .git (never
692
- // project content). dist/build/out are left to the isGeneratedFile guard so
693
- // the includeGenerated second-pass can still find the element there and
694
- // report `generatedMatch`.
695
- for (const entry of entries) {
696
- if (!entry.isDirectory()) continue;
697
- if (entry.name === 'node_modules' || entry.name === '.git') continue;
698
- const result = searchDir(path.join(dir, entry.name), query, seen, depth + 1, genOpts);
699
- if (result) return result;
700
- }
701
-
702
- return null;
729
+ return findSourceFile({
730
+ query,
731
+ cwd,
732
+ extensions: resolveLiveTemplateExtensions(cwd),
733
+ fileFilter: (filePath) => genOpts.includeGenerated || !isGeneratedFile(filePath, genOpts),
734
+ });
703
735
  }
704
736
 
705
737
  /**
@@ -876,6 +908,7 @@ function findClosingLine(lines, start) {
876
908
  // Auto-execute when run directly (node live-wrap.mjs ...)
877
909
  const _running = process.argv[1];
878
910
  if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) {
911
+ enterLiveRoot();
879
912
  wrapCli();
880
913
  }
881
914
 
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * After this, the agent's only remaining steps are:
12
12
  * - Open the project's live dev/preview URL in the browser (optional, if browser automation exists)—not `serverPort`; that port is the Impeccable helper for /live.js and /poll
13
- * - Enter the poll loop: `node live-poll.mjs`
13
+ * - Enter the harness-native poll loop: `node live-poll.mjs`
14
14
  *
15
15
  * Usage:
16
16
  * node live.mjs # Prepare everything, print JSON, exit
@@ -21,10 +21,13 @@ import { execSync } from 'node:child_process';
21
21
  import fs from 'node:fs';
22
22
  import path from 'node:path';
23
23
  import { fileURLToPath } from 'node:url';
24
- import { loadContext, resolveTargetSelection } from './context.mjs';
24
+ import { resolveTargetSelection } from './context.mjs';
25
25
  import { resolveFiles } from './live-inject.mjs';
26
26
  import { readLiveServerInfo } from './lib/impeccable-paths.mjs';
27
+ import { resolveSurfaceBrief } from './lib/surface-briefs.mjs';
27
28
  import { resolveLiveTarget } from './live-target.mjs';
29
+ import { bootInstructions } from './live/instructions.mjs';
30
+ import { resolveRoots, writeRootsManifest } from './live/roots.mjs';
28
31
 
29
32
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
33
 
@@ -40,6 +43,7 @@ Prepare everything for live variant mode in a single command:
40
43
  - Starts (or reuses) the live server in the background
41
44
  - Injects the browser script tag
42
45
  - Reads PRODUCT.md / DESIGN.md for project context
46
+ - Prepares the harness-native foreground/background poll loop
43
47
  - In monorepos, choose a child app first; --target <path> is the fallback/manual path
44
48
 
45
49
  On success, prints a JSON blob with:
@@ -59,6 +63,8 @@ The agent should then:
59
63
  process.exit(0);
60
64
  }
61
65
 
66
+ // Legacy workspace-monorepo selection first: it carries richer candidate
67
+ // metadata (context inheritance status) than the roots scan.
62
68
  const targetSelection = resolveTargetSelection(liveTarget.originalCwd, liveTarget.targetOptions);
63
69
  if (targetSelection) {
64
70
  console.log(JSON.stringify({
@@ -70,11 +76,31 @@ The agent should then:
70
76
  process.exit(0);
71
77
  }
72
78
 
73
- const ctx = loadContext(liveTarget.originalCwd, liveTarget.targetOptions);
74
- const activeCwd = ctx.projectRoot;
79
+ const rootsResult = resolveRoots({
80
+ cwd: liveTarget.originalCwd,
81
+ targetPath: liveTarget.absoluteTargetPath,
82
+ });
83
+ if (rootsResult.selection) {
84
+ console.log(JSON.stringify({
85
+ ok: false,
86
+ error: 'target_selection_required',
87
+ targetCandidates: rootsResult.selection.candidates,
88
+ hint: 'Several apps with a dev-server config exist. Ask the user which one to use, then rerun with --target <path into that app>.',
89
+ }, null, 2));
90
+ process.exit(0);
91
+ }
92
+ const roots = rootsResult.manifest;
93
+ const activeCwd = roots.appRoot;
75
94
  const outputTargetPath = liveTarget.targetPath || null;
76
95
 
77
- const missingContext = missingLiveContext(ctx);
96
+ // Gate on readable CONTENT, not path existence, so an empty or unreadable
97
+ // PRODUCT.md routes to init instead of passing the gate and then reporting
98
+ // hasProduct: false in the same payload.
99
+ const product = safeRead(roots.productPath);
100
+ const design = safeRead(roots.designPath);
101
+ const missingContext = [];
102
+ if (!product) missingContext.push('PRODUCT.md');
103
+ if (!design) missingContext.push('DESIGN.md');
78
104
  if (missingContext.length > 0) {
79
105
  console.log(JSON.stringify({
80
106
  ok: false,
@@ -82,14 +108,18 @@ The agent should then:
82
108
  missing: missingContext,
83
109
  nextCommand: missingContext.includes('PRODUCT.md') ? 'init' : 'document',
84
110
  targetPath: outputTargetPath,
85
- projectRoot: ctx.projectRoot,
86
- repoRoot: ctx.repoRoot,
87
- productPath: ctx.productPath,
88
- designPath: ctx.designPath,
111
+ projectRoot: roots.appRoot,
112
+ repoRoot: roots.repoRoot,
113
+ productPath: relOrNull(liveTarget.originalCwd, roots.productPath),
114
+ designPath: relOrNull(liveTarget.originalCwd, roots.designPath),
89
115
  }, null, 2));
90
116
  process.exit(0);
91
117
  }
92
118
 
119
+ // Persist the decision before anything else spawns, so every helper the
120
+ // agent runs later (from any cwd inside the repo) lands on the same roots.
121
+ writeRootsManifest(roots);
122
+
93
123
  // 1. Check config (fail fast if missing — no point starting anything else)
94
124
  const checkOut = runScript('live-inject.mjs', ['--check'], { cwd: activeCwd });
95
125
  const checkResult = safeParse(checkOut);
@@ -97,8 +127,8 @@ The agent should then:
97
127
  console.log(JSON.stringify({
98
128
  ...(checkResult || { ok: false, error: 'check_failed', raw: checkOut }),
99
129
  targetPath: outputTargetPath,
100
- projectRoot: ctx.projectRoot,
101
- repoRoot: ctx.repoRoot,
130
+ projectRoot: roots.appRoot,
131
+ repoRoot: roots.repoRoot,
102
132
  }));
103
133
  process.exit(0);
104
134
  }
@@ -111,7 +141,11 @@ The agent should then:
111
141
  }
112
142
 
113
143
  // 3. Inject the script tag at the current port
114
- const injectOut = runScript('live-inject.mjs', ['--port', String(serverInfo.port)], { cwd: activeCwd });
144
+ const injectOut = runScript(
145
+ 'live-inject.mjs',
146
+ ['--port', String(serverInfo.port), '--token', String(serverInfo.token)],
147
+ { cwd: activeCwd },
148
+ );
115
149
  const injectResult = safeParse(injectOut);
116
150
  if (!injectResult || !injectResult.ok) {
117
151
  console.log(JSON.stringify({
@@ -129,7 +163,28 @@ The agent should then:
129
163
  const resolvedFiles = resolveFiles(activeCwd, checkResult.config);
130
164
  const drift = scanForDrift(activeCwd, resolvedFiles, checkResult.config);
131
165
 
132
- // 5. Emit everything the agent needs
166
+ // 5. Emit everything the agent needs. The surface brief rides along so the
167
+ // agent does not spend three more tool calls (and a --help miss) on
168
+ // surface-brief.mjs before the first poll.
169
+ let surfaceBrief = null;
170
+ let surfaceBriefPath = null;
171
+ try {
172
+ // Briefs live under .impeccable/surfaces, which in a nested-app repo sits
173
+ // at the CONTEXT or repo root, not the app root; context.mjs already finds
174
+ // them there, and live must not report "no brief" for the same project.
175
+ const briefRoots = [roots.appRoot, roots.contextRoot, roots.repoRoot]
176
+ .filter(Boolean)
177
+ .filter((dir, i, arr) => arr.findIndex((other) => path.resolve(other) === path.resolve(dir)) === i);
178
+ for (const briefRoot of briefRoots) {
179
+ const resolvedBrief = resolveSurfaceBrief(briefRoot, liveTarget.absoluteTargetPath || null);
180
+ if (!resolvedBrief?.brief) continue;
181
+ surfaceBrief = resolvedBrief.brief.text ?? safeRead(resolvedBrief.brief.path);
182
+ surfaceBriefPath = resolvedBrief.brief.path
183
+ ? path.relative(liveTarget.originalCwd, resolvedBrief.brief.path)
184
+ : null;
185
+ break;
186
+ }
187
+ } catch { /* briefs are optional context */ }
133
188
  console.log(JSON.stringify({
134
189
  ok: true,
135
190
  serverPort: serverInfo.port,
@@ -138,22 +193,29 @@ The agent should then:
138
193
  liveConfigPath: checkResult.path,
139
194
  configDrift: drift,
140
195
  targetPath: outputTargetPath,
141
- projectRoot: ctx.projectRoot,
142
- repoRoot: ctx.repoRoot,
143
- hasProduct: ctx.hasProduct,
144
- product: ctx.product,
145
- productPath: ctx.productPath,
146
- hasDesign: ctx.hasDesign,
147
- design: ctx.design,
148
- designPath: ctx.designPath,
196
+ projectRoot: roots.appRoot,
197
+ repoRoot: roots.repoRoot,
198
+ roots,
199
+ hasProduct: !!product,
200
+ product,
201
+ productPath: relOrNull(liveTarget.originalCwd, roots.productPath),
202
+ hasDesign: !!design,
203
+ design,
204
+ designPath: relOrNull(liveTarget.originalCwd, roots.designPath),
205
+ hasSurfaceBrief: !!surfaceBrief,
206
+ surfaceBrief,
207
+ surfaceBriefPath,
208
+ _instructions: bootInstructions({ scriptsPath: __dirname }),
149
209
  }, null, 2));
150
210
  }
151
211
 
152
- function missingLiveContext(ctx) {
153
- const missing = [];
154
- if (!ctx.hasProduct) missing.push('PRODUCT.md');
155
- if (!ctx.hasDesign) missing.push('DESIGN.md');
156
- return missing;
212
+ function safeRead(p) {
213
+ if (!p) return null;
214
+ try { return fs.readFileSync(p, 'utf-8'); } catch { return null; }
215
+ }
216
+
217
+ function relOrNull(base, p) {
218
+ return p ? path.relative(base, p) : null;
157
219
  }
158
220
 
159
221
  /**