@minamocms/framework 0.0.0-stage → 0.10.2

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 (286) hide show
  1. package/CHANGELOG.md +240 -0
  2. package/LICENSE +105 -0
  3. package/README.md +133 -2
  4. package/dist/boot.d.ts +19 -0
  5. package/dist/boot.js +22 -0
  6. package/dist/config.d.ts +18 -0
  7. package/dist/config.js +44 -0
  8. package/dist/create-site/cli.d.ts +2 -0
  9. package/dist/create-site/cli.js +31 -0
  10. package/dist/create-site/generate-site.d.ts +12 -0
  11. package/dist/create-site/generate-site.js +168 -0
  12. package/dist/create-site/mint-token-cli.d.ts +2 -0
  13. package/dist/create-site/mint-token-cli.js +26 -0
  14. package/dist/create-site/mint-token.d.ts +7 -0
  15. package/dist/create-site/mint-token.js +49 -0
  16. package/dist/create-site/template/AGENTS.md +473 -0
  17. package/dist/create-site/template/content/menus/footerCompany.json +9 -0
  18. package/dist/create-site/template/content/menus/footerProduct.json +9 -0
  19. package/dist/create-site/template/content/menus/footerResources.json +9 -0
  20. package/dist/create-site/template/content/menus/main.json +9 -0
  21. package/dist/create-site/template/content/pages/404.json +23 -0
  22. package/dist/create-site/template/content/pages/about/careers.json +22 -0
  23. package/dist/create-site/template/content/pages/about/team.json +22 -0
  24. package/dist/create-site/template/content/pages/about.json +22 -0
  25. package/dist/create-site/template/content/pages/docs/deployment.json +22 -0
  26. package/dist/create-site/template/content/pages/docs/getting-started/quickstart.json +22 -0
  27. package/dist/create-site/template/content/pages/docs/getting-started.json +24 -0
  28. package/dist/create-site/template/content/pages/docs.json +22 -0
  29. package/dist/create-site/template/content/pages/index.json +182 -0
  30. package/dist/create-site/template/content/redirects.json +4 -0
  31. package/dist/create-site/template/dockerignore +6 -0
  32. package/dist/create-site/template/gitignore +11 -0
  33. package/dist/create-site/template/theme/assets/site.js +49 -0
  34. package/dist/create-site/template/theme/assets/style.css +1223 -0
  35. package/dist/create-site/template/theme/blocks/button.liquid +21 -0
  36. package/dist/create-site/template/theme/blocks/comparison-row.liquid +19 -0
  37. package/dist/create-site/template/theme/blocks/faq-item.liquid +22 -0
  38. package/dist/create-site/template/theme/blocks/feature-card.liquid +23 -0
  39. package/dist/create-site/template/theme/blocks/logo-mark.liquid +13 -0
  40. package/dist/create-site/template/theme/blocks/pricing-tier.liquid +42 -0
  41. package/dist/create-site/template/theme/blocks/step-card.liquid +19 -0
  42. package/dist/create-site/template/theme/blocks/testimonial-card.liquid +25 -0
  43. package/dist/create-site/template/theme/config/site_settings.json +11 -0
  44. package/dist/create-site/template/theme/layouts/theme.liquid +102 -0
  45. package/dist/create-site/template/theme/sections/comparison.liquid +41 -0
  46. package/dist/create-site/template/theme/sections/cta-banner.liquid +36 -0
  47. package/dist/create-site/template/theme/sections/faq.liquid +27 -0
  48. package/dist/create-site/template/theme/sections/feature-grid.liquid +27 -0
  49. package/dist/create-site/template/theme/sections/hero.liquid +48 -0
  50. package/dist/create-site/template/theme/sections/how-it-works.liquid +27 -0
  51. package/dist/create-site/template/theme/sections/pricing.liquid +27 -0
  52. package/dist/create-site/template/theme/sections/social-proof.liquid +21 -0
  53. package/dist/create-site/template/theme/sections/testimonials.liquid +27 -0
  54. package/dist/create-site/template/theme/snippets/icon.liquid +24 -0
  55. package/dist/create-site/template/theme/snippets/logo.liquid +7 -0
  56. package/dist/create-site/template/theme/snippets/responsive-media.liquid +107 -0
  57. package/dist/create-site/template/theme/snippets/site-name.liquid +1 -0
  58. package/dist/create-site/template/vhost/Dockerfile +33 -0
  59. package/dist/create-site/template/vhost/docker-entrypoint.sh +58 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.js +4 -0
  62. package/dist/media/drivers/driver.d.ts +11 -0
  63. package/dist/media/drivers/driver.js +1 -0
  64. package/dist/media/drivers/local-fs-driver.d.ts +3 -0
  65. package/dist/media/drivers/local-fs-driver.js +94 -0
  66. package/dist/media/filename.d.ts +2 -0
  67. package/dist/media/filename.js +54 -0
  68. package/dist/media/manage-media.d.ts +19 -0
  69. package/dist/media/manage-media.js +45 -0
  70. package/dist/media/seed-media-cli.d.ts +2 -0
  71. package/dist/media/seed-media-cli.js +22 -0
  72. package/dist/media/seed-media.d.ts +11 -0
  73. package/dist/media/seed-media.js +76 -0
  74. package/dist/migrations/index.d.ts +3 -0
  75. package/dist/migrations/index.js +83 -0
  76. package/dist/pull-site/cli.d.ts +2 -0
  77. package/dist/pull-site/cli.js +78 -0
  78. package/dist/pull-site/prompt-token.d.ts +16 -0
  79. package/dist/pull-site/prompt-token.js +69 -0
  80. package/dist/pull-site/prompts.d.ts +22 -0
  81. package/dist/pull-site/prompts.js +127 -0
  82. package/dist/pull-site/pull-site.d.ts +23 -0
  83. package/dist/pull-site/pull-site.js +211 -0
  84. package/dist/renderer/engine.d.ts +2 -0
  85. package/dist/renderer/engine.js +48 -0
  86. package/dist/renderer/render-cache.d.ts +11 -0
  87. package/dist/renderer/render-cache.js +11 -0
  88. package/dist/renderer/render-page.d.ts +34 -0
  89. package/dist/renderer/render-page.js +209 -0
  90. package/dist/renderer/theme-templates.d.ts +7 -0
  91. package/dist/renderer/theme-templates.js +82 -0
  92. package/dist/routes/admin-redirect.d.ts +5 -0
  93. package/dist/routes/admin-redirect.js +26 -0
  94. package/dist/routes/assets.d.ts +6 -0
  95. package/dist/routes/assets.js +30 -0
  96. package/dist/routes/batch.d.ts +10 -0
  97. package/dist/routes/batch.js +148 -0
  98. package/dist/routes/capabilities.d.ts +6 -0
  99. package/dist/routes/capabilities.js +27 -0
  100. package/dist/routes/content.d.ts +8 -0
  101. package/dist/routes/content.js +169 -0
  102. package/dist/routes/deleted-pages.d.ts +8 -0
  103. package/dist/routes/deleted-pages.js +9 -0
  104. package/dist/routes/drafts.d.ts +10 -0
  105. package/dist/routes/drafts.js +73 -0
  106. package/dist/routes/git.d.ts +8 -0
  107. package/dist/routes/git.js +158 -0
  108. package/dist/routes/index.d.ts +12 -0
  109. package/dist/routes/index.js +86 -0
  110. package/dist/routes/links.d.ts +8 -0
  111. package/dist/routes/links.js +18 -0
  112. package/dist/routes/media-public.d.ts +11 -0
  113. package/dist/routes/media-public.js +125 -0
  114. package/dist/routes/media.d.ts +9 -0
  115. package/dist/routes/media.js +80 -0
  116. package/dist/routes/menus.d.ts +8 -0
  117. package/dist/routes/menus.js +148 -0
  118. package/dist/routes/preview-revision.d.ts +10 -0
  119. package/dist/routes/preview-revision.js +96 -0
  120. package/dist/routes/preview.d.ts +10 -0
  121. package/dist/routes/preview.js +56 -0
  122. package/dist/routes/public.d.ts +10 -0
  123. package/dist/routes/public.js +123 -0
  124. package/dist/routes/publish.d.ts +10 -0
  125. package/dist/routes/publish.js +139 -0
  126. package/dist/routes/redirects.d.ts +8 -0
  127. package/dist/routes/redirects.js +132 -0
  128. package/dist/routes/search-public.d.ts +6 -0
  129. package/dist/routes/search-public.js +104 -0
  130. package/dist/routes/search.d.ts +8 -0
  131. package/dist/routes/search.js +15 -0
  132. package/dist/routes/settings.d.ts +10 -0
  133. package/dist/routes/settings.js +68 -0
  134. package/dist/routes/sitemap.d.ts +7 -0
  135. package/dist/routes/sitemap.js +79 -0
  136. package/dist/routes/theme.d.ts +10 -0
  137. package/dist/routes/theme.js +116 -0
  138. package/dist/schemas/instance.schema.json +17 -0
  139. package/dist/schemas/menu.schema.json +24 -0
  140. package/dist/schemas/page.schema.json +26 -0
  141. package/dist/schemas/redirects.schema.json +24 -0
  142. package/dist/search/drivers/driver.d.ts +9 -0
  143. package/dist/search/drivers/driver.js +1 -0
  144. package/dist/search/drivers/node-sqlite-driver.d.ts +7 -0
  145. package/dist/search/drivers/node-sqlite-driver.js +19 -0
  146. package/dist/search/query-content.d.ts +32 -0
  147. package/dist/search/query-content.js +207 -0
  148. package/dist/search/query-index.d.ts +5 -0
  149. package/dist/search/query-index.js +21 -0
  150. package/dist/search/rebuild-index.d.ts +3 -0
  151. package/dist/search/rebuild-index.js +320 -0
  152. package/dist/server-config.d.ts +23 -0
  153. package/dist/server-config.js +237 -0
  154. package/dist/server.d.ts +13 -0
  155. package/dist/server.js +235 -0
  156. package/dist/services/batch.d.ts +35 -0
  157. package/dist/services/batch.js +145 -0
  158. package/dist/services/checkpoint.d.ts +4 -0
  159. package/dist/services/checkpoint.js +45 -0
  160. package/dist/services/content-read.d.ts +27 -0
  161. package/dist/services/content-read.js +151 -0
  162. package/dist/services/delete-content.d.ts +12 -0
  163. package/dist/services/delete-content.js +166 -0
  164. package/dist/services/deleted-pages.d.ts +11 -0
  165. package/dist/services/deleted-pages.js +83 -0
  166. package/dist/services/dev-tunnel.d.ts +5 -0
  167. package/dist/services/dev-tunnel.js +5 -0
  168. package/dist/services/drafts.d.ts +16 -0
  169. package/dist/services/drafts.js +164 -0
  170. package/dist/services/etag.d.ts +2 -0
  171. package/dist/services/etag.js +30 -0
  172. package/dist/services/fs-walk.d.ts +1 -0
  173. package/dist/services/fs-walk.js +26 -0
  174. package/dist/services/git-commit.d.ts +3 -0
  175. package/dist/services/git-commit.js +12 -0
  176. package/dist/services/git-history.d.ts +28 -0
  177. package/dist/services/git-history.js +131 -0
  178. package/dist/services/git-revert.d.ts +12 -0
  179. package/dist/services/git-revert.js +140 -0
  180. package/dist/services/git.d.ts +16 -0
  181. package/dist/services/git.js +127 -0
  182. package/dist/services/interval-job.d.ts +4 -0
  183. package/dist/services/interval-job.js +9 -0
  184. package/dist/services/ip-allowlist.d.ts +7 -0
  185. package/dist/services/ip-allowlist.js +31 -0
  186. package/dist/services/links.d.ts +22 -0
  187. package/dist/services/links.js +169 -0
  188. package/dist/services/manage-menus.d.ts +15 -0
  189. package/dist/services/manage-menus.js +138 -0
  190. package/dist/services/manage-redirects.d.ts +25 -0
  191. package/dist/services/manage-redirects.js +156 -0
  192. package/dist/services/menu-references.d.ts +3 -0
  193. package/dist/services/menu-references.js +38 -0
  194. package/dist/services/menus.d.ts +9 -0
  195. package/dist/services/menus.js +43 -0
  196. package/dist/services/migration-runner.d.ts +16 -0
  197. package/dist/services/migration-runner.js +169 -0
  198. package/dist/services/mime-types.d.ts +3 -0
  199. package/dist/services/mime-types.js +43 -0
  200. package/dist/services/move.d.ts +16 -0
  201. package/dist/services/move.js +236 -0
  202. package/dist/services/path-safety.d.ts +6 -0
  203. package/dist/services/path-safety.js +112 -0
  204. package/dist/services/pid-file.d.ts +13 -0
  205. package/dist/services/pid-file.js +54 -0
  206. package/dist/services/post-urls.d.ts +3 -0
  207. package/dist/services/post-urls.js +27 -0
  208. package/dist/services/prepared-operation.d.ts +4 -0
  209. package/dist/services/prepared-operation.js +1 -0
  210. package/dist/services/publish.d.ts +15 -0
  211. package/dist/services/publish.js +313 -0
  212. package/dist/services/rate-limit-config.d.ts +9 -0
  213. package/dist/services/rate-limit-config.js +22 -0
  214. package/dist/services/redirects.d.ts +27 -0
  215. package/dist/services/redirects.js +175 -0
  216. package/dist/services/reindex-on-write.d.ts +3 -0
  217. package/dist/services/reindex-on-write.js +30 -0
  218. package/dist/services/resolve-blog-url.d.ts +11 -0
  219. package/dist/services/resolve-blog-url.js +31 -0
  220. package/dist/services/resolve-url.d.ts +11 -0
  221. package/dist/services/resolve-url.js +20 -0
  222. package/dist/services/site-settings.d.ts +28 -0
  223. package/dist/services/site-settings.js +116 -0
  224. package/dist/services/startup-checks.d.ts +10 -0
  225. package/dist/services/startup-checks.js +81 -0
  226. package/dist/services/static-file.d.ts +7 -0
  227. package/dist/services/static-file.js +37 -0
  228. package/dist/services/theme-component-file.d.ts +6 -0
  229. package/dist/services/theme-component-file.js +39 -0
  230. package/dist/services/theme-files.d.ts +33 -0
  231. package/dist/services/theme-files.js +148 -0
  232. package/dist/services/theme-page-templates.d.ts +7 -0
  233. package/dist/services/theme-page-templates.js +53 -0
  234. package/dist/services/theme-schemas.d.ts +2 -0
  235. package/dist/services/theme-schemas.js +137 -0
  236. package/dist/services/token-auth.d.ts +10 -0
  237. package/dist/services/token-auth.js +64 -0
  238. package/dist/services/urls.d.ts +2 -0
  239. package/dist/services/urls.js +20 -0
  240. package/dist/services/validation.d.ts +26 -0
  241. package/dist/services/validation.js +195 -0
  242. package/dist/services/write-queue.d.ts +1 -0
  243. package/dist/services/write-queue.js +14 -0
  244. package/dist/site-check/cli.d.ts +2 -0
  245. package/dist/site-check/cli.js +48 -0
  246. package/dist/site-check/run-check.d.ts +12 -0
  247. package/dist/site-check/run-check.js +188 -0
  248. package/dist/site-sync/cli-helpers.d.ts +23 -0
  249. package/dist/site-sync/cli-helpers.js +75 -0
  250. package/dist/site-sync/deploy.d.ts +36 -0
  251. package/dist/site-sync/deploy.js +124 -0
  252. package/dist/site-sync/live-content.d.ts +30 -0
  253. package/dist/site-sync/live-content.js +68 -0
  254. package/dist/site-sync/prompts.d.ts +30 -0
  255. package/dist/site-sync/prompts.js +237 -0
  256. package/dist/site-sync/pull-cli.d.ts +2 -0
  257. package/dist/site-sync/pull-cli.js +75 -0
  258. package/dist/site-sync/pull-site.d.ts +28 -0
  259. package/dist/site-sync/pull-site.js +171 -0
  260. package/dist/site-sync/push-cli.d.ts +2 -0
  261. package/dist/site-sync/push-cli.js +268 -0
  262. package/dist/site-sync/push-site.d.ts +96 -0
  263. package/dist/site-sync/push-site.js +376 -0
  264. package/dist/site-sync/remote-site.d.ts +38 -0
  265. package/dist/site-sync/remote-site.js +122 -0
  266. package/dist/site-sync/sync-record.d.ts +26 -0
  267. package/dist/site-sync/sync-record.js +76 -0
  268. package/dist/site-sync/theme-sync.d.ts +34 -0
  269. package/dist/site-sync/theme-sync.js +128 -0
  270. package/dist/stop-site/cli.d.ts +2 -0
  271. package/dist/stop-site/cli.js +28 -0
  272. package/dist/stop-site/stop-site.d.ts +26 -0
  273. package/dist/stop-site/stop-site.js +56 -0
  274. package/dist/theme-state.d.ts +20 -0
  275. package/dist/theme-state.js +33 -0
  276. package/dist/upgrade/changelog.d.ts +8 -0
  277. package/dist/upgrade/changelog.js +46 -0
  278. package/dist/upgrade/finish.d.ts +9 -0
  279. package/dist/upgrade/finish.js +30 -0
  280. package/dist/upgrade/owned-files.d.ts +11 -0
  281. package/dist/upgrade/owned-files.js +42 -0
  282. package/dist/upgrade/package-names.d.ts +4 -0
  283. package/dist/upgrade/package-names.js +26 -0
  284. package/dist/upgrade/upgrade-cli.d.ts +2 -0
  285. package/dist/upgrade/upgrade-cli.js +170 -0
  286. package/package.json +64 -4
@@ -0,0 +1,128 @@
1
+ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { listFilesRecursively } from "../services/fs-walk.js";
4
+ import { sanitisePath } from "../services/path-safety.js";
5
+ import { hashThemeFile, isValidThemePath } from "../services/theme-files.js";
6
+ import { encodePath } from "./live-content.js";
7
+ import { SiteSyncError } from "./remote-site.js";
8
+ export async function fetchLiveTheme(remote) {
9
+ let response;
10
+ try {
11
+ response = await remote.get('/v1/theme/files', { allow404: true });
12
+ }
13
+ catch (error) {
14
+ if (error instanceof SiteSyncError && error.reason === 'unauthorised') {
15
+ return { unavailable: 'the token doesn\'t have the "theme" scope' };
16
+ }
17
+ throw error;
18
+ }
19
+ // A live site on a CMS older than 0.6.0 has no theme endpoints.
20
+ if (response === null) {
21
+ return { unavailable: 'the live site\'s CMS is too old to sync its theme; upgrade it first' };
22
+ }
23
+ const listed = await response.json().catch(() => null);
24
+ if (!Array.isArray(listed)) {
25
+ throw new SiteSyncError('fetch-failed', 'GET /v1/theme/files did not return a list');
26
+ }
27
+ const theme = new Map();
28
+ for (const entry of listed) {
29
+ const { path, hash } = entry;
30
+ if (typeof path !== 'string' || !isValidThemePath(path) || typeof hash !== 'string') {
31
+ throw new SiteSyncError('unsafe-path', `The live site listed an unexpected theme file: ${JSON.stringify(path)}`);
32
+ }
33
+ theme.set(path, hash);
34
+ }
35
+ return { theme };
36
+ }
37
+ export function readLocalTheme(config) {
38
+ const files = new Map();
39
+ for (const path of listFilesRecursively(config.themeRoot, config.themeRoot, '').filter(isValidThemePath)) {
40
+ files.set(path, readFileSync(sanitisePath(config.themeRoot, path)));
41
+ }
42
+ return files;
43
+ }
44
+ // Makes theme/ a copy of the live site's theme. Only files that differ
45
+ // are downloaded; all of them before anything is written, so a failure
46
+ // part way leaves theme/ as it was.
47
+ export async function pullTheme(config, remote, live) {
48
+ const local = readLocalTheme(config);
49
+ const downloads = new Map();
50
+ for (const [path, hash] of live) {
51
+ const here = local.get(path);
52
+ if (here && hashThemeFile(here) === hash) {
53
+ continue;
54
+ }
55
+ const bytes = await remote.getBytes(`/v1/theme/files/${encodePath(path)}`);
56
+ if (bytes === null || hashThemeFile(bytes) !== hash) {
57
+ throw new SiteSyncError('fetch-failed', `theme/${path} changed on the live site while it was being pulled; pull again`);
58
+ }
59
+ downloads.set(path, bytes);
60
+ }
61
+ const removed = [];
62
+ for (const path of local.keys()) {
63
+ if (!live.has(path)) {
64
+ unlinkSync(sanitisePath(config.themeRoot, path));
65
+ removed.push(path);
66
+ }
67
+ }
68
+ mkdirSync(config.themeRoot, { recursive: true });
69
+ for (const [path, bytes] of downloads) {
70
+ const target = sanitisePath(config.themeRoot, path);
71
+ mkdirSync(dirname(target), { recursive: true });
72
+ writeFileSync(target, bytes);
73
+ }
74
+ return { files: live.size, downloaded: downloads.size, removed: removed.sort() };
75
+ }
76
+ // The same three-way rule as pages (see planPush): only a file changed
77
+ // here since the pull is pushed, and never over one changed on the live
78
+ // site since.
79
+ export function planThemePush(pulled, local, live) {
80
+ const changes = [];
81
+ const conflicts = [];
82
+ const paths = new Set([...Object.keys(pulled), ...local.keys(), ...live.keys()]);
83
+ for (const path of [...paths].sort()) {
84
+ const was = pulled[path];
85
+ const here = local.has(path) ? hashThemeFile(local.get(path)) : undefined;
86
+ const there = live.get(path);
87
+ if (here === was || here === there) {
88
+ continue;
89
+ }
90
+ if (there !== was) {
91
+ conflicts.push({
92
+ path,
93
+ reason: there === undefined
94
+ ? 'deleted on the live site since your last pull'
95
+ : was === undefined
96
+ ? 'created on the live site since your last pull'
97
+ : 'changed on the live site since your last pull',
98
+ });
99
+ continue;
100
+ }
101
+ changes.push({ path, action: here === undefined ? 'delete' : there === undefined ? 'create' : 'update' });
102
+ }
103
+ return { changes, conflicts };
104
+ }
105
+ // Sends the whole theme change as one request: the site checks every
106
+ // file against what was expected, writes, commits once and switches to
107
+ // the new theme, or changes nothing. Returns the files pushed, with the
108
+ // hashes the live site now has for them.
109
+ export async function executeThemePush(remote, plan, local, live, author) {
110
+ const writes = plan.changes
111
+ .filter((change) => change.action !== 'delete')
112
+ .map((change) => ({
113
+ path: change.path,
114
+ content: local.get(change.path).toString('base64'),
115
+ expected: live.get(change.path) ?? null,
116
+ }));
117
+ const deletes = plan.changes
118
+ .filter((change) => change.action === 'delete')
119
+ .map((change) => ({ path: change.path, expected: live.get(change.path) }));
120
+ const result = (await remote.sendJson('POST', '/v1/theme/push', {
121
+ writes,
122
+ deletes,
123
+ message: `Push ${plan.changes.length} theme file${plan.changes.length === 1 ? '' : 's'} from a local copy`,
124
+ author,
125
+ }));
126
+ const warnings = Array.isArray(result?.warnings) ? result.warnings.filter((entry) => typeof entry === 'string') : [];
127
+ return { warnings };
128
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'node:path';
3
+ import { loadSiteConfig } from "../config.js";
4
+ import { stopSite } from "./stop-site.js";
5
+ // Run from vhost/ (the scaffold's own "stop" script), so the site root
6
+ // is one level up - the same relationship check-site relies on.
7
+ const config = loadSiteConfig(resolve(process.cwd(), '..'));
8
+ const result = await stopSite(config);
9
+ switch (result.outcome) {
10
+ case 'not-running':
11
+ console.log("The site isn't running.");
12
+ break;
13
+ case 'removed-stale':
14
+ console.log(`The site isn't running. Removed a leftover record of process ${result.pid}, which has already ended.`);
15
+ break;
16
+ case 'not-a-site':
17
+ console.error(`Process ${result.pid} is recorded as this site, but no site is answering on port ${result.port}, so nothing was stopped.`);
18
+ console.error('If you are sure the site is not running, delete vhost/data/server.pid.');
19
+ process.exit(1);
20
+ break;
21
+ case 'stopped':
22
+ console.log(`Stopped the site (was on port ${result.port}).`);
23
+ break;
24
+ case 'still-stopping':
25
+ console.error(`Asked the site to stop (process ${result.pid}), but it is still shutting down.`);
26
+ process.exit(1);
27
+ break;
28
+ }
@@ -0,0 +1,26 @@
1
+ import type { SiteConfig } from '../config.ts';
2
+ export type StopSiteResult = {
3
+ outcome: 'not-running';
4
+ } | {
5
+ outcome: 'removed-stale';
6
+ pid: number;
7
+ } | {
8
+ outcome: 'not-a-site';
9
+ pid: number;
10
+ port: number;
11
+ } | {
12
+ outcome: 'stopped';
13
+ port: number;
14
+ } | {
15
+ outcome: 'still-stopping';
16
+ pid: number;
17
+ };
18
+ export interface StopSiteDeps {
19
+ isAlive: (pid: number) => boolean;
20
+ signal: (pid: number) => void;
21
+ isSiteListening: (port: number) => Promise<boolean>;
22
+ sleep: (ms: number) => Promise<void>;
23
+ timeoutMs: number;
24
+ }
25
+ export declare const defaultStopSiteDeps: StopSiteDeps;
26
+ export declare function stopSite(config: SiteConfig, deps?: StopSiteDeps): Promise<StopSiteResult>;
@@ -0,0 +1,56 @@
1
+ import { isProcessAlive, readPidFile, removePidFile } from "../services/pid-file.js";
2
+ // GET /v1/capabilities is unauthenticated and always answers with the
3
+ // agent's own version, so it's the check that something on the recorded
4
+ // port really is a site - not just any process that happens to have
5
+ // been given the recorded id after the site's own ended.
6
+ async function capabilitiesAnswer(port) {
7
+ try {
8
+ const response = await fetch(new URL('/v1/capabilities', `http://127.0.0.1:${port}`), {
9
+ signal: AbortSignal.timeout(2_000),
10
+ });
11
+ const body = (await response.json());
12
+ return response.ok && typeof body.agentVersion === 'string';
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ export const defaultStopSiteDeps = {
19
+ isAlive: isProcessAlive,
20
+ signal: (pid) => process.kill(pid, 'SIGTERM'),
21
+ isSiteListening: capabilitiesAnswer,
22
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
23
+ // Long enough for the final draft checkpoint a graceful shutdown runs.
24
+ timeoutMs: 15_000,
25
+ };
26
+ // Stops the site recorded in vhost/data/server.pid with SIGTERM - the
27
+ // same graceful shutdown as Ctrl+C (the server closes and runs its
28
+ // final draft checkpoint) - then waits for it to exit. It only ever
29
+ // signals a process that is both alive and answering as a site on the
30
+ // recorded port, so a leftover file from a crash never gets an
31
+ // unrelated process that has since been given the same id killed.
32
+ export async function stopSite(config, deps = defaultStopSiteDeps) {
33
+ const record = readPidFile(config);
34
+ if (!record) {
35
+ return { outcome: 'not-running' };
36
+ }
37
+ if (!deps.isAlive(record.pid)) {
38
+ removePidFile(config);
39
+ return { outcome: 'removed-stale', pid: record.pid };
40
+ }
41
+ if (!(await deps.isSiteListening(record.port))) {
42
+ return { outcome: 'not-a-site', pid: record.pid, port: record.port };
43
+ }
44
+ deps.signal(record.pid);
45
+ const pollMs = 200;
46
+ for (let waited = 0; waited < deps.timeoutMs; waited += pollMs) {
47
+ if (!deps.isAlive(record.pid) && !deps.isAlive(record.serverPid)) {
48
+ // The server removes its own record on a graceful shutdown; this
49
+ // covers one that exited without getting that far.
50
+ removePidFile(config);
51
+ return { outcome: 'stopped', port: record.port };
52
+ }
53
+ await deps.sleep(pollMs);
54
+ }
55
+ return { outcome: 'still-stopping', pid: record.pid };
56
+ }
@@ -0,0 +1,20 @@
1
+ import type { Liquid } from 'liquidjs';
2
+ import type { SiteConfig } from './config.ts';
3
+ import type { ThemeTemplates } from './renderer/theme-templates.ts';
4
+ import type { PageTemplate } from './services/theme-page-templates.ts';
5
+ import type { ThemeSchemas } from './services/validation.ts';
6
+ export interface LoadedTheme {
7
+ themeSchemas: ThemeSchemas;
8
+ themeTemplates: ThemeTemplates;
9
+ layouts: Record<string, string>;
10
+ pageTemplates: PageTemplate[];
11
+ engine: Liquid;
12
+ generation: number;
13
+ }
14
+ export declare function loadTheme(config: SiteConfig, generation?: number): LoadedTheme;
15
+ export declare class ThemeState {
16
+ current: LoadedTheme;
17
+ private readonly config;
18
+ constructor(config: SiteConfig, initial: LoadedTheme);
19
+ reload(): LoadedTheme;
20
+ }
@@ -0,0 +1,33 @@
1
+ import { createEngine } from "./renderer/engine.js";
2
+ import { loadLayouts, loadSnippets, loadThemeTemplates } from "./renderer/theme-templates.js";
3
+ import { loadPageTemplates } from "./services/theme-page-templates.js";
4
+ import { loadThemeSchemas } from "./services/theme-schemas.js";
5
+ export function loadTheme(config, generation = 0) {
6
+ const themeSchemas = loadThemeSchemas(config.themeRoot);
7
+ return {
8
+ themeSchemas,
9
+ themeTemplates: loadThemeTemplates(config.themeRoot),
10
+ layouts: loadLayouts(config.themeRoot),
11
+ pageTemplates: loadPageTemplates(config.templatesRoot, themeSchemas),
12
+ engine: createEngine(loadSnippets(config.themeRoot)),
13
+ generation,
14
+ };
15
+ }
16
+ // The theme a running server is using right now. Routes hold this one
17
+ // object and read `current` on every request, rather than a copy of the
18
+ // theme taken at start-up, so a theme pushed to a live site
19
+ // (POST /v1/theme/push) takes effect straight away, with no restart.
20
+ // Replaced whole, never mutated in place: a request already rendering
21
+ // keeps the theme it started with.
22
+ export class ThemeState {
23
+ current;
24
+ config;
25
+ constructor(config, initial) {
26
+ this.config = config;
27
+ this.current = initial;
28
+ }
29
+ reload() {
30
+ this.current = loadTheme(this.config, this.current.generation + 1);
31
+ return this.current;
32
+ }
33
+ }
@@ -0,0 +1,8 @@
1
+ export declare function parseVersion(version: string): [number, number, number] | null;
2
+ export declare function compareVersions(a: string, b: string): number;
3
+ export declare function mayBreak(from: string, to: string): boolean;
4
+ export interface ChangelogSection {
5
+ version: string;
6
+ text: string;
7
+ }
8
+ export declare function changesBetween(changelog: string, from: string, to: string): ChangelogSection[];
@@ -0,0 +1,46 @@
1
+ // Versions are plain x.y.z here: the package never publishes
2
+ // pre-releases, and anything else is treated as not comparable.
3
+ export function parseVersion(version) {
4
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
5
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
6
+ }
7
+ export function compareVersions(a, b) {
8
+ const x = parseVersion(a);
9
+ const y = parseVersion(b);
10
+ if (!x || !y) {
11
+ return a.localeCompare(b);
12
+ }
13
+ for (let index = 0; index < 3; index += 1) {
14
+ const difference = x[index] - y[index];
15
+ if (difference !== 0) {
16
+ return Math.sign(difference);
17
+ }
18
+ }
19
+ return 0;
20
+ }
21
+ // Below 1.0.0, a minor bump (0.5 -> 0.6) may break things, as a major
22
+ // bump would above it.
23
+ export function mayBreak(from, to) {
24
+ const x = parseVersion(from);
25
+ const y = parseVersion(to);
26
+ if (!x || !y) {
27
+ return true;
28
+ }
29
+ return x[0] !== y[0] || (x[0] === 0 && x[1] !== y[1]);
30
+ }
31
+ // The CHANGELOG.md sections after `from`, up to and including `to`,
32
+ // newest first - what an upgrade from one to the other brings.
33
+ export function changesBetween(changelog, from, to) {
34
+ const sections = [];
35
+ const headings = [...changelog.matchAll(/^## \[(\d+\.\d+\.\d+)\][^\n]*$/gm)];
36
+ headings.forEach((heading, index) => {
37
+ const version = heading[1];
38
+ if (compareVersions(version, from) <= 0 || compareVersions(version, to) > 0) {
39
+ return;
40
+ }
41
+ const start = heading.index + heading[0].length;
42
+ const end = index + 1 < headings.length ? headings[index + 1]?.index : changelog.length;
43
+ sections.push({ version, text: changelog.slice(start, end).trim() });
44
+ });
45
+ return sections;
46
+ }
@@ -0,0 +1,9 @@
1
+ import { type CheckResult } from '../site-check/run-check.ts';
2
+ import { type OwnedFileResult } from './owned-files.ts';
3
+ export interface FinishUpgradeResult {
4
+ files: OwnedFileResult[];
5
+ migrated: string[];
6
+ migrationProblem: string | null;
7
+ check: CheckResult;
8
+ }
9
+ export declare function finishUpgrade(siteRoot: string): Promise<FinishUpgradeResult>;
@@ -0,0 +1,30 @@
1
+ import { loadSiteConfig } from "../config.js";
2
+ import { SCAFFOLD_SCRIPTS, SERVER_JS, TEMPLATE_ROOT } from "../create-site/generate-site.js";
3
+ import { CURRENT_SCHEMA_VERSION, migrations } from "../migrations/index.js";
4
+ import { readAgentVersion } from "../routes/capabilities.js";
5
+ import { CHECKPOINT_AUTHOR } from "../services/checkpoint.js";
6
+ import { MigrationError, runMigrations } from "../services/migration-runner.js";
7
+ import { loadThemeSchemas } from "../services/theme-schemas.js";
8
+ import { runSiteCheck } from "../site-check/run-check.js";
9
+ import { refreshOwnedFiles } from "./owned-files.js";
10
+ // The second half of npm run upgrade, always run by the version just
11
+ // installed - so each version brings its own files and content changes,
12
+ // which the version upgraded from couldn't have known about. Nothing is
13
+ // committed: the developer reviews and commits, as after a pull.
14
+ export async function finishUpgrade(siteRoot) {
15
+ const files = refreshOwnedFiles(siteRoot, { templateRoot: TEMPLATE_ROOT, serverJs: SERVER_JS, scripts: SCAFFOLD_SCRIPTS, version: readAgentVersion() });
16
+ const config = loadSiteConfig(siteRoot);
17
+ let migrated = [];
18
+ let migrationProblem = null;
19
+ try {
20
+ migrated = await runMigrations(config, loadThemeSchemas(config.themeRoot), migrations, CURRENT_SCHEMA_VERSION, CHECKPOINT_AUTHOR, { commit: false });
21
+ }
22
+ catch (error) {
23
+ if (!(error instanceof MigrationError)) {
24
+ throw error;
25
+ }
26
+ migrationProblem = error.message;
27
+ }
28
+ const check = await runSiteCheck(siteRoot);
29
+ return { files, migrated, migrationProblem, check };
30
+ }
@@ -0,0 +1,11 @@
1
+ export interface OwnedFileResult {
2
+ path: string;
3
+ status: 'updated' | 'created' | 'unchanged';
4
+ }
5
+ export interface OwnedFilesSource {
6
+ templateRoot: string;
7
+ serverJs: string;
8
+ scripts: Record<string, string>;
9
+ version: string;
10
+ }
11
+ export declare function refreshOwnedFiles(siteRoot: string, source: OwnedFilesSource): OwnedFileResult[];
@@ -0,0 +1,42 @@
1
+ import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { upgradedDependencies } from "./package-names.js";
4
+ function place(siteRoot, path, contents, mode) {
5
+ const full = join(siteRoot, path);
6
+ const before = existsSync(full) ? readFileSync(full, 'utf-8') : null;
7
+ if (before === contents) {
8
+ return { path, status: 'unchanged' };
9
+ }
10
+ writeFileSync(full, contents);
11
+ if (mode !== undefined) {
12
+ chmodSync(full, mode);
13
+ }
14
+ return { path, status: before === null ? 'created' : 'updated' };
15
+ }
16
+ // The files in a site that belong to the CMS rather than to the site,
17
+ // put back exactly as the installed version ships them - so an upgrade
18
+ // brings its own Dockerfile, start-up script and scripts along, instead
19
+ // of every release needing them copied in by hand. vhost/package.json is
20
+ // merged, not replaced: the CMS's own scripts are set, the dependency is
21
+ // the installed version under the CMS's current name (never the name
22
+ // before, @o-a/cms-agent), and the developer's own scripts and
23
+ // dependencies are kept.
24
+ // Nothing outside this list is ever touched: not the theme, content,
25
+ // site.config.json (tokens), or AGENTS.md.
26
+ export function refreshOwnedFiles(siteRoot, source) {
27
+ const read = (path) => readFileSync(join(source.templateRoot, path), 'utf-8');
28
+ const results = [
29
+ place(siteRoot, 'vhost/Dockerfile', read('vhost/Dockerfile')),
30
+ place(siteRoot, 'vhost/docker-entrypoint.sh', read('vhost/docker-entrypoint.sh'), 0o755),
31
+ place(siteRoot, 'vhost/server.js', source.serverJs),
32
+ place(siteRoot, '.dockerignore', read('dockerignore')),
33
+ ];
34
+ const packagePath = join(siteRoot, 'vhost', 'package.json');
35
+ const raw = readFileSync(packagePath, 'utf-8');
36
+ const pkg = JSON.parse(raw);
37
+ const theirs = Object.fromEntries(Object.entries(pkg.scripts ?? {}).filter(([name]) => !(name in source.scripts)));
38
+ pkg.scripts = { ...source.scripts, ...theirs };
39
+ pkg.dependencies = upgradedDependencies(pkg.dependencies, source.version);
40
+ results.push(place(siteRoot, 'vhost/package.json', `${JSON.stringify(pkg, null, 2)}\n`));
41
+ return results;
42
+ }
@@ -0,0 +1,4 @@
1
+ export declare const PACKAGE_NAME = "@minamocms/framework";
2
+ export declare const FORMER_PACKAGE_NAMES: string[];
3
+ export declare function upgradedDependencies(dependencies: Record<string, string> | undefined, version: string): Record<string, string>;
4
+ export declare function requestedVersion(dependencies: Record<string, unknown> | undefined): unknown;
@@ -0,0 +1,26 @@
1
+ // The CMS is published as @minamocms/framework from 0.10.1, and was
2
+ // @o-a/cms-agent up to 0.9.x (0.10.0 was never published: npm refused
3
+ // the unscoped name "minamo"). From 0.9.3, @o-a/cms-agent's npm run
4
+ // upgrade moves a site over; what's here makes sure the old name
5
+ // doesn't linger in vhost/package.json once it has.
6
+ export const PACKAGE_NAME = '@minamocms/framework';
7
+ export const FORMER_PACKAGE_NAMES = ['@o-a/cms-agent'];
8
+ // vhost/package.json's dependencies with the CMS at the given version,
9
+ // under its current name only. The site's own dependencies are kept.
10
+ export function upgradedDependencies(dependencies, version) {
11
+ const next = { ...dependencies };
12
+ for (const name of FORMER_PACKAGE_NAMES) {
13
+ delete next[name];
14
+ }
15
+ next[PACKAGE_NAME] = version;
16
+ return next;
17
+ }
18
+ // The exact CMS version vhost/package.json asks for, under either name.
19
+ export function requestedVersion(dependencies) {
20
+ for (const name of [PACKAGE_NAME, ...FORMER_PACKAGE_NAMES]) {
21
+ if (dependencies?.[name] !== undefined) {
22
+ return dependencies[name];
23
+ }
24
+ }
25
+ return undefined;
26
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync, spawnSync } from 'node:child_process';
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join, resolve } from 'node:path';
6
+ import { ask, parseSyncArgs } from "../site-sync/cli-helpers.js";
7
+ import { changesBetween, compareVersions, mayBreak } from "./changelog.js";
8
+ import { finishUpgrade } from "./finish.js";
9
+ import { PACKAGE_NAME, upgradedDependencies } from "./package-names.js";
10
+ // npm run upgrade [-- <version>] [--yes] [--force]
11
+ //
12
+ // In two halves. This version (the one installed now) picks the new
13
+ // version, shows what's new, asks, and installs it. Then it hands over
14
+ // to the NEW version's own copy of this command (--finish), which puts
15
+ // back the CMS's own files, updates content to the new format, and
16
+ // checks the site: only the new version knows what it changed.
17
+ // Nothing is committed, and the live site is not touched - npm run
18
+ // push offers the upgrade once it has been tested here.
19
+ const args = parseSyncArgs(process.argv.slice(2));
20
+ const vhostDir = process.cwd();
21
+ const siteRoot = resolve(vhostDir, '..');
22
+ function fail(message) {
23
+ console.error(message);
24
+ process.exit(1);
25
+ }
26
+ function installedVersion() {
27
+ try {
28
+ const pkg = JSON.parse(readFileSync(join(vhostDir, 'node_modules', ...PACKAGE_NAME.split('/'), 'package.json'), 'utf-8'));
29
+ return pkg.version;
30
+ }
31
+ catch {
32
+ return fail('The CMS isn\'t installed here. Run npm run upgrade from your site\'s vhost/ folder, after npm install.');
33
+ }
34
+ }
35
+ function npm(npmArgs, cwd = vhostDir) {
36
+ return execFileSync('npm', npmArgs, { cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString('utf-8');
37
+ }
38
+ // The CHANGELOG.md inside the version about to be installed, fetched
39
+ // the same way npm install will fetch it - so it's the real thing, from
40
+ // the same registry.
41
+ function changelogOf(name, version) {
42
+ const scratch = mkdtempSync(join(tmpdir(), 'cms-upgrade-'));
43
+ try {
44
+ const packed = JSON.parse(npm(['pack', `${name}@${version}`, '--pack-destination', scratch, '--json'], scratch));
45
+ const tarball = join(scratch, packed[0]?.filename);
46
+ execFileSync('tar', ['-xzf', tarball, '-C', scratch, 'package/CHANGELOG.md'], { stdio: 'ignore' });
47
+ return readFileSync(join(scratch, 'package', 'CHANGELOG.md'), 'utf-8');
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ finally {
53
+ rmSync(scratch, { recursive: true, force: true });
54
+ }
55
+ }
56
+ function hasUncommittedChanges() {
57
+ try {
58
+ const status = execFileSync('git', ['status', '--porcelain', '--untracked-files=no', '--', 'vhost', 'content', 'theme'], {
59
+ cwd: siteRoot,
60
+ });
61
+ return status.toString('utf-8').trim() !== '';
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ async function start() {
68
+ const from = installedVersion();
69
+ let latest;
70
+ let versions;
71
+ try {
72
+ const info = JSON.parse(npm(['view', PACKAGE_NAME, 'versions', 'dist-tags', '--json']));
73
+ versions = Array.isArray(info.versions) ? info.versions : [info.versions];
74
+ latest = info['dist-tags'].latest;
75
+ }
76
+ catch {
77
+ return fail(`Couldn't ask npm which versions of ${PACKAGE_NAME} exist. Check your internet connection and try again.`);
78
+ }
79
+ const to = args.positional[0] ?? latest;
80
+ if (!versions.includes(to)) {
81
+ fail(`${PACKAGE_NAME} has no version ${to}. The latest is ${latest}.`);
82
+ }
83
+ if (compareVersions(to, from) === 0) {
84
+ console.log(`Already on ${from}${to === latest ? ', the latest' : ''}. Nothing to upgrade.`);
85
+ return;
86
+ }
87
+ if (compareVersions(to, from) < 0) {
88
+ fail(`${to} is older than the ${from} installed here. npm run upgrade only goes forward.`);
89
+ }
90
+ if (!args.flags.has('--force') && hasUncommittedChanges()) {
91
+ fail('vhost/, content/ or theme/ has uncommitted changes. Commit them first, so the upgrade\'s own changes are easy to review (or pass --force).');
92
+ }
93
+ console.log(`Upgrading the CMS from ${from} to ${to}.\n`);
94
+ const changelog = changelogOf(PACKAGE_NAME, to);
95
+ if (changelog) {
96
+ for (const section of changesBetween(changelog, from, to)) {
97
+ console.log(`--- ${section.version} ---\n${section.text}\n`);
98
+ }
99
+ }
100
+ else {
101
+ console.log(`(Couldn't read the changelog for ${to}.)\n`);
102
+ }
103
+ if (mayBreak(from, to)) {
104
+ console.log(`Note: ${from} to ${to} is a bigger step, which can include breaking changes. Read the notes above before going ahead.\n`);
105
+ }
106
+ if (!args.flags.has('--yes')) {
107
+ const answer = (await ask(`Upgrade from ${from} to ${to}? (y/N) `)).toLowerCase();
108
+ if (answer !== 'y' && answer !== 'yes') {
109
+ console.log('Cancelled. Nothing was changed.');
110
+ return;
111
+ }
112
+ }
113
+ const packagePath = join(vhostDir, 'package.json');
114
+ const lockPath = join(vhostDir, 'package-lock.json');
115
+ const packageBefore = readFileSync(packagePath, 'utf-8');
116
+ const lockBefore = existsSync(lockPath) ? readFileSync(lockPath, 'utf-8') : null;
117
+ const pkg = JSON.parse(packageBefore);
118
+ pkg.dependencies = upgradedDependencies(pkg.dependencies, to);
119
+ writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
120
+ console.log(`Installing ${PACKAGE_NAME}@${to}...`);
121
+ try {
122
+ execFileSync('npm', ['install', '--no-audit', '--no-fund'], { cwd: vhostDir, stdio: 'inherit' });
123
+ }
124
+ catch {
125
+ writeFileSync(packagePath, packageBefore);
126
+ if (lockBefore !== null) {
127
+ writeFileSync(lockPath, lockBefore);
128
+ }
129
+ fail(`Installing ${to} failed; package.json and package-lock.json are back as they were. Run npm install to restore ${from}.`);
130
+ }
131
+ // Hand over to the version just installed.
132
+ const next = join(vhostDir, 'node_modules', ...PACKAGE_NAME.split('/'), 'dist', 'upgrade', 'upgrade-cli.js');
133
+ if (!existsSync(next)) {
134
+ fail(`${to} is installed, but has no upgrade step of its own to finish with.`);
135
+ }
136
+ const finished = spawnSync(process.execPath, [next, '--finish'], { cwd: vhostDir, stdio: 'inherit' });
137
+ process.exit(finished.status ?? 1);
138
+ }
139
+ async function finish() {
140
+ const version = installedVersion();
141
+ console.log(`\nFinishing the upgrade to ${version}...`);
142
+ const result = await finishUpgrade(siteRoot);
143
+ const changed = result.files.filter((file) => file.status !== 'unchanged');
144
+ if (changed.length > 0) {
145
+ console.log('The CMS\'s own files:');
146
+ for (const file of changed) {
147
+ console.log(` ${file.status.padEnd(8)} ${file.path}`);
148
+ }
149
+ }
150
+ if (result.migrated.length > 0) {
151
+ console.log(`Updated ${result.migrated.length} content file${result.migrated.length === 1 ? '' : 's'} to the new content format.`);
152
+ }
153
+ if (result.migrationProblem) {
154
+ console.log(`Content couldn't be updated to the new format, so none of it was changed:\n ${result.migrationProblem}\nFix that file, then run npm run upgrade -- --finish again.`);
155
+ }
156
+ if (result.check.ok) {
157
+ console.log('Site check: no problems found.');
158
+ }
159
+ else {
160
+ // Reported, but not a failed upgrade: the check covers the whole
161
+ // site, so these can be problems it already had.
162
+ const count = result.check.findings.length;
163
+ console.log(`Site check: ${count} problem${count === 1 ? '' : 's'} (some may be from before the upgrade). Run npm run check to see them.`);
164
+ }
165
+ console.log(`\nThe CMS here is now ${version}. Nothing was committed, and the live site hasn't changed.\nTest it with npm run dev, review with git diff, then put it live with npm run push.`);
166
+ if (result.migrationProblem) {
167
+ process.exit(1);
168
+ }
169
+ }
170
+ await (args.flags.has('--finish') ? finish() : start());