zz-meridian 0.4.0 → 0.6.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 (234) hide show
  1. package/README.md +31 -4
  2. package/dist/adopt.js +59 -24
  3. package/dist/cli.js +36 -0
  4. package/dist/context.js +82 -0
  5. package/dist/create.js +27 -3
  6. package/dist/files.js +8 -1
  7. package/dist/inventory.js +131 -0
  8. package/dist/merge-report.js +87 -0
  9. package/dist/migrations.js +220 -0
  10. package/dist/ownership.js +112 -0
  11. package/dist/rebrand.js +212 -0
  12. package/dist/reconcile.js +195 -0
  13. package/dist/replay.js +204 -0
  14. package/dist/session.js +910 -0
  15. package/dist/update-session.js +292 -0
  16. package/dist/update.js +187 -0
  17. package/package.json +1 -1
  18. package/payload/AGENTS.md +1 -1
  19. package/payload/CHANGELOG.md +157 -0
  20. package/payload/CONTRIBUTING.md +10 -6
  21. package/payload/README.md +14 -9
  22. package/payload/app/(dashboard)/{README.md → (overview)/README.md} +2 -2
  23. package/payload/app/(dashboard)/(overview)/loading.tsx +52 -0
  24. package/payload/app/(dashboard)/(overview)/page.tsx +52 -0
  25. package/payload/app/(dashboard)/_loading.tsx +73 -0
  26. package/payload/app/(dashboard)/analytics/README.md +2 -2
  27. package/payload/app/(dashboard)/analytics/loading.tsx +23 -0
  28. package/payload/app/(dashboard)/analytics/page.tsx +29 -9
  29. package/payload/app/(dashboard)/customers/README.md +5 -1
  30. package/payload/app/(dashboard)/customers/loading.tsx +11 -0
  31. package/payload/app/(dashboard)/customers/page.tsx +23 -5
  32. package/payload/app/(dashboard)/health/README.md +4 -3
  33. package/payload/app/(dashboard)/health/loading.tsx +33 -0
  34. package/payload/app/(dashboard)/health/page.tsx +4 -4
  35. package/payload/app/(dashboard)/keys/README.md +4 -2
  36. package/payload/app/(dashboard)/keys/actions.ts +25 -10
  37. package/payload/app/(dashboard)/keys/loading.tsx +12 -0
  38. package/payload/app/(dashboard)/keys/page.tsx +4 -5
  39. package/payload/app/(dashboard)/layout.tsx +11 -7
  40. package/payload/app/(dashboard)/members/README.md +2 -1
  41. package/payload/app/(dashboard)/members/actions.ts +23 -11
  42. package/payload/app/(dashboard)/members/loading.tsx +10 -0
  43. package/payload/app/(dashboard)/members/page.tsx +4 -5
  44. package/payload/app/(dashboard)/requests/README.md +11 -3
  45. package/payload/app/(dashboard)/requests/[id]/README.md +6 -3
  46. package/payload/app/(dashboard)/requests/[id]/actions.ts +34 -0
  47. package/payload/app/(dashboard)/requests/[id]/loading.tsx +23 -0
  48. package/payload/app/(dashboard)/requests/[id]/page.tsx +10 -6
  49. package/payload/app/(dashboard)/requests/loading.tsx +11 -0
  50. package/payload/app/(dashboard)/requests/page.tsx +63 -8
  51. package/payload/app/(dashboard)/settings/README.md +4 -2
  52. package/payload/app/(dashboard)/settings/loading.tsx +19 -0
  53. package/payload/app/api/assistant/route.ts +20 -3
  54. package/payload/app/api/export/requests/route.ts +59 -0
  55. package/payload/app/api/live/route.ts +32 -0
  56. package/payload/app/embed/health/page.tsx +1 -2
  57. package/payload/app/embed/overview/page.tsx +11 -2
  58. package/payload/app/embed/overview/view.tsx +1 -1
  59. package/payload/app/embed/requests/README.md +2 -2
  60. package/payload/app/embed/requests/page.tsx +13 -4
  61. package/payload/app/embed/requests/view.tsx +40 -28
  62. package/payload/app/not-found/README.md +1 -1
  63. package/payload/app/not-found.tsx +33 -4
  64. package/payload/app/sign-in/page.tsx +1 -1
  65. package/payload/app/system/(atlas)/[section]/[id]/page.tsx +12 -61
  66. package/payload/app/system/(atlas)/_article.tsx +83 -0
  67. package/payload/app/system/(atlas)/page.tsx +1 -1
  68. package/payload/app/system/(atlas)/start/[id]/page.tsx +9 -0
  69. package/payload/app/system/(atlas)/system/[id]/page.tsx +9 -0
  70. package/payload/app/system/preview/[section]/[card]/keys.ts +76 -0
  71. package/payload/app/system/preview/[section]/[card]/page.tsx +11 -4
  72. package/payload/app/system/preview/[section]/[card]/stage.tsx +7 -7
  73. package/payload/app/system/states/layout.tsx +1 -1
  74. package/payload/app/system/states/loading/page.tsx +2 -2
  75. package/payload/decisions/0010-the-adopter-contract.md +145 -0
  76. package/payload/docs/assistant.md +11 -4
  77. package/payload/docs/benchmark.md +2 -2
  78. package/payload/docs/distribution.md +131 -28
  79. package/payload/docs/evidence/0.5.0.md +76 -0
  80. package/payload/docs/start-a-dashboard.md +11 -8
  81. package/payload/next.config.ts +3 -1
  82. package/payload/package.json +1 -1
  83. package/payload/scripts/audit.ts +10 -8
  84. package/payload/scripts/brand.ts +34 -49
  85. package/payload/scripts/check.ts +141 -6
  86. package/payload/scripts/gate.ts +8 -3
  87. package/payload/scripts/interactions.ts +6 -4
  88. package/payload/scripts/keyboard.ts +4 -6
  89. package/payload/scripts/lib/budgets.ts +70 -0
  90. package/payload/scripts/lib/chrome.ts +23 -2
  91. package/payload/scripts/lib/context-check.ts +88 -0
  92. package/payload/scripts/lib/coverage.ts +75 -0
  93. package/payload/scripts/lib/next-config.ts +20 -0
  94. package/payload/scripts/lib/routes.ts +23 -2
  95. package/payload/scripts/lib/sizes.ts +57 -0
  96. package/payload/scripts/lib/timing.ts +33 -0
  97. package/payload/scripts/lib/update-session.ts +298 -0
  98. package/payload/scripts/live.ts +518 -0
  99. package/payload/scripts/navigate.ts +627 -0
  100. package/payload/scripts/perf.ts +238 -0
  101. package/payload/scripts/registry.ts +53 -11
  102. package/payload/scripts/route-policy.ts +62 -0
  103. package/payload/scripts/sizes.ts +130 -0
  104. package/payload/scripts/verify.baseline.json +27 -0
  105. package/payload/scripts/verify.config.ts +60 -1
  106. package/payload/scripts/verify.ts +379 -132
  107. package/payload/scripts/vitals.ts +4 -3
  108. package/payload/skills/zz-meridian/SKILL.md +102 -26
  109. package/payload/skills/zz-meridian/references/agents.md +72 -0
  110. package/payload/skills/zz-meridian/references/cache.md +249 -0
  111. package/payload/skills/zz-meridian/references/customize.md +53 -39
  112. package/payload/skills/zz-meridian/references/existing-project.md +38 -32
  113. package/payload/skills/zz-meridian/references/live.md +497 -0
  114. package/payload/skills/zz-meridian/references/ownership.md +19 -0
  115. package/payload/skills/zz-meridian/references/update.md +234 -0
  116. package/payload/skills/zz-meridian/references/validation.md +33 -7
  117. package/payload/skills/zz-meridian/references/voice.md +35 -0
  118. package/payload/src/app.config.ts +3 -0
  119. package/payload/src/components/base/app-mark/README.md +7 -4
  120. package/payload/src/components/base/app-mark/index.tsx +16 -4
  121. package/payload/src/components/base/app-mark/preview.tsx +1 -0
  122. package/payload/src/components/base/providers.tsx +2 -1
  123. package/payload/src/components/base/shell/README.md +2 -2
  124. package/payload/src/components/base/shell/index.tsx +42 -16
  125. package/payload/src/components/charts/timeline/README.md +1 -1
  126. package/payload/src/components/charts/timeline/index.tsx +1 -1
  127. package/payload/src/components/charts/uptime-bars/README.md +16 -15
  128. package/payload/src/components/charts/uptime-bars/index.tsx +35 -27
  129. package/payload/src/components/charts/uptime-bars/preview.tsx +12 -2
  130. package/payload/src/components/patterns/assistant/README.md +3 -3
  131. package/payload/src/components/patterns/data-table/README.md +2 -2
  132. package/payload/src/components/patterns/data-table/index.tsx +53 -28
  133. package/payload/src/components/patterns/export-button/README.md +7 -3
  134. package/payload/src/components/patterns/export-button/index.tsx +17 -3
  135. package/payload/src/components/patterns/export-button/preview.tsx +2 -1
  136. package/payload/src/components/patterns/filter-bar/index.tsx +1 -1
  137. package/payload/src/components/patterns/freshness/README.md +2 -2
  138. package/payload/src/components/patterns/freshness/index.tsx +4 -3
  139. package/payload/src/components/patterns/prose/README.md +1 -1
  140. package/payload/src/components/patterns/rail/README.md +1 -1
  141. package/payload/src/components/patterns/rail/index.tsx +12 -6
  142. package/payload/src/components/patterns/shell-tools/README.md +2 -2
  143. package/payload/src/components/patterns/shell-tools/index.tsx +3 -3
  144. package/payload/src/components/patterns/status-list/index.tsx +14 -7
  145. package/payload/src/components/patterns/status-list/summarise.ts +13 -1
  146. package/payload/src/components/ui/breadcrumb/README.md +2 -2
  147. package/payload/src/components/ui/card/preview.tsx +1 -1
  148. package/payload/src/components/ui/copy-field/README.md +1 -0
  149. package/payload/src/components/ui/toast/README.md +1 -1
  150. package/payload/src/components/ui/toast/index.tsx +1 -1
  151. package/payload/src/data/README.md +9 -1
  152. package/payload/src/data/access.ts +73 -0
  153. package/payload/src/data/collections.ts +15 -5
  154. package/payload/src/data/live-actions.ts +22 -0
  155. package/payload/src/data/live-stream.ts +104 -0
  156. package/payload/src/data/read.ts +30 -0
  157. package/payload/src/data/requests.ts +102 -0
  158. package/payload/src/data/sample.ts +12 -0
  159. package/payload/src/lib/assistant/brief.ts +15 -0
  160. package/payload/src/lib/assistant/prompt.ts +64 -3
  161. package/payload/src/lib/assistant/respond.ts +7 -6
  162. package/payload/src/lib/assistant/tools.ts +23 -5
  163. package/payload/src/lib/collection.ts +85 -11
  164. package/payload/src/lib/csv.ts +14 -3
  165. package/payload/src/lib/format-date.ts +1 -1
  166. package/payload/src/lib/live.ts +307 -0
  167. package/payload/src/lib/logo.ts +13 -0
  168. package/payload/src/lib/preferences.ts +10 -5
  169. package/payload/src/styles/base.css +10 -14
  170. package/payload/src/system/content.ts +11 -3
  171. package/payload/src/system/fixtures/sample-records.ts +11 -6
  172. package/payload/src/system/fixtures/sample.ts +2 -0
  173. package/payload/src/system/preview-loaders.ts +80 -0
  174. package/payload/src/views/analytics.tsx +10 -2
  175. package/payload/src/views/console-live.tsx +45 -0
  176. package/payload/src/views/customers.tsx +49 -47
  177. package/payload/src/views/health.tsx +17 -4
  178. package/payload/src/views/key-scopes.ts +4 -0
  179. package/payload/src/views/keys.tsx +58 -31
  180. package/payload/src/views/members.tsx +58 -17
  181. package/payload/src/views/not-found-address.tsx +1 -1
  182. package/payload/src/views/overview.tsx +12 -2
  183. package/payload/src/views/request.tsx +15 -7
  184. package/payload/src/views/requests.tsx +73 -94
  185. package/payload/src/views/sample-footer.tsx +1 -1
  186. package/payload/src/views/settings.tsx +26 -5
  187. package/payload/tests/assistant-brief.test.ts +50 -0
  188. package/payload/tests/assistant-errors.test.ts +4 -1
  189. package/payload/tests/assistant-guard.test.ts +37 -0
  190. package/payload/tests/assistant-panel.test.tsx +14 -11
  191. package/payload/tests/assistant-respond.test.ts +5 -2
  192. package/payload/tests/assistant-safety.test.ts +10 -4
  193. package/payload/tests/assistant-settings.test.tsx +15 -12
  194. package/payload/tests/assistant-tools.test.ts +15 -13
  195. package/payload/tests/brand-config.test.tsx +47 -0
  196. package/payload/tests/brand-literal.test.ts +30 -0
  197. package/payload/tests/chrome-cleanup.test.ts +26 -0
  198. package/payload/tests/clock.test.tsx +30 -0
  199. package/payload/tests/collection.test.ts +25 -0
  200. package/payload/tests/console-live.test.tsx +33 -0
  201. package/payload/tests/context-check.test.ts +56 -0
  202. package/payload/tests/csv-safety.test.ts +19 -0
  203. package/payload/tests/customers-masthead.test.tsx +20 -0
  204. package/payload/tests/data-access.test.ts +93 -0
  205. package/payload/tests/data-read.test.ts +106 -0
  206. package/payload/tests/data-table.test.tsx +9 -1
  207. package/payload/tests/data-write.test.ts +56 -0
  208. package/payload/tests/embed-requests-provenance.test.tsx +39 -0
  209. package/payload/tests/empty-views.test.tsx +29 -0
  210. package/payload/tests/keys-actions.test.ts +34 -0
  211. package/payload/tests/keys-scopes-view.test.tsx +19 -0
  212. package/payload/tests/keys-secret-view.test.tsx +34 -0
  213. package/payload/tests/live-actions.test.ts +27 -0
  214. package/payload/tests/live-client.test.tsx +228 -0
  215. package/payload/tests/live-route.test.ts +125 -0
  216. package/payload/tests/loading-routes.test.tsx +40 -0
  217. package/payload/tests/members-optimistic.test.tsx +127 -0
  218. package/payload/tests/page-fallbacks.test.tsx +42 -0
  219. package/payload/tests/request-replay.test.ts +35 -0
  220. package/payload/tests/request-view-replay.test.tsx +36 -0
  221. package/payload/tests/requests-export.test.ts +51 -0
  222. package/payload/tests/requests-query.test.ts +39 -0
  223. package/payload/tests/route-policy.test.ts +19 -0
  224. package/payload/tests/settings-disconnect-undo.test.tsx +25 -0
  225. package/payload/tests/shell-promise.test.tsx +50 -0
  226. package/payload/tests/update-session.test.ts +127 -0
  227. package/payload/tests/uptime-svg.test.tsx +60 -0
  228. package/payload/tests/verify-coverage.test.ts +41 -0
  229. package/payload/tests/verify-next-config.test.ts +32 -0
  230. package/payload/tests/verify-sizes.test.ts +51 -0
  231. package/payload/tests/verify-suite-outcome.test.ts +26 -0
  232. package/payload/tests/verify-timing.test.ts +28 -0
  233. package/payload/app/(dashboard)/loading.tsx +0 -40
  234. package/payload/app/(dashboard)/page.tsx +0 -33
package/README.md CHANGED
@@ -16,15 +16,18 @@ Give your coding agent (Codex, Claude Code, or any agent that can run a shell) t
16
16
  ```sh
17
17
  npx zz-meridian@latest adopt [brand flags] # bring Meridian into this Next.js App Router project
18
18
  npx zz-meridian@latest create <dir> [brand flags] # start a new dashboard
19
+ npx zz-meridian@latest update [--dry-run] [--verbose] # update this project (the dry-run writes nothing)
20
+ npx zz-meridian@<version> brand [brand flags] # rebrand this project, with the version in .meridian/manifest.json
19
21
  npx zz-meridian@latest skill [--global] # install only the agent skill
20
22
  ```
21
23
 
22
- Brand flags: `--name "Acme Ops"`, `--hex '#2E6BE4'` (or `--accent indigo|cobalt|jade|graphite`), `--workspace`,
23
- `--timezone`, `--currency`, `--user`, `--role`.
24
+ Brand flags: `--name "Acme Ops"`, `--hex '#2E6BE4'` (or `--accent indigo|cobalt|jade|graphite`), `--theme dark|light`,
25
+ `--workspace`, `--timezone`, `--currency`, `--user`, `--role`.
24
26
 
25
27
  **adopt** copies Meridian's tokens, styles, components, gates and scripts into `src/`, `tokens/` and `scripts/`;
26
28
  merges the dependencies and scripts it needs into your `package.json`; replaces your global stylesheet (yours is kept
27
- beside it as `*.before.css`); brands it; appends Meridian's rules to your `AGENTS.md`; installs the skill into
29
+ beside it as `*.before.css`); brands it; adds Meridian's managed block to your `AGENTS.md` between two markers, keeping your own text there byte
30
+ for byte; writes an empty `docs/brief.md` (your product, users, data, decisions and glossary) if you have none; installs the skill into
28
31
  `.agents/skills/` (Codex) and `.claude/skills/` (Claude Code); records every copied file in `.meridian/manifest.json`;
29
32
  installs and type checks. Your routes, your data layer and your own components are not touched. Meridian's files
30
33
  import each other by relative path, so a `components/ui/button` of your own is never confused with Meridian's; your
@@ -36,7 +39,31 @@ the project is not Next.js with the App Router, or when a file it would copy alr
36
39
  **create** copies the template into a new folder, branded as your product, with the Design Atlas and the design
37
40
  system's own documents left out.
38
41
 
39
- Requires Node 22.18 or newer. The checks (`pnpm verify`) also need Google Chrome.
42
+ **update** moves a project adopted or created with 0.3.0 or later to this release. `update --dry-run` shows the plan and
43
+ writes nothing; run it first.
44
+ - It fetches the release the project was copied from, checks it against the registry's integrity record, and replays
45
+ that release and this one in a scratch folder. If a file the manifest recorded does not match the replay, it stops.
46
+ - It replaces every Meridian file you have not touched, adds the new ones and removes the retired ones. A file you edited,
47
+ deleted or kept (`.meridian/keep.json`) is never overwritten: an edited or deleted one is staged as base/ours/new copies
48
+ for you to merge.
49
+ - It adds the dependencies and scripts this release needs to `package.json` and updates Meridian's managed block in
50
+ `AGENTS.md`; a version or script you chose yourself becomes a migration to resolve, never a silent change.
51
+ - It reports a migration for each interface the release changed where one of your own files still has the old shape;
52
+ moving to 0.5.0 can report ten, such as `cache-components-config`. Resolve them as the skill's
53
+ `references/update.md` says; `docs/distribution.md` lists them.
54
+ - Everything it did and everything left to do is in `.meridian/update/<version>/MERGE.md`. Resolve the items, then run
55
+ the pinned `npx zz-meridian@<version> update --finalize`; `--resume` continues an interrupted run and `--abort` restores
56
+ what it changed. Only finalize records the new version. `--finalize --verify` validates with the project's default
57
+ `verify` instead of the gate and the build, and reports its coverage line.
58
+ - It refuses, writing nothing, on a dirty git tree (unless `--allow-dirty`) or while another update is open. `--verbose`
59
+ lists every file.
60
+
61
+ **brand** changes the brand of a project built on Meridian with no hand edits: it rebuilds the brand outputs (tokens and
62
+ styles) and `src/app.config.ts` for the new flags and records them in the manifest together, or changes nothing. It
63
+ refuses when one of those outputs was edited, while an update is open, or on a dirty tree (unless `--allow-dirty`). Run
64
+ it with the version the manifest records. The project's own `pnpm brand` still works, but its changes count as your edits.
65
+
66
+ Requires Node 22.18 or newer. The default `pnpm verify` runs without Google Chrome and reports the browser checks as not run; `--full` and `--perf` need it.
40
67
 
41
68
  ## What this package does not do
42
69
 
package/dist/adopt.js CHANGED
@@ -8,25 +8,11 @@
8
8
  */
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
- import { PAYLOAD, VERSION, brandArgs, inside, installSkill, packageManager, payloadFiles, readJsonc, readPayload, relativeImports, run, runTool, sha256, writeIn, writeManifest, } from './files.js';
12
- /** The library modules Meridian's components and gates import; the rest of src/lib belongs to the template's pages. */
13
- const LIB = ['cn', 'format', 'format-date', 'period', 'color', 'host', 'preferences', 'csv', 'safe-markdown'].map((n) => `src/lib/${n}.ts`);
14
- /** What adopt copies from the template, as payload paths. The fixture build in CI is what keeps this list complete. */
15
- function adoptSet() {
16
- const own = (f) => !/(^|\/)(README\.md|preview\.tsx)$/.test(f);
17
- return [
18
- ...payloadFiles('tokens'),
19
- ...payloadFiles('src/styles'),
20
- ...payloadFiles('src/components').filter(own),
21
- ...payloadFiles('scripts').filter((f) => f !== 'scripts/verify.config.ts'),
22
- ...LIB,
23
- 'src/lib/assistant/prompt.ts',
24
- 'src/views/console-chrome.tsx',
25
- 'tests/setup.ts',
26
- ];
27
- }
11
+ import { PAYLOAD, VERSION, brandArgs, effectiveBrand, inside, installSkill, packageManager, payloadFiles, readJsonc, readPayload, relativeImports, run, runTool, sha256, writeIn, writeManifest, } from './files.js';
12
+ import { BRIEF_TEMPLATE, managedBlock, upsertManagedBlock } from './context.js';
13
+ import { FIXED, adoptSetOf, managedPaths } from './ownership.js';
28
14
  /** The template's dependencies a dashboard built from these files needs. */
29
- const DEPS = ['next', 'react', 'react-dom', 'radix-ui', 'lucide-react', 'clsx', 'tailwind-merge', 'react-markdown', 'remark-gfm', 'ai', '@ai-sdk/react'];
15
+ const DEPS = ['next', 'react', 'react-dom', 'radix-ui', 'lucide-react', 'clsx', 'tailwind-merge', 'react-markdown', 'remark-gfm', 'ai', '@ai-sdk/react', 'zod'];
30
16
  const DEV_DEPS = ['typescript', '@types/node', '@types/react', '@types/react-dom', 'tailwindcss', '@tailwindcss/postcss', 'eslint', 'eslint-config-next', 'vitest', '@vitejs/plugin-react', 'jsdom', '@testing-library/react', '@testing-library/jest-dom'];
31
17
  const SCRIPTS = ['typecheck', 'test', 'tokens', 'check', 'contrast', 'gate', 'audit', 'verify', 'brand', 'shot', 'interactions', 'keyboard', 'vitals'];
32
18
  const major = (spec) => Number(/(\d+)/.exec(spec)?.[1] ?? NaN);
@@ -45,6 +31,22 @@ function exactImports(rel, source, known) {
45
31
  };
46
32
  return relativeImports(rel, source).replace(/(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(['"])([\w@./()[\]-]+)\2/g, (_m, lead, q, spec) => `${lead}${q}${pin(spec)}${q}`);
47
33
  }
34
+ /** Every file under one folder of the project, as posix paths; none when the folder is absent. */
35
+ function listFiles(root, dir) {
36
+ const out = [];
37
+ const walk = (rel) => {
38
+ if (!fs.existsSync(path.join(root, rel)))
39
+ return;
40
+ for (const e of fs.readdirSync(path.join(root, rel), { withFileTypes: true })) {
41
+ if (e.isDirectory())
42
+ walk(`${rel}/${e.name}`);
43
+ else if (e.isFile())
44
+ out.push(`${rel}/${e.name}`);
45
+ }
46
+ };
47
+ walk(dir);
48
+ return out;
49
+ }
48
50
  function appDir(root) {
49
51
  for (const d of ['app', 'src/app'])
50
52
  if (['tsx', 'jsx', 'ts', 'js'].some((x) => fs.existsSync(path.join(root, d, `layout.${x}`))))
@@ -128,7 +130,18 @@ https://github.com/zhixuan312/zz-meridian/blob/master/skills/zz-meridian/referen
128
130
  if (git.stdout.trim())
129
131
  return fail('the git tree has uncommitted changes. Commit or stash them first, so adopt\'s change is one reviewable diff (or pass --allow-dirty).');
130
132
  }
131
- const set = adoptSet();
133
+ let brand;
134
+ try {
135
+ brand = effectiveBrand(o.brand);
136
+ }
137
+ catch (e) {
138
+ return fail(e.message);
139
+ }
140
+ const all = payloadFiles();
141
+ const missing = FIXED.filter((f) => !all.includes(f));
142
+ if (missing.length)
143
+ return fail(`this package's template is missing files adopt needs, so nothing was written:\n${missing.map((m) => ` ${m}`).join('\n')}\nReinstall zz-meridian, or report this if it persists.`);
144
+ const set = adoptSetOf(all);
132
145
  const known = new Set(set);
133
146
  const writes = new Map();
134
147
  const owned = [];
@@ -257,18 +270,39 @@ Rename or move them, then run adopt again.`);
257
270
  }
258
271
  for (const [rel, content] of writes)
259
272
  writeIn(root, rel, content);
260
- const name = o.brand.name ?? String(pkg.name ?? path.basename(root)).replace(/^@[^/]+\//, '').replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
261
- const brand = { ...o.brand, name };
273
+ const name = brand.name ?? String(pkg.name ?? path.basename(root)).replace(/^@[^/]+\//, '').replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
274
+ brand = effectiveBrand({ ...brand, name });
275
+ // The brand outputs this run creates under tokens/ and src/styles/ (absent before it) are Meridian's, so the manifest records them.
276
+ const brandDirs = () => ['tokens', 'src/styles'].flatMap((d) => listFiles(root, d));
277
+ const existed = new Set(brandDirs());
262
278
  const b = run(process.execPath, ['scripts/brand.ts', ...brandArgs(brand), '--existing'], root);
263
279
  if (b.status !== 0)
264
280
  return fail('scripts/brand.ts failed (above); the files are copied, so fix the cause and run it again with the same flags');
281
+ // The project's own AGENTS.md keeps every byte and gains the managed block; the brief is the team's, written only when absent.
282
+ // Neither is a Meridian file, so neither is recorded in the manifest.
283
+ const pm = packageManager(root);
284
+ const agentsAbs = path.join(root, 'AGENTS.md');
285
+ try {
286
+ const text = fs.existsSync(agentsAbs) ? fs.readFileSync(agentsAbs, 'utf8') : '';
287
+ writeIn(root, 'AGENTS.md', upsertManagedBlock(text, managedBlock(VERSION, pm)));
288
+ }
289
+ catch (e) {
290
+ return fail(`AGENTS.md: ${e.message} The files are copied; fix AGENTS.md by hand, then add the managed block.`);
291
+ }
292
+ const briefWritten = !fs.existsSync(path.join(root, 'docs/brief.md'));
293
+ if (briefWritten)
294
+ writeIn(root, 'docs/brief.md', BRIEF_TEMPLATE);
295
+ const generated = brandDirs().filter((f) => !existed.has(f));
296
+ if (owned.includes('scripts/package.json'))
297
+ generated.push('scripts/package.json');
298
+ const managed = managedPaths(all, 'adopt', generated);
265
299
  const files = {};
266
- for (const rel of owned)
267
- files[rel] = sha256(fs.readFileSync(inside(root, rel)));
300
+ for (const rel of [...owned, ...generated])
301
+ if (managed.has(rel))
302
+ files[rel] = sha256(fs.readFileSync(inside(root, rel)));
268
303
  installSkill(root, files);
269
304
  writeManifest(root, { version: VERSION, route: 'adopt', brand, files });
270
305
  // ── Install and prove it compiles ─────────────────────────────────────────────────────────────────────
271
- const pm = packageManager(root);
272
306
  let typed = 'not run (--no-install)';
273
307
  if (o.install) {
274
308
  say(`\nInstalling with ${pm}…`);
@@ -283,6 +317,7 @@ Rename or move them, then run adopt again.`);
283
317
  Meridian ${VERSION} is in ${root}.
284
318
  copied: ${owned.length} files (recorded in .meridian/manifest.json)
285
319
  skill: .agents/skills/zz-meridian (Codex) and .claude/skills/zz-meridian (Claude Code)
320
+ agent context: AGENTS.md has the managed block; docs/brief.md ${briefWritten ? 'is the empty brief to fill in' : 'was kept as it is'}
286
321
  types: ${typed}${notes.length ? `\n notes:\n${notes.map((n) => ` - ${n}`).join('\n')}` : ''}
287
322
 
288
323
  Next: follow .agents/skills/zz-meridian/references/existing-project.md, Route A, from step 2 (step 1 was this). The
package/dist/cli.js CHANGED
@@ -9,6 +9,8 @@ import { parseArgs } from 'node:util';
9
9
  import { adopt } from './adopt.js';
10
10
  import { create } from './create.js';
11
11
  import { BRAND_FLAGS, VERSION, installSkill } from './files.js';
12
+ import { brandCommand } from './rebrand.js';
13
+ import { update } from './update.js';
12
14
  const HELP = `zz-meridian ${VERSION}
13
15
 
14
16
  npx zz-meridian@latest adopt [brand flags] [--allow-dirty] [--no-install]
@@ -17,6 +19,19 @@ const HELP = `zz-meridian ${VERSION}
17
19
  npx zz-meridian@latest create <dir> [brand flags] [--no-install]
18
20
  Start a new dashboard on Meridian in <dir>.
19
21
 
22
+ npx zz-meridian@latest update --dry-run [--verbose]
23
+ Show what updating this project to the running version would change, and what needs your decision. Writes nothing.
24
+
25
+ npx zz-meridian@latest update [--allow-dirty] [--no-install] [--verbose]
26
+ Apply the safe changes, stage everything you changed with base/ours/new copies, and write .meridian/update/<version>/MERGE.md.
27
+
28
+ npx zz-meridian@<version> update --resume [--no-install] | --finalize [--verify] | --abort
29
+ Continue, complete or roll back the update in progress. Use the version named in MERGE.md.
30
+
31
+ npx zz-meridian@<installed version> brand [brand flags] [--allow-dirty]
32
+ Change this project's brand with no hand edits: the brand outputs, src/app.config.ts and the manifest move together, or nothing changes.
33
+ Use the version in .meridian/manifest.json; a project on an older release runs update first.
34
+
20
35
  npx zz-meridian@latest skill [--global]
21
36
  Install only the agent skill: into this project, or for every project (~/.agents/skills, ~/.claude/skills).
22
37
 
@@ -30,6 +45,12 @@ const { values, positionals } = parseArgs({
30
45
  ...Object.fromEntries(BRAND_FLAGS.map((f) => [f, { type: 'string' }])),
31
46
  'allow-dirty': { type: 'boolean' },
32
47
  'no-install': { type: 'boolean' },
48
+ 'dry-run': { type: 'boolean' },
49
+ resume: { type: 'boolean' },
50
+ finalize: { type: 'boolean' },
51
+ abort: { type: 'boolean' },
52
+ verify: { type: 'boolean' },
53
+ verbose: { type: 'boolean' },
33
54
  global: { type: 'boolean' },
34
55
  help: { type: 'boolean', short: 'h' },
35
56
  version: { type: 'boolean', short: 'v' },
@@ -63,6 +84,21 @@ function main() {
63
84
  }
64
85
  return create({ dir: arg, brand, install });
65
86
  }
87
+ if (command === 'update') {
88
+ const modes = ['dry-run', 'resume', 'finalize', 'abort'].filter((m) => values[m]);
89
+ if (modes.length > 1) {
90
+ console.error(`zz-meridian update: ${modes.map((m) => `--${m}`).join(' and ')} cannot be combined`);
91
+ return 1;
92
+ }
93
+ if (values.verify && !values.finalize) {
94
+ console.error('zz-meridian update: --verify belongs to --finalize');
95
+ return 1;
96
+ }
97
+ const mode = modes[0] ?? 'update';
98
+ return update({ root: process.cwd(), mode, flags: { verbose: Boolean(values.verbose), allowDirty: Boolean(values['allow-dirty']), install, verify: Boolean(values.verify) } });
99
+ }
100
+ if (command === 'brand')
101
+ return brandCommand({ root: process.cwd(), flags: brand, allowDirty: Boolean(values['allow-dirty']) });
66
102
  if (command === 'skill') {
67
103
  const roots = values.global ? [os.homedir()] : [process.cwd()];
68
104
  for (const r of roots)
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The agent-facing context a project carries: the frozen managed block in `AGENTS.md` and the brief template.
3
+ * Pure text in, text out; the caller does the reading and writing.
4
+ */
5
+ export const BEGIN = '<!-- BEGIN:zz-meridian-agent-rules -->';
6
+ export const END = '<!-- END:zz-meridian-agent-rules -->';
7
+ const BLOCK = `${BEGIN}
8
+ # Built on ZZ Meridian <version>
9
+
10
+ Follow the zz-meridian skill in \`.agents/skills/zz-meridian/SKILL.md\`.
11
+ Read \`optional:docs/brief.md\` for this team's product, users, data and decisions.
12
+ When it is absent, use the skill's brief template with the person's answers; do not invent product facts.
13
+
14
+ - What is yours: product pages, data, other team files, \`src/app.config.ts\` and \`scripts/verify.config.ts\`.
15
+ Managed paths are recorded in \`.meridian/manifest.json\`; declared kept divergences are in
16
+ \`optional:.meridian/keep.json\`. Explicit shared-file operations are described by the skill.
17
+ - Extend rather than edit: follow \`.agents/skills/zz-meridian/references/customize.md\`.
18
+ - Update: follow \`.agents/skills/zz-meridian/references/update.md\`. Installed files do not mean a completed
19
+ upgrade: resolve the report and run the pinned finalization command.
20
+ - Before finishing: safely configured \`pnpm verify\` for the default gate/build/smoke path; report any partial coverage.
21
+ Use \`pnpm gate\` alone for static feedback. Do not automatically chain it before verify, which already runs it.
22
+ Use \`pnpm verify --full\` or \`pnpm verify --perf\` when deep checks are requested; release CI runs both.
23
+ - Agents in the product read through authorized collections and write only through an approved Proposal.
24
+ ${END}`;
25
+ /** The brief a team keeps at `docs/brief.md`; its guidance lines equal `BRIEF_GUIDANCE` in the assistant prompt. */
26
+ export const BRIEF_TEMPLATE = `# Product name
27
+
28
+ ## Product
29
+ What this dashboard is for, in two or three sentences: who opens it, what decision it helps them make.
30
+
31
+ ## Users
32
+ Who uses it and how often; what they know already; what they must never be shown.
33
+
34
+ ## Data
35
+ Where the numbers come from (systems, tables, APIs), how fresh they are, and what now means for this product.
36
+
37
+ ## Decisions
38
+ Brand, layout and behaviour choices already made, one line each with its reason, so no session re-decides them.
39
+
40
+ ## Glossary
41
+ The team's own words for things, one per line: term and what it means here.
42
+ `;
43
+ /** One `pnpm <script> [flags]` command, spelled for the package manager; npm needs `--` before flags. */
44
+ function command(pm, script, flags) {
45
+ if (pm === 'pnpm')
46
+ return `pnpm ${script}${flags}`;
47
+ if (pm === 'npm')
48
+ return `npm run ${script}${flags ? ` --${flags}` : ''}`;
49
+ return `${pm === 'bun' ? 'bun run' : 'yarn'} ${script}${flags}`;
50
+ }
51
+ /** The frozen managed block for a version, with every script command in the project's package manager. */
52
+ export function managedBlock(version, pm) {
53
+ return BLOCK.replace('<version>', () => version).replace(/`pnpm ([a-z]+)((?: [^`]+)?)`/g, (_m, script, flags) => `\`${command(pm, script, flags)}\``);
54
+ }
55
+ const count = (text, needle) => text.split(needle).length - 1;
56
+ /** Put the block into an `AGENTS.md` text: replace the marked span, else an exact legacy section, else append. Refuses anything ambiguous. */
57
+ export function upsertManagedBlock(text, block, legacy) {
58
+ const begins = count(text, BEGIN);
59
+ const ends = count(text, END);
60
+ const legacies = legacy ? count(text, legacy) : 0;
61
+ if (begins > 1 || ends > 1)
62
+ throw new Error('AGENTS.md has a duplicate managed block marker; remove the extra one and run again.');
63
+ if (legacies > 1)
64
+ throw new Error('AGENTS.md has a duplicate legacy section; remove the extra copy and run again.');
65
+ const begin = text.indexOf(BEGIN);
66
+ const end = text.indexOf(END);
67
+ if (begins !== ends || end < begin)
68
+ throw new Error('AGENTS.md has an unterminated managed block; each marker needs its partner, BEGIN first.');
69
+ if (begins === 1 && legacies === 1)
70
+ throw new Error('AGENTS.md has both a managed block and a legacy section; remove one and run again.');
71
+ if (begins === 1)
72
+ return text.slice(0, begin) + block + text.slice(end + END.length);
73
+ if (legacy && legacies === 1) {
74
+ const at = text.indexOf(legacy);
75
+ return text.slice(0, at) + block + text.slice(at + legacy.length);
76
+ }
77
+ if (legacy && text.includes(legacy.split('\n')[0]))
78
+ throw new Error('AGENTS.md has an edited legacy section; replace it by hand with the managed block.');
79
+ if (text === '')
80
+ return `${block}\n`;
81
+ return `${text}${text.endsWith('\n') ? '' : '\n'}\n${block}\n`;
82
+ }
package/dist/create.js CHANGED
@@ -5,18 +5,27 @@
5
5
  */
6
6
  import fs from 'node:fs';
7
7
  import path from 'node:path';
8
- import { PAYLOAD, VERSION, brandArgs, hasCommand, inside, installSkill, payloadFiles, run, sha256, writeIn, writeManifest } from './files.js';
8
+ import { managedPaths } from './ownership.js';
9
+ import { BRIEF_TEMPLATE, managedBlock, upsertManagedBlock } from './context.js';
10
+ import { PAYLOAD, VERSION, brandArgs, effectiveBrand, hasCommand, inside, installSkill, payloadFiles, run, sha256, writeIn, writeManifest } from './files.js';
9
11
  export function create(o) {
10
12
  const root = path.resolve(o.dir);
11
13
  if (fs.existsSync(root) && fs.readdirSync(root).length)
12
14
  return fail(`${root} exists and is not empty: name a new folder`);
15
+ let brand;
16
+ try {
17
+ brand = effectiveBrand(o.brand);
18
+ }
19
+ catch (e) {
20
+ return fail(e.message);
21
+ }
13
22
  for (const rel of payloadFiles()) {
14
23
  if (rel.startsWith('skills/'))
15
24
  continue;
16
25
  writeIn(root, rel, fs.readFileSync(path.join(PAYLOAD, rel)));
17
26
  }
18
- const name = o.brand.name ?? path.basename(root).replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
19
- const brand = { ...o.brand, name };
27
+ const name = brand.name ?? path.basename(root).replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
28
+ brand = effectiveBrand({ ...brand, name });
20
29
  if (run(process.execPath, ['scripts/brand.ts', ...brandArgs(brand), '--product'], root).status !== 0) {
21
30
  return fail('scripts/brand.ts failed (above); the template is copied, so fix the cause and run it again with the same flags');
22
31
  }
@@ -29,6 +38,15 @@ export function create(o) {
29
38
  fs.rmSync(path.join(root, f), { force: true });
30
39
  console.log('pnpm is not available: installing with npm from the version ranges, without a lockfile.');
31
40
  }
41
+ // The managed block goes in only now that the installer is known, so its commands match; the template's own lines
42
+ // above `# Working in Meridian` stay. The brief is the template. The manifest walk below records both.
43
+ try {
44
+ writeIn(root, 'AGENTS.md', upsertManagedBlock(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'), managedBlock(VERSION, pm)));
45
+ }
46
+ catch (e) {
47
+ return fail(`AGENTS.md: ${e.message}`);
48
+ }
49
+ writeIn(root, 'docs/brief.md', BRIEF_TEMPLATE);
32
50
  const files = {};
33
51
  const walk = (rel) => {
34
52
  for (const e of fs.readdirSync(inside(root, rel || '.'), { withFileTypes: true })) {
@@ -42,6 +60,12 @@ export function create(o) {
42
60
  }
43
61
  };
44
62
  walk('');
63
+ // The managed set over what is on disk after branding: the payload rule for the files still there, plus the brand outputs.
64
+ const onDisk = Object.keys(files);
65
+ const managed = managedPaths([...payloadFiles().filter((f) => f.startsWith('skills/') || onDisk.includes(f))], 'create', onDisk);
66
+ for (const rel of onDisk)
67
+ if (!managed.has(rel))
68
+ delete files[rel];
45
69
  installSkill(root, files);
46
70
  writeManifest(root, { version: VERSION, route: 'create', brand, files });
47
71
  if (hasCommand('git'))
package/dist/files.js CHANGED
@@ -140,5 +140,12 @@ export function installSkill(root, record) {
140
140
  }
141
141
  }
142
142
  /** Brand flags passed through to scripts/brand.ts, in the order it reads them. */
143
- export const BRAND_FLAGS = ['name', 'workspace', 'timezone', 'currency', 'user', 'role', 'accent', 'hex', 'hue', 'chroma'];
143
+ export const BRAND_FLAGS = ['name', 'workspace', 'timezone', 'currency', 'user', 'role', 'theme', 'accent', 'hex', 'hue', 'chroma'];
144
144
  export const brandArgs = (brand) => BRAND_FLAGS.flatMap((k) => (brand[k] ? [`--${k}`, brand[k]] : []));
145
+ /** The brand a run effectively applied: one accent choice (hex over hue over a preset), flags in order, empty ones dropped. */
146
+ export function effectiveBrand(flags) {
147
+ if (flags.theme && flags.theme !== 'dark' && flags.theme !== 'light')
148
+ throw new Error(`--theme must be dark or light, got ${JSON.stringify(flags.theme)}`);
149
+ const drop = flags.hex ? ['hue', 'chroma', 'accent'] : flags.hue ? ['accent'] : [];
150
+ return Object.fromEntries(BRAND_FLAGS.flatMap((k) => (flags[k] && !drop.includes(k) ? [[k, flags[k]]] : [])));
151
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The typed inventory finalize validates against: every path the checks must leave alone (protected inputs), and the
3
+ * few outputs a gate and a build are expected to write (permitted outputs). A fingerprint is existence plus a SHA-256
4
+ * and lives in memory only; the inventory is never written anywhere, and only the names of changed paths are reported.
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { readJsonc, run, sha256 } from './files.js';
9
+ /** Folders that are never walked: the repository, dependency contents, and the updater's own state. */
10
+ const SKIPPED_DIRS = new Set(['.git', 'node_modules']);
11
+ const UPDATER = ['.meridian/update', '.meridian/history', '.meridian/update.lock'];
12
+ const NEXT_CONFIGS = ['next.config.ts', 'next.config.mts', 'next.config.js', 'next.config.mjs', 'next.config.cjs'];
13
+ const overlaps = (a, b) => a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
14
+ /** The Next build folder: `distDir` when `next.config` sets it as a string literal, else `.next`. */
15
+ function distDir(root) {
16
+ for (const name of NEXT_CONFIGS) {
17
+ const file = path.join(root, name);
18
+ if (!fs.existsSync(file))
19
+ continue;
20
+ const m = /\bdistDir\s*:\s*(['"`])([^'"`$]+)\1/.exec(fs.readFileSync(file, 'utf8'));
21
+ if (m)
22
+ return m[2];
23
+ }
24
+ return '.next';
25
+ }
26
+ /** The build-info file TypeScript writes: the configured one, or `tsconfig.tsbuildinfo` when the build is incremental. */
27
+ function buildInfo(root) {
28
+ let options;
29
+ try {
30
+ options = readJsonc(path.join(root, 'tsconfig.json')).compilerOptions ?? {};
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ if (typeof options.tsBuildInfoFile === 'string')
36
+ return options.tsBuildInfoFile;
37
+ if (options.incremental !== true && options.composite !== true)
38
+ return null;
39
+ return `${typeof options.outDir === 'string' ? `${options.outDir.replace(/\/+$/, '')}/` : ''}tsconfig.tsbuildinfo`;
40
+ }
41
+ /** The permitted outputs, as project-relative posix paths. A path that leaves the project is reported as a conflict. */
42
+ function outputsOf(root) {
43
+ const outputs = [];
44
+ const conflicts = [];
45
+ const add = (spec, directory) => {
46
+ const abs = path.resolve(root, spec);
47
+ const rel = path.relative(root, abs).split(path.sep).join('/');
48
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel))
49
+ conflicts.push(`${spec}: an output path outside the project is not exempt`);
50
+ else
51
+ outputs.push({ rel, directory });
52
+ };
53
+ add(distDir(root), true);
54
+ add('out', true);
55
+ add('coverage', true);
56
+ const info = buildInfo(root);
57
+ if (info)
58
+ add(info, false);
59
+ const env = path.join(root, 'next-env.d.ts');
60
+ const canonical = !fs.existsSync(env) || fs.readFileSync(env, 'utf8').includes('reference types="next');
61
+ if (canonical)
62
+ add('next-env.d.ts', false);
63
+ return { outputs, conflicts };
64
+ }
65
+ /** True when git tracks a file inside `dir`: an output folder that holds the team's own files is not an output. */
66
+ function tracksFiles(root, dir) {
67
+ if (!fs.existsSync(path.join(root, '.git')))
68
+ return false;
69
+ const r = run('git', ['ls-files', '--', dir], root, true);
70
+ return r.status === 0 && (r.stdout ?? '').trim() !== '';
71
+ }
72
+ /** A symbolic link on the way to `rel` (or at `rel` itself), as the first such project-relative path. */
73
+ function linkOn(root, rel) {
74
+ let cur = root;
75
+ for (const seg of rel.split('/')) {
76
+ cur = path.join(cur, seg);
77
+ if (fs.lstatSync(cur, { throwIfNoEntry: false })?.isSymbolicLink())
78
+ return path.relative(root, cur).split(path.sep).join('/');
79
+ }
80
+ return null;
81
+ }
82
+ /**
83
+ * Fingerprints every protected input under `root`. `managed` are the project paths Meridian manages or the team keeps:
84
+ * an output that overlaps one of them is a conflict, as is an output behind a symbolic link or holding tracked files.
85
+ */
86
+ export function takeInventory(root, managed) {
87
+ const { outputs, conflicts } = outputsOf(root);
88
+ for (const o of outputs) {
89
+ const clash = managed.find((p) => overlaps(p, o.rel));
90
+ if (clash)
91
+ conflicts.push(`${o.rel}: an output path that overlaps ${clash}, a managed or kept file, is not exempt`);
92
+ const link = linkOn(root, o.rel);
93
+ if (link)
94
+ conflicts.push(`${link}: an output path that is a symbolic link is not exempt`);
95
+ else if (o.directory && tracksFiles(root, o.rel))
96
+ conflicts.push(`${o.rel}: an output folder that holds tracked files is not exempt`);
97
+ }
98
+ const exempt = new Set([...UPDATER, ...outputs.map((o) => o.rel)]);
99
+ const prints = new Map();
100
+ const walk = (rel) => {
101
+ for (const e of fs.readdirSync(path.join(root, rel), { withFileTypes: true })) {
102
+ const p = rel ? `${rel}/${e.name}` : e.name;
103
+ if (exempt.has(p))
104
+ continue;
105
+ const abs = path.join(root, p);
106
+ if (e.isDirectory()) {
107
+ if (!SKIPPED_DIRS.has(e.name))
108
+ walk(p);
109
+ }
110
+ else if (e.isSymbolicLink())
111
+ prints.set(p, `link ${fs.readlinkSync(abs)}`);
112
+ else if (e.isFile())
113
+ prints.set(p, sha256(fs.readFileSync(abs)));
114
+ else
115
+ prints.set(p, 'special');
116
+ }
117
+ };
118
+ walk('');
119
+ return { prints, conflicts };
120
+ }
121
+ /** The paths that exist in only one inventory or differ between them, sorted. */
122
+ export function changedPaths(before, after) {
123
+ const out = [];
124
+ for (const [p, h] of after.prints)
125
+ if (before.prints.get(p) !== h)
126
+ out.push(p);
127
+ for (const p of before.prints.keys())
128
+ if (!after.prints.has(p))
129
+ out.push(p);
130
+ return [...new Set(out)].sort();
131
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `MERGE.md`: the report a person (or their agent) reads to finish an update. Pure text out of the journal, so the same
3
+ * session always renders the same report.
4
+ */
5
+ import { isNewer } from './update.js';
6
+ /** The sections of a changelog whose version is above `source`, `[Unreleased]` included, each with its own heading line. */
7
+ export function changelogSections(changelog, source) {
8
+ const parts = changelog.split(/^(?=## \[)/m).filter((s) => s.startsWith('## ['));
9
+ return parts.filter((s) => {
10
+ const v = /^## \[([^\]]+)\]/.exec(s)[1];
11
+ return v === 'Unreleased' || (/^\d+\.\d+\.\d+$/.test(v) && isNewer(v, source));
12
+ }).map((s) => s.trimEnd());
13
+ }
14
+ /** A fence long enough that nothing inside the text can close it. */
15
+ function fenced(text) {
16
+ const longest = Math.max(2, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
17
+ const fence = '`'.repeat(longest + 1);
18
+ return `${fence}markdown\n${text}\n${fence}`;
19
+ }
20
+ const json = (v) => `\`\`\`json\n${JSON.stringify(v, null, 2)}\n\`\`\``;
21
+ /** The path of one staged copy, or `absent` when that side does not exist. */
22
+ const copy = (session, folder, side, path) => (side.exists ? `\`${session}/${folder}/${path}\`` : '`absent`');
23
+ const WHAT = {
24
+ edited: 'You edited this managed file and Meridian changed it too. Combine the two versions.',
25
+ collision: 'Meridian now ships this path and you already have a different file there. Decide which one stays, or combine them.',
26
+ removed: 'Meridian removed this file, and you edited it. Decide whether to delete it or keep it.',
27
+ 'local-deletion': 'You deleted this managed file and Meridian still ships it. Decide whether it stays out or comes back.',
28
+ };
29
+ export function renderMergeReport(i) {
30
+ const j = i.journal;
31
+ const finalize = `npx zz-meridian@${j.targetVersion} update --finalize`;
32
+ const resume = `npx zz-meridian@${j.targetVersion} update --resume`;
33
+ const abort = `npx zz-meridian@${j.targetVersion} update --abort`;
34
+ const staged = j.operations.filter((o) => o.action === 'stage');
35
+ const out = [];
36
+ out.push(`# Update ${j.sourceVersion} to ${j.targetVersion}`, '');
37
+ out.push(`- Source: ${j.sourceVersion}`, `- Target: ${j.targetVersion}`, `- Route: ${j.route}`, `- Session: ${j.id}`, `- Phase: ${j.phase}`, `- Outcome: ${i.outcome}`, '');
38
+ out.push('Commands, pinned to the release that started this session:', '', '```', `${finalize} # when every item below is resolved`, `${resume} # after an interruption, or to run a skipped install`, `${abort} # restore what the update changed and archive the session`, '```', '');
39
+ if (j.failure)
40
+ out.push('## Failure', '', j.failure, '');
41
+ out.push('## What needs you', '');
42
+ if (staged.length === 0 && j.migrations.length === 0)
43
+ out.push('Nothing was staged and no migration is required.', '');
44
+ out.push('Resolve each item, then record it in `resolutions.json` next to this file. A resolution is an object with the item id, `resolved`, a reason in a sentence, and the hash each named file has right now. Compute a hash as `sha256-` plus the file\'s SHA-256 (`shasum -a 256 <file>`), or `null` for a file that is absent. A file edited after its resolution needs a new one.', '');
45
+ for (const o of staged)
46
+ out.push(...stagedItem(i, o));
47
+ for (const m of j.migrations) {
48
+ out.push(`### migration:${m.id}`, '', m.summary, '', `Paths: ${m.paths.map((p) => `\`${p}\``).join(', ') || 'none'}`, '', m.instructions, '');
49
+ if (m.checks.length)
50
+ out.push('Checks:', '', ...m.checks.map((c) => `- \`${c}\``), '');
51
+ out.push('Resolve with `resolved` or `not-applicable`:', '', json({ id: `migration:${m.id}`, status: 'resolved', reason: '', files: Object.fromEntries(m.paths.map((p) => [p, i.hashOf(p)])) }), '');
52
+ }
53
+ const warnings = [
54
+ ...i.keepWarnings,
55
+ ...j.operations.filter((o) => o.disposition === 'retired-kept').map((o) => `${o.path}: Meridian removed this file; your kept copy stays and is now yours alone. It is recorded as retired.`),
56
+ ];
57
+ out.push('## Kept and retired files', '');
58
+ out.push(...(warnings.length ? warnings.map((w) => `- ${w}`) : ['None.']), '');
59
+ const team = [...new Set([...j.operations.filter((o) => o.disposition === 'team-preserved' && o.action === 'write').map((o) => o.path), ...j.migrations.flatMap((m) => m.paths)])].sort();
60
+ out.push('## Team files this update touches or asks you to review', '');
61
+ out.push(...(team.length ? team.map((p) => `- \`${p}\``) : ['None.']), '', 'Meridian changes a team file only where it says so here: `package.json` (dependencies and scripts), `AGENTS.md` (the managed block) and the lockfile after an install. Everything else is advice in the items above.', '');
62
+ const counts = new Map();
63
+ for (const o of j.operations)
64
+ counts.set(`${o.disposition}/${o.action}`, (counts.get(`${o.disposition}/${o.action}`) ?? 0) + 1);
65
+ out.push('## Every disposition', '', ...[...counts].sort().map(([k, n]) => `- ${k}: ${n}`), '', '<details><summary>Every path</summary>', '', '```');
66
+ for (const o of j.operations)
67
+ out.push(`${o.disposition.padEnd(15)} ${o.action.padEnd(6)} ${o.path}`);
68
+ out.push('```', '', '</details>', '');
69
+ out.push('## Verify', '', 'Finalizing runs the project gate and one build. To check by hand first:', '', '```', `${i.pm} run gate`, 'node node_modules/next/dist/bin/next build', '```', '');
70
+ const sections = changelogSections(i.changelog, j.sourceVersion);
71
+ out.push('## Changelog', '', sections.length ? `The target release's changelog sections above ${j.sourceVersion}:` : `The target release has no changelog section above ${j.sourceVersion}.`, '');
72
+ if (sections.length)
73
+ out.push(fenced(sections.join('\n\n')), '');
74
+ return `${out.join('\n')}\n`;
75
+ }
76
+ function stagedItem(i, o) {
77
+ const s = i.session;
78
+ return [
79
+ `### file:${o.path}`, '',
80
+ `${o.disposition}: ${WHAT[o.disposition] ?? 'Review this file.'}`, '',
81
+ `- base: ${copy(s, 'base', o.base, o.path)}`,
82
+ `- ours: ${copy(s, 'ours', o.ours, o.path)}`,
83
+ `- new: ${copy(s, 'new', o.target, o.path)}`, '',
84
+ 'Resolution stub:', '',
85
+ json({ id: `file:${o.path}`, status: 'resolved', reason: '', files: { [o.path]: i.hashOf(o.path) } }), '',
86
+ ];
87
+ }