@citadel-labs/beads-ui 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/worktrees/agent-a6ab297c/LICENSE +21 -0
- package/.claude/worktrees/agent-a6ab297c/README.md +71 -0
- package/.claude/worktrees/agent-a6ab297c/bin/beads-board.js +183 -0
- package/.claude/worktrees/agent-a6ab297c/package.json +16 -0
- package/.claude/worktrees/agent-a6ab297c/server/dist/assets/index-B8Dp1QU-.js +53 -0
- package/.claude/worktrees/agent-a6ab297c/server/dist/assets/index-Cx3XpFYM.css +1 -0
- package/.claude/worktrees/agent-a6ab297c/server/dist/index.html +14 -0
- package/.claude/worktrees/agent-a6ab297c/server/dist/vite.svg +1 -0
- package/.claude/worktrees/agent-a6ab297c/server/index.js +242 -0
- package/.claude/worktrees/agent-a8b08b0a/LICENSE +21 -0
- package/.claude/worktrees/agent-a8b08b0a/README.md +71 -0
- package/.claude/worktrees/agent-a8b08b0a/bin/beads-board.js +183 -0
- package/.claude/worktrees/agent-a8b08b0a/package.json +16 -0
- package/.claude/worktrees/agent-a8b08b0a/server/dist/assets/index-CidSj3mC.css +1 -0
- package/.claude/worktrees/agent-a8b08b0a/server/dist/assets/index-tKlN_npR.js +53 -0
- package/.claude/worktrees/agent-a8b08b0a/server/dist/index.html +14 -0
- package/.claude/worktrees/agent-a8b08b0a/server/dist/vite.svg +1 -0
- package/.claude/worktrees/agent-a8b08b0a/server/index.js +242 -0
- package/package.json +1 -1
- package/screenshot-badge-error.png +0 -0
- package/screenshot-card-modal.png +0 -0
- package/screenshot-collapsed.png +0 -0
- package/screenshot-collapsed2.png +0 -0
- package/screenshot-hover-commit.png +0 -0
- package/screenshot-modal-from-badge.png +0 -0
- package/screenshot-overview.png +0 -0
- package/screenshot-polished-modal.png +0 -0
- package/screenshot-tooltip-hover.png +0 -0
- package/server/dist/assets/index-BaFX4Ey_.css +1 -0
- package/server/dist/assets/index-CkFv0lE0.js +57 -0
- package/server/dist/index.html +2 -2
- package/server/index.js +18 -7
- package/server/dist/assets/index-C62XoEh1.js +0 -49
- package/server/dist/assets/index-DXoLXViw.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-lg:32rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-xs:.125rem;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.\@container\/card-header{container:card-header/inline-size}.pointer-events-none{pointer-events:none}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.top-0{top:calc(var(--spacing) * 0)}.top-4{top:calc(var(--spacing) * 4)}.top-\[50\%\]{top:50%}.right-2{right:calc(var(--spacing) * 2)}.right-4{right:calc(var(--spacing) * 4)}.left-\[50\%\]{left:50%}.z-10{z-index:10}.z-50{z-index:50}.col-start-2{grid-column-start:2}.row-span-2{grid-row:span 2/span 2}.row-start-1{grid-row-start:1}.-mx-1{margin-inline:calc(var(--spacing) * -1)}.mx-0\.5{margin-inline:calc(var(--spacing) * .5)}.mx-3{margin-inline:calc(var(--spacing) * 3)}.my-1{margin-block:calc(var(--spacing) * 1)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.size-2\.5{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-full{width:100%;height:100%}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-4{height:calc(var(--spacing) * 4)}.h-9{height:calc(var(--spacing) * 9)}.h-16{height:calc(var(--spacing) * 16)}.h-24{height:calc(var(--spacing) * 24)}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-\(--radix-select-content-available-height\){max-height:var(--radix-select-content-available-height)}.max-h-\[80vh\]{max-height:80vh}.min-h-0{min-height:calc(var(--spacing) * 0)}.w-1{width:calc(var(--spacing) * 1)}.w-3{width:calc(var(--spacing) * 3)}.w-4{width:calc(var(--spacing) * 4)}.w-fit{width:fit-content}.w-full{width:100%}.max-w-\[calc\(100\%-2rem\)\]{max-width:calc(100% - 2rem)}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[8rem\]{min-width:8rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.origin-\(--radix-select-content-transform-origin\){transform-origin:var(--radix-select-content-transform-origin)}.origin-\(--radix-tooltip-content-transform-origin\){transform-origin:var(--radix-tooltip-content-transform-origin)}.translate-x-\[-50\%\]{--tw-translate-x:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-\[calc\(-50\%_-_2px\)\]{--tw-translate-y: calc(-50% - 2px) ;translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-45{rotate:45deg}.animate-pulse{animation:var(--animate-pulse)}.cursor-col-resize{cursor:col-resize}.cursor-default{cursor:default}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.scroll-my-1{scroll-margin-block:calc(var(--spacing) * 1)}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-rows-\[auto_auto\]{grid-template-rows:auto auto}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.self-start{align-self:flex-start}.justify-self-end{justify-self:flex-end}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-\[2px\]{border-radius:2px}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-xl{border-radius:calc(var(--radius) + 4px)}.rounded-xs{border-radius:var(--radius-xs)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-\[\#1f6feb\]{border-color:#1f6feb}.border-\[\#1f6feb\]\/40{border-color:#1f6feb66}.border-\[\#2dd4bf\]\/30{border-color:#2dd4bf4d}.border-\[\#7d8590\]{border-color:#7d8590}.border-\[\#7d8590\]\/30{border-color:#7d85904d}.border-\[\#7d8590\]\/40{border-color:#7d859066}.border-\[\#388bfd\]\/40{border-color:#388bfd66}.border-\[\#484f58\]{border-color:#484f58}.border-\[\#8957e5\]\/30{border-color:#8957e54d}.border-\[\#238636\]{border-color:#238636}.border-\[\#238636\]\/40{border-color:#23863666}.border-\[\#d29922\]\/30{border-color:#d299224d}.border-\[\#d29922\]\/40{border-color:#d2992266}.border-\[\#da3633\]\/30{border-color:#da36334d}.border-\[\#da3633\]\/40{border-color:#da363366}.border-border{border-color:var(--border)}.border-destructive\/20{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/20{border-color:color-mix(in oklab,var(--destructive) 20%,transparent)}}.border-input{border-color:var(--input)}.border-transparent{border-color:#0000}.border-t-transparent{border-top-color:#0000}.border-l-transparent{border-left-color:#0000}.bg-\[\#1f6feb\]\/20{background-color:#1f6feb33}.bg-\[\#2dd4bf\]\/15{background-color:#2dd4bf26}.bg-\[\#7d8590\]\/15{background-color:#7d859026}.bg-\[\#7d8590\]\/20{background-color:#7d859033}.bg-\[\#388bfd\]\/20{background-color:#388bfd33}.bg-\[\#8957e5\]\/15{background-color:#8957e526}.bg-\[\#238636\]\/20{background-color:#23863633}.bg-\[\#d29922\]\/15{background-color:#d2992226}.bg-\[\#d29922\]\/20{background-color:#d2992233}.bg-\[\#da3633\]\/15{background-color:#da363326}.bg-\[\#da3633\]\/20{background-color:#da363333}.bg-accent{background-color:var(--accent)}.bg-background{background-color:var(--background)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-border{background-color:var(--border)}.bg-card{background-color:var(--card)}.bg-destructive,.bg-destructive\/10{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/10{background-color:color-mix(in oklab,var(--destructive) 10%,transparent)}}.bg-muted{background-color:var(--muted)}.bg-popover{background-color:var(--popover)}.bg-primary{background-color:var(--primary)}.bg-secondary{background-color:var(--secondary)}.bg-transparent{background-color:#0000}.fill-popover{fill:var(--popover)}.p-0{padding:calc(var(--spacing) * 0)}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-px{padding:1px}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pr-0{padding-right:calc(var(--spacing) * 0)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-balance{text-wrap:balance}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#3fb950\]{color:#3fb950}.text-\[\#5eead4\]{color:#5eead4}.text-\[\#7d8590\]{color:#7d8590}.text-\[\#58a6ff\]{color:#58a6ff}.text-\[\#bc8cff\]{color:#bc8cff}.text-\[\#e3b341\]{color:#e3b341}.text-\[\#f85149\]{color:#f85149}.text-card-foreground{color:var(--card-foreground)}.text-destructive{color:var(--destructive)}.text-foreground{color:var(--foreground)}.text-muted-foreground{color:var(--muted-foreground)}.text-popover-foreground{color:var(--popover-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-secondary-foreground{color:var(--secondary-foreground)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.underline-offset-4{text-underline-offset:4px}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-offset-background{--tw-ring-offset-color:var(--background)}.outline-hidden{--tw-outline-style:none;outline-style:none}@media(forced-colors:active){.outline-hidden{outline-offset:2px;outline:2px solid #0000}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-\[color\,box-shadow\]{transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}@media(hover:hover){.hover\:border-primary\/50:hover{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab,var(--primary) 50%,transparent)}}.hover\:bg-accent:hover{background-color:var(--accent)}.hover\:bg-border:hover{background-color:var(--border)}.hover\:bg-muted:hover{background-color:var(--muted)}.hover\:text-accent-foreground:hover{color:var(--accent-foreground)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:opacity-100:hover{opacity:1}}.focus\:bg-accent:focus{background-color:var(--accent)}.focus\:text-accent-foreground:focus{color:var(--accent-foreground)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-ring:focus{--tw-ring-color:var(--ring)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-hidden:focus{--tw-outline-style:none;outline-style:none}@media(forced-colors:active){.focus\:outline-hidden:focus{outline-offset:2px;outline:2px solid #0000}}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab, var(--ring) 50%, transparent)}}.focus-visible\:outline-1:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.active\:bg-primary\/50:active{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.active\:bg-primary\/50:active{background-color:color-mix(in oklab,var(--primary) 50%,transparent)}}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.has-data-\[slot\=card-action\]\:grid-cols-\[1fr_auto\]:has([data-slot=card-action]){grid-template-columns:1fr auto}.aria-invalid\:border-destructive[aria-invalid=true]{border-color:var(--destructive)}.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[orientation\=horizontal\]\:h-px[data-orientation=horizontal]{height:1px}.data-\[orientation\=horizontal\]\:w-full[data-orientation=horizontal]{width:100%}.data-\[orientation\=vertical\]\:h-full[data-orientation=vertical]{height:100%}.data-\[orientation\=vertical\]\:w-px[data-orientation=vertical]{width:1px}.data-\[placeholder\]\:text-muted-foreground[data-placeholder]{color:var(--muted-foreground)}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[size\=default\]\:h-9[data-size=default]{height:calc(var(--spacing) * 9)}.data-\[size\=sm\]\:h-8[data-size=sm]{height:calc(var(--spacing) * 8)}:is(.\*\:data-\[slot\=select-value\]\:line-clamp-1>*)[data-slot=select-value]{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:is(.\*\:data-\[slot\=select-value\]\:flex>*)[data-slot=select-value]{display:flex}:is(.\*\:data-\[slot\=select-value\]\:items-center>*)[data-slot=select-value]{align-items:center}:is(.\*\:data-\[slot\=select-value\]\:gap-2>*)[data-slot=select-value]{gap:calc(var(--spacing) * 2)}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:var(--accent)}.data-\[state\=open\]\:text-muted-foreground[data-state=open]{color:var(--muted-foreground)}@media(min-width:40rem){.sm\:max-w-\[560px\]{max-width:560px}.sm\:max-w-lg{max-width:var(--container-lg)}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:text-left{text-align:left}}.dark\:bg-destructive\/60:is(.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-destructive\/60:is(.dark *){background-color:color-mix(in oklab,var(--destructive) 60%,transparent)}}.dark\:bg-input\/30:is(.dark *){background-color:var(--input)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-input\/30:is(.dark *){background-color:color-mix(in oklab,var(--input) 30%,transparent)}}@media(hover:hover){.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:var(--input)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:color-mix(in oklab,var(--input) 50%,transparent)}}}.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\[\&_svg\:not\(\[class\*\=\'text-\'\]\)\]\:text-muted-foreground svg:not([class*=text-]){color:var(--muted-foreground)}.\[\.border-b\]\:pb-6.border-b{padding-bottom:calc(var(--spacing) * 6)}.\[\.border-t\]\:pt-6.border-t{padding-top:calc(var(--spacing) * 6)}:is(.\*\:\[span\]\:last\:flex>*):is(span):last-child{display:flex}:is(.\*\:\[span\]\:last\:items-center>*):is(span):last-child{align-items:center}:is(.\*\:\[span\]\:last\:gap-2>*):is(span):last-child{gap:calc(var(--spacing) * 2)}.\[\&\>svg\]\:pointer-events-none>svg{pointer-events:none}.\[\&\>svg\]\:size-3>svg{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}@media(hover:hover){a.\[a\&\]\:hover\:bg-accent:hover{background-color:var(--accent)}a.\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){a.\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive) 90%,transparent)}}a.\[a\&\]\:hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){a.\[a\&\]\:hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary) 90%,transparent)}}a.\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab,red,red)){a.\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:color-mix(in oklab,var(--secondary) 90%,transparent)}}a.\[a\&\]\:hover\:text-accent-foreground:hover{color:var(--accent-foreground)}a.\[a\&\]\:hover\:underline:hover{text-decoration-line:underline}}}:root{--radius:.625rem;--background:#fff;--foreground:#1f2328;--card:#fff;--card-foreground:#1f2328;--popover:#fff;--popover-foreground:#1f2328;--primary:#1f2328;--primary-foreground:#fff;--secondary:#f6f8fa;--secondary-foreground:#1f2328;--muted:#f6f8fa;--muted-foreground:#656d76;--accent:#f6f8fa;--accent-foreground:#1f2328;--destructive:#d1242f;--border:#d0d7de;--input:#d0d7de;--ring:#0969da;--sidebar:#f6f8fa;--sidebar-foreground:#1f2328;--sidebar-primary:#1f2328;--sidebar-primary-foreground:#fff;--sidebar-accent:#f6f8fa;--sidebar-accent-foreground:#1f2328;--sidebar-border:#d0d7de;--sidebar-ring:#0969da}.dark{--background:#0d1117;--foreground:#e6edf3;--card:#161b22;--card-foreground:#e6edf3;--popover:#161b22;--popover-foreground:#e6edf3;--primary:#e6edf3;--primary-foreground:#0d1117;--secondary:#21262d;--secondary-foreground:#e6edf3;--muted:#21262d;--muted-foreground:#7d8590;--accent:#21262d;--accent-foreground:#e6edf3;--destructive:#f85149;--border:#30363d;--input:#21262d;--ring:#388bfd;--sidebar:#0d1117;--sidebar-foreground:#e6edf3;--sidebar-primary:#e6edf3;--sidebar-primary-foreground:#0d1117;--sidebar-accent:#21262d;--sidebar-accent-foreground:#e6edf3;--sidebar-border:#30363d;--sidebar-ring:#388bfd}@keyframes bead-enter{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.animate-bead-enter{animation:.3s ease-out bead-enter}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-duration{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>ui</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-B8Dp1QU-.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cx3XpFYM.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const http = require('node:http');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const { execFile } = require('node:child_process');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const url = require('node:url');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PORT = 8377;
|
|
8
|
+
const DIST_DIR = path.join(__dirname, 'dist');
|
|
9
|
+
|
|
10
|
+
const MIME_TYPES = {
|
|
11
|
+
'.html': 'text/html',
|
|
12
|
+
'.js': 'application/javascript',
|
|
13
|
+
'.css': 'text/css',
|
|
14
|
+
'.json': 'application/json',
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
'.svg': 'image/svg+xml',
|
|
17
|
+
'.ico': 'image/x-icon',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Project directory: passed as CLI arg or cwd
|
|
21
|
+
const PROJECT_DIR = process.argv[2] || process.cwd();
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// CLI helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
function execCmd(cmd, args, cwd) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
execFile(cmd, args, { cwd, timeout: 10000, windowsHide: true }, (err, stdout, stderr) => {
|
|
30
|
+
if (err) {
|
|
31
|
+
reject(new Error(`${cmd} ${args.join(' ')} failed: ${stderr || err.message}`));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
resolve(stdout);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function execBd(args) {
|
|
40
|
+
const stdout = await execCmd('bd', [...args, '--json'], PROJECT_DIR);
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(stdout);
|
|
43
|
+
} catch {
|
|
44
|
+
// bd list with no issues prints "No issues found." instead of JSON
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function execGit(args) {
|
|
50
|
+
return execCmd('git', args, PROJECT_DIR);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Issue normalization — bd CLI outputs `issue_type`, UI expects `type`
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function normalizeIssue(issue) {
|
|
58
|
+
if (issue.issue_type && !issue.type) {
|
|
59
|
+
issue.type = issue.issue_type;
|
|
60
|
+
}
|
|
61
|
+
return issue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// JSON response helpers
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
function jsonResponse(res, data, status = 200) {
|
|
69
|
+
res.writeHead(status, {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
'Access-Control-Allow-Origin': '*',
|
|
72
|
+
});
|
|
73
|
+
res.end(JSON.stringify(data));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function errorResponse(res, message, status = 500) {
|
|
77
|
+
jsonResponse(res, { error: message }, status);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Request handler
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
async function handleRequest(req, res) {
|
|
85
|
+
const parsed = url.parse(req.url, true);
|
|
86
|
+
const pathname = parsed.pathname;
|
|
87
|
+
|
|
88
|
+
if (req.method === 'OPTIONS') {
|
|
89
|
+
res.writeHead(204, {
|
|
90
|
+
'Access-Control-Allow-Origin': '*',
|
|
91
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
92
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
93
|
+
});
|
|
94
|
+
res.end();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
if (pathname === '/api/issues') {
|
|
100
|
+
const issues = await execBd(['list', '--flat', '--status=all']);
|
|
101
|
+
jsonResponse(res, Array.isArray(issues) ? issues.map(normalizeIssue) : issues);
|
|
102
|
+
} else if (pathname === '/api/ready') {
|
|
103
|
+
const ready = await execBd(['ready']);
|
|
104
|
+
jsonResponse(res, Array.isArray(ready) ? ready.map(normalizeIssue) : ready);
|
|
105
|
+
} else if (pathname === '/api/blocked') {
|
|
106
|
+
const blocked = await execBd(['blocked']);
|
|
107
|
+
jsonResponse(res, Array.isArray(blocked) ? blocked.map(normalizeIssue) : blocked);
|
|
108
|
+
} else if (pathname.startsWith('/api/issue/')) {
|
|
109
|
+
const id = pathname.split('/api/issue/')[1];
|
|
110
|
+
if (!id || !/^[\w-]+$/.test(id)) {
|
|
111
|
+
errorResponse(res, 'Invalid issue ID', 400);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const issue = await execBd(['show', id]);
|
|
115
|
+
jsonResponse(res, issue);
|
|
116
|
+
} else if (pathname === '/api/git-log') {
|
|
117
|
+
const branch = parsed.query.branch || '';
|
|
118
|
+
const limit = Math.min(Math.max(parseInt(parsed.query.limit || '50', 10) || 50, 1), 500);
|
|
119
|
+
if (branch && !/^[\w\/.@{}-]+$/.test(branch)) {
|
|
120
|
+
errorResponse(res, 'Invalid branch name', 400);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const format = '%h%x00%s%x00%b%x00%an%x00%ai%x1e';
|
|
124
|
+
const args = ['log', `--format=${format}`, `-n`, `${limit}`];
|
|
125
|
+
if (branch) args.splice(1, 0, branch);
|
|
126
|
+
const stdout = await execGit(args);
|
|
127
|
+
const commits = stdout.split('\x1e').filter(s => s.trim()).map(record => {
|
|
128
|
+
const [hash, message, body, author, date] = record.trim().split('\0');
|
|
129
|
+
return { hash, message, body: body?.trim() || '', author, date };
|
|
130
|
+
});
|
|
131
|
+
jsonResponse(res, commits);
|
|
132
|
+
} else if (pathname === '/api/project') {
|
|
133
|
+
let name = path.basename(PROJECT_DIR);
|
|
134
|
+
try {
|
|
135
|
+
const origin = (await execGit(['remote', 'get-url', 'origin'])).trim();
|
|
136
|
+
const match = origin.match(/\/([^/]+?)(?:\.git)?$/);
|
|
137
|
+
if (match) name = match[1];
|
|
138
|
+
} catch {}
|
|
139
|
+
jsonResponse(res, { name });
|
|
140
|
+
} else if (pathname === '/api/branches') {
|
|
141
|
+
const stdout = await execGit(['branch', '--format=%(refname:short)']);
|
|
142
|
+
const branches = stdout.trim().split('\n').filter(Boolean);
|
|
143
|
+
const current = (await execGit(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
|
144
|
+
jsonResponse(res, { branches, current });
|
|
145
|
+
} else {
|
|
146
|
+
// Serve static files from dist/
|
|
147
|
+
let filePath = path.join(DIST_DIR, pathname === '/' ? 'index.html' : pathname);
|
|
148
|
+
const resolved = path.resolve(filePath);
|
|
149
|
+
if (!resolved.startsWith(path.resolve(DIST_DIR))) {
|
|
150
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
151
|
+
res.end('Forbidden');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// SPA fallback: serve index.html for non-file paths
|
|
155
|
+
if (!path.extname(filePath)) {
|
|
156
|
+
filePath = path.join(DIST_DIR, 'index.html');
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const data = fs.readFileSync(filePath);
|
|
160
|
+
const ext = path.extname(filePath);
|
|
161
|
+
res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
|
|
162
|
+
res.end(data);
|
|
163
|
+
} catch {
|
|
164
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
165
|
+
res.end('Not found');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
errorResponse(res, err.message);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Pidfile management
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
const PIDFILE = path.join(PROJECT_DIR, '.beads-board.pid');
|
|
178
|
+
|
|
179
|
+
function writePidfile(port) {
|
|
180
|
+
fs.writeFileSync(PIDFILE, JSON.stringify({ pid: process.pid, port }));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function removePidfile() {
|
|
184
|
+
try { fs.unlinkSync(PIDFILE); } catch {}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getRunningInstance() {
|
|
188
|
+
try {
|
|
189
|
+
const data = JSON.parse(fs.readFileSync(PIDFILE, 'utf8'));
|
|
190
|
+
// Check if process is still running
|
|
191
|
+
process.kill(data.pid, 0);
|
|
192
|
+
return data;
|
|
193
|
+
} catch {
|
|
194
|
+
removePidfile();
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Port detection
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
function findAvailablePort(startPort) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
const s = http.createServer();
|
|
206
|
+
s.listen(startPort, () => {
|
|
207
|
+
s.close(() => resolve(startPort));
|
|
208
|
+
});
|
|
209
|
+
s.on('error', () => {
|
|
210
|
+
if (startPort < DEFAULT_PORT + 10) {
|
|
211
|
+
resolve(findAvailablePort(startPort + 1));
|
|
212
|
+
} else {
|
|
213
|
+
reject(new Error('No available port found'));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Server startup
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
const server = http.createServer(handleRequest);
|
|
224
|
+
|
|
225
|
+
async function start() {
|
|
226
|
+
const existing = getRunningInstance();
|
|
227
|
+
if (existing) {
|
|
228
|
+
console.log(`beads-board already running at http://localhost:${existing.port}`);
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const port = await findAvailablePort(parseInt(process.env.PORT || DEFAULT_PORT, 10));
|
|
233
|
+
server.listen(port, () => {
|
|
234
|
+
writePidfile(port);
|
|
235
|
+
console.log(`beads-board server running at http://localhost:${port}`);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
process.on('SIGTERM', () => { removePidfile(); process.exit(0); });
|
|
240
|
+
process.on('SIGINT', () => { removePidfile(); process.exit(0); });
|
|
241
|
+
|
|
242
|
+
start();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stuart Rimel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# beads-board
|
|
2
|
+
|
|
3
|
+
A minimal kanban dashboard and git log viewer for [Beads](https://github.com/steveyegge/beads). Works standalone as a CLI tool or as a Claude Code plugin.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Kanban board** — Issues organized by status: Ready, In Progress, Blocked, Done
|
|
8
|
+
- **Git log** — Scrollable commit history with branch selector
|
|
9
|
+
- **Bead ID linking** — Bead IDs in commit messages are highlighted as badges
|
|
10
|
+
- **Dark/light theme** — Toggle between themes, dark by default
|
|
11
|
+
- **Auto-refresh** — Polls for updates every 5 seconds
|
|
12
|
+
- **Zero runtime dependencies** — Server uses only Node.js stdlib
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Standalone CLI (works with any editor)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install globally
|
|
20
|
+
npm install -g @citadel-labs/beads-ui
|
|
21
|
+
|
|
22
|
+
# Or run directly with npx (installs temporarily)
|
|
23
|
+
npx @citadel-labs/beads-ui
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then from any project that uses [Beads](https://github.com/steveyegge/beads):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd /path/to/your/project
|
|
30
|
+
bdui # Start dashboard in background, prints URL
|
|
31
|
+
bdui /path/to/project # Specify a project directory
|
|
32
|
+
bdui --port 9000 # Custom port
|
|
33
|
+
bdui status # Check if dashboard is running
|
|
34
|
+
bdui stop # Stop the dashboard
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The server starts in the background and returns control to your terminal. Open the printed URL (default **http://localhost:8377**) in your browser. Port auto-increments if taken.
|
|
38
|
+
|
|
39
|
+
### As a Claude Code Plugin
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Install from local directory
|
|
43
|
+
claude --plugin-dir /path/to/beads-board
|
|
44
|
+
|
|
45
|
+
# Then in any project with .beads/
|
|
46
|
+
/beads-board:start # Start the dashboard server
|
|
47
|
+
/beads-board:stop # Stop it
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The plugin auto-detects `.beads/` in your project. If your project doesn't use Beads, it will let you know.
|
|
51
|
+
|
|
52
|
+
## How It Works
|
|
53
|
+
|
|
54
|
+
The server shells out to `bd` and `git` CLI commands to fetch data, then serves a React dashboard that polls the API every 5 seconds. No direct database access — all data flows through the Beads CLI.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Browser → GET /api/* → Node.js server → bd/git CLI → JSON response
|
|
58
|
+
← React app ← server/dist/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
See [docs/architecture.md](docs/architecture.md) for details.
|
|
62
|
+
|
|
63
|
+
## Documentation
|
|
64
|
+
|
|
65
|
+
- [Architecture](docs/architecture.md) — How the server, UI, and plugin fit together
|
|
66
|
+
- [API Reference](docs/api.md) — All API endpoints with examples
|
|
67
|
+
- [Contributing](docs/contributing.md) — How to set up a dev environment and make changes
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const { spawn } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const SERVER_SCRIPT = path.join(__dirname, '..', 'server', 'index.js');
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Argument parsing
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const rawArgs = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
// Extract subcommand (start, stop, status) — default to "start"
|
|
16
|
+
const SUBCOMMANDS = ['start', 'stop', 'status'];
|
|
17
|
+
let subcommand = 'start';
|
|
18
|
+
const args = [];
|
|
19
|
+
for (const arg of rawArgs) {
|
|
20
|
+
if (SUBCOMMANDS.includes(arg) && args.length === 0 && subcommand === 'start') {
|
|
21
|
+
subcommand = arg;
|
|
22
|
+
} else {
|
|
23
|
+
args.push(arg);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --help
|
|
28
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
29
|
+
console.log(`bdui — Kanban dashboard and git log viewer for Beads
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
bdui [project-dir] [options] Start the dashboard (default)
|
|
33
|
+
bdui start [project-dir] [options] Start the dashboard
|
|
34
|
+
bdui stop [project-dir] Stop a running dashboard
|
|
35
|
+
bdui status [project-dir] Show running dashboard info
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--port <port> Port to listen on (default: 8377)
|
|
39
|
+
--foreground Run in foreground (don't daemonize)
|
|
40
|
+
--help, -h Show this help message
|
|
41
|
+
--version, -v Show version number
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
bdui # Start dashboard for current directory
|
|
45
|
+
bdui /path/to/project # Specify project directory
|
|
46
|
+
bdui --port 9000 # Custom port
|
|
47
|
+
bdui stop # Stop the running dashboard
|
|
48
|
+
bdui status # Check if dashboard is running`);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --version
|
|
53
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
54
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
55
|
+
console.log(pkg.version);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --foreground
|
|
60
|
+
const foreground = args.includes('--foreground');
|
|
61
|
+
const filteredArgs = args.filter(a => a !== '--foreground');
|
|
62
|
+
|
|
63
|
+
// --port
|
|
64
|
+
let customPort = null;
|
|
65
|
+
const portIdx = filteredArgs.indexOf('--port');
|
|
66
|
+
if (portIdx !== -1) {
|
|
67
|
+
customPort = filteredArgs[portIdx + 1];
|
|
68
|
+
if (!customPort || isNaN(parseInt(customPort, 10))) {
|
|
69
|
+
console.error('Error: --port requires a numeric value');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
filteredArgs.splice(portIdx, 2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Remaining arg is the project directory
|
|
76
|
+
const projectDir = filteredArgs[0] ? path.resolve(filteredArgs[0]) : process.cwd();
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Validation
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(projectDir)) {
|
|
83
|
+
console.error(`Error: directory not found: ${projectDir}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(path.join(projectDir, '.beads'))) {
|
|
88
|
+
console.error(`Error: no .beads/ directory found in ${projectDir}`);
|
|
89
|
+
console.error('This project does not appear to use Beads issue tracking.');
|
|
90
|
+
console.error('See https://github.com/steveyegge/beads to get started.');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Pidfile helpers
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
const PIDFILE = path.join(projectDir, '.beads-board.pid');
|
|
99
|
+
|
|
100
|
+
function getRunningInstance() {
|
|
101
|
+
try {
|
|
102
|
+
const data = JSON.parse(fs.readFileSync(PIDFILE, 'utf8'));
|
|
103
|
+
process.kill(data.pid, 0); // throws if not running
|
|
104
|
+
return data;
|
|
105
|
+
} catch {
|
|
106
|
+
// Clean up stale pidfile
|
|
107
|
+
try { fs.unlinkSync(PIDFILE); } catch {}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Subcommands
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
if (subcommand === 'status') {
|
|
117
|
+
const instance = getRunningInstance();
|
|
118
|
+
if (instance) {
|
|
119
|
+
console.log(`beads-board running at http://localhost:${instance.port} (pid ${instance.pid})`);
|
|
120
|
+
} else {
|
|
121
|
+
console.log('beads-board is not running');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (subcommand === 'stop') {
|
|
128
|
+
const instance = getRunningInstance();
|
|
129
|
+
if (!instance) {
|
|
130
|
+
console.log('beads-board is not running');
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
process.kill(instance.pid, 'SIGTERM');
|
|
135
|
+
console.log(`beads-board stopped (was http://localhost:${instance.port}, pid ${instance.pid})`);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(`Failed to stop beads-board (pid ${instance.pid}): ${err.message}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// subcommand === 'start'
|
|
144
|
+
const existing = getRunningInstance();
|
|
145
|
+
if (existing) {
|
|
146
|
+
console.log(`beads-board already running at http://localhost:${existing.port} (pid ${existing.pid})`);
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (foreground) {
|
|
151
|
+
// Run server in foreground (debugging)
|
|
152
|
+
if (customPort) process.env.PORT = customPort;
|
|
153
|
+
process.argv = [process.argv[0], __filename, projectDir];
|
|
154
|
+
require(SERVER_SCRIPT);
|
|
155
|
+
} else {
|
|
156
|
+
// Spawn detached server process
|
|
157
|
+
const env = { ...process.env };
|
|
158
|
+
if (customPort) env.PORT = customPort;
|
|
159
|
+
|
|
160
|
+
const child = spawn(process.execPath, [SERVER_SCRIPT, projectDir], {
|
|
161
|
+
detached: true,
|
|
162
|
+
stdio: 'ignore',
|
|
163
|
+
env,
|
|
164
|
+
windowsHide: true,
|
|
165
|
+
});
|
|
166
|
+
child.unref();
|
|
167
|
+
|
|
168
|
+
// Wait for pidfile to confirm startup (poll up to 3s)
|
|
169
|
+
const start = Date.now();
|
|
170
|
+
const poll = setInterval(() => {
|
|
171
|
+
const instance = getRunningInstance();
|
|
172
|
+
if (instance) {
|
|
173
|
+
clearInterval(poll);
|
|
174
|
+
console.log(`beads-board running at http://localhost:${instance.port}`);
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
if (Date.now() - start > 3000) {
|
|
178
|
+
clearInterval(poll);
|
|
179
|
+
console.error('Error: server failed to start within 3 seconds');
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
}, 100);
|
|
183
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@citadel-labs/beads-ui",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Kanban dashboard and git log viewer for Beads",
|
|
5
|
+
"bin": {
|
|
6
|
+
"bdui": "bin/beads-board.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "cd ui && npm run build",
|
|
10
|
+
"start": "node server/index.js",
|
|
11
|
+
"publish:patch": "npm version patch && git push && git push --tags && npm publish --access public",
|
|
12
|
+
"publish:minor": "npm version minor && git push && git push --tags && npm publish --access public",
|
|
13
|
+
"publish:major": "npm version major && git push && git push --tags && npm publish --access public"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT"
|
|
16
|
+
}
|