banhaten 0.3.0 → 0.5.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/CHANGELOG.md +139 -73
- package/CONTRIBUTING.md +46 -44
- package/README.md +192 -190
- package/SECURITY.md +15 -15
- package/banhaten.config.example.json +11 -11
- package/docs/design-spacing.md +13 -0
- package/docs/design-system/README.md +22 -20
- package/docs/design-system/adoption-quality-remediation.md +87 -87
- package/docs/design-system/agent-usage.md +13 -13
- package/docs/design-system/appearance-presets.md +189 -189
- package/docs/design-system/appearances/default.md +94 -94
- package/docs/design-system/appearances/rounded.md +95 -95
- package/docs/design-system/appearances/sharp.md +95 -95
- package/docs/design-system/component-docs-pattern.md +81 -81
- package/docs/design-system/component-implementation-ownership-audit.md +245 -244
- package/docs/design-system/component-quality-audit-2026-08-20.md +225 -223
- package/docs/design-system/component-showcase-consistency-report.md +219 -219
- package/docs/design-system/component-token-consistency-audit.md +173 -173
- package/docs/design-system/components/README.md +90 -76
- package/docs/design-system/components/accordion.md +53 -53
- package/docs/design-system/components/activity-feed.md +94 -94
- package/docs/design-system/components/activity-heatmap.md +57 -57
- package/docs/design-system/components/agent-input.md +85 -0
- package/docs/design-system/components/alert-dialog.md +72 -72
- package/docs/design-system/components/alert.md +98 -98
- package/docs/design-system/components/approval-card.md +134 -0
- package/docs/design-system/components/aspect-ratio.md +45 -45
- package/docs/design-system/components/attribute.md +89 -89
- package/docs/design-system/components/autocomplete.md +75 -75
- package/docs/design-system/components/avatar.md +54 -54
- package/docs/design-system/components/badge.md +54 -54
- package/docs/design-system/components/banner.md +87 -87
- package/docs/design-system/components/breadcrumbs.md +177 -176
- package/docs/design-system/components/button-group.md +98 -98
- package/docs/design-system/components/button.md +83 -80
- package/docs/design-system/components/card.md +88 -88
- package/docs/design-system/components/carousel.md +46 -46
- package/docs/design-system/components/catalog-components.md +47 -47
- package/docs/design-system/components/chart.md +95 -24
- package/docs/design-system/components/checkbox.md +54 -54
- package/docs/design-system/components/collapsible.md +67 -53
- package/docs/design-system/components/command.md +62 -62
- package/docs/design-system/components/context-menu.md +46 -46
- package/docs/design-system/components/date-picker.md +81 -79
- package/docs/design-system/components/divider.md +102 -102
- package/docs/design-system/components/empty-state.md +56 -56
- package/docs/design-system/components/field.md +70 -70
- package/docs/design-system/components/file-upload.md +187 -187
- package/docs/design-system/components/hover-card.md +48 -48
- package/docs/design-system/components/icons.md +48 -48
- package/docs/design-system/components/inline-citations.md +93 -0
- package/docs/design-system/components/input-group.md +58 -58
- package/docs/design-system/components/input-otp.md +56 -56
- package/docs/design-system/components/input.md +117 -61
- package/docs/design-system/components/item.md +59 -57
- package/docs/design-system/components/kbd.md +54 -49
- package/docs/design-system/components/label.md +49 -49
- package/docs/design-system/components/menu.md +71 -61
- package/docs/design-system/components/menubar.md +47 -47
- package/docs/design-system/components/modal.md +105 -105
- package/docs/design-system/components/native-select.md +54 -54
- package/docs/design-system/components/navigation-menu.md +50 -50
- package/docs/design-system/components/onboarding-step-list-item.md +82 -82
- package/docs/design-system/components/page-header.md +93 -86
- package/docs/design-system/components/pagination.md +57 -57
- package/docs/design-system/components/popover.md +60 -60
- package/docs/design-system/components/progress-slider.md +50 -50
- package/docs/design-system/components/progress.md +76 -76
- package/docs/design-system/components/radio-card.md +51 -51
- package/docs/design-system/components/radio-group.md +57 -57
- package/docs/design-system/components/resizable.md +44 -44
- package/docs/design-system/components/rolling-count.md +61 -0
- package/docs/design-system/components/scroll-area.md +51 -51
- package/docs/design-system/components/segmented-control.md +54 -54
- package/docs/design-system/components/select.md +68 -62
- package/docs/design-system/components/sheet.md +67 -67
- package/docs/design-system/components/shimmer-text.md +67 -0
- package/docs/design-system/components/sidebar.md +127 -76
- package/docs/design-system/components/skeleton.md +75 -75
- package/docs/design-system/components/slideout.md +65 -65
- package/docs/design-system/components/slider.md +63 -63
- package/docs/design-system/components/social-button.md +50 -50
- package/docs/design-system/components/spinner.md +62 -62
- package/docs/design-system/components/steps.md +65 -65
- package/docs/design-system/components/streaming-text.md +82 -0
- package/docs/design-system/components/table.md +419 -414
- package/docs/design-system/components/tabs.md +59 -55
- package/docs/design-system/components/tag.md +80 -80
- package/docs/design-system/components/task-list.md +95 -0
- package/docs/design-system/components/textarea.md +50 -50
- package/docs/design-system/components/thinking-reasoning.md +72 -0
- package/docs/design-system/components/thinking-state.md +67 -0
- package/docs/design-system/components/timeline.md +83 -83
- package/docs/design-system/components/toast.md +58 -58
- package/docs/design-system/components/toggle.md +81 -81
- package/docs/design-system/components/toolbar.md +92 -87
- package/docs/design-system/components/tooltip.md +94 -92
- package/docs/design-system/components/typography.md +18 -18
- package/docs/design-system/density-and-elevation.md +65 -61
- package/docs/design-system/design-system-test-missing-items.md +368 -368
- package/docs/design-system/direction-and-localization.md +23 -23
- package/docs/design-system/form-recipes.md +73 -73
- package/docs/design-system/gap-promotion-policy.md +21 -21
- package/docs/design-system/icons.md +74 -69
- package/docs/design-system/installation.md +70 -68
- package/docs/design-system/launch-comparison-shadcn.md +98 -98
- package/docs/design-system/registry-and-cli.md +64 -64
- package/docs/design-system/release-policy.md +30 -30
- package/docs/design-system/roadmap.md +14 -14
- package/docs/design-system/support-matrix.md +23 -23
- package/docs/design-system/tabs.md +54 -53
- package/docs/design-system/template-gap-audit.md +32 -0
- package/docs/design-system/templates.md +59 -0
- package/docs/design-system/token-governance.md +72 -60
- package/package.json +98 -98
- package/registry/component-contract.json +40 -40
- package/registry/components/accordion.tsx +155 -155
- package/registry/components/agent-input-parts.tsx +552 -0
- package/registry/components/agent-input-state.ts +156 -0
- package/registry/components/agent-input.css +307 -0
- package/registry/components/agent-input.tsx +382 -0
- package/registry/components/alert-dialog.tsx +297 -297
- package/registry/components/alert.tsx +293 -293
- package/registry/components/approval-card.css +409 -0
- package/registry/components/approval-card.tsx +401 -0
- package/registry/components/approval-frame.tsx +108 -0
- package/registry/components/approval-questions.tsx +559 -0
- package/registry/components/aspect-ratio.tsx +30 -30
- package/registry/components/attribute.tsx +454 -448
- package/registry/components/autocomplete.tsx +598 -651
- package/registry/components/avatar.tsx +385 -381
- package/registry/components/badge.tsx +629 -629
- package/registry/components/button-group.tsx +565 -565
- package/registry/components/button.tsx +127 -123
- package/registry/components/card.tsx +638 -638
- package/registry/components/carousel.tsx +296 -296
- package/registry/components/chart-block-data.ts +177 -0
- package/registry/components/chart-block.css +171 -0
- package/registry/components/chart-block.tsx +260 -0
- package/registry/components/chart-interactions.ts +123 -0
- package/registry/components/chart-options.cartesian.ts +162 -0
- package/registry/components/chart-options.paint.ts +99 -0
- package/registry/components/chart-options.parts.ts +72 -0
- package/registry/components/chart-options.radial.ts +109 -0
- package/registry/components/chart-options.ts +44 -0
- package/registry/components/chart-options.types.ts +67 -0
- package/registry/components/chart.tsx +203 -203
- package/registry/components/checkbox.tsx +247 -247
- package/registry/components/collapsible.tsx +109 -69
- package/registry/components/command.tsx +208 -208
- package/registry/components/context-menu.tsx +236 -236
- package/registry/components/data-chart.tsx +40 -0
- package/registry/components/date-picker.tsx +1289 -1277
- package/registry/components/echart-theme.ts +246 -0
- package/registry/components/echart.css +49 -0
- package/registry/components/echart.tsx +178 -0
- package/registry/components/expanded/ActivityFeed.tsx +227 -227
- package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
- package/registry/components/expanded/Banner.tsx +179 -179
- package/registry/components/expanded/Breadcrumbs.tsx +131 -131
- package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
- package/registry/components/expanded/CatalogDivider.tsx +49 -49
- package/registry/components/expanded/EmptyState.tsx +155 -155
- package/registry/components/expanded/FileUpload.tsx +543 -543
- package/registry/components/expanded/OnboardingStepListItem.tsx +60 -59
- package/registry/components/expanded/PageHeader.tsx +179 -166
- package/registry/components/expanded/Slideout.tsx +108 -108
- package/registry/components/expanded/Steps.tsx +402 -402
- package/registry/components/expanded/Table.tsx +1092 -1071
- package/registry/components/expanded/Timeline.tsx +203 -203
- package/registry/components/expanded/activityFeed.css +248 -248
- package/registry/components/expanded/activityHeatmap.css +66 -66
- package/registry/components/expanded/banner.css +260 -255
- package/registry/components/expanded/breadcrumbs.css +140 -140
- package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
- package/registry/components/expanded/divider.css +75 -75
- package/registry/components/expanded/emptyState.css +111 -111
- package/registry/components/expanded/fileUpload.css +457 -457
- package/registry/components/expanded/onboardingStepListItem.css +135 -126
- package/registry/components/expanded/pageHeader.css +248 -290
- package/registry/components/expanded/slideout.css +276 -276
- package/registry/components/expanded/steps.css +356 -356
- package/registry/components/expanded/table.css +606 -603
- package/registry/components/expanded/timeline.css +286 -286
- package/registry/components/field.tsx +283 -283
- package/registry/components/hover-card.tsx +48 -48
- package/registry/components/inline-citations.css +126 -0
- package/registry/components/inline-citations.tsx +292 -0
- package/registry/components/input-content.ts +107 -107
- package/registry/components/input-group.tsx +130 -130
- package/registry/components/input-otp.tsx +584 -584
- package/registry/components/input.tsx +990 -972
- package/registry/components/item.tsx +189 -189
- package/registry/components/kbd.tsx +246 -151
- package/registry/components/label.tsx +78 -78
- package/registry/components/menu.tsx +978 -986
- package/registry/components/menubar.tsx +226 -226
- package/registry/components/modal.tsx +872 -872
- package/registry/components/native-select.tsx +205 -205
- package/registry/components/navigation-menu.tsx +171 -171
- package/registry/components/pagination.css +36 -36
- package/registry/components/pagination.tsx +560 -555
- package/registry/components/popover.tsx +338 -338
- package/registry/components/progress-slider.tsx +216 -216
- package/registry/components/radio-card.tsx +676 -677
- package/registry/components/radio-group.tsx +570 -570
- package/registry/components/resizable.tsx +108 -108
- package/registry/components/rolling-count.css +37 -0
- package/registry/components/rolling-count.tsx +110 -0
- package/registry/components/scroll-area.tsx +91 -91
- package/registry/components/segmented-control.tsx +87 -87
- package/registry/components/select-content.tsx +218 -218
- package/registry/components/select.tsx +937 -932
- package/registry/components/sheet.tsx +305 -305
- package/registry/components/shimmer-text.css +69 -0
- package/registry/components/shimmer-text.tsx +43 -0
- package/registry/components/sidebar.tsx +1433 -358
- package/registry/components/skeleton.css +54 -54
- package/registry/components/skeleton.tsx +482 -482
- package/registry/components/slider.tsx +380 -380
- package/registry/components/social-button.tsx +387 -387
- package/registry/components/spinner.tsx +78 -78
- package/registry/components/streaming-text.css +45 -0
- package/registry/components/streaming-text.tsx +165 -0
- package/registry/components/table-elements.tsx +128 -128
- package/registry/components/tabs.css +222 -218
- package/registry/components/tabs.tsx +241 -241
- package/registry/components/tag.tsx +423 -423
- package/registry/components/task-list.css +328 -0
- package/registry/components/task-list.tsx +429 -0
- package/registry/components/textarea.tsx +610 -610
- package/registry/components/thinking-reasoning.css +178 -0
- package/registry/components/thinking-reasoning.tsx +253 -0
- package/registry/components/thinking-state.css +8 -0
- package/registry/components/thinking-state.tsx +46 -0
- package/registry/components/toast.tsx +136 -136
- package/registry/components/toggle.tsx +256 -256
- package/registry/components/toolbar.tsx +539 -539
- package/registry/components/tooltip.tsx +433 -431
- package/registry/examples/agent-input-demo.tsx +90 -0
- package/registry/examples/alert-demo.tsx +34 -34
- package/registry/examples/alert-dialog-demo.tsx +42 -42
- package/registry/examples/approval-card-demo.tsx +83 -0
- package/registry/examples/aspect-ratio-demo.tsx +11 -11
- package/registry/examples/attribute-demo.tsx +65 -65
- package/registry/examples/autocomplete-demo.tsx +109 -109
- package/registry/examples/avatar-demo.tsx +132 -132
- package/registry/examples/badge-demo.tsx +69 -69
- package/registry/examples/button-demo.tsx +86 -83
- package/registry/examples/button-group-demo.tsx +63 -63
- package/registry/examples/card-demo.tsx +86 -86
- package/registry/examples/carousel-demo.tsx +43 -43
- package/registry/examples/chart-block-demo.tsx +5 -0
- package/registry/examples/chart-demo.tsx +18 -33
- package/registry/examples/checkbox-demo.tsx +62 -62
- package/registry/examples/collapsible-demo.tsx +16 -16
- package/registry/examples/collection-filter-bar-demo.tsx +135 -135
- package/registry/examples/command-demo.tsx +42 -42
- package/registry/examples/command-palette-demo.tsx +235 -236
- package/registry/examples/context-menu-demo.tsx +29 -29
- package/registry/examples/control-density-contract-demo.tsx +65 -61
- package/registry/examples/date-picker-demo.tsx +127 -127
- package/registry/examples/divider-demo.tsx +17 -17
- package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
- package/registry/examples/expanded/banner-demo.tsx +48 -48
- package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
- package/registry/examples/expanded/empty-state-demo.tsx +39 -39
- package/registry/examples/expanded/file-upload-demo.tsx +60 -60
- package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
- package/registry/examples/expanded/slideout-demo.tsx +15 -15
- package/registry/examples/expanded/steps-demo.tsx +29 -29
- package/registry/examples/expanded/tabs-demo.tsx +13 -13
- package/registry/examples/expanded/timeline-demo.tsx +18 -18
- package/registry/examples/field-demo.tsx +51 -51
- package/registry/examples/hover-card-demo.tsx +23 -23
- package/registry/examples/inline-citations-demo.tsx +31 -0
- package/registry/examples/input-actions-demo.tsx +36 -0
- package/registry/examples/input-demo.tsx +87 -87
- package/registry/examples/input-group-demo.tsx +16 -16
- package/registry/examples/input-otp-demo.tsx +72 -72
- package/registry/examples/item-demo.tsx +46 -46
- package/registry/examples/kbd-demo.tsx +12 -11
- package/registry/examples/label-demo.tsx +20 -20
- package/registry/examples/menu-demo.tsx +122 -122
- package/registry/examples/menubar-demo.tsx +34 -34
- package/registry/examples/modal-demo.tsx +74 -74
- package/registry/examples/native-select-demo.tsx +16 -16
- package/registry/examples/navigation-menu-demo.tsx +29 -29
- package/registry/examples/pagination-demo.tsx +20 -20
- package/registry/examples/popover-demo.tsx +547 -546
- package/registry/examples/progress-demo.tsx +37 -37
- package/registry/examples/progress-slider-demo.tsx +29 -29
- package/registry/examples/radio-card-demo.tsx +51 -51
- package/registry/examples/radio-group-demo.tsx +62 -62
- package/registry/examples/resizable-demo.tsx +22 -22
- package/registry/examples/results-toolbar-demo.tsx +44 -44
- package/registry/examples/rolling-count-demo.tsx +39 -0
- package/registry/examples/scroll-area-demo.tsx +15 -15
- package/registry/examples/segmented-control-demo.tsx +39 -39
- package/registry/examples/select-demo.tsx +87 -87
- package/registry/examples/sheet-demo.tsx +47 -47
- package/registry/examples/shimmer-text-demo.tsx +20 -0
- package/registry/examples/sidebar-demo.tsx +101 -57
- package/registry/examples/slider-demo.tsx +31 -31
- package/registry/examples/social-button-demo.tsx +61 -61
- package/registry/examples/spinner-demo.tsx +59 -59
- package/registry/examples/streaming-text-demo.tsx +28 -0
- package/registry/examples/tabs-demo.tsx +13 -13
- package/registry/examples/tag-demo.tsx +29 -29
- package/registry/examples/task-list-demo.tsx +74 -0
- package/registry/examples/textarea-demo.tsx +79 -79
- package/registry/examples/thinking-reasoning-demo.tsx +74 -0
- package/registry/examples/thinking-state-demo.tsx +13 -0
- package/registry/examples/toast-demo.tsx +35 -35
- package/registry/examples/toggle-demo.tsx +59 -59
- package/registry/examples/tooltip-demo.tsx +115 -115
- package/registry/guidance/USAGE.md +27 -27
- package/registry/guidance/charts.LICENSE.txt +24 -0
- package/registry/hooks/use-controllable-state.ts +33 -0
- package/registry/hooks/use-direction.ts +207 -207
- package/registry/hooks/use-listbox-navigation.ts +73 -0
- package/registry/hooks/use-motion-end.ts +55 -0
- package/registry/hooks/use-reduced-motion.ts +28 -0
- package/registry/index.json +1634 -184
- package/registry/styles/globals.css +1462 -1570
- package/registry.json +364 -10
- package/schema/config.schema.json +51 -51
- package/schema/registry.schema.json +194 -190
- package/schema/tokens.schema.json +167 -163
- package/src/cli/command-line.js +194 -194
- package/src/cli/config-editors.js +9 -9
- package/src/cli/doctor.js +399 -399
- package/src/cli/file-transaction.js +302 -295
- package/src/cli/global-css.js +153 -153
- package/src/cli/index.js +1002 -1009
- package/src/cli/install-plan.js +592 -592
- package/src/cli/install-receipt.js +171 -171
- package/src/cli/jsonc.js +203 -203
- package/src/cli/package-install-state.js +254 -254
- package/src/cli/package-manager.js +238 -238
- package/src/cli/package-plan.js +185 -185
- package/src/cli/package-version.js +97 -97
- package/src/cli/registry-query.js +207 -207
- package/src/cli/reporting.js +55 -55
- package/src/cli/schema-validation.js +19 -19
- package/src/cli/source-scanner.js +686 -686
- package/src/cli/vite-config.js +478 -478
- package/tokens/banhaten.tokens.json +458 -224
package/src/cli/doctor.js
CHANGED
|
@@ -1,399 +1,399 @@
|
|
|
1
|
-
import { createHash } from "node:crypto"
|
|
2
|
-
import fs from "node:fs/promises"
|
|
3
|
-
import path from "node:path"
|
|
4
|
-
|
|
5
|
-
const RECEIPT_FILE = ".banhaten/installed.json"
|
|
6
|
-
const GUIDANCE_FILE = ".banhaten/USAGE.md"
|
|
7
|
-
const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"])
|
|
8
|
-
const IGNORED_DIRECTORIES = new Set([
|
|
9
|
-
".banhaten",
|
|
10
|
-
".git",
|
|
11
|
-
".next",
|
|
12
|
-
".nuxt",
|
|
13
|
-
".output",
|
|
14
|
-
".turbo",
|
|
15
|
-
".vercel",
|
|
16
|
-
"build",
|
|
17
|
-
"coverage",
|
|
18
|
-
"dist",
|
|
19
|
-
"node_modules",
|
|
20
|
-
"out",
|
|
21
|
-
])
|
|
22
|
-
const BANHATEN_CONTROL_TAG =
|
|
23
|
-
/<(Input|Select|DatePicker|DateRangePicker|Button|ButtonGroup|ToggleGroup|SegmentedControl|ToolbarSearch|ToolbarSelect|ToolbarButton)(?=\s|\/?>)[\s\S]*?>/g
|
|
24
|
-
const GEOMETRY_ATTRIBUTE =
|
|
25
|
-
/(?:className|controlClassName|inputClassName|surfaceClassName|triggerClassName)\s*=\s*(?:["']([^"']*)["']|\{\s*["']([^"']*)["']\s*\})/g
|
|
26
|
-
const RAW_GEOMETRY_CLASS =
|
|
27
|
-
/(?:^|\s)(?:h-|min-h-|max-h-|p(?:x|y|s|e|t|r|b|l)?-|rounded(?:-|$)|text-\[)/
|
|
28
|
-
|
|
29
|
-
export async function inspectBanhatenProject({ cwd, registryVersion }) {
|
|
30
|
-
const projectRoot = path.resolve(cwd)
|
|
31
|
-
const diagnostics = []
|
|
32
|
-
const receiptPath = path.join(projectRoot, RECEIPT_FILE)
|
|
33
|
-
const guidancePath = path.join(projectRoot, GUIDANCE_FILE)
|
|
34
|
-
const receipt = await readJsonIfExists(receiptPath, diagnostics, projectRoot)
|
|
35
|
-
|
|
36
|
-
if (!receipt) {
|
|
37
|
-
diagnostics.push(
|
|
38
|
-
diagnostic({
|
|
39
|
-
code: "setup-not-initialized",
|
|
40
|
-
message: "No Banhaten install receipt was found.",
|
|
41
|
-
recommendation: "Run `banhaten init` before installing components.",
|
|
42
|
-
severity: "warning",
|
|
43
|
-
})
|
|
44
|
-
)
|
|
45
|
-
} else {
|
|
46
|
-
inspectReceiptVersion(receipt, registryVersion, diagnostics)
|
|
47
|
-
await inspectManagedFiles(projectRoot, receipt, diagnostics)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!(await exists(guidancePath))) {
|
|
51
|
-
diagnostics.push(
|
|
52
|
-
diagnostic({
|
|
53
|
-
code: "agent-guidance-missing",
|
|
54
|
-
file: GUIDANCE_FILE,
|
|
55
|
-
message: "The installed coding-agent usage rules are missing.",
|
|
56
|
-
recommendation: "Run `banhaten init` to install .banhaten/USAGE.md.",
|
|
57
|
-
severity: "warning",
|
|
58
|
-
})
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const sourceFiles = await collectSourceFiles(projectRoot)
|
|
63
|
-
for (const filePath of sourceFiles) {
|
|
64
|
-
await inspectSourceFile(projectRoot, filePath, diagnostics)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
diagnostics.sort(compareDiagnostics)
|
|
68
|
-
const summary = diagnostics.reduce(
|
|
69
|
-
(counts, item) => {
|
|
70
|
-
const key =
|
|
71
|
-
item.severity === "error"
|
|
72
|
-
? "errors"
|
|
73
|
-
: item.severity === "warning"
|
|
74
|
-
? "warnings"
|
|
75
|
-
: "info"
|
|
76
|
-
counts[key] += 1
|
|
77
|
-
return counts
|
|
78
|
-
},
|
|
79
|
-
{ errors: 0, info: 0, warnings: 0 }
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
cwd: projectRoot,
|
|
84
|
-
registryVersion,
|
|
85
|
-
installedVersion: receipt?.registryVersion ?? null,
|
|
86
|
-
installedComponents: Object.keys(receipt?.components || {}).sort(),
|
|
87
|
-
scannedFiles: sourceFiles.length,
|
|
88
|
-
summary,
|
|
89
|
-
diagnostics,
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function printBanhatenDoctor(report, { json = false, silent = false } = {}) {
|
|
94
|
-
if (json) {
|
|
95
|
-
console.log(JSON.stringify(report, null, 2))
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
if (silent) return
|
|
99
|
-
|
|
100
|
-
console.log("Banhaten doctor")
|
|
101
|
-
console.log(`Project: ${report.cwd}`)
|
|
102
|
-
console.log(
|
|
103
|
-
`Registry: ${report.installedVersion ?? "not installed"} -> ${report.registryVersion}`
|
|
104
|
-
)
|
|
105
|
-
console.log(
|
|
106
|
-
`Scanned ${report.scannedFiles} source files and ${report.installedComponents.length} installed components.`
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
if (report.diagnostics.length === 0) {
|
|
110
|
-
console.log("No design-system adoption issues found.")
|
|
111
|
-
return
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
for (const item of report.diagnostics) {
|
|
115
|
-
const location = item.file
|
|
116
|
-
? ` ${item.file}${item.line ? `:${item.line}` : ""}`
|
|
117
|
-
: ""
|
|
118
|
-
console.log(`${item.severity.toUpperCase()} ${item.code}${location}`)
|
|
119
|
-
console.log(` ${item.message}`)
|
|
120
|
-
console.log(` ${item.recommendation}`)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
console.log(
|
|
124
|
-
`${report.summary.errors} errors, ${report.summary.warnings} warnings, ${report.summary.info} info.`
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function inspectReceiptVersion(receipt, registryVersion, diagnostics) {
|
|
129
|
-
if (receipt.schemaVersion !== 1 || typeof receipt.components !== "object") {
|
|
130
|
-
diagnostics.push(
|
|
131
|
-
diagnostic({
|
|
132
|
-
code: "install-receipt-invalid",
|
|
133
|
-
file: RECEIPT_FILE,
|
|
134
|
-
message: "The Banhaten install receipt has an unsupported shape.",
|
|
135
|
-
recommendation: "Re-run an explicit `banhaten add <component> --force` after reviewing local changes.",
|
|
136
|
-
severity: "error",
|
|
137
|
-
})
|
|
138
|
-
)
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (receipt.registryVersion !== registryVersion) {
|
|
143
|
-
diagnostics.push(
|
|
144
|
-
diagnostic({
|
|
145
|
-
code: "registry-version-drift",
|
|
146
|
-
file: RECEIPT_FILE,
|
|
147
|
-
message: `Installed registry version ${receipt.registryVersion ?? "unknown"} differs from CLI version ${registryVersion}.`,
|
|
148
|
-
recommendation: "Run `banhaten diff`, then update reviewed components with `banhaten update`.",
|
|
149
|
-
severity: "warning",
|
|
150
|
-
})
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function inspectManagedFiles(projectRoot, receipt, diagnostics) {
|
|
156
|
-
for (const [target, record] of Object.entries(receipt.managedFiles || {})) {
|
|
157
|
-
const resolved = path.resolve(projectRoot, target)
|
|
158
|
-
const relative = path.relative(projectRoot, resolved)
|
|
159
|
-
|
|
160
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
161
|
-
diagnostics.push(
|
|
162
|
-
diagnostic({
|
|
163
|
-
code: "managed-target-unsafe",
|
|
164
|
-
file: target,
|
|
165
|
-
message: "The install receipt points outside the project.",
|
|
166
|
-
recommendation: "Remove the unsafe receipt entry and re-run `banhaten init`.",
|
|
167
|
-
severity: "error",
|
|
168
|
-
})
|
|
169
|
-
)
|
|
170
|
-
continue
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
let content
|
|
174
|
-
try {
|
|
175
|
-
content = await fs.readFile(resolved)
|
|
176
|
-
} catch (error) {
|
|
177
|
-
if (error.code === "ENOENT") {
|
|
178
|
-
diagnostics.push(
|
|
179
|
-
diagnostic({
|
|
180
|
-
code: "managed-file-missing",
|
|
181
|
-
file: normalizePath(relative),
|
|
182
|
-
message: `The ${record.owner || "Banhaten"}-managed file is missing.`,
|
|
183
|
-
recommendation: "Run `banhaten diff`, then restore it with `banhaten update`.",
|
|
184
|
-
severity: "error",
|
|
185
|
-
})
|
|
186
|
-
)
|
|
187
|
-
continue
|
|
188
|
-
}
|
|
189
|
-
throw error
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const actualHash = createHash("sha256").update(content).digest("hex")
|
|
193
|
-
if (record.sha256 && actualHash !== record.sha256) {
|
|
194
|
-
diagnostics.push(
|
|
195
|
-
diagnostic({
|
|
196
|
-
code: "managed-file-modified",
|
|
197
|
-
file: normalizePath(relative),
|
|
198
|
-
message: `The ${record.owner || "Banhaten"}-managed file differs from its install receipt.`,
|
|
199
|
-
recommendation: "Run `banhaten diff` and either keep the intentional extension or update the registry copy after review.",
|
|
200
|
-
severity: "warning",
|
|
201
|
-
})
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function inspectSourceFile(projectRoot, filePath, diagnostics) {
|
|
208
|
-
const source = await fs.readFile(filePath, "utf8")
|
|
209
|
-
if (source.length > 1024 * 1024) return
|
|
210
|
-
const file = normalizePath(path.relative(projectRoot, filePath))
|
|
211
|
-
|
|
212
|
-
for (const match of source.matchAll(BANHATEN_CONTROL_TAG)) {
|
|
213
|
-
const [tagSource, tagName] = match
|
|
214
|
-
const line = getLine(source, match.index)
|
|
215
|
-
|
|
216
|
-
for (const attribute of tagSource.matchAll(GEOMETRY_ATTRIBUTE)) {
|
|
217
|
-
const classes = attribute[1] ?? attribute[2] ?? ""
|
|
218
|
-
if (!RAW_GEOMETRY_CLASS.test(classes)) continue
|
|
219
|
-
diagnostics.push(
|
|
220
|
-
diagnostic({
|
|
221
|
-
code: "raw-control-geometry",
|
|
222
|
-
file,
|
|
223
|
-
line,
|
|
224
|
-
message: `${tagName} overrides control geometry with ${JSON.stringify(classes)}.`,
|
|
225
|
-
recommendation: "Use density for height and component tokens for padding, radius, and typography; keep classes for width and layout only.",
|
|
226
|
-
severity: "warning",
|
|
227
|
-
})
|
|
228
|
-
)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (tagName === "Input") {
|
|
232
|
-
if (hasTruthyBooleanProp(tagSource, "hasLeadingIcon") &&
|
|
233
|
-
!hasProp(tagSource, "leadingIcon") &&
|
|
234
|
-
!hasProp(tagSource, "showAtSign")) {
|
|
235
|
-
diagnostics.push(
|
|
236
|
-
diagnostic({
|
|
237
|
-
code: "ambiguous-input-leading-icon",
|
|
238
|
-
file,
|
|
239
|
-
line,
|
|
240
|
-
message: "Input uses the deprecated hasLeadingIcon flag without an explicit icon.",
|
|
241
|
-
recommendation: "Pass leadingIcon={<Icon />} or use showAtSign for the literal @ affordance.",
|
|
242
|
-
severity: "warning",
|
|
243
|
-
})
|
|
244
|
-
)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (hasTruthyBooleanProp(tagSource, "hasInformationIcon") &&
|
|
248
|
-
!hasProp(tagSource, "trailingIcon") &&
|
|
249
|
-
!hasProp(tagSource, "showInfo")) {
|
|
250
|
-
diagnostics.push(
|
|
251
|
-
diagnostic({
|
|
252
|
-
code: "ambiguous-input-information-icon",
|
|
253
|
-
file,
|
|
254
|
-
line,
|
|
255
|
-
message: "Input uses the deprecated hasInformationIcon flag without an explicit icon.",
|
|
256
|
-
recommendation: "Pass trailingIcon={<Icon />} or use showInfo for the literal information affordance.",
|
|
257
|
-
severity: "warning",
|
|
258
|
-
})
|
|
259
|
-
)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (hasProp(tagSource, "shortcut") && !hasProp(tagSource, "shortcutKeys")) {
|
|
263
|
-
diagnostics.push(
|
|
264
|
-
diagnostic({
|
|
265
|
-
code: "legacy-input-shortcut",
|
|
266
|
-
file,
|
|
267
|
-
line,
|
|
268
|
-
message: "Input uses a single legacy shortcut keycap.",
|
|
269
|
-
recommendation: "Use shortcutKeys={[\"Mod\", \"K\"]} for platform-aware, multi-key rendering.",
|
|
270
|
-
severity: "warning",
|
|
271
|
-
})
|
|
272
|
-
)
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
inspectNativeTags(source, file, diagnostics)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function inspectNativeTags(source, file, diagnostics) {
|
|
281
|
-
for (const match of source.matchAll(/<select(?=\s|>)[\s\S]*?>/g)) {
|
|
282
|
-
diagnostics.push(
|
|
283
|
-
diagnostic({
|
|
284
|
-
code: "native-select-control",
|
|
285
|
-
file,
|
|
286
|
-
line: getLine(source, match.index),
|
|
287
|
-
message: "A native select bypasses the Banhaten Select contract.",
|
|
288
|
-
recommendation: "Use Select for product UI, unless native platform behavior is an explicit requirement.",
|
|
289
|
-
severity: "warning",
|
|
290
|
-
})
|
|
291
|
-
)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
for (const match of source.matchAll(/<input(?=\s|>)[\s\S]*?>/g)) {
|
|
295
|
-
if (!/\btype\s*=\s*["']date["']/.test(match[0])) continue
|
|
296
|
-
diagnostics.push(
|
|
297
|
-
diagnostic({
|
|
298
|
-
code: "native-date-control",
|
|
299
|
-
file,
|
|
300
|
-
line: getLine(source, match.index),
|
|
301
|
-
message: "A native date input bypasses the Banhaten calendar and density contracts.",
|
|
302
|
-
recommendation: "Use DatePicker or DateRangePicker.",
|
|
303
|
-
severity: "warning",
|
|
304
|
-
})
|
|
305
|
-
)
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async function collectSourceFiles(projectRoot) {
|
|
310
|
-
const files = []
|
|
311
|
-
await visit(projectRoot)
|
|
312
|
-
return files.sort()
|
|
313
|
-
|
|
314
|
-
async function visit(directory) {
|
|
315
|
-
const entries = await fs.readdir(directory, { withFileTypes: true })
|
|
316
|
-
for (const entry of entries) {
|
|
317
|
-
if (entry.name.startsWith(".") && entry.isDirectory()) {
|
|
318
|
-
if (IGNORED_DIRECTORIES.has(entry.name)) continue
|
|
319
|
-
}
|
|
320
|
-
if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) continue
|
|
321
|
-
|
|
322
|
-
const target = path.join(directory, entry.name)
|
|
323
|
-
if (entry.isDirectory()) {
|
|
324
|
-
await visit(target)
|
|
325
|
-
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
326
|
-
files.push(target)
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function hasProp(tagSource, prop) {
|
|
333
|
-
return new RegExp(`\\b${prop}(?:\\s*=|(?=\\s|\\/?>))`).test(tagSource)
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function hasTruthyBooleanProp(tagSource, prop) {
|
|
337
|
-
return new RegExp(
|
|
338
|
-
`\\b${prop}(?:\\s*=\\s*\\{\\s*true\\s*\\})?(?=\\s|\\/?>)`
|
|
339
|
-
).test(tagSource)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function getLine(source, index = 0) {
|
|
343
|
-
return source.slice(0, index).split("\n").length
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function diagnostic({ code, file, line, message, recommendation, severity }) {
|
|
347
|
-
return {
|
|
348
|
-
code,
|
|
349
|
-
severity,
|
|
350
|
-
...(file ? { file } : {}),
|
|
351
|
-
...(line ? { line } : {}),
|
|
352
|
-
message,
|
|
353
|
-
recommendation,
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function compareDiagnostics(left, right) {
|
|
358
|
-
const severityOrder = { error: 0, warning: 1, info: 2 }
|
|
359
|
-
return (
|
|
360
|
-
severityOrder[left.severity] - severityOrder[right.severity] ||
|
|
361
|
-
(left.file || "").localeCompare(right.file || "") ||
|
|
362
|
-
(left.line || 0) - (right.line || 0) ||
|
|
363
|
-
left.code.localeCompare(right.code)
|
|
364
|
-
)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function normalizePath(value) {
|
|
368
|
-
return value.replaceAll("\\", "/")
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async function exists(filePath) {
|
|
372
|
-
try {
|
|
373
|
-
await fs.access(filePath)
|
|
374
|
-
return true
|
|
375
|
-
} catch {
|
|
376
|
-
return false
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async function readJsonIfExists(filePath, diagnostics, projectRoot) {
|
|
381
|
-
try {
|
|
382
|
-
return JSON.parse(await fs.readFile(filePath, "utf8"))
|
|
383
|
-
} catch (error) {
|
|
384
|
-
if (error.code === "ENOENT") return null
|
|
385
|
-
if (error instanceof SyntaxError) {
|
|
386
|
-
diagnostics.push(
|
|
387
|
-
diagnostic({
|
|
388
|
-
code: "install-receipt-invalid-json",
|
|
389
|
-
file: normalizePath(path.relative(projectRoot, filePath)),
|
|
390
|
-
message: "The Banhaten install receipt is not valid JSON.",
|
|
391
|
-
recommendation: "Review the file, then rebuild it with an explicit `banhaten add <component> --force`.",
|
|
392
|
-
severity: "error",
|
|
393
|
-
})
|
|
394
|
-
)
|
|
395
|
-
return null
|
|
396
|
-
}
|
|
397
|
-
throw error
|
|
398
|
-
}
|
|
399
|
-
}
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import fs from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
const RECEIPT_FILE = ".banhaten/installed.json"
|
|
6
|
+
const GUIDANCE_FILE = ".banhaten/USAGE.md"
|
|
7
|
+
const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"])
|
|
8
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
9
|
+
".banhaten",
|
|
10
|
+
".git",
|
|
11
|
+
".next",
|
|
12
|
+
".nuxt",
|
|
13
|
+
".output",
|
|
14
|
+
".turbo",
|
|
15
|
+
".vercel",
|
|
16
|
+
"build",
|
|
17
|
+
"coverage",
|
|
18
|
+
"dist",
|
|
19
|
+
"node_modules",
|
|
20
|
+
"out",
|
|
21
|
+
])
|
|
22
|
+
const BANHATEN_CONTROL_TAG =
|
|
23
|
+
/<(Input|Select|DatePicker|DateRangePicker|Button|ButtonGroup|ToggleGroup|SegmentedControl|ToolbarSearch|ToolbarSelect|ToolbarButton)(?=\s|\/?>)[\s\S]*?>/g
|
|
24
|
+
const GEOMETRY_ATTRIBUTE =
|
|
25
|
+
/(?:className|controlClassName|inputClassName|surfaceClassName|triggerClassName)\s*=\s*(?:["']([^"']*)["']|\{\s*["']([^"']*)["']\s*\})/g
|
|
26
|
+
const RAW_GEOMETRY_CLASS =
|
|
27
|
+
/(?:^|\s)(?:h-|min-h-|max-h-|p(?:x|y|s|e|t|r|b|l)?-|rounded(?:-|$)|text-\[)/
|
|
28
|
+
|
|
29
|
+
export async function inspectBanhatenProject({ cwd, registryVersion }) {
|
|
30
|
+
const projectRoot = path.resolve(cwd)
|
|
31
|
+
const diagnostics = []
|
|
32
|
+
const receiptPath = path.join(projectRoot, RECEIPT_FILE)
|
|
33
|
+
const guidancePath = path.join(projectRoot, GUIDANCE_FILE)
|
|
34
|
+
const receipt = await readJsonIfExists(receiptPath, diagnostics, projectRoot)
|
|
35
|
+
|
|
36
|
+
if (!receipt) {
|
|
37
|
+
diagnostics.push(
|
|
38
|
+
diagnostic({
|
|
39
|
+
code: "setup-not-initialized",
|
|
40
|
+
message: "No Banhaten install receipt was found.",
|
|
41
|
+
recommendation: "Run `banhaten init` before installing components.",
|
|
42
|
+
severity: "warning",
|
|
43
|
+
})
|
|
44
|
+
)
|
|
45
|
+
} else {
|
|
46
|
+
inspectReceiptVersion(receipt, registryVersion, diagnostics)
|
|
47
|
+
await inspectManagedFiles(projectRoot, receipt, diagnostics)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!(await exists(guidancePath))) {
|
|
51
|
+
diagnostics.push(
|
|
52
|
+
diagnostic({
|
|
53
|
+
code: "agent-guidance-missing",
|
|
54
|
+
file: GUIDANCE_FILE,
|
|
55
|
+
message: "The installed coding-agent usage rules are missing.",
|
|
56
|
+
recommendation: "Run `banhaten init` to install .banhaten/USAGE.md.",
|
|
57
|
+
severity: "warning",
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const sourceFiles = await collectSourceFiles(projectRoot)
|
|
63
|
+
for (const filePath of sourceFiles) {
|
|
64
|
+
await inspectSourceFile(projectRoot, filePath, diagnostics)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
diagnostics.sort(compareDiagnostics)
|
|
68
|
+
const summary = diagnostics.reduce(
|
|
69
|
+
(counts, item) => {
|
|
70
|
+
const key =
|
|
71
|
+
item.severity === "error"
|
|
72
|
+
? "errors"
|
|
73
|
+
: item.severity === "warning"
|
|
74
|
+
? "warnings"
|
|
75
|
+
: "info"
|
|
76
|
+
counts[key] += 1
|
|
77
|
+
return counts
|
|
78
|
+
},
|
|
79
|
+
{ errors: 0, info: 0, warnings: 0 }
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
cwd: projectRoot,
|
|
84
|
+
registryVersion,
|
|
85
|
+
installedVersion: receipt?.registryVersion ?? null,
|
|
86
|
+
installedComponents: Object.keys(receipt?.components || {}).sort(),
|
|
87
|
+
scannedFiles: sourceFiles.length,
|
|
88
|
+
summary,
|
|
89
|
+
diagnostics,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function printBanhatenDoctor(report, { json = false, silent = false } = {}) {
|
|
94
|
+
if (json) {
|
|
95
|
+
console.log(JSON.stringify(report, null, 2))
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
if (silent) return
|
|
99
|
+
|
|
100
|
+
console.log("Banhaten doctor")
|
|
101
|
+
console.log(`Project: ${report.cwd}`)
|
|
102
|
+
console.log(
|
|
103
|
+
`Registry: ${report.installedVersion ?? "not installed"} -> ${report.registryVersion}`
|
|
104
|
+
)
|
|
105
|
+
console.log(
|
|
106
|
+
`Scanned ${report.scannedFiles} source files and ${report.installedComponents.length} installed components.`
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if (report.diagnostics.length === 0) {
|
|
110
|
+
console.log("No design-system adoption issues found.")
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const item of report.diagnostics) {
|
|
115
|
+
const location = item.file
|
|
116
|
+
? ` ${item.file}${item.line ? `:${item.line}` : ""}`
|
|
117
|
+
: ""
|
|
118
|
+
console.log(`${item.severity.toUpperCase()} ${item.code}${location}`)
|
|
119
|
+
console.log(` ${item.message}`)
|
|
120
|
+
console.log(` ${item.recommendation}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(
|
|
124
|
+
`${report.summary.errors} errors, ${report.summary.warnings} warnings, ${report.summary.info} info.`
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function inspectReceiptVersion(receipt, registryVersion, diagnostics) {
|
|
129
|
+
if (receipt.schemaVersion !== 1 || typeof receipt.components !== "object") {
|
|
130
|
+
diagnostics.push(
|
|
131
|
+
diagnostic({
|
|
132
|
+
code: "install-receipt-invalid",
|
|
133
|
+
file: RECEIPT_FILE,
|
|
134
|
+
message: "The Banhaten install receipt has an unsupported shape.",
|
|
135
|
+
recommendation: "Re-run an explicit `banhaten add <component> --force` after reviewing local changes.",
|
|
136
|
+
severity: "error",
|
|
137
|
+
})
|
|
138
|
+
)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (receipt.registryVersion !== registryVersion) {
|
|
143
|
+
diagnostics.push(
|
|
144
|
+
diagnostic({
|
|
145
|
+
code: "registry-version-drift",
|
|
146
|
+
file: RECEIPT_FILE,
|
|
147
|
+
message: `Installed registry version ${receipt.registryVersion ?? "unknown"} differs from CLI version ${registryVersion}.`,
|
|
148
|
+
recommendation: "Run `banhaten diff`, then update reviewed components with `banhaten update`.",
|
|
149
|
+
severity: "warning",
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function inspectManagedFiles(projectRoot, receipt, diagnostics) {
|
|
156
|
+
for (const [target, record] of Object.entries(receipt.managedFiles || {})) {
|
|
157
|
+
const resolved = path.resolve(projectRoot, target)
|
|
158
|
+
const relative = path.relative(projectRoot, resolved)
|
|
159
|
+
|
|
160
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
161
|
+
diagnostics.push(
|
|
162
|
+
diagnostic({
|
|
163
|
+
code: "managed-target-unsafe",
|
|
164
|
+
file: target,
|
|
165
|
+
message: "The install receipt points outside the project.",
|
|
166
|
+
recommendation: "Remove the unsafe receipt entry and re-run `banhaten init`.",
|
|
167
|
+
severity: "error",
|
|
168
|
+
})
|
|
169
|
+
)
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let content
|
|
174
|
+
try {
|
|
175
|
+
content = await fs.readFile(resolved)
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (error.code === "ENOENT") {
|
|
178
|
+
diagnostics.push(
|
|
179
|
+
diagnostic({
|
|
180
|
+
code: "managed-file-missing",
|
|
181
|
+
file: normalizePath(relative),
|
|
182
|
+
message: `The ${record.owner || "Banhaten"}-managed file is missing.`,
|
|
183
|
+
recommendation: "Run `banhaten diff`, then restore it with `banhaten update`.",
|
|
184
|
+
severity: "error",
|
|
185
|
+
})
|
|
186
|
+
)
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
throw error
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const actualHash = createHash("sha256").update(content).digest("hex")
|
|
193
|
+
if (record.sha256 && actualHash !== record.sha256) {
|
|
194
|
+
diagnostics.push(
|
|
195
|
+
diagnostic({
|
|
196
|
+
code: "managed-file-modified",
|
|
197
|
+
file: normalizePath(relative),
|
|
198
|
+
message: `The ${record.owner || "Banhaten"}-managed file differs from its install receipt.`,
|
|
199
|
+
recommendation: "Run `banhaten diff` and either keep the intentional extension or update the registry copy after review.",
|
|
200
|
+
severity: "warning",
|
|
201
|
+
})
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function inspectSourceFile(projectRoot, filePath, diagnostics) {
|
|
208
|
+
const source = await fs.readFile(filePath, "utf8")
|
|
209
|
+
if (source.length > 1024 * 1024) return
|
|
210
|
+
const file = normalizePath(path.relative(projectRoot, filePath))
|
|
211
|
+
|
|
212
|
+
for (const match of source.matchAll(BANHATEN_CONTROL_TAG)) {
|
|
213
|
+
const [tagSource, tagName] = match
|
|
214
|
+
const line = getLine(source, match.index)
|
|
215
|
+
|
|
216
|
+
for (const attribute of tagSource.matchAll(GEOMETRY_ATTRIBUTE)) {
|
|
217
|
+
const classes = attribute[1] ?? attribute[2] ?? ""
|
|
218
|
+
if (!RAW_GEOMETRY_CLASS.test(classes)) continue
|
|
219
|
+
diagnostics.push(
|
|
220
|
+
diagnostic({
|
|
221
|
+
code: "raw-control-geometry",
|
|
222
|
+
file,
|
|
223
|
+
line,
|
|
224
|
+
message: `${tagName} overrides control geometry with ${JSON.stringify(classes)}.`,
|
|
225
|
+
recommendation: "Use density for height and component tokens for padding, radius, and typography; keep classes for width and layout only.",
|
|
226
|
+
severity: "warning",
|
|
227
|
+
})
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (tagName === "Input") {
|
|
232
|
+
if (hasTruthyBooleanProp(tagSource, "hasLeadingIcon") &&
|
|
233
|
+
!hasProp(tagSource, "leadingIcon") &&
|
|
234
|
+
!hasProp(tagSource, "showAtSign")) {
|
|
235
|
+
diagnostics.push(
|
|
236
|
+
diagnostic({
|
|
237
|
+
code: "ambiguous-input-leading-icon",
|
|
238
|
+
file,
|
|
239
|
+
line,
|
|
240
|
+
message: "Input uses the deprecated hasLeadingIcon flag without an explicit icon.",
|
|
241
|
+
recommendation: "Pass leadingIcon={<Icon />} or use showAtSign for the literal @ affordance.",
|
|
242
|
+
severity: "warning",
|
|
243
|
+
})
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (hasTruthyBooleanProp(tagSource, "hasInformationIcon") &&
|
|
248
|
+
!hasProp(tagSource, "trailingIcon") &&
|
|
249
|
+
!hasProp(tagSource, "showInfo")) {
|
|
250
|
+
diagnostics.push(
|
|
251
|
+
diagnostic({
|
|
252
|
+
code: "ambiguous-input-information-icon",
|
|
253
|
+
file,
|
|
254
|
+
line,
|
|
255
|
+
message: "Input uses the deprecated hasInformationIcon flag without an explicit icon.",
|
|
256
|
+
recommendation: "Pass trailingIcon={<Icon />} or use showInfo for the literal information affordance.",
|
|
257
|
+
severity: "warning",
|
|
258
|
+
})
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (hasProp(tagSource, "shortcut") && !hasProp(tagSource, "shortcutKeys")) {
|
|
263
|
+
diagnostics.push(
|
|
264
|
+
diagnostic({
|
|
265
|
+
code: "legacy-input-shortcut",
|
|
266
|
+
file,
|
|
267
|
+
line,
|
|
268
|
+
message: "Input uses a single legacy shortcut keycap.",
|
|
269
|
+
recommendation: "Use shortcutKeys={[\"Mod\", \"K\"]} for platform-aware, multi-key rendering.",
|
|
270
|
+
severity: "warning",
|
|
271
|
+
})
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
inspectNativeTags(source, file, diagnostics)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function inspectNativeTags(source, file, diagnostics) {
|
|
281
|
+
for (const match of source.matchAll(/<select(?=\s|>)[\s\S]*?>/g)) {
|
|
282
|
+
diagnostics.push(
|
|
283
|
+
diagnostic({
|
|
284
|
+
code: "native-select-control",
|
|
285
|
+
file,
|
|
286
|
+
line: getLine(source, match.index),
|
|
287
|
+
message: "A native select bypasses the Banhaten Select contract.",
|
|
288
|
+
recommendation: "Use Select for product UI, unless native platform behavior is an explicit requirement.",
|
|
289
|
+
severity: "warning",
|
|
290
|
+
})
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const match of source.matchAll(/<input(?=\s|>)[\s\S]*?>/g)) {
|
|
295
|
+
if (!/\btype\s*=\s*["']date["']/.test(match[0])) continue
|
|
296
|
+
diagnostics.push(
|
|
297
|
+
diagnostic({
|
|
298
|
+
code: "native-date-control",
|
|
299
|
+
file,
|
|
300
|
+
line: getLine(source, match.index),
|
|
301
|
+
message: "A native date input bypasses the Banhaten calendar and density contracts.",
|
|
302
|
+
recommendation: "Use DatePicker or DateRangePicker.",
|
|
303
|
+
severity: "warning",
|
|
304
|
+
})
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function collectSourceFiles(projectRoot) {
|
|
310
|
+
const files = []
|
|
311
|
+
await visit(projectRoot)
|
|
312
|
+
return files.sort()
|
|
313
|
+
|
|
314
|
+
async function visit(directory) {
|
|
315
|
+
const entries = await fs.readdir(directory, { withFileTypes: true })
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
if (entry.name.startsWith(".") && entry.isDirectory()) {
|
|
318
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) continue
|
|
319
|
+
}
|
|
320
|
+
if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) continue
|
|
321
|
+
|
|
322
|
+
const target = path.join(directory, entry.name)
|
|
323
|
+
if (entry.isDirectory()) {
|
|
324
|
+
await visit(target)
|
|
325
|
+
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
326
|
+
files.push(target)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function hasProp(tagSource, prop) {
|
|
333
|
+
return new RegExp(`\\b${prop}(?:\\s*=|(?=\\s|\\/?>))`).test(tagSource)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function hasTruthyBooleanProp(tagSource, prop) {
|
|
337
|
+
return new RegExp(
|
|
338
|
+
`\\b${prop}(?:\\s*=\\s*\\{\\s*true\\s*\\})?(?=\\s|\\/?>)`
|
|
339
|
+
).test(tagSource)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getLine(source, index = 0) {
|
|
343
|
+
return source.slice(0, index).split("\n").length
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function diagnostic({ code, file, line, message, recommendation, severity }) {
|
|
347
|
+
return {
|
|
348
|
+
code,
|
|
349
|
+
severity,
|
|
350
|
+
...(file ? { file } : {}),
|
|
351
|
+
...(line ? { line } : {}),
|
|
352
|
+
message,
|
|
353
|
+
recommendation,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function compareDiagnostics(left, right) {
|
|
358
|
+
const severityOrder = { error: 0, warning: 1, info: 2 }
|
|
359
|
+
return (
|
|
360
|
+
severityOrder[left.severity] - severityOrder[right.severity] ||
|
|
361
|
+
(left.file || "").localeCompare(right.file || "") ||
|
|
362
|
+
(left.line || 0) - (right.line || 0) ||
|
|
363
|
+
left.code.localeCompare(right.code)
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function normalizePath(value) {
|
|
368
|
+
return value.replaceAll("\\", "/")
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function exists(filePath) {
|
|
372
|
+
try {
|
|
373
|
+
await fs.access(filePath)
|
|
374
|
+
return true
|
|
375
|
+
} catch {
|
|
376
|
+
return false
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function readJsonIfExists(filePath, diagnostics, projectRoot) {
|
|
381
|
+
try {
|
|
382
|
+
return JSON.parse(await fs.readFile(filePath, "utf8"))
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (error.code === "ENOENT") return null
|
|
385
|
+
if (error instanceof SyntaxError) {
|
|
386
|
+
diagnostics.push(
|
|
387
|
+
diagnostic({
|
|
388
|
+
code: "install-receipt-invalid-json",
|
|
389
|
+
file: normalizePath(path.relative(projectRoot, filePath)),
|
|
390
|
+
message: "The Banhaten install receipt is not valid JSON.",
|
|
391
|
+
recommendation: "Review the file, then rebuild it with an explicit `banhaten add <component> --force`.",
|
|
392
|
+
severity: "error",
|
|
393
|
+
})
|
|
394
|
+
)
|
|
395
|
+
return null
|
|
396
|
+
}
|
|
397
|
+
throw error
|
|
398
|
+
}
|
|
399
|
+
}
|