@michaelthielemann/kestrel 4.1.3 → 5.0.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 (342) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +284 -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 +162 -0
  15. package/dist/defineConfig.js +42 -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/allowlist.d.ts +9 -0
  43. package/dist/triggers/allowlist.js +49 -0
  44. package/dist/triggers/cron.d.ts +10 -0
  45. package/dist/triggers/cron.js +44 -0
  46. package/dist/triggers/cronMatch.d.ts +10 -0
  47. package/dist/triggers/cronMatch.js +68 -0
  48. package/dist/triggers/http.d.ts +59 -0
  49. package/dist/triggers/http.js +300 -0
  50. package/package.json +25 -151
  51. package/NOTICE +0 -12
  52. package/kestrel.config.ts +0 -28
  53. package/layers/access/nuxt.config.ts +0 -5
  54. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  55. package/layers/access/server/middleware/access-guard.ts +0 -32
  56. package/layers/access/server/types.d.ts +0 -7
  57. package/layers/admin/app/components/AdminAccount.vue +0 -212
  58. package/layers/admin/app/components/AdminNav.vue +0 -44
  59. package/layers/admin/app/components/BlockFields.vue +0 -67
  60. package/layers/admin/app/components/BlockPreview.vue +0 -413
  61. package/layers/admin/app/components/BlockTree.vue +0 -433
  62. package/layers/admin/app/components/BlocksBody.vue +0 -163
  63. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  64. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  65. package/layers/admin/app/components/CollectionList.vue +0 -429
  66. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  67. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  68. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  69. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  70. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  71. package/layers/admin/app/components/EditorStatus.vue +0 -211
  72. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  73. package/layers/admin/app/components/FieldsBody.vue +0 -23
  74. package/layers/admin/app/components/LocaleBar.vue +0 -107
  75. package/layers/admin/app/components/PageFields.vue +0 -129
  76. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  77. package/layers/admin/app/components/SeoFields.vue +0 -210
  78. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  79. package/layers/admin/app/composables/useAuth.ts +0 -68
  80. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  81. package/layers/admin/app/composables/useBlocks.ts +0 -7
  82. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  83. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  84. package/layers/admin/app/composables/useCollections.ts +0 -6
  85. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  86. package/layers/admin/app/composables/useEditForm.ts +0 -402
  87. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  88. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  89. package/layers/admin/app/composables/useListColumns.ts +0 -53
  90. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  91. package/layers/admin/app/composables/useListRows.ts +0 -75
  92. package/layers/admin/app/composables/useListSelection.ts +0 -28
  93. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  94. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  95. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  96. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  97. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  98. package/layers/admin/app/composables/useTheme.ts +0 -36
  99. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  100. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  101. package/layers/admin/app/layouts/admin.vue +0 -266
  102. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  103. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  104. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  105. package/layers/admin/app/pages/admin/index.vue +0 -118
  106. package/layers/admin/app/pages/admin/login.vue +0 -68
  107. package/layers/admin/app/pages/admin/media.vue +0 -28
  108. package/layers/admin/app/pages/admin/references.vue +0 -146
  109. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  110. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  111. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  112. package/layers/admin/app/utils/block-tree.ts +0 -214
  113. package/layers/admin/app/utils/collection-ops.ts +0 -69
  114. package/layers/admin/app/utils/dead-refs.ts +0 -30
  115. package/layers/admin/app/utils/edit-form.ts +0 -267
  116. package/layers/admin/app/utils/editor-expose.ts +0 -36
  117. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  118. package/layers/admin/app/utils/editor-registry.ts +0 -20
  119. package/layers/admin/app/utils/field-keys.ts +0 -11
  120. package/layers/admin/app/utils/list-cell.ts +0 -46
  121. package/layers/admin/app/utils/list-columns.ts +0 -142
  122. package/layers/admin/app/utils/list-query.ts +0 -96
  123. package/layers/admin/app/utils/nav-active.ts +0 -9
  124. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  125. package/layers/admin/app/utils/preview-channel.ts +0 -62
  126. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  127. package/layers/admin/app/utils/reauth.ts +0 -67
  128. package/layers/admin/app/utils/record-title.ts +0 -15
  129. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  130. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  131. package/layers/admin/nuxt.config.ts +0 -20
  132. package/layers/auth/nuxt.config.ts +0 -1
  133. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  134. package/layers/collections/nuxt.config.ts +0 -1
  135. package/layers/collections/server/plugins/01.register.ts +0 -29
  136. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  137. package/layers/core/app/composables/layouts.ts +0 -5
  138. package/layers/core/app/utils/field-factories.ts +0 -129
  139. package/layers/core/app/utils/layouts.ts +0 -38
  140. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  141. package/layers/core/modules/auto-discovery/index.ts +0 -112
  142. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  143. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  144. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  145. package/layers/core/modules/component-namespace/index.ts +0 -44
  146. package/layers/core/modules/component-namespace/shared.ts +0 -24
  147. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  148. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  149. package/layers/core/modules/kestrel/index.ts +0 -144
  150. package/layers/core/modules/plugin-order/index.ts +0 -69
  151. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  152. package/layers/core/nuxt.config.ts +0 -10
  153. package/layers/core/server/api/[...path].ts +0 -98
  154. package/layers/core/server/plugins/00.config.ts +0 -12
  155. package/layers/core/server/plugins/00.migrate.ts +0 -12
  156. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  157. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  158. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  159. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  160. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  161. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  162. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  163. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  164. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  165. package/layers/core/server/tasks/db/migrate.ts +0 -50
  166. package/layers/core/server/utils/server-config.ts +0 -81
  167. package/layers/fields/nuxt.config.ts +0 -1
  168. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  169. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  170. package/layers/media/app/components/KestrelImg.vue +0 -94
  171. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  172. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  173. package/layers/media/app/components/MediaGrid.vue +0 -113
  174. package/layers/media/app/components/MediaImage.vue +0 -70
  175. package/layers/media/app/components/MediaLibrary.vue +0 -456
  176. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  177. package/layers/media/app/components/MediaPathBar.vue +0 -163
  178. package/layers/media/app/components/MediaPicker.vue +0 -19
  179. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  180. package/layers/media/app/components/MediaTable.vue +0 -75
  181. package/layers/media/app/components/MediaThumb.vue +0 -25
  182. package/layers/media/app/components/MediaToolbar.vue +0 -90
  183. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  184. package/layers/media/app/components/MediaViewer.vue +0 -137
  185. package/layers/media/app/components/field/Media.vue +0 -263
  186. package/layers/media/app/composables/useBlurUp.ts +0 -55
  187. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  188. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  189. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  190. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  191. package/layers/media/app/composables/useMediaOps.ts +0 -50
  192. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  193. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  194. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  195. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  196. package/layers/media/app/plugins/field-media.client.ts +0 -6
  197. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  198. package/layers/media/app/utils/dnd.ts +0 -69
  199. package/layers/media/app/utils/library.ts +0 -81
  200. package/layers/media/app/utils/ops.ts +0 -60
  201. package/layers/media/app/utils/picture-model.ts +0 -125
  202. package/layers/media/app/utils/thumbhash.ts +0 -19
  203. package/layers/media/app/utils/variant-name.ts +0 -20
  204. package/layers/media/nuxt.config.ts +0 -7
  205. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  206. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  207. package/layers/media/server/plugins/02.register-media.ts +0 -22
  208. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  209. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  210. package/layers/media/server/tasks/media/backfill.ts +0 -17
  211. package/layers/public/app/app.vue +0 -5
  212. package/layers/public/app/components/BlockRenderer.vue +0 -56
  213. package/layers/public/app/components/KestrelLink.vue +0 -26
  214. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  215. package/layers/public/app/composables/public-page.ts +0 -24
  216. package/layers/public/app/error.vue +0 -12
  217. package/layers/public/app/layouts/default.vue +0 -5
  218. package/layers/public/app/pages/[...slug].vue +0 -239
  219. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  220. package/layers/public/app/utils/block-edit-context.ts +0 -16
  221. package/layers/public/app/utils/json-ld.ts +0 -139
  222. package/layers/public/app/utils/link-href.ts +0 -46
  223. package/layers/public/app/utils/page-head.ts +0 -84
  224. package/layers/public/app/utils/page-layout.ts +0 -18
  225. package/layers/public/app/utils/preview-protocol.ts +0 -117
  226. package/layers/public/app/utils/public-route.ts +0 -46
  227. package/layers/public/app/utils/site-head.ts +0 -43
  228. package/layers/public/modules/deploy-output/index.ts +0 -80
  229. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  230. package/layers/public/modules/prerender-routes/index.ts +0 -88
  231. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  232. package/layers/public/modules/prune-media/index.ts +0 -48
  233. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  234. package/layers/public/nuxt.config.ts +0 -14
  235. package/layers/public/server/delivery-live/serve.ts +0 -111
  236. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  237. package/layers/public/server/pipelines/index.ts +0 -23
  238. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  239. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  240. package/layers/public/server/plugins/02.register-links.ts +0 -16
  241. package/layers/public/server/plugins/03.redirects.ts +0 -55
  242. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  243. package/layers/public/server/plugins/zz.publish.ts +0 -127
  244. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  245. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  246. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  247. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  248. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  249. package/layers/public/server/tasks/publish/run.ts +0 -32
  250. package/layers/public/server/utils/publish/render-live.ts +0 -26
  251. package/layers/ui/app/assets/scss/_base.scss +0 -16
  252. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  253. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  254. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  255. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  256. package/layers/ui/app/assets/scss/main.scss +0 -4
  257. package/layers/ui/app/components/field/Boolean.vue +0 -30
  258. package/layers/ui/app/components/field/Choice.vue +0 -73
  259. package/layers/ui/app/components/field/Datetime.vue +0 -98
  260. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  261. package/layers/ui/app/components/field/Json.vue +0 -62
  262. package/layers/ui/app/components/field/Layout.vue +0 -138
  263. package/layers/ui/app/components/field/Link.vue +0 -112
  264. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  265. package/layers/ui/app/components/field/Number.vue +0 -30
  266. package/layers/ui/app/components/field/Relation.vue +0 -49
  267. package/layers/ui/app/components/field/Renderer.vue +0 -30
  268. package/layers/ui/app/components/field/Repeater.vue +0 -460
  269. package/layers/ui/app/components/field/Richtext.vue +0 -26
  270. package/layers/ui/app/components/field/Slug.vue +0 -65
  271. package/layers/ui/app/components/field/Text.vue +0 -35
  272. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  273. package/layers/ui/app/components/ui/Alert.vue +0 -50
  274. package/layers/ui/app/components/ui/Brand.vue +0 -22
  275. package/layers/ui/app/components/ui/Button.vue +0 -116
  276. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  277. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  278. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  279. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  280. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  281. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  282. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  283. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  284. package/layers/ui/app/components/ui/Field.vue +0 -58
  285. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  286. package/layers/ui/app/components/ui/Icon.vue +0 -44
  287. package/layers/ui/app/components/ui/Menu.vue +0 -54
  288. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  289. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  290. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  291. package/layers/ui/app/components/ui/Select.vue +0 -52
  292. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  293. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  294. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  295. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  296. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  297. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  298. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  299. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  300. package/layers/ui/app/composables/useLinkField.ts +0 -70
  301. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  302. package/layers/ui/app/composables/useRepeater.ts +0 -99
  303. package/layers/ui/app/composables/useT.ts +0 -32
  304. package/layers/ui/app/composables/useToast.ts +0 -46
  305. package/layers/ui/app/i18n/de.ts +0 -432
  306. package/layers/ui/app/i18n/en.ts +0 -434
  307. package/layers/ui/app/utils/boolean-field.ts +0 -10
  308. package/layers/ui/app/utils/field-component.ts +0 -22
  309. package/layers/ui/app/utils/field-empty.ts +0 -21
  310. package/layers/ui/app/utils/field-registry.ts +0 -36
  311. package/layers/ui/app/utils/humanize.ts +0 -14
  312. package/layers/ui/app/utils/icons.ts +0 -143
  313. package/layers/ui/app/utils/iso-date.ts +0 -18
  314. package/layers/ui/app/utils/localized.ts +0 -13
  315. package/layers/ui/app/utils/record-context.ts +0 -15
  316. package/layers/ui/app/utils/reorder.ts +0 -8
  317. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  318. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  319. package/layers/ui/nuxt.config.ts +0 -9
  320. package/nuxt.config.ts +0 -108
  321. package/scripts/consumer-template-ci.mjs +0 -160
  322. package/scripts/copy-create-payload.mjs +0 -49
  323. package/scripts/coverage-all.mjs +0 -163
  324. package/scripts/dashboard.mjs +0 -89
  325. package/scripts/hash-password.mjs +0 -29
  326. package/scripts/kestrel.mjs +0 -240
  327. package/scripts/lib/cli.mjs +0 -129
  328. package/scripts/lib/coverage-model.mjs +0 -51
  329. package/scripts/lib/password.mjs +0 -19
  330. package/scripts/lib/scaffold.mjs +0 -205
  331. package/scripts/port-graph.mjs +0 -350
  332. package/scripts/typecheck.mjs +0 -62
  333. package/templates/starter/README.md +0 -65
  334. package/templates/starter/_env.example +0 -17
  335. package/templates/starter/_gitignore +0 -26
  336. package/templates/starter/_package.json +0 -22
  337. package/templates/starter/app/app.vue +0 -7
  338. package/templates/starter/app/blocks/Prose.vue +0 -13
  339. package/templates/starter/app/layouts/default.vue +0 -6
  340. package/templates/starter/nuxt.config.ts +0 -23
  341. package/templates/starter/pnpm-workspace.yaml +0 -8
  342. 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
- }