@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.
- package/README.md +26 -201
- package/dist/boot.d.ts +45 -0
- package/dist/boot.js +274 -0
- package/dist/cast.d.ts +3 -0
- package/dist/cast.js +6 -0
- package/dist/catalogue.d.ts +11 -0
- package/dist/catalogue.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +55 -0
- package/dist/context.d.ts +62 -0
- package/dist/context.js +61 -0
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.js +65 -0
- package/dist/defineConfig.d.ts +157 -0
- package/dist/defineConfig.js +41 -0
- package/dist/defineContract.d.ts +12 -0
- package/dist/defineContract.js +16 -0
- package/dist/defineModule.d.ts +75 -0
- package/dist/defineModule.js +9 -0
- package/dist/definePipeline.d.ts +7 -0
- package/dist/definePipeline.js +11 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/load.d.ts +7 -0
- package/dist/load.js +79 -0
- package/dist/logger.d.ts +18 -0
- package/dist/logger.js +36 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +87 -0
- package/dist/result.d.ts +19 -0
- package/dist/result.js +19 -0
- package/dist/runner.d.ts +29 -0
- package/dist/runner.js +133 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +142 -0
- package/dist/sort.d.ts +2 -0
- package/dist/sort.js +50 -0
- package/dist/testing/runPipeline.d.ts +9 -0
- package/dist/testing/runPipeline.js +22 -0
- package/dist/triggers/cron.d.ts +10 -0
- package/dist/triggers/cron.js +44 -0
- package/dist/triggers/cronMatch.d.ts +10 -0
- package/dist/triggers/cronMatch.js +68 -0
- package/dist/triggers/http.d.ts +58 -0
- package/dist/triggers/http.js +292 -0
- package/package.json +25 -151
- package/NOTICE +0 -12
- package/kestrel.config.ts +0 -28
- package/layers/access/nuxt.config.ts +0 -5
- package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
- package/layers/access/server/middleware/access-guard.ts +0 -32
- package/layers/access/server/types.d.ts +0 -7
- package/layers/admin/app/components/AdminAccount.vue +0 -212
- package/layers/admin/app/components/AdminNav.vue +0 -44
- package/layers/admin/app/components/BlockFields.vue +0 -67
- package/layers/admin/app/components/BlockPreview.vue +0 -413
- package/layers/admin/app/components/BlockTree.vue +0 -433
- package/layers/admin/app/components/BlocksBody.vue +0 -163
- package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
- package/layers/admin/app/components/CollectionEditor.vue +0 -324
- package/layers/admin/app/components/CollectionList.vue +0 -429
- package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
- package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
- package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
- package/layers/admin/app/components/CollectionListPager.vue +0 -70
- package/layers/admin/app/components/CollectionListTable.vue +0 -347
- package/layers/admin/app/components/EditorStatus.vue +0 -211
- package/layers/admin/app/components/EditorUnsupported.vue +0 -28
- package/layers/admin/app/components/FieldsBody.vue +0 -23
- package/layers/admin/app/components/LocaleBar.vue +0 -107
- package/layers/admin/app/components/PageFields.vue +0 -129
- package/layers/admin/app/components/PageFieldsPane.vue +0 -39
- package/layers/admin/app/components/SeoFields.vue +0 -210
- package/layers/admin/app/components/SingletonEditor.vue +0 -93
- package/layers/admin/app/composables/useAuth.ts +0 -68
- package/layers/admin/app/composables/useBlockTree.ts +0 -95
- package/layers/admin/app/composables/useBlocks.ts +0 -7
- package/layers/admin/app/composables/useCachedResource.ts +0 -35
- package/layers/admin/app/composables/useCollectionOps.ts +0 -102
- package/layers/admin/app/composables/useCollections.ts +0 -6
- package/layers/admin/app/composables/useContentLocales.ts +0 -23
- package/layers/admin/app/composables/useEditForm.ts +0 -402
- package/layers/admin/app/composables/useLinkResolver.ts +0 -40
- package/layers/admin/app/composables/useListBatchActions.ts +0 -78
- package/layers/admin/app/composables/useListColumns.ts +0 -53
- package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
- package/layers/admin/app/composables/useListRows.ts +0 -75
- package/layers/admin/app/composables/useListSelection.ts +0 -28
- package/layers/admin/app/composables/useListUrlState.ts +0 -53
- package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
- package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
- package/layers/admin/app/composables/usePublishStatus.ts +0 -107
- package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
- package/layers/admin/app/composables/useTheme.ts +0 -36
- package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
- package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
- package/layers/admin/app/layouts/admin.vue +0 -266
- package/layers/admin/app/middleware/admin-auth.ts +0 -4
- package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
- package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
- package/layers/admin/app/pages/admin/index.vue +0 -118
- package/layers/admin/app/pages/admin/login.vue +0 -68
- package/layers/admin/app/pages/admin/media.vue +0 -28
- package/layers/admin/app/pages/admin/references.vue +0 -146
- package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
- package/layers/admin/app/plugins/reauth.client.ts +0 -20
- package/layers/admin/app/utils/admin-style-guard.ts +0 -20
- package/layers/admin/app/utils/block-tree.ts +0 -214
- package/layers/admin/app/utils/collection-ops.ts +0 -69
- package/layers/admin/app/utils/dead-refs.ts +0 -30
- package/layers/admin/app/utils/edit-form.ts +0 -267
- package/layers/admin/app/utils/editor-expose.ts +0 -36
- package/layers/admin/app/utils/editor-form-context.ts +0 -82
- package/layers/admin/app/utils/editor-registry.ts +0 -20
- package/layers/admin/app/utils/field-keys.ts +0 -11
- package/layers/admin/app/utils/list-cell.ts +0 -46
- package/layers/admin/app/utils/list-columns.ts +0 -142
- package/layers/admin/app/utils/list-query.ts +0 -96
- package/layers/admin/app/utils/nav-active.ts +0 -9
- package/layers/admin/app/utils/populate-blocks.ts +0 -288
- package/layers/admin/app/utils/preview-channel.ts +0 -62
- package/layers/admin/app/utils/preview-viewport.ts +0 -80
- package/layers/admin/app/utils/reauth.ts +0 -67
- package/layers/admin/app/utils/record-title.ts +0 -15
- package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
- package/layers/admin/app/utils/safe-redirect.ts +0 -12
- package/layers/admin/nuxt.config.ts +0 -20
- package/layers/auth/nuxt.config.ts +0 -1
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
- package/layers/collections/nuxt.config.ts +0 -1
- package/layers/collections/server/plugins/01.register.ts +0 -29
- package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
- package/layers/core/app/composables/layouts.ts +0 -5
- package/layers/core/app/utils/field-factories.ts +0 -129
- package/layers/core/app/utils/layouts.ts +0 -38
- package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
- package/layers/core/modules/auto-discovery/index.ts +0 -112
- package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
- package/layers/core/modules/auto-discovery/scan.ts +0 -170
- package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
- package/layers/core/modules/component-namespace/index.ts +0 -44
- package/layers/core/modules/component-namespace/shared.ts +0 -24
- package/layers/core/modules/kestrel/app-shell.ts +0 -55
- package/layers/core/modules/kestrel/auto-imports.ts +0 -44
- package/layers/core/modules/kestrel/index.ts +0 -144
- package/layers/core/modules/plugin-order/index.ts +0 -69
- package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
- package/layers/core/nuxt.config.ts +0 -10
- package/layers/core/server/api/[...path].ts +0 -98
- package/layers/core/server/plugins/00.config.ts +0 -12
- package/layers/core/server/plugins/00.migrate.ts +0 -12
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
- package/layers/core/server/plugins/02.schema-sync.ts +0 -58
- package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
- package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
- package/layers/core/server/tasks/db/migrate-module.ts +0 -83
- package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
- package/layers/core/server/tasks/db/migrate.ts +0 -50
- package/layers/core/server/utils/server-config.ts +0 -81
- package/layers/fields/nuxt.config.ts +0 -1
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
- package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
- package/layers/media/app/components/KestrelImg.vue +0 -94
- package/layers/media/app/components/MediaConflictDialog.vue +0 -35
- package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
- package/layers/media/app/components/MediaGrid.vue +0 -113
- package/layers/media/app/components/MediaImage.vue +0 -70
- package/layers/media/app/components/MediaLibrary.vue +0 -456
- package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
- package/layers/media/app/components/MediaPathBar.vue +0 -163
- package/layers/media/app/components/MediaPicker.vue +0 -19
- package/layers/media/app/components/MediaRenameDialog.vue +0 -32
- package/layers/media/app/components/MediaTable.vue +0 -75
- package/layers/media/app/components/MediaThumb.vue +0 -25
- package/layers/media/app/components/MediaToolbar.vue +0 -90
- package/layers/media/app/components/MediaUploadDialog.vue +0 -87
- package/layers/media/app/components/MediaViewer.vue +0 -137
- package/layers/media/app/components/field/Media.vue +0 -263
- package/layers/media/app/composables/useBlurUp.ts +0 -55
- package/layers/media/app/composables/useMediaClipboard.ts +0 -14
- package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
- package/layers/media/app/composables/useMediaDnd.ts +0 -56
- package/layers/media/app/composables/useMediaLibrary.ts +0 -152
- package/layers/media/app/composables/useMediaOps.ts +0 -50
- package/layers/media/app/composables/useMediaRelocate.ts +0 -39
- package/layers/media/app/composables/useMediaResolver.ts +0 -50
- package/layers/media/app/composables/useMediaUpload.ts +0 -93
- package/layers/media/app/composables/useMediaVariant.ts +0 -34
- package/layers/media/app/plugins/field-media.client.ts +0 -6
- package/layers/media/app/utils/ai-disclosure.ts +0 -19
- package/layers/media/app/utils/dnd.ts +0 -69
- package/layers/media/app/utils/library.ts +0 -81
- package/layers/media/app/utils/ops.ts +0 -60
- package/layers/media/app/utils/picture-model.ts +0 -125
- package/layers/media/app/utils/thumbhash.ts +0 -19
- package/layers/media/app/utils/variant-name.ts +0 -20
- package/layers/media/nuxt.config.ts +0 -7
- package/layers/media/server/middleware/ondemand-variants.ts +0 -38
- package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
- package/layers/media/server/plugins/02.register-media.ts +0 -22
- package/layers/media/server/plugins/04.variant-capture.ts +0 -27
- package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
- package/layers/media/server/tasks/media/backfill.ts +0 -17
- package/layers/public/app/app.vue +0 -5
- package/layers/public/app/components/BlockRenderer.vue +0 -56
- package/layers/public/app/components/KestrelLink.vue +0 -26
- package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
- package/layers/public/app/composables/public-page.ts +0 -24
- package/layers/public/app/error.vue +0 -12
- package/layers/public/app/layouts/default.vue +0 -5
- package/layers/public/app/pages/[...slug].vue +0 -239
- package/layers/public/app/pages/__kestrel/preview.vue +0 -46
- package/layers/public/app/utils/block-edit-context.ts +0 -16
- package/layers/public/app/utils/json-ld.ts +0 -139
- package/layers/public/app/utils/link-href.ts +0 -46
- package/layers/public/app/utils/page-head.ts +0 -84
- package/layers/public/app/utils/page-layout.ts +0 -18
- package/layers/public/app/utils/preview-protocol.ts +0 -117
- package/layers/public/app/utils/public-route.ts +0 -46
- package/layers/public/app/utils/site-head.ts +0 -43
- package/layers/public/modules/deploy-output/index.ts +0 -80
- package/layers/public/modules/prerender-routes/discover.ts +0 -37
- package/layers/public/modules/prerender-routes/index.ts +0 -88
- package/layers/public/modules/prerender-routes/routes.ts +0 -16
- package/layers/public/modules/prune-media/index.ts +0 -48
- package/layers/public/modules/prune-media/prune-media.ts +0 -81
- package/layers/public/nuxt.config.ts +0 -14
- package/layers/public/server/delivery-live/serve.ts +0 -111
- package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
- package/layers/public/server/pipelines/index.ts +0 -23
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
- package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
- package/layers/public/server/plugins/02.register-links.ts +0 -16
- package/layers/public/server/plugins/03.redirects.ts +0 -55
- package/layers/public/server/plugins/05.plan-publish.ts +0 -5
- package/layers/public/server/plugins/zz.publish.ts +0 -127
- package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
- package/layers/public/server/routes/llms.txt.get.ts +0 -66
- package/layers/public/server/routes/redirects.json.get.ts +0 -58
- package/layers/public/server/routes/robots.txt.get.ts +0 -11
- package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
- package/layers/public/server/tasks/publish/run.ts +0 -32
- package/layers/public/server/utils/publish/render-live.ts +0 -26
- package/layers/ui/app/assets/scss/_base.scss +0 -16
- package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
- package/layers/ui/app/assets/scss/_mixins.scss +0 -50
- package/layers/ui/app/assets/scss/_reset.scss +0 -65
- package/layers/ui/app/assets/scss/_tokens.scss +0 -183
- package/layers/ui/app/assets/scss/main.scss +0 -4
- package/layers/ui/app/components/field/Boolean.vue +0 -30
- package/layers/ui/app/components/field/Choice.vue +0 -73
- package/layers/ui/app/components/field/Datetime.vue +0 -98
- package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
- package/layers/ui/app/components/field/Json.vue +0 -62
- package/layers/ui/app/components/field/Layout.vue +0 -138
- package/layers/ui/app/components/field/Link.vue +0 -112
- package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
- package/layers/ui/app/components/field/Number.vue +0 -30
- package/layers/ui/app/components/field/Relation.vue +0 -49
- package/layers/ui/app/components/field/Renderer.vue +0 -30
- package/layers/ui/app/components/field/Repeater.vue +0 -460
- package/layers/ui/app/components/field/Richtext.vue +0 -26
- package/layers/ui/app/components/field/Slug.vue +0 -65
- package/layers/ui/app/components/field/Text.vue +0 -35
- package/layers/ui/app/components/field/Unsupported.vue +0 -34
- package/layers/ui/app/components/ui/Alert.vue +0 -50
- package/layers/ui/app/components/ui/Brand.vue +0 -22
- package/layers/ui/app/components/ui/Button.vue +0 -116
- package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
- package/layers/ui/app/components/ui/Checkbox.vue +0 -53
- package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
- package/layers/ui/app/components/ui/Combobox.vue +0 -345
- package/layers/ui/app/components/ui/DatePicker.vue +0 -92
- package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
- package/layers/ui/app/components/ui/Dialog.vue +0 -107
- package/layers/ui/app/components/ui/EmptyState.vue +0 -46
- package/layers/ui/app/components/ui/Field.vue +0 -58
- package/layers/ui/app/components/ui/Fieldset.vue +0 -65
- package/layers/ui/app/components/ui/Icon.vue +0 -44
- package/layers/ui/app/components/ui/Menu.vue +0 -54
- package/layers/ui/app/components/ui/NumberInput.vue +0 -175
- package/layers/ui/app/components/ui/Richtext.vue +0 -141
- package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
- package/layers/ui/app/components/ui/Select.vue +0 -52
- package/layers/ui/app/components/ui/TextInput.vue +0 -111
- package/layers/ui/app/components/ui/Textarea.vue +0 -39
- package/layers/ui/app/components/ui/TimeInput.vue +0 -18
- package/layers/ui/app/components/ui/Toasts.vue +0 -103
- package/layers/ui/app/components/ui/Tooltip.vue +0 -45
- package/layers/ui/app/composables/useAdminLang.ts +0 -8
- package/layers/ui/app/composables/useDragReorder.ts +0 -41
- package/layers/ui/app/composables/useEchoGuard.ts +0 -28
- package/layers/ui/app/composables/useLinkField.ts +0 -70
- package/layers/ui/app/composables/useRecordOptions.ts +0 -84
- package/layers/ui/app/composables/useRepeater.ts +0 -99
- package/layers/ui/app/composables/useT.ts +0 -32
- package/layers/ui/app/composables/useToast.ts +0 -46
- package/layers/ui/app/i18n/de.ts +0 -432
- package/layers/ui/app/i18n/en.ts +0 -434
- package/layers/ui/app/utils/boolean-field.ts +0 -10
- package/layers/ui/app/utils/field-component.ts +0 -22
- package/layers/ui/app/utils/field-empty.ts +0 -21
- package/layers/ui/app/utils/field-registry.ts +0 -36
- package/layers/ui/app/utils/humanize.ts +0 -14
- package/layers/ui/app/utils/icons.ts +0 -143
- package/layers/ui/app/utils/iso-date.ts +0 -18
- package/layers/ui/app/utils/localized.ts +0 -13
- package/layers/ui/app/utils/record-context.ts +0 -15
- package/layers/ui/app/utils/reorder.ts +0 -8
- package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
- package/layers/ui/app/utils/useFieldA11y.ts +0 -40
- package/layers/ui/nuxt.config.ts +0 -9
- package/nuxt.config.ts +0 -108
- package/scripts/consumer-template-ci.mjs +0 -160
- package/scripts/copy-create-payload.mjs +0 -49
- package/scripts/coverage-all.mjs +0 -163
- package/scripts/dashboard.mjs +0 -89
- package/scripts/hash-password.mjs +0 -29
- package/scripts/kestrel.mjs +0 -240
- package/scripts/lib/cli.mjs +0 -129
- package/scripts/lib/coverage-model.mjs +0 -51
- package/scripts/lib/password.mjs +0 -19
- package/scripts/lib/scaffold.mjs +0 -205
- package/scripts/port-graph.mjs +0 -350
- package/scripts/typecheck.mjs +0 -62
- package/templates/starter/README.md +0 -65
- package/templates/starter/_env.example +0 -17
- package/templates/starter/_gitignore +0 -26
- package/templates/starter/_package.json +0 -22
- package/templates/starter/app/app.vue +0 -7
- package/templates/starter/app/blocks/Prose.vue +0 -13
- package/templates/starter/app/layouts/default.vue +0 -6
- package/templates/starter/nuxt.config.ts +0 -23
- package/templates/starter/pnpm-workspace.yaml +0 -8
- package/templates/starter/tsconfig.json +0 -3
package/scripts/coverage-all.mjs
DELETED
|
@@ -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()
|
package/scripts/dashboard.mjs
DELETED
|
@@ -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
|
-
}
|
package/scripts/kestrel.mjs
DELETED
|
@@ -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\`.`)
|
package/scripts/lib/cli.mjs
DELETED
|
@@ -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
|
-
}
|