megit-app 0.4.1 → 0.5.1

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.
@@ -0,0 +1 @@
1
+ :root{--bg:#1e2228;--bg-panel:#23272e;--bg-tabbar:#14171c;--bg-tab:#1a1e24;--bg-hover:#2c313a;--bg-selected:#354254;--bg-wip:#2a2e25;--border:#3a404b;--fg:#d4d8de;--fg-dim:#9aa4b2;--fg-faint:#6b7280;--head-bg:#2e4436;--head-bg-hover:#3d5a47;--head-fg:#a8d99a}:root[data-theme=light]{--bg:#f6f8fa;--bg-panel:#fff;--bg-tabbar:#e4e7eb;--bg-tab:#eef0f3;--bg-hover:#e8eaed;--bg-selected:#cfdff5;--bg-wip:#f3f5e8;--border:#d0d7de;--fg:#1f2328;--fg-dim:#57606a;--fg-faint:#8b949e;--head-bg:#d3f2d9;--head-bg-hover:#b4e7c0;--head-fg:#116329}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg);color:var(--fg);font:16px/1.4 Ubuntu Mono,ui-monospace,monospace}button{background:var(--bg-hover);color:inherit;border:1px solid var(--border);cursor:pointer;border-radius:4px}button:hover{background:var(--border)}.app{flex-direction:column;height:100%;display:flex}.empty{color:var(--fg-faint);flex:1;justify-content:center;align-items:center;display:flex}.error{color:#e06c75;white-space:pre-wrap;padding:8px}.toasts{z-index:20;flex-direction:column;gap:6px;max-width:380px;display:flex;position:fixed;bottom:12px;right:12px}.toast{background:var(--bg-panel);color:var(--fg);white-space:pre-wrap;cursor:pointer;border:1px solid #e06c75;border-left-width:3px;border-radius:6px;padding:8px 10px;animation:.15s ease-out appear;box-shadow:0 2px 8px #0000004d}.tabbar{background:var(--bg-tabbar);flex-shrink:0;gap:2px;padding:6px 6px 0;display:flex}.logo{pointer-events:none;align-self:center;width:18px;height:18px;margin:0 4px 6px 2px}.tab{background:var(--bg-tab);cursor:pointer;border-radius:6px 6px 0 0;align-items:center;gap:6px;padding:6px 10px;display:flex}.tab.active{background:var(--bg-panel)}.tab.dragging{opacity:.5}.tab-close{width:18px;height:18px;color:var(--fg-dim);background:0 0;border:none;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;margin:-2px -4px -2px 0;padding:0;display:flex}.tab-close:hover{background:var(--border);color:var(--fg)}.tab-add{background:0 0;border:none;padding:6px 12px;font-size:15px}.build-tag{color:var(--fg-faint);-webkit-user-select:none;user-select:none;align-self:center;margin:0 4px 6px auto;font-size:11px}.modal-backdrop{z-index:10;background:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.modal{background:var(--bg-panel);border:1px solid var(--border);border-radius:8px;flex-direction:column;width:480px;max-height:70vh;display:flex}.modal-head{border-bottom:1px solid var(--border);align-items:center;gap:8px;padding:10px;display:flex}.modal-path{text-overflow:ellipsis;white-space:nowrap;color:var(--fg-dim);flex:1;overflow:hidden}.dirlist{margin:0;padding:4px;list-style:none;overflow-y:auto}.dirlist li{border-radius:4px;align-items:center;padding:4px 8px;display:flex}.dirlist li:hover{background:var(--bg-hover)}.dirname{cursor:pointer;text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.modal-label{text-transform:uppercase;letter-spacing:.5px;color:var(--fg-dim);padding:8px 12px 2px;font-size:11px}.modal-path b{color:var(--fg)}.picker{flex-direction:row;width:720px;height:440px;max-height:80vh;overflow:hidden}.picker-pane{flex-direction:column;flex:1;min-width:0;display:flex}.picker-recent{border-right:1px solid var(--border);background:var(--bg-tab);flex:none;width:240px}.picker-recent .dirlist li.sel{background:var(--bg-selected)}.recent-dot{background:#2ea043;border-radius:50%;flex-shrink:0;width:6px;height:6px}.dir-chevron{color:var(--fg-faint);cursor:pointer;padding-left:8px}.repoview{background:var(--bg-panel);flex-direction:column;flex:1;min-height:0;animation:.5s ease-out appear;display:flex}@keyframes appear{0%{opacity:0}}@media (prefers-reduced-motion:reduce){.repoview{animation:none}}.toolbar{border-bottom:1px solid var(--border);flex-shrink:0;grid-template-columns:1fr auto 1fr;align-items:center;gap:10px;margin-bottom:6px;padding:6px 10px;display:grid}.tb-left{align-items:center;gap:8px;min-width:0;display:flex}.tb-right{justify-content:flex-end;align-items:center;gap:6px;display:flex}.toolbar button{padding:3px 10px}.refresh-btn{align-items:center;padding:3px 5px;display:inline-flex}.refresh-btn .spin{animation:.5s cubic-bezier(.45,0,.55,1) infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.tb-actions{align-items:center;gap:2px;display:flex}.toolbar .tb-btn{color:var(--fg-dim);background:0 0;border:none;align-items:center;gap:5px;padding:4px 8px;display:inline-flex}.tb-btn:hover:not(:disabled){background:var(--bg-hover);color:var(--fg)}.tb-btn:disabled{opacity:.4;cursor:default}.tb-badge{background:var(--bg-selected);min-width:15px;color:var(--fg);text-align:center;border-radius:7px;padding:0 4px;font-size:11px;line-height:15px}.repo-path{min-width:0;color:var(--fg-dim);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.github-link{color:var(--fg-dim);flex-shrink:0;align-items:center;display:inline-flex}.github-link:hover{color:var(--fg)}.term-btn{color:var(--fg-dim);align-items:center;padding:3px 5px;display:inline-flex}.term-btn:hover{color:var(--fg)}.term-btn.active{color:var(--fg);background:var(--bg-selected)}.term-panel{border-top:1px solid var(--border);background:var(--bg);flex-direction:column;flex-shrink:0;height:220px;display:flex;position:relative}.term-resizer{cursor:ns-resize;touch-action:none;z-index:3;height:6px;position:absolute;top:-3px;left:0;right:0}.term-resizer:hover,.term-resizer:active{background:#5a80b0}.term-head{flex-shrink:0;justify-content:space-between;align-items:center;padding:3px 6px 3px 8px;display:flex}.term-title{color:var(--fg-dim);align-items:center;gap:6px;font-size:12px;display:inline-flex}.term-head-actions{align-items:center;gap:2px;display:inline-flex}.term-close{color:var(--fg-dim);background:0 0;border:none;align-items:center;padding:0 4px;font-size:13px;display:inline-flex}.term-close:hover{color:var(--fg);background:0 0}.term-close:disabled{opacity:.35}.term-panes{flex:1;min-height:0;display:flex}.term-pane{flex:1;min-width:0;display:flex;position:relative}.term-pane+.term-pane{border-left:1px solid var(--border)}.term-pane-kill{z-index:2;color:#0000;background:0 0;border:none;padding:0 3px;font-size:12px;line-height:1.2;position:absolute;top:0;right:4px}.term-pane:hover .term-pane-kill{color:var(--fg-dim)}.term-pane-kill:hover,.term-pane-kill:focus-visible{color:var(--fg);background:0 0}.term-host{flex:1;min-width:0;min-height:0;padding:4px 0 0 8px}.term-host .xterm{height:100%}.theme-switch{background:var(--bg-hover);border:1px solid var(--border);width:40px;height:20px;color:var(--fg-dim);cursor:pointer;border-radius:10px;flex-shrink:0;justify-content:space-between;align-items:center;padding:0 4px;display:inline-flex;position:relative}.theme-switch input{opacity:0;cursor:pointer;margin:0;position:absolute;inset:0}.theme-switch svg{position:relative}.theme-switch .knob{background:var(--fg);border-radius:50%;width:14px;height:14px;transition:left .15s;position:absolute;top:2px;left:2px}.theme-switch input:checked~.knob{left:22px}.theme-switch:has(input:focus-visible){outline-offset:1px;outline:2px solid #5a80b0}.panes{flex:1;min-height:0;display:flex}.graph-pane{width:var(--graph-w,55%);flex-direction:column;flex-shrink:0;min-width:0;display:flex;position:relative}.graphview{flex:1;padding-bottom:8px;overflow:auto}.refs,.graph-col{transition:filter .15s}.graph-pane.busy .refs,.graph-pane.busy .graph-col{filter:blur(1px);pointer-events:none}.graph-pane.busy,.graph-pane.busy .row{cursor:progress}.graphview .row:not(.wip){content-visibility:auto;contain-intrinsic-size:auto 28px}.diff-overlay{z-index:2;background:var(--bg-panel);flex-direction:column;display:flex;position:absolute;inset:0}.diff-overlay-head{border-bottom:1px solid var(--border);flex-shrink:0;align-items:center;gap:8px;padding:6px 10px;display:flex}.diff-overlay-head .file-path{flex:1}.diff-close{background:0 0;border:none;padding:0 4px;font-size:14px}.searchbar{top:8px;left:calc(var(--refs-w) + var(--graph-col-w) + 24px);z-index:3;background:var(--bg-selected);border:1px solid var(--border);border-radius:8px;align-items:center;gap:6px;padding:5px 8px;animation:.12s ease-out appear;display:flex;position:absolute;box-shadow:0 3px 12px #00000059}.searchbar input{background:var(--bg-panel);width:220px;color:var(--fg);border:1px solid var(--border);font:inherit;border-radius:5px;padding:3px 6px}.searchbar .count{text-align:center;min-width:78px;color:var(--fg-dim);font-size:12px}.searchbar button{min-width:22px;color:var(--fg-dim);background:0 0;border:none;justify-content:center;align-items:center;padding:3px 5px;display:inline-flex}.searchbar button:hover,.searchbar button.deep.active{background:var(--bg-hover);color:var(--fg)}.splitter{cursor:col-resize;background:var(--border);touch-action:none;background-clip:padding-box;border:2px solid #0000;flex-shrink:0;width:5px}.splitter:hover,.splitter:active{background-color:#5a80b0}.row{cursor:pointer;white-space:nowrap;align-items:center;gap:8px;height:28px;padding:0 8px;scroll-margin-top:28px;display:flex;position:relative}.row.wip{background-color:var(--bg-panel);background-image:linear-gradient(to right, transparent calc(8px + var(--refs-w,120px)), var(--bg-wip) 0);z-index:1;font-style:italic;position:sticky;top:0}.row:hover:not(:has(.refs:hover)):not(.selected){background-image:linear-gradient(to right, transparent calc(8px + var(--refs-w,120px)), var(--bg-hover) 0)}.row.selected{background-image:linear-gradient(to right, transparent calc(8px + var(--refs-w,120px)), var(--bg-selected) 0)}.graph-cell{flex-shrink:0;overflow:visible}.refs{width:var(--refs-w,120px);cursor:default;flex-shrink:0;justify-content:flex-start;align-self:stretch;align-items:center;gap:4px;display:flex;overflow:hidden}.graph-col{width:var(--graph-col-w,90px);flex-shrink:0;justify-content:flex-start;align-self:stretch;align-items:center;padding:0 4px;display:flex;overflow:hidden}.col-splitter{cursor:col-resize;touch-action:none;z-index:1;width:5px;position:absolute;top:0;bottom:0}.col-splitter:hover,.col-splitter:active{background:#5a80b0}.ref-chips{gap:4px;min-width:0;display:flex}.ref-chip{color:var(--fg);background:var(--bg-panel);flex:1 1 calc(14px + var(--n-icons,0) * 15px);-webkit-user-select:none;user-select:none;cursor:pointer;border:1px solid;border-radius:8px;align-items:center;gap:3px;min-width:0;max-width:max-content;padding:1px 6px;font-size:14px;display:inline-flex}.ref-chip:hover{background:var(--bg-hover)}.ref-chip.head{background:var(--head-bg);color:var(--head-fg);font-weight:600}.ref-chip.head:hover{background:var(--head-bg-hover)}.ref-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.ref-line{opacity:.5;flex:1;align-self:center;min-width:6px;height:1px}.ref-bridge{opacity:.5;pointer-events:none;height:1px;position:absolute;top:calc(50% - .5px)}.ref-line.head,.ref-bridge.head{opacity:1;height:2px}.ref-bridge.head{top:calc(50% - 1px)}.ref-icon{opacity:.8;flex-shrink:0}.subject{text-overflow:ellipsis;flex:1;overflow:hidden}.date{color:var(--fg-dim);font-size:12px}.hash{color:var(--fg-faint);font-family:Ubuntu Mono,ui-monospace,monospace;font-size:12px}.load-more{margin:8px;padding:4px 14px}.ctx-menu{border:1px solid var(--border);background:var(--bg-panel);border-radius:6px;min-width:150px;margin:0;padding:4px;position:fixed;inset:auto;box-shadow:0 6px 20px #00000059}.ctx-item{width:100%;color:var(--fg);font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:4px;padding:5px 10px;font-size:14px;display:block}.ctx-item:hover{background:var(--bg-hover)}.ctx-item.sep{border-top:1px solid var(--border);border-radius:0 0 4px 4px;margin-top:5px;padding-top:9px}.ctx-item.danger{color:#e06c75}.ctx-item.danger:hover{color:#fff;background:#e06c75}.panel{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.commit-msg{border-bottom:1px solid var(--border);flex-shrink:0;max-height:30%;padding:10px 12px;position:relative;overflow-y:auto}.commit-subject{font-weight:600}.commit-body{white-space:pre-wrap;color:var(--fg-dim);margin-top:8px;font-size:13px}.msg-edit{vertical-align:-4px;color:var(--fg-dim);opacity:0;margin-left:8px;padding:4px;line-height:0;transition:opacity .1s;display:inline-flex}.msg-edit:hover{color:var(--fg)}.commit-msg:hover .msg-edit,.msg-edit:focus-visible{opacity:1}.msg-box{border:1px solid var(--border);background:var(--bg);width:100%;min-height:84px;color:var(--fg);font:inherit;resize:vertical;border-radius:4px;padding:6px 8px;display:block}.msg-actions{align-items:center;gap:6px;margin-top:6px;display:flex}.msg-hint{color:var(--fg-faint);flex:1;font-size:12px}.msg-actions .primary{background:var(--head-bg);color:var(--head-fg);border-color:var(--head-fg)}.msg-actions .primary:disabled{opacity:.45}.head-actions,.row-action{gap:2px;display:flex}.head-actions button,.row-action{color:var(--fg-dim);background:0 0;border-color:#0000;padding:3px;line-height:0}.head-actions button:hover,.row-action:hover{border-color:var(--border);background:var(--bg-hover);color:var(--fg)}.head-actions .danger:hover,.row-action.danger:hover{color:#e06c75;border-color:#e06c75}.head-actions button:disabled,.row-action:disabled{opacity:.4}.section-head{z-index:1;background:var(--bg-panel);color:var(--fg);text-transform:uppercase;letter-spacing:.05em;cursor:pointer;-webkit-user-select:none;user-select:none;align-items:center;gap:5px;padding:8px 6px 5px;font-size:13px;font-weight:600;display:flex;position:sticky;top:0}.section-head:hover{background:var(--bg-hover)}.section-head .caret{color:var(--fg-dim);line-height:0;transition:transform .1s;display:flex}.section-head .caret.open{transform:rotate(90deg)}.section-head .count{background:var(--bg-selected);min-width:19px;height:19px;color:var(--fg);letter-spacing:0;border-radius:10px;justify-content:center;align-items:center;padding:0 5px;font-size:11px;display:inline-flex}.section-head .count.zero{background:var(--bg-hover);color:var(--fg-faint)}.section-head .head-actions{visibility:hidden;margin-left:auto}.section-head:hover .head-actions,.section-head .head-actions:focus-within{visibility:visible}.composer{border-bottom:1px solid var(--border);flex-shrink:0;padding:8px}.composer .msg-box{min-height:56px}.row-action{visibility:hidden}.row-action:first-of-type{margin-left:auto}.file-row:hover .row-action,.file-row.selected .row-action,.row-action:focus-visible{visibility:visible}.diff-side{border:1px solid var(--border);color:var(--fg-dim);border-radius:8px;padding:0 6px;font-size:12px}.commit-people{border-bottom:1px solid var(--border);flex-direction:column;flex-shrink:0;gap:6px;padding:10px 12px;display:flex;position:relative}.person{align-items:center;gap:8px;min-width:0;display:flex}.person-text{flex-direction:column;min-width:0;display:flex}.person-name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.person-when{color:var(--fg-dim);font-size:12px}.face{object-fit:cover;border-radius:50%;flex-shrink:0;width:26px;height:26px}.face-fallback{background:var(--bg-hover);color:var(--fg-dim);justify-content:center;align-items:center;font-size:11px;display:inline-flex}.commit-hash{color:var(--fg-faint);font-family:Ubuntu Mono,ui-monospace,monospace;font-size:12px;position:absolute;top:10px;right:12px}.files-head{border-bottom:1px solid var(--border);flex-shrink:0;justify-content:space-between;align-items:center;gap:8px;padding:6px 12px;display:flex}.files-counts{color:var(--fg-dim);white-space:nowrap;gap:10px;font-size:12px;display:flex;overflow:hidden}.files-head .view-toggle button{padding:2px 10px;font-size:12px}.filelist{flex:1;padding:4px;overflow-y:auto}.file-dir{color:var(--fg-faint);text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;overflow:hidden}.dir-row{color:var(--fg-dim)}.dir-arrow{width:14px;color:var(--fg-faint);font-size:10px;line-height:inherit;flex-shrink:0}.file-row{cursor:pointer;border-radius:4px;gap:8px;padding:3px 8px;display:flex}.file-row:hover{background:var(--bg-hover)}.file-row.selected{background:var(--bg-selected)}.file-status{flex-shrink:0;width:14px;font-family:Ubuntu Mono,ui-monospace,monospace}.file-path{white-space:nowrap;min-width:0;display:flex;overflow:hidden}.file-name{flex:none}.diffview{flex-direction:column;flex:1;min-height:0;display:flex;overflow:auto}.diffview.empty{text-align:center;justify-content:center;align-items:center}.diff-state{text-align:center;flex:1;justify-content:center;align-items:center;display:flex}.diff-toolbar{border-bottom:1px solid var(--border);flex-shrink:0;justify-content:center;gap:8px;padding:6px 10px;display:flex}.diff-toolbar button{padding:3px 10px}.view-toggle{display:inline-flex}.view-toggle button{border-radius:0}.view-toggle button:first-child{border-radius:4px 0 0 4px}.view-toggle button:last-child{border-left:none;border-radius:0 4px 4px 0}.view-toggle button.active{background:var(--bg-selected)}.diff-html{flex:1;overflow:auto}.diff-html .d2h-file-wrapper{position:relative}.diff-html .d2h-file-diff{overflow-x:auto}.diff-html .d2h-diff-table{font-family:Ubuntu Mono,ui-monospace,monospace}.diff-html .d2h-code-linenumber,.diff-html .d2h-code-side-linenumber{cursor:default}.diff-html td.d2h-info .diff-expand-group{direction:ltr;gap:1px;padding-left:2px;display:flex}.diff-html .diff-expand{width:18px;height:18px;color:var(--fg-dim);cursor:pointer;background:0 0;border:0;border-radius:3px;justify-content:center;align-items:center;padding:0;display:flex}.diff-html .diff-expand:hover{background:var(--bg-hover);color:var(--fg)}.diff-plain{color:var(--fg-dim);padding:10px}.image-diff{flex:1;grid-template-columns:1fr 1fr;align-content:start;gap:12px;padding:12px;display:grid;overflow:auto}.image-pane{flex-direction:column;gap:6px;min-width:0;display:flex}.image-pane-label{color:var(--fg-dim);font-size:12px}.image-pane img{border:1px solid var(--border);background:repeating-conic-gradient(var(--bg-panel) 0% 25%, var(--bg) 0% 50%) 0 0 / 16px 16px;border-radius:4px;max-width:100%}.image-pane-none{text-align:center;color:var(--fg-dim);border:1px dashed var(--border);border-radius:4px;padding:20px}.d2h-wrapper{--d2h-bg-color:var(--bg-panel);--d2h-ins-bg-color:#e6ffec;--d2h-ins-highlight-bg-color:#abf2bc;--d2h-ins-border-color:#ccffd8;--d2h-ins-label-color:#1a7f37;--d2h-del-bg-color:#ffebe9;--d2h-del-highlight-bg-color:#ffcecb;--d2h-del-border-color:#ffd7d5;--d2h-del-label-color:#cf222e;--d2h-change-ins-color:#e6ffec;--d2h-change-del-color:#ffebe9;--d2h-info-bg-color:#ddf4ff;--d2h-info-border-color:#ddf4ff;--d2h-line-border-color:#d0d7de;--d2h-dark-bg-color:#23272e;--d2h-dark-border-color:#30363d;--d2h-dark-line-border-color:#30363d;--d2h-dark-ins-bg-color:#2ea04326;--d2h-dark-ins-highlight-bg-color:#2ea04366;--d2h-dark-ins-border-color:#3fb9504d;--d2h-dark-ins-label-color:#3fb950;--d2h-dark-del-bg-color:#f851491a;--d2h-dark-del-highlight-bg-color:#f8514966;--d2h-dark-del-border-color:#f851494d;--d2h-dark-del-label-color:#f85149;--d2h-dark-change-ins-color:#2ea04326;--d2h-dark-change-del-color:#f851491a;--d2h-dark-info-bg-color:#388bfd1a;--d2h-dark-info-border-color:#388bfd4d}.d2h-wrapper .hljs{background:0 0}.conflict-banner{background:var(--bg-wip);border-bottom:1px solid var(--border);flex-shrink:0;align-items:center;gap:10px;padding:6px 10px;font-size:12px;display:flex}.conflict-banner .cb-verb{font-weight:600}.conflict-banner .cb-label{color:var(--fg-dim);text-overflow:ellipsis;white-space:nowrap;max-width:40ch;overflow:hidden}.conflict-banner .cb-count{color:var(--fg-dim);margin-left:auto}.conflict-banner button{padding:3px 10px;font-size:12px}.conflict-banner button.danger{color:#e06c75;border-color:#e06c75}.conflict-banner button.primary:not(:disabled){background:var(--head-bg);color:var(--head-fg);border-color:var(--head-bg-hover)}.conflict-banner button:disabled{opacity:.5;cursor:default}.cf-view{flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.5;overflow:auto}.cf-progress{z-index:1;background:var(--bg-panel);border-bottom:1px solid var(--border);color:var(--fg-dim);padding:4px 10px;position:sticky;top:0}.cf-view pre{white-space:pre-wrap;word-break:break-word;margin:0;padding:0 10px}.cf-context{color:var(--fg-dim)}.cf-block{border:1px solid var(--border);border-radius:4px;margin:6px 0;overflow:hidden}.cf-block.picked{border-color:var(--head-bg-hover)}.cf-bar{background:var(--bg-hover);border-bottom:1px solid var(--border);align-items:center;gap:6px;padding:3px 8px;display:flex}.cf-side-name{color:var(--fg-dim);margin-right:auto}.cf-chosen{color:var(--head-fg);margin-right:auto}.cf-bar button{padding:2px 8px;font-size:11px}.cf-ours{background:#98c3791f}.cf-theirs{background:#61afef1f}.cf-mid{color:var(--fg-dim);border-top:1px dashed var(--border);border-bottom:1px dashed var(--border);padding:2px 8px}.cf-card{border:1px solid var(--border);text-align:center;border-radius:6px;max-width:420px;margin:20px auto;padding:16px;font-family:inherit}.cf-card-note{color:var(--fg-dim);margin-bottom:12px}.cf-card-actions{justify-content:center;gap:8px;display:flex}.cf-card-actions button.danger{color:#e06c75;border-color:#e06c75}.diff-side.conflict{color:#e06c75}
package/dist/index.html CHANGED
@@ -8,8 +8,8 @@
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
  <link href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" />
10
10
  <title>megit</title>
11
- <script type="module" crossorigin src="/assets/index-o0rujUbV.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-DPM0na5E.css">
11
+ <script type="module" crossorigin src="/assets/index-BC7TJRRI.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-C845Od_q.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -0,0 +1,174 @@
1
+ // A ~150-line stand-in for the slice of express this server actually used:
2
+ // exact-path routing, JSON bodies, static files and an SPA fallback. express
3
+ // itself is fine, but it drags 67 transitive packages into the published
4
+ // install — most of them single-function modules untouched for five years —
5
+ // and every one of those is install-time attack surface for a tool people run
6
+ // against their own repos. Node's http module covers the rest.
7
+ import { createServer } from 'node:http';
8
+ import { createReadStream, statSync } from 'node:fs';
9
+ import { extname, join, sep } from 'node:path';
10
+ // 10mb, not express's 100kb default: a resolved conflicted file goes back as a
11
+ // JSON string, and a few thousand lines of source blows past the default.
12
+ const BODY_LIMIT = 10 * 1024 * 1024;
13
+ const TYPES = {
14
+ '.html': 'text/html; charset=utf-8',
15
+ '.js': 'text/javascript; charset=utf-8',
16
+ '.css': 'text/css; charset=utf-8',
17
+ '.svg': 'image/svg+xml',
18
+ '.json': 'application/json; charset=utf-8',
19
+ '.map': 'application/json; charset=utf-8',
20
+ '.ico': 'image/x-icon',
21
+ '.png': 'image/png',
22
+ '.webp': 'image/webp',
23
+ '.woff2': 'font/woff2',
24
+ '.wasm': 'application/wasm',
25
+ };
26
+ function decorate(res) {
27
+ const r = res;
28
+ r.status = code => { r.statusCode = code; return r; };
29
+ r.set = (name, value) => { r.setHeader(name, value); return r; };
30
+ r.type = mime => r.set('Content-Type', mime);
31
+ r.json = value => {
32
+ if (!r.hasHeader('Content-Type'))
33
+ r.setHeader('Content-Type', 'application/json; charset=utf-8');
34
+ r.end(JSON.stringify(value));
35
+ };
36
+ r.send = body => {
37
+ const buf = Buffer.isBuffer(body) ? body : Buffer.from(body);
38
+ r.setHeader('Content-Length', String(buf.length));
39
+ r.end(buf);
40
+ };
41
+ return r;
42
+ }
43
+ // Resolves to {} unless the request carries a JSON body — matching express.json,
44
+ // which leaves req.body empty for any other content type.
45
+ function readBody(req) {
46
+ if (req.method === 'GET' || req.method === 'HEAD')
47
+ return Promise.resolve({});
48
+ if (!/^application\/json\b/.test(req.headers['content-type'] ?? ''))
49
+ return Promise.resolve({});
50
+ return new Promise((ok, fail) => {
51
+ const chunks = [];
52
+ let size = 0;
53
+ let over = false;
54
+ req.on('data', (c) => {
55
+ size += c.length;
56
+ if (over)
57
+ return;
58
+ if (size > BODY_LIMIT) {
59
+ // Past the cap: drop what was buffered and swallow the rest. Answering
60
+ // now instead would mean closing the socket with an upload still in
61
+ // flight, and that close is a TCP reset — which is allowed to discard
62
+ // the 413 along with it, leaving the client an ECONNRESET (or an EPIPE
63
+ // on its next write) and no status at all. Reading to the end costs
64
+ // constant memory and buys a clean FIN with the response intact.
65
+ chunks.length = 0;
66
+ over = true;
67
+ return;
68
+ }
69
+ chunks.push(c);
70
+ });
71
+ req.on('error', fail);
72
+ req.on('end', () => {
73
+ if (over)
74
+ return fail(Object.assign(new Error('request entity too large'), { status: 413 }));
75
+ if (!chunks.length)
76
+ return ok({});
77
+ try {
78
+ ok(JSON.parse(Buffer.concat(chunks).toString('utf8')));
79
+ }
80
+ catch {
81
+ fail(Object.assign(new Error('invalid JSON body'), { status: 400 }));
82
+ }
83
+ });
84
+ });
85
+ }
86
+ function sendFile(file, req, res) {
87
+ const st = statSync(file, { throwIfNoEntry: false });
88
+ if (!st?.isFile())
89
+ return false;
90
+ // vite fingerprints asset filenames, so a weak validator off size+mtime is
91
+ // enough to keep reloads from re-sending the bundle
92
+ const etag = `W/"${st.size.toString(16)}-${st.mtimeMs.toString(16)}"`;
93
+ res.set('ETag', etag);
94
+ if (req.headers['if-none-match'] === etag) {
95
+ res.status(304).end();
96
+ return true;
97
+ }
98
+ res.set('Content-Type', TYPES[extname(file).toLowerCase()] ?? 'application/octet-stream');
99
+ res.set('Content-Length', String(st.size));
100
+ createReadStream(file).pipe(res);
101
+ return true;
102
+ }
103
+ // Serves `dir`, falling back to its index.html so client-side routes survive a
104
+ // reload. /api/* never falls through — an unknown API path is a 404, not a page.
105
+ export function serveStatic(dir) {
106
+ const index = join(dir, 'index.html');
107
+ return (req, res) => {
108
+ const path = decodeURIComponent(new URL(req.url ?? '/', 'http://localhost').pathname);
109
+ const file = join(dir, path);
110
+ // join() collapses `..`, but only after decoding — `%2e%2e%2f` arrives here
111
+ // as a real traversal and this is what stops it
112
+ if (file !== dir && !file.startsWith(dir + sep)) {
113
+ res.status(403).json({ error: 'forbidden' });
114
+ return;
115
+ }
116
+ if (sendFile(file, req, res))
117
+ return;
118
+ if (path.startsWith('/api')) {
119
+ res.status(404).json({ error: 'not found' });
120
+ return;
121
+ }
122
+ if (!sendFile(index, req, res))
123
+ res.status(404).json({ error: 'not found' });
124
+ };
125
+ }
126
+ export function createApp() {
127
+ const middleware = [];
128
+ const routes = new Map();
129
+ let fallback = (_req, res) => { res.status(404).json({ error: 'not found' }); };
130
+ const on = (method) => (path, ...handlers) => {
131
+ routes.set(`${method} ${path}`, handlers);
132
+ };
133
+ const listener = async (raw, rawRes) => {
134
+ const req = raw;
135
+ const res = decorate(rawRes);
136
+ try {
137
+ const url = new URL(req.url ?? '/', 'http://localhost');
138
+ req.query = Object.fromEntries(url.searchParams);
139
+ req.body = await readBody(req);
140
+ // HEAD reuses the GET handler; node drops the body for us
141
+ const verb = req.method === 'HEAD' ? 'GET' : req.method;
142
+ for (const handler of [...middleware, ...(routes.get(`${verb} ${url.pathname}`) ?? [fallback])]) {
143
+ let advance = false;
144
+ await handler(req, res, () => { advance = true; });
145
+ if (!advance)
146
+ return;
147
+ }
148
+ }
149
+ catch (e) {
150
+ // express 5 turns a rejected handler into a 500 rather than an unhandled
151
+ // rejection that takes the process down; so does this
152
+ const err = e;
153
+ if (res.headersSent) {
154
+ res.end();
155
+ return;
156
+ }
157
+ // a client that overshot the cap gets its connection retired rather than
158
+ // parked for reuse. Safe only because the body was drained to the end
159
+ // first — closing with an upload still in flight is a reset, not a FIN.
160
+ if (err.status === 413)
161
+ res.setHeader('Connection', 'close');
162
+ res.status(err.status ?? 500).json({ error: err.message });
163
+ }
164
+ };
165
+ return {
166
+ use: (handler) => { middleware.push(handler); },
167
+ get: on('GET'),
168
+ post: on('POST'),
169
+ put: on('PUT'),
170
+ delete: on('DELETE'),
171
+ fallback: (handler) => { fallback = handler; },
172
+ listen: (port, host) => createServer(listener).listen(port, host),
173
+ };
174
+ }
@@ -1,16 +1,20 @@
1
- import express from 'express';
1
+ import { createApp, serveStatic } from './http.js';
2
2
  import { execFile } from 'node:child_process';
3
3
  import { existsSync, readdirSync } from 'node:fs';
4
- import { readFile, realpath } from 'node:fs/promises';
4
+ import { readFile, realpath, writeFile } from 'node:fs/promises';
5
5
  import { homedir } from 'node:os';
6
6
  import { dirname, join, resolve, sep } from 'node:path';
7
7
  import { loadConfig, saveConfig, isPermutation, touchRecent } from './config.js';
8
8
  import { resolveAvatar, parseGithubRemote } from './avatars.js';
9
9
  import { mergeMatches, parseBranchHeader, parseLog, parseMatches, parseMeta, parseNameStatus, parseStatus, stashIndex, LOG_FORMAT, META_FORMAT } from './parse.js';
10
+ import { pickOperation, STATE_FILES } from './operation.js';
10
11
  import { subscribe } from './watch.js';
11
12
  import { wireTerminal, hasPty } from './term.js';
12
- const app = express();
13
- app.use(express.json());
13
+ // JSON bodies are capped at 10mb by createApp — a resolved conflicted file goes
14
+ // back as a JSON string, and a few thousand lines of source blows past a smaller
15
+ // limit. The GET side refuses anything over DIFF_CAP, so the client can't
16
+ // assemble a body larger than 1 MB.
17
+ const app = createApp();
14
18
  // The server listens on loopback only, but that alone doesn't stop a page on
15
19
  // attacker.tld from rebinding its DNS to 127.0.0.1: the browser then treats this
16
20
  // API as same-origin and CORS never applies. Pinning Host closes that — a rebound
@@ -219,12 +223,44 @@ app.get('/api/avatar', repoGuard, async (req, res) => {
219
223
  }
220
224
  res.json({ url: await resolveAvatar(String(req.query.repo), email, git) });
221
225
  });
226
+ // `git rev-parse` once per repo: .git is a file, not a directory, in linked
227
+ // worktrees and submodules, so the path can't be assumed. Repos are registered
228
+ // and long-lived, so caching keeps steady-state detection at zero git processes.
229
+ const gitDirs = new Map();
230
+ async function gitDir(repo) {
231
+ let dir = gitDirs.get(repo);
232
+ if (!dir) {
233
+ dir = (await git(repo, ['rev-parse', '--absolute-git-dir'])).trim();
234
+ gitDirs.set(repo, dir);
235
+ }
236
+ return dir;
237
+ }
238
+ // A short label for the banner, from files git already wrote — no extra process.
239
+ async function opLabel(dir, kind) {
240
+ const read = async (name) => (await readFile(join(dir, name), 'utf8').catch(() => '')).trim();
241
+ if (kind === 'merge')
242
+ return (await read('MERGE_MSG')).split('\n')[0];
243
+ if (kind === 'cherry-pick')
244
+ return (await read('CHERRY_PICK_HEAD')).slice(0, 7);
245
+ if (kind === 'revert')
246
+ return (await read('REVERT_HEAD')).slice(0, 7);
247
+ return ''; // rebase: the banner says "Rebasing" and stops there
248
+ }
249
+ async function readOperation(repo) {
250
+ const dir = await gitDir(repo);
251
+ const kind = pickOperation(STATE_FILES.filter(n => existsSync(join(dir, n))));
252
+ return kind ? { kind, label: await opLabel(dir, kind) } : null;
253
+ }
222
254
  app.get('/api/status', repoGuard, async (req, res) => {
223
255
  try {
224
256
  // --branch prepends the branch/upstream/ahead-behind headers to the output this
225
257
  // already parses — the toolbar's Pull/Push badges for no extra git process
226
258
  const raw = await git(String(req.query.repo), ['status', '--porcelain=v2', '-uall', '--branch', '-z']);
227
- res.json({ files: parseStatus(raw), branch: parseBranchHeader(raw) });
259
+ res.json({
260
+ files: parseStatus(raw),
261
+ branch: parseBranchHeader(raw),
262
+ operation: await readOperation(String(req.query.repo)),
263
+ });
228
264
  }
229
265
  catch (e) {
230
266
  res.status(500).json({ error: e.message });
@@ -567,7 +603,10 @@ app.post('/api/wip', repoGuard, async (req, res) => {
567
603
  await git(repo, ['restore', '--staged', '--', (await mustBeDirty()).path]);
568
604
  break;
569
605
  case 'stage-all':
570
- await git(repo, ['add', '-A']);
606
+ // conflicts excluded: a bare `git add -A` stages files that still have
607
+ // markers in them, which is the mistake the Merge Changes section exists
608
+ // to prevent — the batch button must not walk around it
609
+ await git(repo, ['add', '-A', '--', '.', ...await excludeUnmerged(repo)]);
571
610
  break;
572
611
  case 'unstage-all':
573
612
  await git(repo, ['restore', '--staged', '--', '.']);
@@ -586,7 +625,9 @@ app.post('/api/wip', repoGuard, async (req, res) => {
586
625
  // unstaged only: worktree back to the index, so staged content survives.
587
626
  // clean removes untracked files — a staged new file is tracked, so it stays.
588
627
  // No -x — ignored files (node_modules, .env) are not "changes".
589
- await git(repo, ['restore', '--worktree', '--', '.']);
628
+ // Conflicts are excluded too: they have no stage-0 entry to restore from,
629
+ // so including one makes git fail the whole call.
630
+ await git(repo, ['restore', '--worktree', '--', '.', ...await excludeUnmerged(repo)]);
590
631
  await git(repo, ['clean', '-fd']);
591
632
  break;
592
633
  case 'stash': {
@@ -723,6 +764,90 @@ app.get('/api/diff', repoGuard, async (req, res) => {
723
764
  res.status(500).json({ error: e.message });
724
765
  }
725
766
  });
767
+ // git's own list of unmerged paths. Every conflict action checks against it
768
+ // BEFORE touching the filesystem: `file` arrives from the client, and without
769
+ // this check POST /api/conflict is an arbitrary-file-write primitive bounded
770
+ // only by repoGuard. Paths from git can't traverse out of the repo; paths from
771
+ // the client can. Do not reorder this behind a join() or a read.
772
+ async function unmergedPaths(repo) {
773
+ const out = await git(repo, ['diff', '--name-only', '--diff-filter=U', '-z']);
774
+ return out.split('\0').filter(Boolean);
775
+ }
776
+ // Pathspecs that subtract the conflicted files from a whole-tree operation, for
777
+ // the batch buttons that would otherwise stage or clobber a half-merged file.
778
+ const excludeUnmerged = async (repo) => (await unmergedPaths(repo)).map(p => `:(exclude)${p}`);
779
+ app.get('/api/conflict', repoGuard, async (req, res) => {
780
+ const repo = String(req.query.repo);
781
+ const file = String(req.query.file ?? '');
782
+ try {
783
+ if (!(await unmergedPaths(repo)).includes(file))
784
+ throw httpError(409, 'file is not conflicted');
785
+ // delete/modify where our side deleted it: nothing on disk, and nothing to
786
+ // pick — the client shows the whole-file card instead of a block list
787
+ const buf = await readFile(join(repo, file)).catch(() => null);
788
+ if (!buf)
789
+ res.json({ missing: true });
790
+ else if (buf.length > DIFF_CAP)
791
+ res.json({ tooLarge: true, size: buf.length });
792
+ else if (buf.includes(0))
793
+ res.json({ binary: true });
794
+ else
795
+ res.json({ content: buf.toString('utf8') });
796
+ }
797
+ catch (e) {
798
+ const err = e;
799
+ res.status(err.status ?? 500).json({ error: err.message });
800
+ }
801
+ });
802
+ app.post('/api/conflict', repoGuard, async (req, res) => {
803
+ const repo = String(req.query.repo);
804
+ const action = String(req.body.action ?? '');
805
+ try {
806
+ if (action === 'abort' || action === 'continue') {
807
+ const op = await readOperation(repo);
808
+ if (!op)
809
+ throw httpError(409, 'no operation in progress');
810
+ // GIT_EDITOR=true: GIT_ENV deliberately sets no editor, and `--continue`
811
+ // opens one for the commit message — without this the request never
812
+ // returns. `--no-edit` won't do instead: `git rebase --continue` rejects it.
813
+ await git(repo, [op.kind, `--${action}`], [0], NET_TIMEOUT, { GIT_EDITOR: 'true' });
814
+ res.json({ ok: true });
815
+ return;
816
+ }
817
+ const file = String(req.body.file ?? '');
818
+ // the security boundary — see unmergedPaths. Runs before any path is joined.
819
+ if (!(await unmergedPaths(repo)).includes(file))
820
+ throw httpError(409, 'file is not conflicted');
821
+ switch (action) {
822
+ case 'resolve': {
823
+ const content = req.body.content;
824
+ if (typeof content !== 'string')
825
+ throw httpError(400, 'content must be a string');
826
+ await writeFile(join(repo, file), content);
827
+ await git(repo, ['add', '--', file]);
828
+ break;
829
+ }
830
+ case 'ours':
831
+ case 'theirs':
832
+ // fails on a delete/modify conflict where that side has no version —
833
+ // the error reaches the toast, and Delete is the answer there
834
+ await git(repo, ['checkout', `--${action}`, '--', file]);
835
+ await git(repo, ['add', '--', file]);
836
+ break;
837
+ case 'delete':
838
+ // -f: git rm refuses an unmerged path without it
839
+ await git(repo, ['rm', '-f', '--', file]);
840
+ break;
841
+ default:
842
+ throw httpError(400, `unknown action: ${action}`);
843
+ }
844
+ res.json({ ok: true });
845
+ }
846
+ catch (e) {
847
+ const err = e;
848
+ res.status(err.status ?? 409).json({ error: err.message });
849
+ }
850
+ });
726
851
  // raw bytes of one blob, for rendering image diffs as two <img>. `which` picks
727
852
  // the before/after side; a missing blob (added or deleted file) is a 404 the
728
853
  // client renders as an empty pane.
@@ -795,15 +920,12 @@ app.get('/api/blob', repoGuard, async (req, res) => {
795
920
  }
796
921
  });
797
922
  const dist = join(import.meta.dirname, '..', 'dist');
798
- if (existsSync(dist)) {
799
- app.use(express.static(dist));
800
- app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(join(dist, 'index.html')));
801
- }
923
+ if (existsSync(dist))
924
+ app.fallback(serveStatic(dist));
802
925
  const port = Number(process.env.PORT) || 3411;
803
926
  // exported so bin/megit.js can wait for 'listening' before opening the browser.
804
- // The banner hangs off the 'listening' event rather than app.listen's callback:
805
- // express 5 runs that callback even when the bind failed, which would announce a
806
- // URL that never came up.
927
+ // The banner hangs off the 'listening' event rather than a listen callback, which
928
+ // can fire even when the bind failed and would announce a URL that never came up.
807
929
  export const server = app.listen(port, '127.0.0.1');
808
930
  server.on('listening', () => console.log(`megit API on http://127.0.0.1:${port}`));
809
931
  server.on('error', (e) => {
@@ -0,0 +1,16 @@
1
+ // Order is the whole point: a rebase applying a commit also writes
2
+ // CHERRY_PICK_HEAD, and the user is rebasing, not cherry-picking.
3
+ const ORDER = [
4
+ ['rebase', ['rebase-merge', 'rebase-apply']],
5
+ ['merge', ['MERGE_HEAD']],
6
+ ['cherry-pick', ['CHERRY_PICK_HEAD']],
7
+ ['revert', ['REVERT_HEAD']],
8
+ ];
9
+ export const STATE_FILES = ORDER.flatMap(([, names]) => names);
10
+ export function pickOperation(present) {
11
+ const set = new Set(present);
12
+ for (const [kind, names] of ORDER)
13
+ if (names.some(n => set.has(n)))
14
+ return kind;
15
+ return null;
16
+ }
@@ -1,5 +1,6 @@
1
- import { existsSync } from 'node:fs';
1
+ import { chmodSync, existsSync, readdirSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
+ import { dirname, join } from 'node:path';
3
4
  import { WebSocketServer, WebSocket } from 'ws';
4
5
  import { loadConfig } from './config.js';
5
6
  // node-pty is an optionalDependency: it ships prebuilds for darwin and win32 only,
@@ -35,10 +36,37 @@ export function termKey(repo, pane) {
35
36
  return null;
36
37
  return `${repo}\0${p}`;
37
38
  }
39
+ // node-pty ships spawn-helper as a prebuilt binary, and some package managers drop
40
+ // the executable bit when extracting the tarball — the terminal then fails to spawn.
41
+ // This ran as a postinstall script until an install script proved to be the kind of
42
+ // supply-chain surface scanners flag on sight; it costs nothing to do it here, once,
43
+ // on the first shell. Resolve node-pty rather than guessing ./node_modules: under
44
+ // `npx` it is hoisted to the installing project's node_modules, not ours.
45
+ let helperFixed = false;
46
+ function fixSpawnHelper() {
47
+ if (helperFixed)
48
+ return;
49
+ helperFixed = true;
50
+ try {
51
+ const prebuilds = join(dirname(createRequire(import.meta.url).resolve('node-pty/package.json')), 'prebuilds');
52
+ for (const platform of readdirSync(prebuilds)) {
53
+ try {
54
+ chmodSync(join(prebuilds, platform, 'spawn-helper'), 0o755);
55
+ }
56
+ catch {
57
+ // win32 prebuilds have no spawn-helper; nothing to do
58
+ }
59
+ }
60
+ }
61
+ catch {
62
+ // node-pty is optional — absent on Linux installs without build tools
63
+ }
64
+ }
38
65
  async function getSession(key, repo) {
39
66
  const existing = sessions.get(key);
40
67
  if (existing)
41
68
  return existing;
69
+ fixSpawnHelper();
42
70
  // dynamic import: the native module never loads until a terminal is actually opened
43
71
  const { spawn } = await import('node-pty');
44
72
  const shell = process.env.SHELL || '/bin/sh';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "megit-app",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Git repository viewer in the browser: commit graph with branch lanes, diffs, stashes and WIP",
5
5
  "license": "MIT",
6
6
  "author": "Hoang Vuong Vu",
@@ -25,7 +25,6 @@
25
25
  "bin",
26
26
  "dist",
27
27
  "dist-server",
28
- "scripts/fix-pty-perms.mjs",
29
28
  "CHANGELOG.md"
30
29
  ],
31
30
  "engines": {
@@ -37,24 +36,21 @@
37
36
  "build:server": "node -e \"fs.rmSync('dist-server',{recursive:true,force:true})\" && tsc -p tsconfig.server.json",
38
37
  "start": "PORT=4500 node server/index.ts",
39
38
  "test": "vitest run",
40
- "prepublishOnly": "pnpm test && pnpm build && pnpm build:server",
41
- "postinstall": "node scripts/fix-pty-perms.mjs"
39
+ "prepublishOnly": "pnpm test && pnpm build && pnpm build:server"
42
40
  },
43
41
  "packageManager": "pnpm@11.18.0+sha512.33d83c77da82f49fba836925c6f1b841181ec3132b670639bd012f7075f5c7cf634c5f870147c19aae7478fac01df09d8892e880454896edd23ee9b33757563c",
44
42
  "dependencies": {
45
- "express": "^5.2.1",
46
- "ws": "^8.21.1"
43
+ "ws": "^8.21.3"
47
44
  },
48
45
  "optionalDependencies": {
49
46
  "node-pty": "^1.1.0"
50
47
  },
51
48
  "devDependencies": {
52
- "@types/express": "^5.0.6",
53
- "@types/node": "^26.1.2",
54
- "@types/react": "^19.2.17",
55
- "@types/react-dom": "^19.2.3",
49
+ "@types/node": "^26.2.0",
50
+ "@types/react": "^19.2.18",
51
+ "@types/react-dom": "^19.2.4",
56
52
  "@types/ws": "^8.18.1",
57
- "@vitejs/plugin-react": "^6.0.4",
53
+ "@vitejs/plugin-react": "^6.0.5",
58
54
  "@xterm/addon-fit": "^0.11.0",
59
55
  "@xterm/xterm": "^6.0.0",
60
56
  "concurrently": "^10.0.4",
@@ -63,7 +59,7 @@
63
59
  "react": "^19.2.8",
64
60
  "react-dom": "^19.2.8",
65
61
  "typescript": "^7.0.2",
66
- "vite": "^8.1.5",
62
+ "vite": "^8.2.1",
67
63
  "vitest": "^4.1.10"
68
64
  }
69
65
  }