@michaelthielemann/kestrel 4.1.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +274 -0
  4. package/dist/cast.d.ts +3 -0
  5. package/dist/cast.js +6 -0
  6. package/dist/catalogue.d.ts +11 -0
  7. package/dist/catalogue.js +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +55 -0
  10. package/dist/context.d.ts +62 -0
  11. package/dist/context.js +61 -0
  12. package/dist/dataflow.d.ts +3 -0
  13. package/dist/dataflow.js +65 -0
  14. package/dist/defineConfig.d.ts +157 -0
  15. package/dist/defineConfig.js +41 -0
  16. package/dist/defineContract.d.ts +12 -0
  17. package/dist/defineContract.js +16 -0
  18. package/dist/defineModule.d.ts +75 -0
  19. package/dist/defineModule.js +9 -0
  20. package/dist/definePipeline.d.ts +7 -0
  21. package/dist/definePipeline.js +11 -0
  22. package/dist/errors.d.ts +26 -0
  23. package/dist/errors.js +48 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +14 -0
  26. package/dist/load.d.ts +7 -0
  27. package/dist/load.js +79 -0
  28. package/dist/logger.d.ts +18 -0
  29. package/dist/logger.js +36 -0
  30. package/dist/registry.d.ts +14 -0
  31. package/dist/registry.js +87 -0
  32. package/dist/result.d.ts +19 -0
  33. package/dist/result.js +19 -0
  34. package/dist/runner.d.ts +29 -0
  35. package/dist/runner.js +133 -0
  36. package/dist/schema.d.ts +7 -0
  37. package/dist/schema.js +142 -0
  38. package/dist/sort.d.ts +2 -0
  39. package/dist/sort.js +50 -0
  40. package/dist/testing/runPipeline.d.ts +9 -0
  41. package/dist/testing/runPipeline.js +22 -0
  42. package/dist/triggers/cron.d.ts +10 -0
  43. package/dist/triggers/cron.js +44 -0
  44. package/dist/triggers/cronMatch.d.ts +10 -0
  45. package/dist/triggers/cronMatch.js +68 -0
  46. package/dist/triggers/http.d.ts +58 -0
  47. package/dist/triggers/http.js +292 -0
  48. package/package.json +25 -151
  49. package/NOTICE +0 -12
  50. package/kestrel.config.ts +0 -28
  51. package/layers/access/nuxt.config.ts +0 -5
  52. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  53. package/layers/access/server/middleware/access-guard.ts +0 -32
  54. package/layers/access/server/types.d.ts +0 -7
  55. package/layers/admin/app/components/AdminAccount.vue +0 -212
  56. package/layers/admin/app/components/AdminNav.vue +0 -44
  57. package/layers/admin/app/components/BlockFields.vue +0 -67
  58. package/layers/admin/app/components/BlockPreview.vue +0 -413
  59. package/layers/admin/app/components/BlockTree.vue +0 -433
  60. package/layers/admin/app/components/BlocksBody.vue +0 -163
  61. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  62. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  63. package/layers/admin/app/components/CollectionList.vue +0 -429
  64. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  65. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  66. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  67. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  68. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  69. package/layers/admin/app/components/EditorStatus.vue +0 -211
  70. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  71. package/layers/admin/app/components/FieldsBody.vue +0 -23
  72. package/layers/admin/app/components/LocaleBar.vue +0 -107
  73. package/layers/admin/app/components/PageFields.vue +0 -129
  74. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  75. package/layers/admin/app/components/SeoFields.vue +0 -210
  76. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  77. package/layers/admin/app/composables/useAuth.ts +0 -68
  78. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  79. package/layers/admin/app/composables/useBlocks.ts +0 -7
  80. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  81. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  82. package/layers/admin/app/composables/useCollections.ts +0 -6
  83. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  84. package/layers/admin/app/composables/useEditForm.ts +0 -402
  85. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  86. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  87. package/layers/admin/app/composables/useListColumns.ts +0 -53
  88. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  89. package/layers/admin/app/composables/useListRows.ts +0 -75
  90. package/layers/admin/app/composables/useListSelection.ts +0 -28
  91. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  92. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  93. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  94. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  95. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  96. package/layers/admin/app/composables/useTheme.ts +0 -36
  97. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  98. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  99. package/layers/admin/app/layouts/admin.vue +0 -266
  100. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  101. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  102. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  103. package/layers/admin/app/pages/admin/index.vue +0 -118
  104. package/layers/admin/app/pages/admin/login.vue +0 -68
  105. package/layers/admin/app/pages/admin/media.vue +0 -28
  106. package/layers/admin/app/pages/admin/references.vue +0 -146
  107. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  108. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  109. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  110. package/layers/admin/app/utils/block-tree.ts +0 -214
  111. package/layers/admin/app/utils/collection-ops.ts +0 -69
  112. package/layers/admin/app/utils/dead-refs.ts +0 -30
  113. package/layers/admin/app/utils/edit-form.ts +0 -267
  114. package/layers/admin/app/utils/editor-expose.ts +0 -36
  115. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  116. package/layers/admin/app/utils/editor-registry.ts +0 -20
  117. package/layers/admin/app/utils/field-keys.ts +0 -11
  118. package/layers/admin/app/utils/list-cell.ts +0 -46
  119. package/layers/admin/app/utils/list-columns.ts +0 -142
  120. package/layers/admin/app/utils/list-query.ts +0 -96
  121. package/layers/admin/app/utils/nav-active.ts +0 -9
  122. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  123. package/layers/admin/app/utils/preview-channel.ts +0 -62
  124. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  125. package/layers/admin/app/utils/reauth.ts +0 -67
  126. package/layers/admin/app/utils/record-title.ts +0 -15
  127. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  128. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  129. package/layers/admin/nuxt.config.ts +0 -20
  130. package/layers/auth/nuxt.config.ts +0 -1
  131. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  132. package/layers/collections/nuxt.config.ts +0 -1
  133. package/layers/collections/server/plugins/01.register.ts +0 -29
  134. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  135. package/layers/core/app/composables/layouts.ts +0 -5
  136. package/layers/core/app/utils/field-factories.ts +0 -129
  137. package/layers/core/app/utils/layouts.ts +0 -38
  138. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  139. package/layers/core/modules/auto-discovery/index.ts +0 -112
  140. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  141. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  142. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  143. package/layers/core/modules/component-namespace/index.ts +0 -44
  144. package/layers/core/modules/component-namespace/shared.ts +0 -24
  145. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  146. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  147. package/layers/core/modules/kestrel/index.ts +0 -144
  148. package/layers/core/modules/plugin-order/index.ts +0 -69
  149. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  150. package/layers/core/nuxt.config.ts +0 -10
  151. package/layers/core/server/api/[...path].ts +0 -98
  152. package/layers/core/server/plugins/00.config.ts +0 -12
  153. package/layers/core/server/plugins/00.migrate.ts +0 -12
  154. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  155. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  156. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  157. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  158. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  159. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  160. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  161. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  162. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  163. package/layers/core/server/tasks/db/migrate.ts +0 -50
  164. package/layers/core/server/utils/server-config.ts +0 -81
  165. package/layers/fields/nuxt.config.ts +0 -1
  166. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  167. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  168. package/layers/media/app/components/KestrelImg.vue +0 -94
  169. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  170. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  171. package/layers/media/app/components/MediaGrid.vue +0 -113
  172. package/layers/media/app/components/MediaImage.vue +0 -70
  173. package/layers/media/app/components/MediaLibrary.vue +0 -456
  174. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  175. package/layers/media/app/components/MediaPathBar.vue +0 -163
  176. package/layers/media/app/components/MediaPicker.vue +0 -19
  177. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  178. package/layers/media/app/components/MediaTable.vue +0 -75
  179. package/layers/media/app/components/MediaThumb.vue +0 -25
  180. package/layers/media/app/components/MediaToolbar.vue +0 -90
  181. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  182. package/layers/media/app/components/MediaViewer.vue +0 -137
  183. package/layers/media/app/components/field/Media.vue +0 -263
  184. package/layers/media/app/composables/useBlurUp.ts +0 -55
  185. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  186. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  187. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  188. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  189. package/layers/media/app/composables/useMediaOps.ts +0 -50
  190. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  191. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  192. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  193. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  194. package/layers/media/app/plugins/field-media.client.ts +0 -6
  195. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  196. package/layers/media/app/utils/dnd.ts +0 -69
  197. package/layers/media/app/utils/library.ts +0 -81
  198. package/layers/media/app/utils/ops.ts +0 -60
  199. package/layers/media/app/utils/picture-model.ts +0 -125
  200. package/layers/media/app/utils/thumbhash.ts +0 -19
  201. package/layers/media/app/utils/variant-name.ts +0 -20
  202. package/layers/media/nuxt.config.ts +0 -7
  203. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  204. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  205. package/layers/media/server/plugins/02.register-media.ts +0 -22
  206. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  207. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  208. package/layers/media/server/tasks/media/backfill.ts +0 -17
  209. package/layers/public/app/app.vue +0 -5
  210. package/layers/public/app/components/BlockRenderer.vue +0 -56
  211. package/layers/public/app/components/KestrelLink.vue +0 -26
  212. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  213. package/layers/public/app/composables/public-page.ts +0 -24
  214. package/layers/public/app/error.vue +0 -12
  215. package/layers/public/app/layouts/default.vue +0 -5
  216. package/layers/public/app/pages/[...slug].vue +0 -239
  217. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  218. package/layers/public/app/utils/block-edit-context.ts +0 -16
  219. package/layers/public/app/utils/json-ld.ts +0 -139
  220. package/layers/public/app/utils/link-href.ts +0 -46
  221. package/layers/public/app/utils/page-head.ts +0 -84
  222. package/layers/public/app/utils/page-layout.ts +0 -18
  223. package/layers/public/app/utils/preview-protocol.ts +0 -117
  224. package/layers/public/app/utils/public-route.ts +0 -46
  225. package/layers/public/app/utils/site-head.ts +0 -43
  226. package/layers/public/modules/deploy-output/index.ts +0 -80
  227. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  228. package/layers/public/modules/prerender-routes/index.ts +0 -88
  229. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  230. package/layers/public/modules/prune-media/index.ts +0 -48
  231. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  232. package/layers/public/nuxt.config.ts +0 -14
  233. package/layers/public/server/delivery-live/serve.ts +0 -111
  234. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  235. package/layers/public/server/pipelines/index.ts +0 -23
  236. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  237. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  238. package/layers/public/server/plugins/02.register-links.ts +0 -16
  239. package/layers/public/server/plugins/03.redirects.ts +0 -55
  240. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  241. package/layers/public/server/plugins/zz.publish.ts +0 -127
  242. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  243. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  244. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  245. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  246. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  247. package/layers/public/server/tasks/publish/run.ts +0 -32
  248. package/layers/public/server/utils/publish/render-live.ts +0 -26
  249. package/layers/ui/app/assets/scss/_base.scss +0 -16
  250. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  251. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  252. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  253. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  254. package/layers/ui/app/assets/scss/main.scss +0 -4
  255. package/layers/ui/app/components/field/Boolean.vue +0 -30
  256. package/layers/ui/app/components/field/Choice.vue +0 -73
  257. package/layers/ui/app/components/field/Datetime.vue +0 -98
  258. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  259. package/layers/ui/app/components/field/Json.vue +0 -62
  260. package/layers/ui/app/components/field/Layout.vue +0 -138
  261. package/layers/ui/app/components/field/Link.vue +0 -112
  262. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  263. package/layers/ui/app/components/field/Number.vue +0 -30
  264. package/layers/ui/app/components/field/Relation.vue +0 -49
  265. package/layers/ui/app/components/field/Renderer.vue +0 -30
  266. package/layers/ui/app/components/field/Repeater.vue +0 -460
  267. package/layers/ui/app/components/field/Richtext.vue +0 -26
  268. package/layers/ui/app/components/field/Slug.vue +0 -65
  269. package/layers/ui/app/components/field/Text.vue +0 -35
  270. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  271. package/layers/ui/app/components/ui/Alert.vue +0 -50
  272. package/layers/ui/app/components/ui/Brand.vue +0 -22
  273. package/layers/ui/app/components/ui/Button.vue +0 -116
  274. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  275. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  276. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  277. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  278. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  279. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  280. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  281. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  282. package/layers/ui/app/components/ui/Field.vue +0 -58
  283. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  284. package/layers/ui/app/components/ui/Icon.vue +0 -44
  285. package/layers/ui/app/components/ui/Menu.vue +0 -54
  286. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  287. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  288. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  289. package/layers/ui/app/components/ui/Select.vue +0 -52
  290. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  291. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  292. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  293. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  294. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  295. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  296. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  297. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  298. package/layers/ui/app/composables/useLinkField.ts +0 -70
  299. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  300. package/layers/ui/app/composables/useRepeater.ts +0 -99
  301. package/layers/ui/app/composables/useT.ts +0 -32
  302. package/layers/ui/app/composables/useToast.ts +0 -46
  303. package/layers/ui/app/i18n/de.ts +0 -432
  304. package/layers/ui/app/i18n/en.ts +0 -434
  305. package/layers/ui/app/utils/boolean-field.ts +0 -10
  306. package/layers/ui/app/utils/field-component.ts +0 -22
  307. package/layers/ui/app/utils/field-empty.ts +0 -21
  308. package/layers/ui/app/utils/field-registry.ts +0 -36
  309. package/layers/ui/app/utils/humanize.ts +0 -14
  310. package/layers/ui/app/utils/icons.ts +0 -143
  311. package/layers/ui/app/utils/iso-date.ts +0 -18
  312. package/layers/ui/app/utils/localized.ts +0 -13
  313. package/layers/ui/app/utils/record-context.ts +0 -15
  314. package/layers/ui/app/utils/reorder.ts +0 -8
  315. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  316. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  317. package/layers/ui/nuxt.config.ts +0 -9
  318. package/nuxt.config.ts +0 -108
  319. package/scripts/consumer-template-ci.mjs +0 -160
  320. package/scripts/copy-create-payload.mjs +0 -49
  321. package/scripts/coverage-all.mjs +0 -163
  322. package/scripts/dashboard.mjs +0 -89
  323. package/scripts/hash-password.mjs +0 -29
  324. package/scripts/kestrel.mjs +0 -240
  325. package/scripts/lib/cli.mjs +0 -129
  326. package/scripts/lib/coverage-model.mjs +0 -51
  327. package/scripts/lib/password.mjs +0 -19
  328. package/scripts/lib/scaffold.mjs +0 -205
  329. package/scripts/port-graph.mjs +0 -350
  330. package/scripts/typecheck.mjs +0 -62
  331. package/templates/starter/README.md +0 -65
  332. package/templates/starter/_env.example +0 -17
  333. package/templates/starter/_gitignore +0 -26
  334. package/templates/starter/_package.json +0 -22
  335. package/templates/starter/app/app.vue +0 -7
  336. package/templates/starter/app/blocks/Prose.vue +0 -13
  337. package/templates/starter/app/layouts/default.vue +0 -6
  338. package/templates/starter/nuxt.config.ts +0 -23
  339. package/templates/starter/pnpm-workspace.yaml +0 -8
  340. package/templates/starter/tsconfig.json +0 -3
@@ -1,163 +0,0 @@
1
- import { spawnSync } from 'node:child_process'
2
- import fs from 'node:fs'
3
- import path from 'node:path'
4
- import { fileURLToPath } from 'node:url'
5
-
6
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
7
- const VITEST_BIN = path.join(ROOT, 'node_modules/.bin/vitest')
8
- const PARTS_DIR = path.join(ROOT, 'reports/coverage/.parts')
9
- const OUT_DIR = path.join(ROOT, 'reports/coverage')
10
-
11
- function sameKeys(a, b) {
12
- const ka = Object.keys(a)
13
- const kb = Object.keys(b)
14
- if (ka.length !== kb.length) return false
15
- const setB = new Set(kb)
16
- return ka.every((k) => setB.has(k))
17
- }
18
-
19
- function mapsAgree(a, b) {
20
- return sameKeys(a.statementMap, b.statementMap) && sameKeys(a.fnMap, b.fnMap) && sameKeys(a.branchMap, b.branchMap)
21
- }
22
-
23
- function countExecutedFunctions(entry) {
24
- return Object.values(entry.f).filter((n) => n > 0).length
25
- }
26
-
27
- function cloneEntry(entry) {
28
- return structuredClone(entry)
29
- }
30
-
31
- function sumCounters(a, b) {
32
- const out = {}
33
- for (const k of Object.keys(a)) out[k] = a[k] + (b[k] ?? 0)
34
- return out
35
- }
36
-
37
- function sumBranches(a, b) {
38
- const out = {}
39
- for (const k of Object.keys(a)) {
40
- const branchA = a[k]
41
- const branchB = b[k] ?? []
42
- out[k] = branchA.map((count, i) => count + (branchB[i] ?? 0))
43
- }
44
- return out
45
- }
46
-
47
- // Istanbul JSON is keyed by absolute file path; the same path can show up in several parts
48
- // (a package run and the root run both touching a shared file). When their instrumentation
49
- // maps agree, counters are additive. When they disagree, the maps describe different
50
- // transforms of the file — summing them would misattribute counts to the wrong statements/
51
- // functions, so the richer entry wins outright and the conflict is reported instead of hidden.
52
- export function mergeCoverage(parts, conflicts = []) {
53
- const merged = {}
54
- for (const part of parts) {
55
- for (const [filePath, entry] of Object.entries(part)) {
56
- const existing = merged[filePath]
57
- if (!existing) {
58
- merged[filePath] = cloneEntry(entry)
59
- continue
60
- }
61
- if (mapsAgree(existing, entry)) {
62
- merged[filePath] = {
63
- ...existing,
64
- s: sumCounters(existing.s, entry.s),
65
- f: sumCounters(existing.f, entry.f),
66
- b: sumBranches(existing.b, entry.b),
67
- }
68
- } else {
69
- if (countExecutedFunctions(entry) > countExecutedFunctions(existing)) {
70
- merged[filePath] = cloneEntry(entry)
71
- }
72
- conflicts.push(filePath)
73
- }
74
- }
75
- }
76
- return merged
77
- }
78
-
79
- function readCoverage(dir) {
80
- const file = path.join(dir, 'coverage-final.json')
81
- if (!fs.existsSync(file)) return null
82
- return JSON.parse(fs.readFileSync(file, 'utf8'))
83
- }
84
-
85
- function countFiles(coverage) {
86
- return coverage ? Object.keys(coverage).length : 0
87
- }
88
-
89
- function countFunctionsTotal(coverage) {
90
- if (!coverage) return 0
91
- let total = 0
92
- for (const entry of Object.values(coverage)) total += countExecutedFunctions(entry)
93
- return total
94
- }
95
-
96
- function discoverPackageSuites() {
97
- const packagesDir = path.join(ROOT, 'packages')
98
- if (!fs.existsSync(packagesDir)) return []
99
- return fs
100
- .readdirSync(packagesDir)
101
- .filter((name) => fs.existsSync(path.join(packagesDir, name, 'vitest.config.ts')))
102
- .sort()
103
- .map((name) => ({ name, cwd: path.join(packagesDir, name) }))
104
- }
105
-
106
- function runSuite(name, cwd) {
107
- const reportsDirectory = path.join(PARTS_DIR, name)
108
- fs.mkdirSync(reportsDirectory, { recursive: true })
109
- const args = [
110
- 'run',
111
- '--coverage.enabled',
112
- '--coverage.provider=v8',
113
- '--coverage.reporter=json',
114
- `--coverage.reportsDirectory=${reportsDirectory}`,
115
- '--coverage.reportOnFailure=true',
116
- ]
117
- console.log(`\n=== ${name} (${path.relative(ROOT, cwd) || '.'}) ===`)
118
- const result = spawnSync(VITEST_BIN, args, { cwd, stdio: 'inherit' })
119
- const passed = result.status === 0
120
- const coverage = readCoverage(reportsDirectory)
121
- return { name, passed, coverage }
122
- }
123
-
124
- export function main() {
125
- fs.rmSync(PARTS_DIR, { recursive: true, force: true })
126
- fs.mkdirSync(PARTS_DIR, { recursive: true })
127
-
128
- const suites = [{ name: 'root', cwd: ROOT }, ...discoverPackageSuites()]
129
- const results = suites.map(({ name, cwd }) => runSuite(name, cwd))
130
-
131
- const conflicts = []
132
- const merged = mergeCoverage(
133
- results.map((r) => r.coverage).filter(Boolean),
134
- conflicts,
135
- )
136
-
137
- fs.mkdirSync(OUT_DIR, { recursive: true })
138
- fs.writeFileSync(path.join(OUT_DIR, 'coverage-final.json'), JSON.stringify(merged))
139
-
140
- console.log('\n=== coverage-all summary ===')
141
- for (const r of results) {
142
- const status = r.passed ? 'PASS' : 'FAIL'
143
- console.log(
144
- `${r.name.padEnd(24)} ${status} files=${countFiles(r.coverage)} functionsExecuted=${countFunctionsTotal(r.coverage)}`,
145
- )
146
- }
147
-
148
- if (conflicts.length > 0) {
149
- console.log(`\n${conflicts.length} merge conflict(s) (kept the entry with more executed functions):`)
150
- for (const filePath of conflicts) console.log(` ${filePath}`)
151
- } else {
152
- console.log('\nno merge conflicts')
153
- }
154
-
155
- console.log(
156
- `\nmerged: files=${Object.keys(merged).length} functionsExecuted=${countFunctionsTotal(merged)} -> ${path.join(OUT_DIR, 'coverage-final.json')}`,
157
- )
158
-
159
- const anyFailed = results.some((r) => !r.passed)
160
- process.exitCode = anyFailed ? 1 : 0
161
- }
162
-
163
- if (import.meta.url === `file://${process.argv[1]}`) main()
@@ -1,89 +0,0 @@
1
- // Deterministic static dashboard generator — no network. Boots the pipeline/collection registries
2
- // the same way test/architecture/pipeline-invariants.test.ts does (no Nuxt boot needed for that), then
3
- // renders through the SAME renderDashboard() the dev route uses, so the two surfaces never drift.
4
- //
5
- // Run with: npx tsx scripts/dashboard.mjs (or `pnpm dashboard`)
6
-
7
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
8
- import { join, dirname } from 'node:path'
9
- import { fileURLToPath } from 'node:url'
10
- import Database from 'better-sqlite3'
11
- import { drizzle } from 'drizzle-orm/better-sqlite3'
12
- import { fieldTypes } from '@michaelthielemann/kestrel-fields'
13
- import {
14
- buildIntrospectionPipelines, buildPipelineIndex, clearPipelines, clearRegistry,
15
- registerCollection, registerPipeline, renderDashboard, setResolvedKestrelConfig, allCollections,
16
- } from '@michaelthielemann/kestrel-core'
17
- import { buildAuthPipelines } from '@michaelthielemann/kestrel-auth'
18
- import { pagesCollection } from '@michaelthielemann/kestrel-collections'
19
- import { mediaCollection, mediaSettingsCollection } from '@michaelthielemann/kestrel-media'
20
- import { redirectsCollection, siteCollection } from '@michaelthielemann/kestrel-publishing'
21
- import { resolveServerKestrelConfig } from '../layers/core/server/utils/server-config.ts'
22
- import { PLUGIN_ORDER } from '../layers/core/modules/plugin-order/plugin-order.ts'
23
- import posts from '../server/collections/posts.ts'
24
- import settings from '../server/collections/settings.ts'
25
-
26
- const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
27
-
28
- void fieldTypes
29
- setResolvedKestrelConfig(resolveServerKestrelConfig())
30
- Object.assign(globalThis, {
31
- useRuntimeConfig: () => ({
32
- kestrel: { output: { driver: 'local', dir: '', publicDir: '/kestrel-no-such-public-dir', auto: true, publishOnSave: false, reconcileMinutes: 0, verbose: false, s3: {} } },
33
- }),
34
- useDb: () => drizzle(new Database(':memory:')),
35
- primaryLocale: () => 'en',
36
- prefixPrimaryLocale: () => false,
37
- })
38
-
39
- clearRegistry()
40
- for (const c of [pagesCollection, posts, settings, mediaCollection, mediaSettingsCollection, redirectsCollection, siteCollection]) registerCollection(c)
41
-
42
- clearPipelines()
43
- for (const def of buildAuthPipelines()) registerPipeline(def)
44
- for (const def of buildIntrospectionPipelines()) registerPipeline(def)
45
-
46
- const collections = allCollections().map((c) => ({
47
- name: c.name,
48
- mode: c.def.mode,
49
- translatable: Boolean(c.def.translatable),
50
- fieldCount: Object.keys(c.def.fields).length,
51
- }))
52
-
53
- /** Reads a repo-only enrichment file, or returns undefined — a consumer project (or a fresh checkout
54
- * before `graphify update`/`pnpm api:check` have run) ships none of these, so every read is optional. */
55
- function readJsonIfExists(relPath) {
56
- const abs = join(repoRoot, relPath)
57
- if (!existsSync(abs)) return undefined
58
- return JSON.parse(readFileSync(abs, 'utf8'))
59
- }
60
-
61
- function apiSurfaceSection() {
62
- const ceilings = readJsonIfExists('test/architecture/public-api-ceilings.json')
63
- if (!ceilings) return undefined
64
- const out = []
65
- for (const [report, ceiling] of Object.entries(ceilings)) {
66
- const abs = join(repoRoot, report)
67
- if (!existsSync(abs)) continue
68
- const count = readFileSync(abs, 'utf8').split('\n').filter((l) => /^\/\/ @(public|alpha|beta|internal)\b/.test(l)).length
69
- out.push({ report, count, ceiling })
70
- }
71
- return out
72
- }
73
-
74
- const edgeAllowlist = readJsonIfExists('test/architecture/edge-allowlist.json')
75
- const apiSurface = apiSurfaceSection()
76
-
77
- const html = renderDashboard({
78
- pipelines: buildPipelineIndex(),
79
- pluginOrder: PLUGIN_ORDER.map((p) => ({ layer: p.layer, file: p.file, after: p.after })),
80
- collections,
81
- graph: (edgeAllowlist || apiSurface) ? { packageEdges: edgeAllowlist?.edges, apiSurface } : undefined,
82
- generatedAt: new Date().toISOString(),
83
- })
84
-
85
- const outDir = join(repoRoot, 'docs')
86
- mkdirSync(outDir, { recursive: true })
87
- const outFile = join(outDir, 'dashboard.html')
88
- writeFileSync(outFile, html)
89
- console.log(`[dashboard] wrote ${outFile} (${buildPipelineIndex().length} pipelines, ${collections.length} collections)`)
@@ -1,29 +0,0 @@
1
- import { createInterface } from 'node:readline'
2
- import { hashPassword } from './lib/password.mjs'
3
- import { Cancelled, promptPassword } from './lib/cli.mjs'
4
-
5
- // Not used at runtime: this prints a value an operator pastes into KESTREL_ADMIN_PASSWORD_HASH. Kept as a
6
- // standalone entry point because docs/guide/getting-started.md tells consumers to run it straight out of
7
- // node_modules, which must keep working whether or not the `kestrel` bin is on PATH.
8
- const arg = process.argv[2]
9
- if (arg) {
10
- process.stdout.write(hashPassword(arg) + '\n')
11
- } else if (process.stdin.isTTY) {
12
- // Only the hash may reach stdout — an operator pipes it straight into a secret store.
13
- try {
14
- process.stdout.write(hashPassword(await promptPassword({ output: process.stderr })) + '\n')
15
- } catch (err) {
16
- if (!(err instanceof Cancelled)) throw err
17
- process.stderr.write('cancelled\n')
18
- process.exitCode = 1
19
- }
20
- } else {
21
- // `terminal: false` keeps the piped form (`echo pw | node hash-password.mjs`) working; it is only safe
22
- // because stdin is not a tty here, so there is no line discipline echoing the password onto a screen.
23
- const rl = createInterface({ input: process.stdin, terminal: false })
24
- process.stderr.write('Enter password, then press Enter:\n')
25
- rl.on('line', (line) => {
26
- process.stdout.write(hashPassword(line) + '\n')
27
- rl.close()
28
- })
29
- }
@@ -1,240 +0,0 @@
1
- #!/usr/bin/env node
2
- import { existsSync, mkdirSync, readFileSync } from 'node:fs'
3
- import { join, resolve, basename, relative } from 'node:path'
4
- import { fileURLToPath } from 'node:url'
5
- import { hashPassword, sessionSecret } from './lib/password.mjs'
6
- import { PACKAGE_NAME, diagnoseProject, envValue, isPlainObject, mergeEnv, mergePackageJson, renderTemplate, targetName, toPackageName } from './lib/scaffold.mjs'
7
- import { Cancelled, MIN_PASSWORD_LENGTH, makePaint, out, parseArgs, promptPassword, readIf, readStdin, walk, write } from './lib/cli.mjs'
8
-
9
- // Node builtins only, no build step: runs the same from a checkout, from node_modules and via `pnpm dlx`.
10
- const PKG_ROOT = resolve(fileURLToPath(import.meta.url), '../..')
11
- const TEMPLATES = join(PKG_ROOT, 'templates')
12
- const pkg = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'))
13
-
14
- const { dim, bold, red, yellow, green } = makePaint()
15
- const fail = (s) => {
16
- process.stderr.write(`${red('error')} ${s}\n`)
17
- process.exit(1)
18
- }
19
-
20
- const templateVars = (name) => ({
21
- name,
22
- version: `^${pkg.version}`,
23
- nuxtVersion: pkg.dependencies?.nuxt ?? '^4.4.8',
24
- typescriptVersion: pkg.dependencies?.typescript ?? '^6.0.3',
25
- vueTscVersion: pkg.devDependencies?.['vue-tsc'] ?? '^3.3.7',
26
- })
27
-
28
- function inspect(target) {
29
- return diagnoseProject({
30
- packageJson: readIf(join(target, 'package.json')),
31
- nuxtConfig: readIf(join(target, 'nuxt.config.ts')) ?? readIf(join(target, 'nuxt.config.js')),
32
- appVue: readIf(join(target, 'app', 'app.vue')) ?? readIf(join(target, 'app.vue')),
33
- kestrelConfig: readIf(join(target, 'kestrel.config.ts')) ?? readIf(join(target, 'kestrel.config.js')),
34
- env: readIf(join(target, '.env')),
35
- })
36
- }
37
-
38
- const LEVEL_LABEL = { error: () => red('✖ error'), info: () => dim('· note '), warn: () => yellow('▲ warn ') }
39
-
40
- function printFindings(found) {
41
- for (const d of found) {
42
- out(`${(LEVEL_LABEL[d.level] ?? LEVEL_LABEL.warn)()} ${d.message}`)
43
- out()
44
- }
45
- }
46
-
47
- async function init(positional, flags) {
48
- const target = resolve(positional[0] ?? '.')
49
- const templateDir = join(TEMPLATES, 'starter')
50
- if (!existsSync(templateDir)) fail(`template payload missing at ${templateDir} — reinstall ${PACKAGE_NAME}.`)
51
-
52
- // Read the target before touching disk: a half-scaffolded project is worse than a refused one.
53
- const manifestPath = join(target, 'package.json')
54
- const existingManifest = readIf(manifestPath)
55
- if (existingManifest !== null) {
56
- let parsed
57
- try {
58
- parsed = JSON.parse(existingManifest)
59
- } catch {
60
- fail(`${manifestPath} is not valid JSON — fix it first; refusing to scaffold over a broken manifest.`)
61
- }
62
- if (!isPlainObject(parsed)) fail(`${manifestPath} is not a JSON object — fix it first; refusing to scaffold over a broken manifest.`)
63
- }
64
- const envPath = join(target, '.env')
65
- const existingEnv = readIf(envPath)
66
-
67
- // No flag swallows a `-`-prefixed token, so a dash-leading password reaches init as no value at all.
68
- if (flags.password === true) fail('--password needs a value — write --password=<pw> if it starts with a dash.')
69
- const supplied = typeof flags.password === 'string'
70
- let password = supplied ? flags.password : undefined
71
- if (password !== undefined && password.length < MIN_PASSWORD_LENGTH) {
72
- fail(`--password must be at least ${MIN_PASSWORD_LENGTH} characters (an empty one would leave /admin open).`)
73
- }
74
-
75
- mkdirSync(target, { recursive: true })
76
- const projectName = toPackageName(typeof flags.name === 'string' ? flags.name : basename(target))
77
-
78
- out()
79
- out(`${bold('Kestrel')} ${dim(`v${pkg.version}`)} — setting up ${bold(relative(process.cwd(), target) || '.')}`)
80
- out()
81
-
82
- if (password === undefined && !flags.yes && process.stdin.isTTY) {
83
- // The prompt loops until a valid password is typed twice, so it cannot be declined, and the hash is
84
- // folded into the session signing key — asking on a project that already has one would rotate the
85
- // password, and sign everyone out, without ever offering the operator a way to say no.
86
- if (envValue(existingEnv ?? '', 'KESTREL_ADMIN_PASSWORD_HASH')) {
87
- out(dim('KESTREL_ADMIN_PASSWORD_HASH is already set — pass --password to change it.'))
88
- out()
89
- } else {
90
- try {
91
- password = await promptPassword({ warn: (m) => out(yellow(m)), note: (m) => out(dim(m)) })
92
- } catch (err) {
93
- if (err instanceof Cancelled) fail('cancelled')
94
- throw err
95
- }
96
- out()
97
- }
98
- }
99
-
100
- const vars = templateVars(projectName)
101
- const created = []
102
- const kept = []
103
- const merged = []
104
-
105
- for (const rel of walk(templateDir).sort()) {
106
- const name = targetName(rel)
107
- const dest = join(target, name)
108
- const body = renderTemplate(readFileSync(join(templateDir, rel), 'utf8'), vars)
109
- const existing = readIf(dest)
110
-
111
- // A manifest is never overwritten, not even with --force: the caller most likely already ran
112
- // `pnpm add`, and replacing their dependencies and version is not a scaffold, it is data loss.
113
- if (name === 'package.json' && existing !== null) {
114
- const { merged: result, added } = mergePackageJson(JSON.parse(existing), JSON.parse(body))
115
- if (added.length) {
116
- write(dest, `${JSON.stringify(result, null, 2)}\n`)
117
- merged.push(`package.json ${dim(`(+ ${added.join(', ')})`)}`)
118
- } else kept.push('package.json')
119
- continue
120
- }
121
- if (existing !== null && !flags.force) {
122
- kept.push(name)
123
- continue
124
- }
125
- write(dest, body)
126
- created.push(name)
127
- }
128
-
129
- // Seed from `.env.example` so the generated file keeps its per-key comments.
130
- const hadEnv = existingEnv !== null
131
- const envEntries = { KESTREL_SESSION_SECRET: sessionSecret(), KESTREL_SECURE_COOKIES: 'false' }
132
- if (password !== undefined) envEntries.KESTREL_ADMIN_PASSWORD_HASH = hashPassword(password)
133
- const seed = existingEnv ?? readIf(join(target, '.env.example')) ?? ''
134
- // Only an explicitly supplied password replaces a hash that is already set. That is not session-safety:
135
- // the hash is folded into the cookie signing key, so any rotation signs every user out. What keeps a
136
- // plain re-run harmless is that it changes neither the hash nor the secret.
137
- const { text, written } = mergeEnv(seed, envEntries, supplied ? ['KESTREL_ADMIN_PASSWORD_HASH'] : [])
138
- if (written.length) {
139
- write(envPath, text, 0o600)
140
- ;(hadEnv ? merged : created).push(`.env ${dim(`(${written.join(', ')})`)}`)
141
- } else kept.push('.env')
142
-
143
- const report = (label, items, paint) => {
144
- for (const f of items) out(` ${paint(label)} ${f}`)
145
- }
146
- report('created', created, green)
147
- report('updated', merged, green)
148
- report('kept ', kept, dim)
149
-
150
- // Existing files are kept, so init must never report success over a project that still can't serve /admin.
151
- const remaining = inspect(target)
152
- if (remaining.length) {
153
- out()
154
- out(bold('Still to fix:'))
155
- out()
156
- printFindings(remaining)
157
- }
158
-
159
- out()
160
- out(bold('Next:'))
161
- const rel = relative(process.cwd(), target)
162
- if (rel) out(` cd ${rel}`)
163
- out(' pnpm install')
164
- out(' pnpm dev')
165
- out()
166
- out(` Admin: ${bold('http://localhost:3000/admin')}`)
167
- out()
168
- return remaining.some((d) => d.level === 'error') ? 1 : 0
169
- }
170
-
171
- function doctor(positional) {
172
- const target = resolve(positional[0] ?? '.')
173
- const found = inspect(target)
174
-
175
- out()
176
- // An `info` finding is a note about how this project is configured, not a problem with it — so it prints,
177
- // but it must not withhold the all-clear the way a warning does.
178
- if (!found.some((d) => d.level !== 'info')) {
179
- out(`${green('✔')} ${relative(process.cwd(), target) || '.'} looks like a working Kestrel project.`)
180
- out()
181
- }
182
- printFindings(found)
183
- return found.some((d) => d.level === 'error') ? 1 : 0
184
- }
185
-
186
- async function hashPasswordCommand(positional) {
187
- let password = positional[0]
188
- if (password === undefined) {
189
- if (process.stdin.isTTY) {
190
- // Only the hash may reach stdout — an operator redirects it straight into a secret store.
191
- const ask = (m) => process.stderr.write(`${m}\n`)
192
- try {
193
- password = await promptPassword({ output: process.stderr, warn: (m) => ask(yellow(m)), note: (m) => ask(dim(m)) })
194
- } catch (err) {
195
- if (err instanceof Cancelled) fail('cancelled')
196
- throw err
197
- }
198
- } else password = await readStdin()
199
- }
200
- if (!password) fail('no password given')
201
- out(hashPassword(password))
202
- }
203
-
204
- function help() {
205
- out(`
206
- ${bold('kestrel')} ${dim(`v${pkg.version}`)}
207
-
208
- ${bold('kestrel init')} [dir] scaffold a runnable Kestrel project (default: the current directory)
209
- ${bold('kestrel doctor')} [dir] check a project for the things that silently break /admin
210
- ${bold('kestrel hash-password')} [p] print a KESTREL_ADMIN_PASSWORD_HASH value
211
- ${bold('kestrel secret')} print a KESTREL_SESSION_SECRET value
212
-
213
- ${bold('init flags')}
214
- --name <name> package name (default: the directory name, slugified)
215
- --password <pw> set or change the admin password without prompting
216
- --yes never prompt; leaves KESTREL_ADMIN_PASSWORD_HASH for you to fill in
217
- --force overwrite existing files (package.json and .env are always merged, never replaced)
218
-
219
- ${dim('Existing files are kept and re-running init is safe. To create a NEW project: pnpm create kestrel')}
220
- `)
221
- }
222
-
223
- const argv = process.argv.slice(2)
224
- const { flags, positional } = parseArgs(argv, ['yes', 'force', 'help', 'version'])
225
- const command = positional.shift()
226
-
227
- // Only these two read a positional as a directory, and an option that took no value lands there: a
228
- // `--password -pw` would otherwise put the cleartext on disk as a directory name. `--` is how the caller
229
- // says a dash-leading positional is what they meant.
230
- if (!argv.includes('--') && (command === 'init' || command === 'doctor') && positional[0]?.startsWith('-')) {
231
- fail(`unknown option "${positional[0]}" — run \`kestrel help\`.`)
232
- }
233
-
234
- if (flags.version || command === 'version') out(pkg.version)
235
- else if (flags.help || !command || command === 'help') help()
236
- else if (command === 'init') process.exitCode = await init(positional, flags)
237
- else if (command === 'doctor') process.exitCode = doctor(positional)
238
- else if (command === 'hash-password') await hashPasswordCommand(positional)
239
- else if (command === 'secret') out(sessionSecret())
240
- else fail(`unknown command "${command}" — run \`kestrel help\`.`)
@@ -1,129 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, chmodSync } from 'node:fs'
2
- import { dirname, join, relative } from 'node:path'
3
- import { createInterface } from 'node:readline/promises'
4
-
5
- export const MIN_PASSWORD_LENGTH = 8
6
-
7
- /**
8
- * `booleans` never consume the following token, so `init --force my-site` keeps `my-site` as the target
9
- * rather than scaffolding over the current directory. `--` ends flag parsing.
10
- */
11
- export function parseArgs(argv, booleans = []) {
12
- const isBoolean = new Set(booleans)
13
- const flags = {}
14
- const positional = []
15
- for (let i = 0; i < argv.length; i++) {
16
- const a = argv[i]
17
- if (a === '--') {
18
- positional.push(...argv.slice(i + 1))
19
- break
20
- }
21
- if (a === '-h') {
22
- flags.help = true
23
- continue
24
- }
25
- if (a === '-v') {
26
- flags.version = true
27
- continue
28
- }
29
- if (!a.startsWith('--')) {
30
- positional.push(a)
31
- continue
32
- }
33
- const [key, inline] = a.slice(2).split(/=(.*)/s)
34
- if (inline !== undefined) flags[key] = inline
35
- else if (!isBoolean.has(key) && i + 1 < argv.length && !argv[i + 1].startsWith('-')) flags[key] = argv[++i]
36
- else flags[key] = true
37
- }
38
- return { flags, positional }
39
- }
40
-
41
- export function makePaint(stream = process.stdout) {
42
- const on = stream.isTTY && !process.env.NO_COLOR
43
- const wrap = (code) => (s) => (on ? `\u001b[${code}m${s}\u001b[0m` : s)
44
- return { dim: wrap(2), bold: wrap(1), red: wrap(31), yellow: wrap(33), green: wrap(32) }
45
- }
46
-
47
- export const out = (s = '') => process.stdout.write(`${s}\n`)
48
-
49
- export const readIf = (file) => (existsSync(file) ? readFileSync(file, 'utf8') : null)
50
-
51
- /** `mode` is applied even when the file already exists, which `writeFileSync` alone does not do. */
52
- export function write(file, content, mode) {
53
- mkdirSync(dirname(file), { recursive: true })
54
- writeFileSync(file, content, mode === undefined ? undefined : { mode })
55
- if (mode !== undefined) chmodSync(file, mode)
56
- }
57
-
58
- /** Every file under `dir`, as `/`-separated paths relative to it. */
59
- export function walk(dir, base = dir) {
60
- const found = []
61
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
62
- const full = join(dir, entry.name)
63
- if (entry.isDirectory()) found.push(...walk(full, base))
64
- else found.push(relative(base, full).split('\\').join('/'))
65
- }
66
- return found
67
- }
68
-
69
- /** Reads a line the interface is configured never to echo, so the prompt is ours to draw and to close. */
70
- async function askHidden(rl, question, write) {
71
- write(question)
72
- try {
73
- return (await rl.question('')).trim()
74
- } finally {
75
- write('\n')
76
- }
77
- }
78
-
79
- export class Cancelled extends Error {}
80
-
81
- /**
82
- * Owns its readline interface, because the two options that keep the secret off the screen can only be
83
- * set at construction: `output: null` makes readline write nothing at all, and `terminal: true` still
84
- * gives it the keypress handling (backspace, Ctrl-C, Ctrl-D) that would otherwise fall to the tty's own
85
- * echoing line discipline. Erasing readline's echo afterwards cannot match that — a pasted secret arrives
86
- * as one chunk that is echoed AND newlined in a single write, stranding the cleartext on a line no
87
- * repaint can still reach, and a redirected stdout leaves `terminal` false with nothing to suppress.
88
- *
89
- * Throws `Cancelled` on Ctrl-C / Ctrl-D so a caller can exit cleanly instead of on an AbortError stack.
90
- */
91
- export async function promptPassword({
92
- input = process.stdin,
93
- output = process.stdout,
94
- warn = (m) => output.write(`${m}\n`),
95
- note = (m) => output.write(`${m}\n`),
96
- } = {}) {
97
- const draw = (s) => output.write(s)
98
- const rl = createInterface({ input, output: null, terminal: true })
99
- note(`Choose a password for /admin — at least ${MIN_PASSWORD_LENGTH} characters, kept only as a scrypt hash.`)
100
- try {
101
- for (;;) {
102
- let first
103
- try {
104
- first = await askHidden(rl, 'New admin password: ', draw)
105
- if (first.length < MIN_PASSWORD_LENGTH) {
106
- warn(` too short — at least ${MIN_PASSWORD_LENGTH} characters`)
107
- continue
108
- }
109
- if (first !== (await askHidden(rl, 'Repeat password: ', draw))) {
110
- warn(' the two entries differ — try again')
111
- continue
112
- }
113
- } catch (err) {
114
- throw err?.code === 'ABORT_ERR' ? new Cancelled() : err
115
- }
116
- if (first === undefined) throw new Cancelled()
117
- return first
118
- }
119
- } finally {
120
- rl.close()
121
- }
122
- }
123
-
124
- /** All of stdin as a string; `''` at EOF, which the callers turn into a clean error. */
125
- export async function readStdin() {
126
- let text = ''
127
- for await (const chunk of process.stdin) text += chunk
128
- return text.split('\n')[0].trim()
129
- }