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
|
@@ -1,295 +1,302 @@
|
|
|
1
|
-
import fs from "node:fs/promises"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import process from "node:process"
|
|
4
|
-
|
|
5
|
-
let activeFileTransaction = null
|
|
6
|
-
let transactionFileCounter = 0
|
|
7
|
-
|
|
8
|
-
export async function runFileTransaction(action) {
|
|
9
|
-
if (activeFileTransaction) {
|
|
10
|
-
throw new Error("nested file transactions are not supported")
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const transaction = {
|
|
14
|
-
writes: new Map(),
|
|
15
|
-
preconditions: new Map(),
|
|
16
|
-
afterApply: [],
|
|
17
|
-
}
|
|
18
|
-
activeFileTransaction = transaction
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const result = await action()
|
|
22
|
-
await commitFileTransaction(transaction)
|
|
23
|
-
activeFileTransaction = null
|
|
24
|
-
|
|
25
|
-
for (const callback of transaction.afterApply) {
|
|
26
|
-
await callback()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return result
|
|
30
|
-
} catch (error) {
|
|
31
|
-
activeFileTransaction = null
|
|
32
|
-
throw error
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function deferUntilApplied(callback) {
|
|
37
|
-
if (!activeFileTransaction) {
|
|
38
|
-
return callback()
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
activeFileTransaction.afterApply.push(callback)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function commitFileTransaction(transaction) {
|
|
45
|
-
const staged = []
|
|
46
|
-
const committed = []
|
|
47
|
-
const createdDirectories = new Set()
|
|
48
|
-
let failed = false
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
await assertTransactionPreconditions(transaction)
|
|
52
|
-
|
|
53
|
-
for (const entry of transaction.writes.values()) {
|
|
54
|
-
await ensureDirectory(
|
|
55
|
-
path.dirname(entry.targetPath),
|
|
56
|
-
createdDirectories
|
|
57
|
-
)
|
|
58
|
-
const tempPath = `${entry.targetPath}.banhaten-${process.pid}-${transactionFileCounter += 1}.tmp`
|
|
59
|
-
const previous = await readFileSnapshot(entry.targetPath)
|
|
60
|
-
|
|
61
|
-
await fs.writeFile(tempPath, entry.content)
|
|
62
|
-
if (previous?.mode !== undefined) {
|
|
63
|
-
await fs.chmod(tempPath, previous.mode)
|
|
64
|
-
}
|
|
65
|
-
staged.push({ ...entry, tempPath, previous })
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
await assertTransactionPreconditions(transaction)
|
|
69
|
-
|
|
70
|
-
for (const entry of staged) {
|
|
71
|
-
await fs.rename(entry.tempPath, entry.targetPath)
|
|
72
|
-
committed.push(entry)
|
|
73
|
-
}
|
|
74
|
-
} catch (error) {
|
|
75
|
-
failed = true
|
|
76
|
-
for (const entry of [...committed].reverse()) {
|
|
77
|
-
await restoreFileSnapshot(entry.targetPath, entry.previous)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
throw new Error(`file transaction failed and was rolled back: ${error.message}`)
|
|
81
|
-
} finally {
|
|
82
|
-
await Promise.all(
|
|
83
|
-
staged.map((entry) => fs.rm(entry.tempPath, { force: true }).catch(() => {}))
|
|
84
|
-
)
|
|
85
|
-
if (failed) {
|
|
86
|
-
await removeCreatedDirectories(createdDirectories)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function ensureDirectory(targetPath, createdDirectories) {
|
|
92
|
-
const missing = []
|
|
93
|
-
let directory = targetPath
|
|
94
|
-
|
|
95
|
-
while (!(await exists(directory))) {
|
|
96
|
-
missing.push(directory)
|
|
97
|
-
const parent = path.dirname(directory)
|
|
98
|
-
if (parent === directory) break
|
|
99
|
-
directory = parent
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
for (const missingDirectory of missing.reverse()) {
|
|
103
|
-
try {
|
|
104
|
-
await fs.mkdir(missingDirectory)
|
|
105
|
-
createdDirectories.add(missingDirectory)
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (error.code !== "EEXIST") throw error
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function removeCreatedDirectories(createdDirectories) {
|
|
113
|
-
for (const directory of [...createdDirectories].reverse()) {
|
|
114
|
-
await fs.rmdir(directory).catch(() => {})
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function assertTransactionPreconditions(transaction) {
|
|
119
|
-
for (const [targetPath, expectedCurrent] of transaction.preconditions) {
|
|
120
|
-
const current = await readFileSnapshot(targetPath)
|
|
121
|
-
assertExpectedCurrent(targetPath, current?.content ?? null, expectedCurrent)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function readFileSnapshot(targetPath) {
|
|
126
|
-
try {
|
|
127
|
-
const [content, stats] = await Promise.all([
|
|
128
|
-
fs.readFile(targetPath),
|
|
129
|
-
fs.stat(targetPath),
|
|
130
|
-
])
|
|
131
|
-
return { content, mode: stats.mode }
|
|
132
|
-
} catch (error) {
|
|
133
|
-
if (error.code === "ENOENT") return null
|
|
134
|
-
throw error
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function restoreFileSnapshot(targetPath, snapshot) {
|
|
139
|
-
if (!snapshot) {
|
|
140
|
-
await fs.rm(targetPath, { force: true })
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const tempPath = `${targetPath}.banhaten-rollback-${process.pid}-${transactionFileCounter += 1}.tmp`
|
|
145
|
-
await fs.writeFile(tempPath, snapshot.content)
|
|
146
|
-
await fs.chmod(tempPath, snapshot.mode)
|
|
147
|
-
await fs.rename(tempPath, targetPath)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export async function writeFile(targetPath, content, options) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return "would
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
1
|
+
import fs from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import process from "node:process"
|
|
4
|
+
|
|
5
|
+
let activeFileTransaction = null
|
|
6
|
+
let transactionFileCounter = 0
|
|
7
|
+
|
|
8
|
+
export async function runFileTransaction(action) {
|
|
9
|
+
if (activeFileTransaction) {
|
|
10
|
+
throw new Error("nested file transactions are not supported")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const transaction = {
|
|
14
|
+
writes: new Map(),
|
|
15
|
+
preconditions: new Map(),
|
|
16
|
+
afterApply: [],
|
|
17
|
+
}
|
|
18
|
+
activeFileTransaction = transaction
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = await action()
|
|
22
|
+
await commitFileTransaction(transaction)
|
|
23
|
+
activeFileTransaction = null
|
|
24
|
+
|
|
25
|
+
for (const callback of transaction.afterApply) {
|
|
26
|
+
await callback()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return result
|
|
30
|
+
} catch (error) {
|
|
31
|
+
activeFileTransaction = null
|
|
32
|
+
throw error
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function deferUntilApplied(callback) {
|
|
37
|
+
if (!activeFileTransaction) {
|
|
38
|
+
return callback()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
activeFileTransaction.afterApply.push(callback)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function commitFileTransaction(transaction) {
|
|
45
|
+
const staged = []
|
|
46
|
+
const committed = []
|
|
47
|
+
const createdDirectories = new Set()
|
|
48
|
+
let failed = false
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await assertTransactionPreconditions(transaction)
|
|
52
|
+
|
|
53
|
+
for (const entry of transaction.writes.values()) {
|
|
54
|
+
await ensureDirectory(
|
|
55
|
+
path.dirname(entry.targetPath),
|
|
56
|
+
createdDirectories
|
|
57
|
+
)
|
|
58
|
+
const tempPath = `${entry.targetPath}.banhaten-${process.pid}-${transactionFileCounter += 1}.tmp`
|
|
59
|
+
const previous = await readFileSnapshot(entry.targetPath)
|
|
60
|
+
|
|
61
|
+
await fs.writeFile(tempPath, entry.content)
|
|
62
|
+
if (previous?.mode !== undefined) {
|
|
63
|
+
await fs.chmod(tempPath, previous.mode)
|
|
64
|
+
}
|
|
65
|
+
staged.push({ ...entry, tempPath, previous })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await assertTransactionPreconditions(transaction)
|
|
69
|
+
|
|
70
|
+
for (const entry of staged) {
|
|
71
|
+
await fs.rename(entry.tempPath, entry.targetPath)
|
|
72
|
+
committed.push(entry)
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
failed = true
|
|
76
|
+
for (const entry of [...committed].reverse()) {
|
|
77
|
+
await restoreFileSnapshot(entry.targetPath, entry.previous)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw new Error(`file transaction failed and was rolled back: ${error.message}`)
|
|
81
|
+
} finally {
|
|
82
|
+
await Promise.all(
|
|
83
|
+
staged.map((entry) => fs.rm(entry.tempPath, { force: true }).catch(() => {}))
|
|
84
|
+
)
|
|
85
|
+
if (failed) {
|
|
86
|
+
await removeCreatedDirectories(createdDirectories)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function ensureDirectory(targetPath, createdDirectories) {
|
|
92
|
+
const missing = []
|
|
93
|
+
let directory = targetPath
|
|
94
|
+
|
|
95
|
+
while (!(await exists(directory))) {
|
|
96
|
+
missing.push(directory)
|
|
97
|
+
const parent = path.dirname(directory)
|
|
98
|
+
if (parent === directory) break
|
|
99
|
+
directory = parent
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const missingDirectory of missing.reverse()) {
|
|
103
|
+
try {
|
|
104
|
+
await fs.mkdir(missingDirectory)
|
|
105
|
+
createdDirectories.add(missingDirectory)
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error.code !== "EEXIST") throw error
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function removeCreatedDirectories(createdDirectories) {
|
|
113
|
+
for (const directory of [...createdDirectories].reverse()) {
|
|
114
|
+
await fs.rmdir(directory).catch(() => {})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function assertTransactionPreconditions(transaction) {
|
|
119
|
+
for (const [targetPath, expectedCurrent] of transaction.preconditions) {
|
|
120
|
+
const current = await readFileSnapshot(targetPath)
|
|
121
|
+
assertExpectedCurrent(targetPath, current?.content ?? null, expectedCurrent)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readFileSnapshot(targetPath) {
|
|
126
|
+
try {
|
|
127
|
+
const [content, stats] = await Promise.all([
|
|
128
|
+
fs.readFile(targetPath),
|
|
129
|
+
fs.stat(targetPath),
|
|
130
|
+
])
|
|
131
|
+
return { content, mode: stats.mode }
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error.code === "ENOENT") return null
|
|
134
|
+
throw error
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function restoreFileSnapshot(targetPath, snapshot) {
|
|
139
|
+
if (!snapshot) {
|
|
140
|
+
await fs.rm(targetPath, { force: true })
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const tempPath = `${targetPath}.banhaten-rollback-${process.pid}-${transactionFileCounter += 1}.tmp`
|
|
145
|
+
await fs.writeFile(tempPath, snapshot.content)
|
|
146
|
+
await fs.chmod(tempPath, snapshot.mode)
|
|
147
|
+
await fs.rename(tempPath, targetPath)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function writeFile(targetPath, content, options) {
|
|
151
|
+
if (!activeFileTransaction) {
|
|
152
|
+
throw new Error(`refusing non-transactional write to ${targetPath}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const current = await readFileForUpdate(targetPath)
|
|
156
|
+
const alreadyExists = current !== null
|
|
157
|
+
const isBinary = Buffer.isBuffer(content)
|
|
158
|
+
const contentBuffer = isBinary ? content : Buffer.from(content, "utf8")
|
|
159
|
+
const isEqual = current?.equals(contentBuffer) || false
|
|
160
|
+
if (options.expectedCurrent !== undefined) {
|
|
161
|
+
const expectedCurrent = toExpectedBuffer(options.expectedCurrent)
|
|
162
|
+
assertExpectedCurrent(targetPath, current, expectedCurrent)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (options.dryRun) {
|
|
166
|
+
if (!alreadyExists) return "would write"
|
|
167
|
+
if (isEqual) return "would keep"
|
|
168
|
+
|
|
169
|
+
return "would update"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (alreadyExists && isEqual) {
|
|
173
|
+
return "kept"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const existingPlan = activeFileTransaction.writes.get(targetPath)
|
|
177
|
+
if (existingPlan && !existingPlan.content.equals(contentBuffer)) {
|
|
178
|
+
throw new Error(`conflicting planned writes for ${targetPath}`)
|
|
179
|
+
}
|
|
180
|
+
activeFileTransaction.writes.set(targetPath, {
|
|
181
|
+
targetPath,
|
|
182
|
+
content: contentBuffer,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
return alreadyExists ? "updated" : "wrote"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Pin the bytes (or absence) used for ownership checks and read/modify/write
|
|
189
|
+
// planning. Later reads and writes must agree with this first observation.
|
|
190
|
+
export async function readFileForUpdate(targetPath) {
|
|
191
|
+
const current = await readFileContent(targetPath)
|
|
192
|
+
registerPrecondition(targetPath, current)
|
|
193
|
+
return current
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function assertFileUnchanged(targetPath, expectedCurrent) {
|
|
197
|
+
if (!activeFileTransaction) {
|
|
198
|
+
throw new Error(`refusing non-transactional precondition for ${targetPath}`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const expectedBuffer = toExpectedBuffer(expectedCurrent)
|
|
202
|
+
const current = await readFileContent(targetPath)
|
|
203
|
+
assertExpectedCurrent(targetPath, current, expectedBuffer)
|
|
204
|
+
registerPrecondition(targetPath, expectedBuffer)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function registerPrecondition(targetPath, expectedCurrent) {
|
|
208
|
+
if (!activeFileTransaction) {
|
|
209
|
+
throw new Error(`refusing non-transactional precondition for ${targetPath}`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const existing = activeFileTransaction.preconditions.get(targetPath)
|
|
213
|
+
if (existing !== undefined) {
|
|
214
|
+
assertExpectedCurrent(targetPath, existing, expectedCurrent)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
activeFileTransaction.preconditions.set(targetPath, toExpectedBuffer(expectedCurrent))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function assertExpectedCurrent(targetPath, current, expected) {
|
|
221
|
+
const matches = expected === null
|
|
222
|
+
? current === null
|
|
223
|
+
: current?.equals(expected) || false
|
|
224
|
+
if (!matches) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`${targetPath} changed while Banhaten was preparing the install plan`
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function toExpectedBuffer(expectedCurrent) {
|
|
232
|
+
return expectedCurrent === null
|
|
233
|
+
? null
|
|
234
|
+
: Buffer.from(expectedCurrent)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function readFileContent(targetPath) {
|
|
238
|
+
try {
|
|
239
|
+
return await fs.readFile(targetPath)
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error.code === "ENOENT") return null
|
|
242
|
+
throw error
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function formatUnifiedDiff(fileLabel, currentContent, nextContent) {
|
|
247
|
+
const current = toDiffLines(currentContent)
|
|
248
|
+
const next = toDiffLines(nextContent)
|
|
249
|
+
let prefix = 0
|
|
250
|
+
|
|
251
|
+
while (
|
|
252
|
+
prefix < current.length &&
|
|
253
|
+
prefix < next.length &&
|
|
254
|
+
current[prefix] === next[prefix]
|
|
255
|
+
) {
|
|
256
|
+
prefix += 1
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let currentSuffix = current.length
|
|
260
|
+
let nextSuffix = next.length
|
|
261
|
+
while (
|
|
262
|
+
currentSuffix > prefix &&
|
|
263
|
+
nextSuffix > prefix &&
|
|
264
|
+
current[currentSuffix - 1] === next[nextSuffix - 1]
|
|
265
|
+
) {
|
|
266
|
+
currentSuffix -= 1
|
|
267
|
+
nextSuffix -= 1
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const context = 3
|
|
271
|
+
const start = Math.max(0, prefix - context)
|
|
272
|
+
const currentEnd = Math.min(current.length, currentSuffix + context)
|
|
273
|
+
const nextEnd = Math.min(next.length, nextSuffix + context)
|
|
274
|
+
const currentCount = currentEnd - start
|
|
275
|
+
const nextCount = nextEnd - start
|
|
276
|
+
const output = [
|
|
277
|
+
`--- a/${fileLabel}`,
|
|
278
|
+
`+++ b/${fileLabel}`,
|
|
279
|
+
`@@ -${start + 1},${currentCount} +${start + 1},${nextCount} @@`,
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
for (const line of current.slice(start, prefix)) output.push(` ${line}`)
|
|
283
|
+
for (const line of current.slice(prefix, currentSuffix)) output.push(`-${line}`)
|
|
284
|
+
for (const line of next.slice(prefix, nextSuffix)) output.push(`+${line}`)
|
|
285
|
+
for (const line of current.slice(currentSuffix, currentEnd)) output.push(` ${line}`)
|
|
286
|
+
|
|
287
|
+
return output.join("\n")
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function toDiffLines(content) {
|
|
291
|
+
if (Buffer.isBuffer(content)) return [`Binary content (${content.byteLength} bytes)`]
|
|
292
|
+
return String(content).replace(/\r\n/g, "\n").split("\n")
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function exists(filePath) {
|
|
296
|
+
try {
|
|
297
|
+
await fs.access(filePath)
|
|
298
|
+
return true
|
|
299
|
+
} catch {
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
302
|
+
}
|