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
@@ -0,0 +1,508 @@
1
+ /**
2
+ * Live root resolution: the single place that decides which directories a live
3
+ * session operates on. Every live entry script resolves this once at startup
4
+ * (see enterLiveRoot) instead of trusting its ambient cwd, which is how a
5
+ * `cd` used to silently fork the whole system into a second, empty project.
6
+ *
7
+ * Four distinct roots travel together as one manifest:
8
+ *
9
+ * appRoot what the dev server serves; where live session state,
10
+ * injected adapters, and preview modules live.
11
+ * repoRoot the git boundary (falls back to appRoot outside git).
12
+ * contextRoot the nearest directory from appRoot up to repoRoot carrying
13
+ * PRODUCT.md / DESIGN.md (canonical spot or a fallback dir).
14
+ * sessionRoot <appRoot>/.impeccable/live — durable live state.
15
+ *
16
+ * appRoot detection keys on dev-server config presence (vite/svelte/next/
17
+ * astro/nuxt/... config files), not on monorepo brand markers. A nested
18
+ * website/ with vite.config.js wins over a repo root that merely has a
19
+ * package.json. Workspace declarations are one input, not the gatekeeper.
20
+ *
21
+ * The resolved manifest is persisted at <appRoot>/.impeccable/live/roots.json
22
+ * plus a pointer at <repoRoot>/.impeccable/live/app-root.json when the two
23
+ * differ, so a helper invoked from anywhere inside the repo finds the same
24
+ * roots the boot decided on. When several apps in one repo run live, the
25
+ * pointer follows the most recent boot; per-app roots.json files stay put.
26
+ */
27
+ import fs from 'node:fs';
28
+ import os from 'node:os';
29
+ import path from 'node:path';
30
+ import { execFileSync } from 'node:child_process';
31
+ import { resolveProjectRoot } from '../context.mjs';
32
+
33
+ const ROOTS_MANIFEST_VERSION = 1;
34
+ const ROOTS_FILE = 'roots.json';
35
+ const POINTER_FILE = 'app-root.json';
36
+
37
+ const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md'];
38
+ const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md'];
39
+ const CONTEXT_FALLBACK_DIRS = ['.agents/context', 'docs'];
40
+
41
+ // Presence of any of these marks a directory as a dev-served app root.
42
+ const DEV_CONFIG_MARKERS = [
43
+ 'vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.mts', 'vite.config.cjs',
44
+ 'svelte.config.js', 'svelte.config.mjs', 'svelte.config.ts',
45
+ 'next.config.js', 'next.config.mjs', 'next.config.ts',
46
+ 'astro.config.mjs', 'astro.config.js', 'astro.config.ts', 'astro.config.cjs',
47
+ 'nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs',
48
+ 'remix.config.js', 'react-router.config.ts',
49
+ 'angular.json',
50
+ 'webpack.config.js', 'webpack.config.ts',
51
+ ];
52
+
53
+ const CANDIDATE_SCAN_IGNORED = new Set([
54
+ 'node_modules', '.git', 'dist', 'build', 'coverage', 'vendor', 'vendors',
55
+ '.next', '.nuxt', '.svelte-kit', '.astro', '.turbo', '.cache', '.vercel',
56
+ ]);
57
+ const CANDIDATE_SCAN_DEPTH = 2;
58
+
59
+ function exists(p) {
60
+ try { fs.statSync(p); return true; } catch { return false; }
61
+ }
62
+
63
+ function isDir(p) {
64
+ try { return fs.statSync(p).isDirectory(); } catch { return false; }
65
+ }
66
+
67
+ function firstExisting(dir, names) {
68
+ for (const name of names) {
69
+ const abs = path.join(dir, name);
70
+ if (exists(abs)) return abs;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ function hasDevConfig(dir) {
76
+ if (DEV_CONFIG_MARKERS.some((name) => exists(path.join(dir, name)))) return true;
77
+ // A plain Vite app can run with zero config: index.html + package.json.
78
+ return exists(path.join(dir, 'index.html')) && exists(path.join(dir, 'package.json'));
79
+ }
80
+
81
+ function isAppRoot(dir) {
82
+ // A directory already configured for live IS an app root, dev config or not
83
+ // (plain static multi-page projects have no bundler config).
84
+ return hasDevConfig(dir) || exists(path.join(dir, '.impeccable', 'live', 'config.json'));
85
+ }
86
+
87
+ function findContextFile(dir, names) {
88
+ const direct = firstExisting(dir, names);
89
+ if (direct) return direct;
90
+ for (const rel of CONTEXT_FALLBACK_DIRS) {
91
+ const nested = firstExisting(path.join(dir, rel), names);
92
+ if (nested) return nested;
93
+ }
94
+ return null;
95
+ }
96
+
97
+ export function findGitRoot(startDir) {
98
+ let dir = path.resolve(startDir);
99
+ const home = path.resolve(os.homedir());
100
+ while (true) {
101
+ if (dir === home) return null;
102
+ if (exists(path.join(dir, '.git'))) return dir;
103
+ const parent = path.dirname(dir);
104
+ if (parent === dir) return null;
105
+ dir = parent;
106
+ }
107
+ }
108
+
109
+ function walkUp(startDir, upperBound, visit) {
110
+ let dir = path.resolve(startDir);
111
+ const stop = path.resolve(upperBound);
112
+ const home = path.resolve(os.homedir());
113
+ while (true) {
114
+ if (dir === home) return null;
115
+ const hit = visit(dir);
116
+ if (hit) return hit;
117
+ if (dir === stop) return null;
118
+ const parent = path.dirname(dir);
119
+ if (parent === dir) return null;
120
+ dir = parent;
121
+ }
122
+ }
123
+
124
+ function insideOrEqual(candidate, root) {
125
+ const rel = path.relative(path.resolve(root), path.resolve(candidate));
126
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
127
+ }
128
+
129
+ /**
130
+ * Scan downward (bounded depth) for directories carrying a dev-server config.
131
+ * Used when live boots from a directory that is not itself an app root and no
132
+ * --target narrows the choice: one candidate is auto-picked, several become a
133
+ * selection prompt.
134
+ */
135
+ export function discoverAppCandidates(rootDir, depth = CANDIDATE_SCAN_DEPTH) {
136
+ const found = [];
137
+ const scan = (dir, remaining) => {
138
+ let entries;
139
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
140
+ for (const entry of entries) {
141
+ if (!entry.isDirectory()) continue;
142
+ if (entry.name.startsWith('.') || CANDIDATE_SCAN_IGNORED.has(entry.name)) continue;
143
+ const abs = path.join(dir, entry.name);
144
+ // Same criterion as the upward walk (isAppRoot): a live-configured
145
+ // plain-static site with no bundler markers is still an app, and
146
+ // missing it here would silently fall back to the wrong root.
147
+ if (isAppRoot(abs)) {
148
+ found.push(abs);
149
+ continue; // nested apps below an app root are that app's business
150
+ }
151
+ if (remaining > 1) scan(abs, remaining - 1);
152
+ }
153
+ };
154
+ scan(path.resolve(rootDir), depth);
155
+ return found.sort();
156
+ }
157
+
158
+ /**
159
+ * Fresh root resolution. Never reads a persisted manifest.
160
+ *
161
+ * Returns { manifest } on success or { selection } when several candidate
162
+ * apps exist and nothing disambiguates.
163
+ */
164
+ export function resolveRoots({ cwd = process.cwd(), targetPath = null } = {}) {
165
+ const absCwd = path.resolve(cwd);
166
+ const absTarget = targetPath
167
+ ? (path.isAbsolute(targetPath) ? targetPath : path.resolve(absCwd, targetPath))
168
+ : null;
169
+ const targetDir = absTarget
170
+ ? (isDir(absTarget) ? absTarget : path.dirname(absTarget))
171
+ : absCwd;
172
+
173
+ // The walk bound must be an ancestor of the target: a git root found from
174
+ // the CWD is only usable when the target actually lives inside it,
175
+ // otherwise the walk would climb out of both trees.
176
+ const targetGitRoot = findGitRoot(targetDir);
177
+ const cwdGitRoot = targetGitRoot ? null : findGitRoot(absCwd);
178
+ const repoRoot = targetGitRoot
179
+ || (cwdGitRoot && insideOrEqual(targetDir, cwdGitRoot) ? cwdGitRoot : null);
180
+ // Without a git boundary, never ascend above the starting directory: the
181
+ // filesystem above an unversioned project is not ours to interpret.
182
+ const upperBound = repoRoot || targetDir;
183
+
184
+ // The workspace-aware legacy resolution (context.mjs) still decides two
185
+ // things: the fallback when no app marker exists, and how far the marker
186
+ // walk may ascend when an explicit target selected a workspace child. A
187
+ // root-level live config must never shadow a child the target picked.
188
+ const legacyRoot = resolveProjectRoot(absCwd, absTarget ? { targetPath: absTarget } : {});
189
+ const markerBound = absTarget && insideOrEqual(targetDir, legacyRoot) && insideOrEqual(legacyRoot, upperBound)
190
+ ? legacyRoot
191
+ : upperBound;
192
+
193
+ let appRoot = walkUp(targetDir, markerBound, (dir) => (isAppRoot(dir) ? dir : null));
194
+ let resolvedFrom = appRoot
195
+ ? (absTarget ? `target:${path.relative(absCwd, absTarget) || '.'}` : 'cwd')
196
+ : null;
197
+
198
+ if (!appRoot && !absTarget) {
199
+ const candidates = discoverAppCandidates(absCwd);
200
+ if (candidates.length === 1) {
201
+ appRoot = candidates[0];
202
+ resolvedFrom = `candidate:${path.relative(absCwd, appRoot)}`;
203
+ } else if (candidates.length > 1) {
204
+ return {
205
+ selection: {
206
+ candidates: candidates.map((abs) => ({
207
+ name: path.basename(abs),
208
+ path: path.relative(absCwd, abs).split(path.sep).join('/'),
209
+ })),
210
+ },
211
+ };
212
+ }
213
+ }
214
+
215
+ if (!appRoot) {
216
+ // No app marker anywhere: defer to the workspace-aware legacy resolution
217
+ // (workspace child for a targeted monorepo path, cwd otherwise). Never
218
+ // adopt an arbitrary ancestor just because it has a package.json, and
219
+ // never adopt a root that does not even contain the target.
220
+ appRoot = insideOrEqual(targetDir, legacyRoot) ? legacyRoot : targetDir;
221
+ resolvedFrom = 'fallback';
222
+ }
223
+
224
+ const effectiveRepoRoot = repoRoot && insideOrEqual(appRoot, repoRoot) ? repoRoot : appRoot;
225
+
226
+ // Each context file resolves independently: a child app may carry its own
227
+ // PRODUCT.md while inheriting DESIGN.md from the repo root (or vice versa).
228
+ const productPath = walkUp(appRoot, effectiveRepoRoot, (dir) => findContextFile(dir, PRODUCT_NAMES));
229
+ const designPath = walkUp(appRoot, effectiveRepoRoot, (dir) => findContextFile(dir, DESIGN_NAMES));
230
+ const contextRoot = productPath
231
+ ? path.dirname(productPath)
232
+ : designPath
233
+ ? path.dirname(designPath)
234
+ : null;
235
+
236
+ return {
237
+ manifest: {
238
+ version: ROOTS_MANIFEST_VERSION,
239
+ appRoot,
240
+ repoRoot: effectiveRepoRoot,
241
+ contextRoot,
242
+ sessionRoot: path.join(appRoot, '.impeccable', 'live'),
243
+ productPath,
244
+ designPath,
245
+ resolvedFrom,
246
+ },
247
+ };
248
+ }
249
+
250
+ function rootsFilePath(appRoot) {
251
+ return path.join(appRoot, '.impeccable', 'live', ROOTS_FILE);
252
+ }
253
+
254
+ function pointerFilePath(repoRoot) {
255
+ return path.join(repoRoot, '.impeccable', 'live', POINTER_FILE);
256
+ }
257
+
258
+ export function writeRootsManifest(manifest) {
259
+ const file = rootsFilePath(manifest.appRoot);
260
+ fs.mkdirSync(path.dirname(file), { recursive: true });
261
+ fs.writeFileSync(file, JSON.stringify(manifest, null, 2));
262
+ if (path.resolve(manifest.repoRoot) !== path.resolve(manifest.appRoot)) {
263
+ const pointer = pointerFilePath(manifest.repoRoot);
264
+ fs.mkdirSync(path.dirname(pointer), { recursive: true });
265
+ // The pointer records EVERY app that has booted live in this repo, most
266
+ // recent first. A single last-boot-wins value made a helper run from the
267
+ // repo root silently target whichever app booted last, even while an
268
+ // earlier app's session was the one still live.
269
+ const entries = readPointerEntries(manifest.repoRoot)
270
+ .filter((entry) => path.resolve(entry.appRoot) !== path.resolve(manifest.appRoot));
271
+ entries.unshift({ appRoot: manifest.appRoot, bootedAt: new Date().toISOString() });
272
+ fs.writeFileSync(pointer, JSON.stringify({ version: 2, appRoots: entries }));
273
+ }
274
+ return file;
275
+ }
276
+
277
+ function readPointerEntries(repoRoot) {
278
+ try {
279
+ const raw = JSON.parse(fs.readFileSync(pointerFilePath(repoRoot), 'utf-8'));
280
+ if (Array.isArray(raw?.appRoots)) {
281
+ return raw.appRoots.filter((entry) => entry && typeof entry.appRoot === 'string');
282
+ }
283
+ // v1 shape: a single { appRoot } value.
284
+ if (raw && typeof raw.appRoot === 'string') return [{ appRoot: raw.appRoot }];
285
+ return [];
286
+ } catch {
287
+ return [];
288
+ }
289
+ }
290
+
291
+ /**
292
+ * True when the app's live helper server is recorded and its pid is alive.
293
+ * A liveness signal alone misclassifies a REUSED pid (helper died without
294
+ * removing server.json, the OS handed the pid to something else), so the
295
+ * process's command line must also look like a node process; that removes
296
+ * reuse by arbitrary processes. A pid reused by another node process remains
297
+ * a residual false positive, which the multi-app warning and --target
298
+ * escape hatch cover.
299
+ */
300
+ function hasLiveServer(appRoot) {
301
+ let pid;
302
+ let port;
303
+ let token;
304
+ try {
305
+ const info = JSON.parse(fs.readFileSync(path.join(appRoot, '.impeccable', 'live', 'server.json'), 'utf-8'));
306
+ if (!info || typeof info.pid !== 'number') return false;
307
+ pid = info.pid;
308
+ port = Number(info.port);
309
+ token = typeof info.token === 'string' ? info.token : null;
310
+ process.kill(pid, 0);
311
+ } catch (err) {
312
+ // EPERM: the process exists but is not signalable by this user.
313
+ if (err?.code !== 'EPERM') return false;
314
+ }
315
+ // Liveness alone misclassifies a REUSED pid, and a bare TCP connect
316
+ // misclassifies a coincidental listener on a reused port. The decisive
317
+ // signal is IDENTITY: the helper answers its authenticated /status
318
+ // endpoint with the token server.json records; nothing else on that port
319
+ // can. The probe is a spawned node one-liner so it works identically on
320
+ // every platform.
321
+ if (Number.isInteger(port) && port > 0 && token) {
322
+ try {
323
+ execFileSync(process.execPath, ['-e', [
324
+ "const req = require('node:http').get({ host: '127.0.0.1', port: Number(process.argv[1]), path: '/status?token=' + encodeURIComponent(process.argv[2]), timeout: 1200 }, (res) => { res.resume(); process.exit(res.statusCode === 200 ? 0 : 1); });",
325
+ "req.on('timeout', () => { req.destroy(); process.exit(1); });",
326
+ "req.on('error', () => process.exit(1));",
327
+ ].join(''), String(port), token], { timeout: 4000, stdio: 'ignore' });
328
+ return true;
329
+ } catch {
330
+ return false;
331
+ }
332
+ }
333
+ // Every server.json this codebase has ever written records port + token
334
+ // (see writeLiveServerInfo). A record without them is malformed or foreign
335
+ // and cannot be authenticated, so it does not count as a live helper;
336
+ // resolution falls to the durable-session tier, which is the correct
337
+ // recovery path for a stopped or crashed helper anyway.
338
+ return false;
339
+ }
340
+
341
+ const TERMINAL_SESSION_PHASES = new Set(['completed', 'discarded']);
342
+
343
+ /**
344
+ * True when the app's durable session store holds a session that is not
345
+ * terminal. With every helper server stopped, this is what distinguishes
346
+ * "the app whose interrupted session the user is trying to recover" from an
347
+ * app that merely booted more recently.
348
+ */
349
+ function hasActiveDurableSession(appRoot) {
350
+ const dir = path.join(appRoot, '.impeccable', 'live', 'sessions');
351
+ let entries;
352
+ try {
353
+ entries = fs.readdirSync(dir);
354
+ } catch {
355
+ return false;
356
+ }
357
+ for (const name of entries) {
358
+ if (!name.endsWith('.snapshot.json')) continue;
359
+ try {
360
+ const snapshot = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf-8'));
361
+ if (snapshot?.phase && !TERMINAL_SESSION_PHASES.has(snapshot.phase)) return true;
362
+ } catch { /* skip unreadable snapshots */ }
363
+ }
364
+ return false;
365
+ }
366
+
367
+ function readManifestAt(appRoot) {
368
+ try {
369
+ const raw = JSON.parse(fs.readFileSync(rootsFilePath(appRoot), 'utf-8'));
370
+ if (!raw || typeof raw.appRoot !== 'string') return null;
371
+ // A manifest is only trusted where it claims to live; anything else is a
372
+ // copied or stale file.
373
+ if (path.resolve(raw.appRoot) !== path.resolve(appRoot)) return null;
374
+ return raw;
375
+ } catch {
376
+ return null;
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Resolve the roots for the live session governing `cwd`, preferring a
382
+ * persisted manifest (written by the boot) over fresh detection:
383
+ *
384
+ * 1. Walk up from cwd looking for .impeccable/live/roots.json.
385
+ * 2. At the git root, follow .impeccable/live/app-root.json to the app.
386
+ * 3. Fresh resolveRoots().
387
+ *
388
+ * Fresh results are NOT persisted here; only the boot (live.mjs / server
389
+ * startup) writes manifests, so ad-hoc helper invocations cannot mint
390
+ * conflicting truth.
391
+ */
392
+ export function resolveLiveRoots(cwd = process.cwd(), { targetPath = null } = {}) {
393
+ const absCwd = path.resolve(cwd);
394
+
395
+ if (!targetPath) {
396
+ const persisted = walkUp(absCwd, findGitRoot(absCwd) || absCwd, (dir) => readManifestAt(dir));
397
+ if (persisted) return { manifest: persisted, source: 'persisted' };
398
+
399
+ const gitRoot = findGitRoot(absCwd);
400
+ if (gitRoot) {
401
+ // Several apps in one repo may have booted live. Preference order:
402
+ // a running helper server, then an app whose durable store still holds
403
+ // a non-terminal session (the stopped session the user is recovering),
404
+ // then the most recent boot. A stale pointer entry must never redirect
405
+ // status/poll/accept onto the wrong app's session store.
406
+ const candidates = readPointerEntries(gitRoot)
407
+ .map((entry) => readManifestAt(entry.appRoot))
408
+ .filter(Boolean);
409
+ if (candidates.length > 0) {
410
+ const liveApps = candidates.filter((manifest) => hasLiveServer(manifest.appRoot));
411
+ const recoveringApps = liveApps.length > 0
412
+ ? liveApps
413
+ : candidates.filter((manifest) => hasActiveDurableSession(manifest.appRoot));
414
+ const tier = recoveringApps.length > 0 ? recoveringApps : candidates;
415
+ // Multiple apps qualifying at the same tier is inherent ambiguity:
416
+ // intent is unknowable from the repo root. The choice stays
417
+ // deterministic (most recent boot first), but it must be LOUD, not
418
+ // silent, so the agent can re-anchor when it meant the other app.
419
+ if (tier.length > 1) {
420
+ const chosen = tier[0].appRoot;
421
+ const others = tier.slice(1).map((manifest) => manifest.appRoot).join(', ');
422
+ process.stderr.write(
423
+ `[impeccable live] Multiple apps in this repo have live state; using ${chosen}. `
424
+ + `Other candidate(s): ${others}. Run from the app directory (or pass --target) to address a specific app.\n`,
425
+ );
426
+ }
427
+ return { manifest: tier[0], source: 'pointer' };
428
+ }
429
+ }
430
+ }
431
+
432
+ const fresh = resolveRoots({ cwd: absCwd, targetPath });
433
+ if (fresh.selection) return { selection: fresh.selection, source: 'fresh' };
434
+ return { manifest: fresh.manifest, source: 'fresh' };
435
+ }
436
+
437
+ /**
438
+ * Consume a `--target <path>` / `--target=<path>` pair from an argv array,
439
+ * returning the value and removing the tokens so downstream flag parsers
440
+ * (which do not know the option) never see them.
441
+ */
442
+ export function consumeTargetArg(argv = process.argv) {
443
+ for (let i = 0; i < argv.length; i++) {
444
+ const arg = argv[i];
445
+ if (arg === '--target') {
446
+ const value = argv[i + 1];
447
+ // A --target with no usable value must not degrade into implicit root
448
+ // selection: these helpers mutate session state, and "the most recent
449
+ // app" is exactly what the caller was trying NOT to get.
450
+ if (typeof value !== 'string' || value === '' || value.startsWith('--')) {
451
+ throw new Error('--target requires a path value (use --target <path> or --target=<path>)');
452
+ }
453
+ argv.splice(i, 2);
454
+ return value;
455
+ }
456
+ if (typeof arg === 'string' && arg.startsWith('--target=')) {
457
+ const value = arg.slice('--target='.length);
458
+ if (value === '') {
459
+ throw new Error('--target requires a path value (use --target <path> or --target=<path>)');
460
+ }
461
+ argv.splice(i, 1);
462
+ return value;
463
+ }
464
+ }
465
+ return null;
466
+ }
467
+
468
+ /**
469
+ * Entry-point guard for live CLI scripts: resolve the governing roots and
470
+ * make appRoot the process cwd so every downstream path derivation agrees
471
+ * with the boot. An explicit `--target <path>` on the helper's command line
472
+ * overrides pointer resolution, which is what disambiguates a repo with
473
+ * several live apps (the multi-app warning names this escape hatch, so it
474
+ * has to actually work on every helper). Returns the manifest. On selection
475
+ * ambiguity it stays in the current directory (the boot flow handles
476
+ * prompting); a malformed --target exits with an error instead of silently
477
+ * falling back to implicit selection, which could mutate the wrong app.
478
+ */
479
+ export function enterLiveRoot(cwd = process.cwd()) {
480
+ let targetPath;
481
+ try {
482
+ targetPath = consumeTargetArg(process.argv);
483
+ } catch (err) {
484
+ console.error(`[impeccable live] ${err.message}`);
485
+ process.exit(1);
486
+ }
487
+ const resolved = resolveLiveRoots(cwd, targetPath ? { targetPath } : {});
488
+ if (!resolved.manifest) return null;
489
+ const appRoot = resolved.manifest.appRoot;
490
+ if (path.resolve(cwd) !== path.resolve(appRoot)) {
491
+ // Failing to land on the resolved appRoot must be fatal: a helper that
492
+ // silently keeps its ambient cwd derives server, session, and source
493
+ // paths from a different project and mutates the wrong state. A manifest
494
+ // pointing at a deleted directory is stale ambient truth, not a reason
495
+ // to guess.
496
+ if (!isDir(appRoot)) {
497
+ console.error(`[impeccable live] resolved app root does not exist: ${appRoot} (stale roots manifest? re-run the live boot, or pass --target <path>)`);
498
+ process.exit(1);
499
+ }
500
+ try {
501
+ process.chdir(appRoot);
502
+ } catch (err) {
503
+ console.error(`[impeccable live] could not enter app root ${appRoot}: ${err.message}`);
504
+ process.exit(1);
505
+ }
506
+ }
507
+ return resolved.manifest;
508
+ }