crloop 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ :root{--font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;--font-heading: "Oswald", sans-serif;--color-orange: #ff6b35;--color-teal: #00d4aa;--radius: 16px;--radius-sm: 8px;--radius-xs: 4px;--diff-gutter-width: 2.5rem;--sidebar-width: 240px}:root,[data-theme=dark]{color-scheme:dark;--bg-page: #1a1a1a;--bg-card: #212121;--bg-elevated: #2d2d2d;--bg-placeholder: #3d3d3d;--bg-hover: rgba(255, 255, 255, .05);--bg-selected: rgba(255, 107, 53, .12);--text-primary: #e8e8e8;--text-secondary: #aaaaaa;--text-muted: #666666;--text-code: #c9c9c9;--border-color: #2d2d2d;--border-soft: #383838;--diff-code-bg: #1e1e1e;--diff-removed-bg: #2a1505;--diff-removed-text: #ff6b35;--diff-removed-gutter: #ff6b3530;--diff-added-bg: #051a14;--diff-added-text: #00d4aa;--diff-added-gutter: #00d4aa25;--diff-context-text: #c9c9c9;--diff-line-num: #555555;--diff-hunk-bg: #252525;--diff-hunk-text: #666666;--diff-empty-bg: #1a1a1a;--comment-bg: #252525;--comment-border: #333333;--comment-action-bg: #2d2d2d;--tab-active-color: var(--color-orange);--tab-inactive-color: #666666;--toggle-active-bg: var(--color-teal);--toggle-active-text: #000000;--toggle-inactive-bg: #2d2d2d;--toggle-inactive-text: #666666;--color-amber-bg: rgba(184, 122, 0, .18);--color-amber-text: #d4a017}[data-theme=light]{color-scheme:light;--bg-page: #f5f5f5;--bg-card: #ffffff;--bg-elevated: #f0f0f0;--bg-placeholder: #e0e0e0;--bg-hover: rgba(0, 0, 0, .04);--bg-selected: rgba(255, 107, 53, .08);--text-primary: #1a1a1a;--text-secondary: #555555;--text-muted: #999999;--text-code: #333333;--border-color: #e0e0e0;--border-soft: #eeeeee;--diff-code-bg: #ffffff;--diff-removed-bg: #fff0ec;--diff-removed-text: #cc4400;--diff-removed-gutter: #ff6b3520;--diff-added-bg: #f0fff8;--diff-added-text: #007755;--diff-added-gutter: #00d4aa20;--diff-context-text: #333333;--diff-line-num: #aaaaaa;--diff-hunk-bg: #f0f0f0;--diff-hunk-text: #999999;--diff-empty-bg: #f8f8f8;--comment-bg: #fafafa;--comment-border: #e0e0e0;--comment-action-bg: #f0f0f0;--tab-active-color: var(--color-orange);--tab-inactive-color: #999999;--toggle-active-bg: var(--color-teal);--toggle-active-text: #ffffff;--toggle-inactive-bg: #e8e8e8;--toggle-inactive-text: #999999;--color-amber-bg: #fff0c2;--color-amber-text: #b87a00}*,*:before,*:after{box-sizing:border-box}body{margin:0;min-height:100vh;background:var(--bg-page);color:var(--text-primary);font-family:var(--font-mono);font-size:13px;line-height:1.5}h1,h2,h3,h4,h5,h6{font-family:var(--font-heading);margin:0;font-weight:600;letter-spacing:.02em}p{margin:0}button,a,textarea,select,input{font:inherit}.app-shell{display:flex;flex-direction:column;min-height:100vh;background:var(--bg-page)}.layout{display:grid;grid-template-columns:var(--sidebar-width, 240px) 5px 1fr;grid-template-rows:1fr;min-height:100vh;transition:grid-template-columns .18s ease}.layout-sidebar-collapsed{grid-template-columns:48px 0 1fr}.sidebar-widget{background:var(--bg-page);border-right:1px solid var(--border-color);display:flex;flex-direction:column;min-height:100vh;overflow:hidden}.sidebar-brand{display:flex;align-items:center;gap:8px;padding:24px 24px 0;-webkit-user-select:none;user-select:none;flex-shrink:0}.sidebar-logo-icon{display:flex;align-items:center;justify-content:center;flex:0 0 auto;color:var(--color-orange);width:16px;height:16px}.sidebar-logo-icon svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}.sidebar-logo-text{font-family:var(--font-mono);font-size:16px;font-weight:600;color:var(--text-primary);flex:1 1 auto;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-collapse-btn{width:22px;min-width:22px;height:22px;border-radius:4px;border:1px solid var(--border-color);background:var(--bg-elevated);color:var(--text-muted);display:flex;align-items:center;justify-content:center;padding:0;font-size:11px;cursor:pointer;transition:background .12s ease,color .12s ease;margin-left:auto}.sidebar-collapse-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.sidebar-widget-collapsed .sidebar-brand{padding:16px 0 0;justify-content:center}.sidebar-widget-collapsed .sidebar-collapse-btn{margin-left:0}.sidebar-info{display:flex;flex-direction:column;gap:8px;padding:12px 24px 16px;flex-shrink:0}.repo-selector-tabs-wrapper+.sidebar-info{padding-top:8px}.sidebar-repo-row{display:flex;align-items:center;gap:6px}.sidebar-repo-icon,.sidebar-branch-icon{display:flex;align-items:center;flex:0 0 auto}.sidebar-repo-icon svg{width:14px;height:14px;fill:none;stroke:#777;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}.sidebar-branch-icon svg{width:12px;height:12px;fill:none;stroke:var(--color-teal);stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}[data-theme=light] .sidebar-branch-icon svg{stroke:#00a88a}.sidebar-repo-name{font-family:var(--font-mono);font-size:12px;color:#777;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[data-theme=light] .sidebar-repo-name{color:#999}.sidebar-branch-name{font-family:var(--font-mono);font-size:12px;color:var(--color-teal);white-space:nowrap}[data-theme=light] .sidebar-branch-name{color:#00a88a}.sidebar-stats-row{display:flex;align-items:center;justify-content:space-between}.sidebar-stats-label{font-family:var(--font-mono);font-size:9px;color:#777}[data-theme=light] .sidebar-stats-label{color:#aaa}.sidebar-stats-grp{display:flex;align-items:center;gap:6px}.sidebar-stat-add{font-family:var(--font-mono);font-size:11px;color:var(--color-teal)}.sidebar-stat-del{font-family:var(--font-mono);font-size:11px;color:var(--color-orange)}[data-theme=light] .sidebar-stat-add{color:#00a88a}.sidebar-body{flex:1 1 auto;min-width:0;padding:4px 8px 8px;overflow:hidden;transition:transform .18s ease,opacity .18s ease,padding .18s ease}.sidebar-widget-collapsed .sidebar-body{transform:translate(1rem);opacity:0;pointer-events:none;padding-inline:0;padding-bottom:0}.sidebar-widget-collapsed .sidebar-info{display:none}.sidebar-footer{padding:12px 16px 24px}.sidebar-widget-collapsed .sidebar-footer{display:none}.sidebar-export-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:10px 0;border:none;border-radius:var(--radius);background:var(--bg-elevated);color:var(--color-orange);font-size:12px;font-weight:600;font-family:var(--font-mono);text-decoration:none;cursor:pointer;transition:background .12s ease,opacity .12s ease;white-space:nowrap}.sidebar-export-btn:hover{opacity:.85}.sidebar-export-btn svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}[data-theme=light] .sidebar-export-btn{background:#fff0e8}.change-list,.comment-list{list-style:none;margin:0;padding:0}.change-tree{display:flex;flex-direction:column;gap:1px}.change-tree-node{min-width:0}.change-tree-children{list-style:none;margin:0;padding:0}.change-tree-directory,.change-item{width:100%;text-align:left;border-radius:0;border:1px solid transparent;padding:0 8px;background:transparent;display:flex;flex-direction:column;gap:0;cursor:pointer;color:var(--text-secondary);transition:background .1s ease,color .1s ease}.change-tree-directory{flex-direction:row;align-items:center;gap:6px;height:28px;padding:0 8px;padding-left:calc(8px + var(--tree-depth, 0) * 12px);font-size:12px}.change-tree-directory:hover,.change-item:hover{background:var(--bg-hover);color:var(--text-primary)}.change-item.selected{border-color:transparent;border-radius:16px;background:var(--bg-elevated);color:var(--text-primary)}[data-theme=light] .change-item.selected{background:#fff0e8}.change-tree-file{height:28px;padding:0 8px;padding-left:calc(8px + var(--tree-depth, 0) * 12px);font-size:12px;flex-direction:row;align-items:center}.change-tree-file.selected{padding-left:calc(8px + var(--tree-depth, 0) * 12px)}.change-tree-file-row{display:flex;align-items:center;gap:6px;min-width:0;width:100%;height:100%}.change-tree-active-dot{width:6px;height:6px;min-width:6px;border-radius:3px;background:var(--color-orange);flex-shrink:0}.change-tree-chevron,.change-tree-folder-icon,.change-tree-file-icon{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}.change-tree-chevron{width:12px;height:12px;color:#777;transition:transform .15s ease}.change-tree-chevron-expanded{transform:rotate(0)}.change-tree-chevron:not(.change-tree-chevron-expanded){transform:rotate(-90deg)}.change-tree-chevron svg,.change-tree-folder-icon svg,.change-tree-file-icon svg{width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}.change-tree-folder-icon{width:14px;height:14px;color:#ffb347}.change-tree-file-icon{width:14px;height:14px;color:#777}.change-item.selected .change-tree-file-icon{color:#58a6ff}[data-theme=light] .change-item.selected .change-tree-file-icon{color:var(--color-orange)}.change-type-added .change-tree-file-icon,.change-type-untracked .change-tree-file-icon,.change-type-added .path-text,.change-type-untracked .path-text{color:var(--diff-added-text)}.change-type-deleted .change-tree-file-icon,.change-type-deleted .path-text{color:var(--diff-removed-text)}.change-type-renamed .change-tree-file-icon,.change-type-renamed .path-text{color:var(--color-amber-text)}.change-tree-label{min-width:0;font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:12px;color:var(--text-primary)}.path-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:400;min-width:0;flex:1 1 auto;font-size:12px;color:var(--text-secondary)}.change-item.selected .path-text{color:var(--text-primary)}.change-meta{color:var(--text-muted);font-size:10px;white-space:nowrap}.change-comment-count{flex:0 0 auto;min-width:18px;height:18px;padding:2px 6px;border-radius:10px;background:var(--bg-elevated);color:var(--color-orange);display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;line-height:1}.change-item.selected .change-comment-count{background:var(--color-orange);color:#0d0d0d}[data-theme=light] .change-comment-count{background:#fff0e8}[data-theme=light] .change-item.selected .change-comment-count{background:var(--color-orange);color:#fff}.sidebar-resizer{min-height:100vh;cursor:col-resize;position:relative;touch-action:none;background:var(--border-color)}.sidebar-resizer:after{content:"";position:absolute;top:50%;left:50%;width:3px;height:2.5rem;border-radius:999px;background:var(--bg-placeholder);transform:translate(-50%,-50%);transition:background .14s ease}.sidebar-resizer:hover:after,.sidebar-resizer-active:after,.sidebar-resizer:focus-visible:after{background:var(--color-orange)}.sidebar-resizer:focus-visible{outline:none}.layout-sidebar-collapsed .sidebar-resizer{pointer-events:none;opacity:0}.review-pane{display:flex;flex-direction:column;min-height:100vh;background:var(--bg-page);min-width:0}.main-toolbar{display:flex;align-items:center;gap:6px;padding:0 20px;height:48px;min-height:48px;background:var(--bg-card);border-bottom:1px solid var(--border-color);flex-shrink:0}.toolbar-sep{width:1px;height:20px;background:var(--border-soft);flex-shrink:0}.toolbar-spacer{flex:1 1 auto}.view-toggle{display:inline-flex;align-items:center;background:var(--bg-elevated);border-radius:12px;padding:3px;gap:2px}.view-toggle-btn{display:inline-flex;align-items:center;gap:6px;border:none;background:transparent;color:var(--text-muted);font-size:12px;font-family:var(--font-mono);padding:4px 10px;border-radius:10px;cursor:pointer;font-weight:400;white-space:nowrap;transition:background .12s ease,color .12s ease}.view-toggle-btn:hover{color:var(--text-secondary)}.view-toggle-btn.active{background:var(--bg-placeholder);color:var(--color-orange);font-weight:600}.view-toggle-btn svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}.context-select{display:inline-flex;align-items:center;gap:6px;background:var(--bg-elevated);border-radius:12px;padding:6px 12px;font-size:12px;font-family:var(--font-mono);color:var(--text-muted);cursor:pointer;white-space:nowrap}.ctx-prefix{color:var(--text-muted);font-size:12px}.context-select select{border:none;background:transparent;color:var(--text-primary);font-size:12px;font-family:var(--font-mono);appearance:none;cursor:pointer;outline:none}.context-select svg{width:12px;height:12px;fill:none;stroke:var(--text-muted);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}.hide-removed-toggle{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border:none;border-radius:12px;background:transparent;color:var(--text-muted);font-size:12px;font-family:var(--font-mono);cursor:pointer;transition:background .12s ease,color .12s ease;white-space:nowrap}.hide-removed-toggle:hover{background:var(--bg-hover);color:var(--text-secondary)}.hide-removed-toggle.active{color:var(--text-primary)}.hide-removed-toggle svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}.toggle-track{width:32px;height:18px;border-radius:9px;background:var(--bg-placeholder);flex-shrink:0;transition:background .15s ease}.hide-removed-toggle.active .toggle-track{background:var(--color-teal)}.toolbar-pill-btn{display:inline-flex;align-items:center;justify-content:center;padding:6px 10px;border:none;border-radius:12px;background:var(--bg-elevated);color:var(--text-muted);font-size:12px;font-family:var(--font-mono);cursor:pointer;transition:background .12s ease,color .12s ease;white-space:nowrap}.toolbar-pill-btn:hover{color:var(--text-secondary)}.toolbar-pill-btn-labeled{gap:6px;padding:6px 12px}[data-theme=light] .toolbar-pill-btn-theme{background:#fff0e8}.toolbar-pill-btn svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}.file-header{display:flex;align-items:center;gap:10px;padding:0 20px;height:40px;min-height:40px;background:var(--diff-code-bg);border-bottom:1px solid var(--border-color);flex-shrink:0}.fh-file-icon{display:inline-flex;align-items:center;flex-shrink:0;color:#58a6ff}[data-theme=light] .fh-file-icon{color:var(--color-orange)}.fh-file-icon svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}.fh-path{font-size:12px;font-family:var(--font-mono);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.fh-path strong{color:var(--text-primary);font-weight:400}.fh-spacer{flex:1 1 auto}.fh-badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius:10px;font-size:11px;font-family:var(--font-mono);font-weight:600;flex-shrink:0}.fh-badge-added{background:#0a2a1a;color:var(--color-teal)}[data-theme=light] .fh-badge-added{background:#e8fbf5;color:#075}.fh-badge-removed{background:#2a1505;color:var(--color-orange)}[data-theme=light] .fh-badge-removed{background:#fff0ec;color:var(--diff-removed-text)}.fh-collapse-btn{display:inline-flex;align-items:center;justify-content:center;padding:4px 8px;border:none;border-radius:10px;background:var(--bg-elevated);color:var(--text-muted);cursor:pointer;flex-shrink:0;transition:background .12s ease,color .12s ease}.fh-collapse-btn:hover{color:var(--text-primary)}.fh-collapse-btn svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.error-banner{padding:8px 14px;background:#cc44001f;color:var(--color-orange);font-size:12px;border-bottom:1px solid rgba(204,68,0,.2);flex-shrink:0}.diff-scroll-wrap{flex:1 1 auto;overflow:auto;background:var(--diff-code-bg)}.diff-scroll{overflow:auto;background:var(--diff-code-bg);flex:1 1 auto}.hunk-block+.hunk-block{margin-top:0}.hunk-header{padding:3px 10px;border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color);background:var(--diff-hunk-bg);color:var(--diff-hunk-text);font-family:var(--font-mono);font-size:11px;line-height:1.4;-webkit-user-select:none;user-select:none}.hunk-block:first-child .hunk-header{border-top:0}.diff-table{width:max-content;min-width:100%;border-collapse:collapse;font-family:var(--font-mono);font-size:12px;line-height:1.5;table-layout:auto;color:var(--diff-context-text);background:var(--diff-code-bg)}.side-table{width:100%;table-layout:fixed}.diff-row td{border-bottom:0}.gutter{width:var(--diff-gutter-width);color:var(--diff-line-num);text-align:right;padding:0 6px 0 4px;vertical-align:top;background:var(--diff-code-bg);font-size:10px;-webkit-user-select:none;user-select:none;position:relative}.gutter-removed{background:var(--diff-removed-gutter);color:var(--diff-removed-text);box-shadow:inset 3px 0 0 var(--color-orange)}.gutter-added{background:var(--diff-added-gutter);color:var(--diff-added-text);box-shadow:inset 3px 0 0 var(--color-teal)}.gutter-context{background:var(--diff-code-bg)}.code-cell{white-space:pre;word-break:normal;padding:0 10px;background:var(--diff-code-bg);color:var(--diff-context-text)}.code-cell-removed,.line-removed{background:var(--diff-removed-bg);color:var(--diff-removed-text)}.code-cell-added,.line-added{background:var(--diff-added-bg);color:var(--diff-added-text)}.code-cell-context{background:var(--diff-code-bg)}.side-table .code-cell{width:calc((100% - (var(--diff-gutter-width) * 4)) / 2);min-width:0;white-space:pre-wrap;overflow-wrap:anywhere}.side-table .lane-left.code-cell,.side-table .lane-left.empty-cell{border-right:1px solid var(--border-color)}.empty-cell{background:var(--diff-empty-bg)}.empty-gutter{background:var(--diff-code-bg)}.diff-row-clickable,.line-clickable-cell{cursor:pointer}.diff-row-clickable:hover .gutter,.diff-row-clickable:hover .code-cell,.line-clickable-cell:hover{filter:brightness(1.08)}.selected-cell,.diff-row-selected td{outline:none;box-shadow:inset 0 0 0 1px #ff6b3566}.diff-syntax{margin:0;padding:0;min-width:0;background:transparent}.diff-syntax-code{display:block;margin:0;padding:0;background:transparent;color:inherit;font:inherit;white-space:inherit;overflow-wrap:inherit;word-break:inherit}[data-theme=dark] .diff-syntax-code .token.comment,[data-theme=dark] .diff-syntax-code .token.prolog,[data-theme=dark] .diff-syntax-code .token.cdata{color:#6a9955}[data-theme=dark] .diff-syntax-code .token.punctuation,[data-theme=dark] .diff-syntax-code .token.operator{color:#d4d4d4}[data-theme=dark] .diff-syntax-code .token.keyword,[data-theme=dark] .diff-syntax-code .token.atrule{color:#569cd6;font-weight:500}[data-theme=dark] .diff-syntax-code .token.string,[data-theme=dark] .diff-syntax-code .token.char,[data-theme=dark] .diff-syntax-code .token.attr-value{color:#ce9178}[data-theme=dark] .diff-syntax-code .token.number,[data-theme=dark] .diff-syntax-code .token.boolean,[data-theme=dark] .diff-syntax-code .token.constant{color:#b5cea8}[data-theme=dark] .diff-syntax-code .token.function{color:#dcdcaa}[data-theme=dark] .diff-syntax-code .token.class-name{color:#4ec9b0}[data-theme=dark] .diff-syntax-code .token.property,[data-theme=dark] .diff-syntax-code .token.attr-name{color:#9cdcfe}[data-theme=light] .diff-syntax-code .token.comment,[data-theme=light] .diff-syntax-code .token.prolog{color:#5f6f82}[data-theme=light] .diff-syntax-code .token.keyword,[data-theme=light] .diff-syntax-code .token.atrule{color:#0b5cab;font-weight:600}[data-theme=light] .diff-syntax-code .token.string,[data-theme=light] .diff-syntax-code .token.attr-value{color:#1f7a5c}[data-theme=light] .diff-syntax-code .token.function{color:#005a7a}[data-theme=light] .diff-syntax-code .token.class-name{color:#6b46c1}[data-theme=light] .diff-syntax-code .token.number,[data-theme=light] .diff-syntax-code .token.boolean{color:#8a5712}.inline-thread-row td{border-bottom:0}.inline-thread-gutter-spacer{width:calc(var(--diff-gutter-width) * 2);background:var(--diff-code-bg)}.inline-thread-cell{padding:0}.inline-thread-cell-code{background:var(--diff-code-bg)}.inline-thread-panel{margin:0;padding:6px 8px 8px;background:var(--comment-bg);border-top:1px solid var(--comment-border);border-bottom:1px solid var(--comment-border)}.side-inline-thread-gutter-spacer{background:var(--diff-code-bg)}.side-inline-thread-code-cell{padding:0;vertical-align:top}.comment-card{border:1px solid var(--comment-border);border-radius:var(--radius-sm);background:var(--comment-bg);padding:8px 10px}.comment-card+.comment-card{margin-top:6px}.comment-card-inline{border-radius:var(--radius-xs);border-color:var(--comment-border);background:var(--comment-bg);padding:8px 10px}.comment-card-inline+.comment-card-inline,.comment-card-inline+.inline-composer,.inline-composer+.comment-card-inline{margin-top:6px}.comment-card-muted{opacity:.78}.comment-card-header{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-bottom:5px}.comment-anchor{color:var(--text-muted);font-size:10px;font-family:var(--font-mono)}.comment-date{color:var(--text-muted);font-size:10px}.comment-status{display:inline-flex;align-items:center;border-radius:999px;padding:1px 8px;background:var(--color-amber-bg);color:var(--color-amber-text);font-size:10px;font-family:var(--font-mono);font-weight:600;letter-spacing:.05em}.comment-body{white-space:pre-wrap;font-size:12px;color:var(--text-primary);line-height:1.5}.comment-actions{display:flex;gap:4px;margin-top:6px}.comment-card-actions{margin-top:6px}.inline-composer{border-radius:var(--radius-xs);border:1px solid var(--comment-border);background:var(--comment-bg);padding:8px 10px}textarea{width:100%;border:1px solid var(--border-soft);border-radius:var(--radius-xs);padding:6px 8px;resize:vertical;margin-bottom:6px;background:var(--bg-elevated);color:var(--text-primary);font-family:var(--font-mono);font-size:12px}textarea:focus{outline:none;border-color:var(--color-orange)}button{cursor:pointer}.btn{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border:1px solid var(--border-soft);border-radius:var(--radius-xs);background:var(--bg-elevated);color:var(--text-secondary);font-size:11px;font-family:var(--font-mono);cursor:pointer;transition:background .12s ease,color .12s ease,border-color .12s ease}.btn:hover{background:var(--bg-hover);color:var(--text-primary)}.btn-primary{background:var(--color-orange);color:#fff;border-color:var(--color-orange)}.btn-primary:hover{background:#e55a25;border-color:#e55a25}.inline-thread-panel button,.inline-thread-panel .button-link{display:inline-flex;align-items:center;padding:2px 8px;border:1px solid var(--border-soft);border-radius:var(--radius-xs);background:var(--bg-elevated);color:var(--text-secondary);font-size:11px;font-family:var(--font-mono);cursor:pointer;transition:background .12s ease,color .12s ease}.inline-thread-panel button:hover,.inline-thread-panel .button-link:hover{background:var(--bg-hover);color:var(--text-primary)}.ghost-button{background:transparent;border-color:transparent}.danger-button{color:var(--color-orange);border-color:#ff6b354d}.inline-thread-panel .danger-button:hover{background:#ff6b351a;color:var(--color-orange)}.empty-state,.binary-state,.renamed-state,.empty-panel{padding:20px;color:var(--text-muted);font-size:12px}.compact-empty-panel{padding:6px 10px;font-size:11px;color:var(--text-muted)}.export-header{display:flex;align-items:center;gap:10px;padding:0 24px;height:48px;min-height:48px;background:var(--bg-card);border-bottom:1px solid var(--border-color);flex-shrink:0}.export-header-icon{display:flex;align-items:center;flex-shrink:0;color:var(--color-orange)}.export-header-icon svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}.export-header-title{font-family:var(--font-mono);font-size:14px;font-weight:600;color:var(--text-primary);white-space:nowrap}.export-header-subtitle{font-family:var(--font-mono);font-size:11px;color:#555;white-space:nowrap}.export-header-spacer{flex:1 1 auto}.export-copy-btn,.export-download-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border:none;border-radius:var(--radius);font-family:var(--font-mono);font-size:12px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:opacity .12s ease}.export-copy-btn{background:var(--bg-placeholder);color:var(--text-primary)}.export-download-btn{background:var(--color-orange);color:#0d0d0d;font-weight:600}.export-copy-btn:hover,.export-download-btn:hover{opacity:.85}.export-copy-btn svg,.export-download-btn svg,.export-close-btn svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}.export-close-btn{display:inline-flex;align-items:center;justify-content:center;padding:6px;border:none;border-radius:8px;background:transparent;color:var(--text-muted);cursor:pointer;flex-shrink:0;transition:background .12s ease,color .12s ease}.export-close-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.export-content{flex:1 1 auto;display:flex;flex-direction:column;gap:16px;padding:20px 32px;background:var(--bg-card);overflow:hidden;min-height:0}.export-tip-bar{display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:var(--radius);background:var(--bg-elevated);flex-shrink:0}.export-tip-icon{display:flex;align-items:center;flex-shrink:0;color:var(--color-orange)}.export-tip-icon svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}.export-tip-text{font-family:var(--font-mono);font-size:11px;color:#777}.export-text-area{flex:1 1 auto;min-height:0;overflow-y:auto;background:var(--bg-page);border-radius:var(--radius);padding:24px}.export-text-pre{margin:0;min-height:100%;font-family:var(--font-mono);font-size:12px;line-height:1.6;color:var(--text-code);white-space:pre-wrap;overflow-wrap:anywhere}.export-loading-text{font-family:var(--font-mono);font-size:12px;color:var(--text-muted)}.export-total-label{display:block;font-family:var(--font-mono);font-size:11px;color:#555;padding:12px 24px 24px}[data-theme=light] .export-total-label{color:#999}:root,[data-theme=dark]{--repo-tab-active-bg: #2d2d2d;--repo-tab-active-text: #ffffff;--repo-tab-inactive-text: #555555;--repo-overflow-bg: #1e1e1e;--repo-overflow-text: #777777;--repo-overflow-chevron: #555555;--repo-add-btn-bg: #222222;--repo-add-btn-icon: #555555;--repo-divider-color: #252525;--repo-dropdown-bg: #1e1e1e;--repo-dropdown-item-hover: #2d2d2d;--repo-dropdown-text: #777777}[data-theme=light]{--repo-tab-active-bg: #e0e0e0;--repo-tab-active-text: #1a1a1a;--repo-tab-inactive-text: #777777;--repo-overflow-bg: #eeeeee;--repo-overflow-text: #777777;--repo-overflow-chevron: #aaaaaa;--repo-add-btn-bg: #e0e0e0;--repo-add-btn-icon: #888888;--repo-divider-color: #e0e0e0;--repo-dropdown-bg: #ffffff;--repo-dropdown-item-hover: #f0f0f0;--repo-dropdown-text: #666666}.repo-selector-divider{height:1px;background:var(--repo-divider-color);margin:10px 24px 8px;flex-shrink:0}.repo-selector-tabs-wrapper{position:relative;flex-shrink:0}.repo-selector-tabs{display:flex;align-items:center;gap:4px;padding:0 24px}.repo-tab{display:flex;align-items:center;border:none;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;font-weight:400;color:var(--repo-tab-inactive-text);border-radius:16px;padding:5px 12px;white-space:nowrap;transition:color .12s ease,background .12s ease;flex-shrink:0}.repo-tab:hover{color:var(--text-primary)}.repo-tab-active{background:var(--repo-tab-active-bg);color:var(--repo-tab-active-text);font-weight:600}.repo-tab-active:hover{color:var(--repo-tab-active-text)}.repo-selector-spacer{flex:1}.repo-overflow-pill{display:flex;align-items:center;gap:4px;padding:5px 10px;background:var(--repo-overflow-bg);border-radius:16px;border:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;font-weight:400;color:var(--repo-overflow-text);white-space:nowrap;transition:opacity .12s ease}.repo-overflow-pill:hover{color:var(--text-primary)}.repo-overflow-chevron{display:flex;align-items:center;color:var(--repo-overflow-chevron);flex-shrink:0}.repo-overflow-chevron svg{width:10px;height:10px;fill:none;stroke:currentColor;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round}.repo-overflow-dropdown{position:absolute;top:calc(100% + 4px);left:24px;z-index:200;background:var(--repo-dropdown-bg);border-radius:8px;padding:3px 4px;display:flex;flex-direction:column;gap:1px;width:calc(100% - 48px)}.repo-overflow-item{display:flex;align-items:center;padding:4px 8px;border-radius:6px;border:none;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--repo-tab-inactive-text);text-align:left;white-space:nowrap;width:100%;transition:background 80ms ease}.repo-overflow-item:hover{background:var(--repo-dropdown-item-hover);color:var(--text-primary)}.repo-selector-add-btn{display:flex;align-items:center;justify-content:center;padding:4px 7px;background:var(--repo-add-btn-bg);border-radius:8px;border:none;cursor:pointer;color:var(--repo-add-btn-icon);flex-shrink:0;transition:opacity .12s ease}.repo-selector-add-btn:hover{opacity:.8}.repo-selector-add-btn svg,.repo-selector-add-inline svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;display:block}.repo-selector-empty{padding:16px 24px 8px;display:flex;flex-direction:column;gap:8px;flex-shrink:0}.repo-selector-empty-text{color:var(--text-muted);font-size:12px;font-family:var(--font-mono)}.repo-selector-empty-hint{color:var(--text-muted);font-size:11px;font-family:var(--font-mono);line-height:1.8}.repo-selector-empty-hint code{color:var(--text-secondary);background:var(--bg-elevated);padding:1px 4px;border-radius:4px;font-size:10px;font-family:var(--font-mono)}.repo-selector-add-inline{display:inline-flex;align-items:center;padding:1px 6px;background:var(--repo-add-btn-bg);border-radius:6px;border:none;cursor:pointer;color:var(--repo-add-btn-icon);font-family:var(--font-mono);font-size:11px;transition:opacity .12s ease}.repo-selector-add-inline:hover{opacity:.8}.modal-backdrop{position:fixed;inset:0;z-index:1000;background:#0009;display:flex;align-items:center;justify-content:center}[data-theme=light] .modal-backdrop{background:#00000040}.modal-panel{background:var(--bg-card);border:1px solid var(--border-color);border-radius:12px;width:360px;max-width:calc(100vw - 48px);display:flex;flex-direction:column;gap:0;box-shadow:0 8px 32px #00000080}[data-theme=light] .modal-panel{box-shadow:0 8px 32px #00000026}.modal-header{display:flex;align-items:center;padding:16px 20px 12px;border-bottom:1px solid var(--border-color)}.modal-title{font-family:var(--font-mono);font-size:13px;font-weight:600;color:var(--text-primary);flex:1}.modal-close-btn{width:24px;height:24px;border:none;background:none;cursor:pointer;color:var(--text-muted);font-size:18px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:color .12s ease,background .12s ease}.modal-close-btn:hover{color:var(--text-primary);background:var(--bg-hover)}.modal-body{display:flex;flex-direction:column;gap:14px;padding:16px 20px}.modal-field{display:flex;flex-direction:column;gap:6px}.modal-label{font-family:var(--font-mono);font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em}.modal-input{background:var(--bg-elevated);border:1px solid var(--border-soft);border-radius:6px;padding:7px 10px;font-family:var(--font-mono);font-size:12px;color:var(--text-primary);outline:none;transition:border-color .12s ease}.modal-input:focus{border-color:var(--color-teal)}.modal-error{font-family:var(--font-mono);font-size:11px;color:var(--diff-removed-text)}.modal-footer{display:flex;justify-content:flex-end;padding:12px 20px 16px;border-top:1px solid var(--border-color)}.modal-add-btn{background:var(--bg-elevated);border:1px solid var(--border-soft);border-radius:8px;padding:6px 20px;font-family:var(--font-mono);font-size:12px;font-weight:600;color:var(--text-primary);cursor:pointer;transition:background .12s ease,opacity .12s ease}.modal-add-btn:hover:not(:disabled){background:var(--bg-placeholder)}.modal-add-btn:disabled{opacity:.4;cursor:not-allowed}@media(max-width:900px){.layout,.layout-sidebar-collapsed{grid-template-columns:1fr}.sidebar-resizer{display:none}.sidebar-widget{min-height:auto}.sidebar-widget-collapsed .sidebar-body{max-height:0;padding-block:0}.review-pane{min-height:auto}.main-toolbar{flex-wrap:wrap;height:auto;padding:6px 12px;gap:4px}.file-header{flex-wrap:wrap;height:auto;padding:6px 12px;gap:6px}.fh-spacer{display:none}}
@@ -0,0 +1,42 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <rect width="64" height="64" rx="14" fill="#1f1f1f" />
3
+ <circle
4
+ cx="44"
5
+ cy="44"
6
+ r="7"
7
+ fill="none"
8
+ stroke="#ff6b35"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ stroke-width="4"
12
+ />
13
+ <circle
14
+ cx="20"
15
+ cy="20"
16
+ r="7"
17
+ fill="none"
18
+ stroke="#ff6b35"
19
+ stroke-linecap="round"
20
+ stroke-linejoin="round"
21
+ stroke-width="4"
22
+ />
23
+ <path
24
+ d="M34 20h7a3 3 0 0 1 3 3v14"
25
+ fill="none"
26
+ stroke="#ff6b35"
27
+ stroke-linecap="round"
28
+ stroke-linejoin="round"
29
+ stroke-width="4"
30
+ />
31
+ <line
32
+ x1="20"
33
+ y1="27"
34
+ x2="20"
35
+ y2="51"
36
+ fill="none"
37
+ stroke="#ff6b35"
38
+ stroke-linecap="round"
39
+ stroke-linejoin="round"
40
+ stroke-width="4"
41
+ />
42
+ </svg>
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>code_review</title>
7
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Oswald:wght@400;500;600;700&display=swap" rel="stylesheet" />
11
+ <script type="module" crossorigin src="/assets/index-BY1o75E8.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-QcmHr2Xi.css">
13
+ </head>
14
+ <body>
15
+ <div id="root"></div>
16
+ </body>
17
+ </html>
@@ -0,0 +1,51 @@
1
+ import path from "node:path";
2
+ export function deriveRepoId(repoPath) {
3
+ const base = path.basename(repoPath)
4
+ .toLowerCase()
5
+ .replace(/[^a-z0-9]+/g, "-")
6
+ .replace(/^-+|-+$/g, "");
7
+ return base || "repo";
8
+ }
9
+ export function parseServerOptions(argv) {
10
+ const rawRepos = [];
11
+ let port = 3000;
12
+ for (let index = 0; index < argv.length; index += 1) {
13
+ const arg = argv[index];
14
+ if (arg === "--repo") {
15
+ const value = argv[index + 1] ?? null;
16
+ index += 1;
17
+ if (!value)
18
+ continue;
19
+ const colonIndex = value.indexOf(":");
20
+ let id;
21
+ let repoPath;
22
+ if (colonIndex > 0) {
23
+ id = value.slice(0, colonIndex);
24
+ repoPath = value.slice(colonIndex + 1);
25
+ }
26
+ else {
27
+ repoPath = value;
28
+ id = deriveRepoId(path.basename(repoPath));
29
+ }
30
+ rawRepos.push({ id, path: path.resolve(repoPath) });
31
+ continue;
32
+ }
33
+ if (arg === "--port") {
34
+ const parsed = Number(argv[index + 1]);
35
+ if (!Number.isInteger(parsed) || parsed <= 0) {
36
+ throw new Error("Invalid --port value");
37
+ }
38
+ port = parsed;
39
+ index += 1;
40
+ }
41
+ }
42
+ const seen = new Map();
43
+ for (const repo of rawRepos) {
44
+ if (seen.has(repo.id)) {
45
+ throw new Error(`Duplicate repo id "${repo.id}" for paths: ${seen.get(repo.id)} and ${repo.path}. ` +
46
+ `Use name:/path syntax to assign unique ids.`);
47
+ }
48
+ seen.set(repo.id, repo.path);
49
+ }
50
+ return { repos: rawRepos, port };
51
+ }
@@ -0,0 +1,17 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export function resolveClientDistDirectory(importMetaUrl) {
5
+ const moduleDir = path.dirname(fileURLToPath(importMetaUrl));
6
+ const candidates = [
7
+ path.resolve(moduleDir, "../../client"),
8
+ path.resolve(moduleDir, "../../dist/client"),
9
+ path.resolve(process.cwd(), "dist/client")
10
+ ];
11
+ for (const candidate of candidates) {
12
+ if (fs.existsSync(path.join(candidate, "index.html"))) {
13
+ return candidate;
14
+ }
15
+ }
16
+ return candidates[0];
17
+ }
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { deriveRepoId, parseServerOptions } from "./args.js";
7
+ import { logFatalError, runServer } from "./runServer.js";
8
+ function getCurrentVersion() {
9
+ try {
10
+ let dir = dirname(fileURLToPath(import.meta.url));
11
+ for (let i = 0; i < 5; i++) {
12
+ try {
13
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
14
+ if (pkg.name === "crloop" && pkg.version)
15
+ return pkg.version;
16
+ }
17
+ catch { /* keep walking up */ }
18
+ dir = dirname(dir);
19
+ }
20
+ }
21
+ catch { /* ignore */ }
22
+ return "0.0.0";
23
+ }
24
+ function isNewer(latest, current) {
25
+ const parse = (v) => v.split(".").map(Number);
26
+ const [la, lb, lc] = parse(latest);
27
+ const [ca, cb, cc] = parse(current);
28
+ return la > ca || (la === ca && lb > cb) || (la === ca && lb === cb && lc > cc);
29
+ }
30
+ async function checkForUpdate() {
31
+ try {
32
+ const current = getCurrentVersion();
33
+ if (current === "0.0.0")
34
+ return null;
35
+ const controller = new AbortController();
36
+ const timer = setTimeout(() => controller.abort(), 2000);
37
+ const response = await fetch("https://registry.npmjs.org/crloop/latest", { signal: controller.signal });
38
+ clearTimeout(timer);
39
+ if (!response.ok)
40
+ return null;
41
+ const data = await response.json();
42
+ if (isNewer(data.version, current)) {
43
+ return `\nUpdate available: ${current} → ${data.version} Run: npm install -g crloop@latest\n`;
44
+ }
45
+ return null;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ const DEFAULT_URL = "http://localhost:3000";
52
+ function getFlag(args, flag) {
53
+ const idx = args.indexOf(flag);
54
+ return idx !== -1 ? args[idx + 1] : undefined;
55
+ }
56
+ function hasFlag(args, flag) {
57
+ return args.includes(flag);
58
+ }
59
+ async function apiFetch(url, method, body) {
60
+ const response = await fetch(url, {
61
+ method,
62
+ headers: body ? { "Content-Type": "application/json" } : {},
63
+ body: body ? JSON.stringify(body) : undefined,
64
+ });
65
+ const data = response.status !== 204 ? await response.json().catch(() => null) : null;
66
+ return { status: response.status, data };
67
+ }
68
+ async function cmdRepos(args) {
69
+ const baseUrl = getFlag(args, "--url") ?? DEFAULT_URL;
70
+ const json = hasFlag(args, "--json");
71
+ let result;
72
+ try {
73
+ result = await apiFetch(`${baseUrl}/api/repos`, "GET");
74
+ }
75
+ catch {
76
+ console.error("Connection failed");
77
+ process.exit(1);
78
+ }
79
+ if (result.status !== 200) {
80
+ console.error(`Server error: ${result.status}`);
81
+ process.exit(1);
82
+ }
83
+ const repos = result.data;
84
+ if (json) {
85
+ console.log(JSON.stringify(repos));
86
+ }
87
+ else if (repos.length === 0) {
88
+ console.log("(no repos registered)");
89
+ }
90
+ else {
91
+ const maxLen = Math.max(...repos.map((r) => r.id.length));
92
+ for (const repo of repos) {
93
+ console.log(`${repo.id.padEnd(maxLen)} ${repo.path}`);
94
+ }
95
+ }
96
+ }
97
+ async function cmdAddRepo(args) {
98
+ const repoPath = args[0];
99
+ const id = getFlag(args, "--id");
100
+ const baseUrl = getFlag(args, "--url") ?? DEFAULT_URL;
101
+ const json = hasFlag(args, "--json");
102
+ const dryRun = hasFlag(args, "--dry-run");
103
+ if (!repoPath || repoPath.startsWith("-")) {
104
+ console.error("Usage: crloop add-repo <path> [--id <repoId>] [--url URL] [--json] [--dry-run]");
105
+ process.exit(1);
106
+ }
107
+ if (dryRun) {
108
+ const derivedId = id ?? deriveRepoId(repoPath);
109
+ if (json) {
110
+ console.log(JSON.stringify({ dryRun: true, id: derivedId, path: repoPath }));
111
+ }
112
+ else {
113
+ console.log(`Would register: ${derivedId} → ${repoPath}`);
114
+ }
115
+ return;
116
+ }
117
+ const bodyPayload = { path: repoPath };
118
+ if (id)
119
+ bodyPayload.id = id;
120
+ let result;
121
+ try {
122
+ result = await apiFetch(`${baseUrl}/api/repos`, "POST", bodyPayload);
123
+ }
124
+ catch {
125
+ console.error("Connection failed");
126
+ process.exit(1);
127
+ }
128
+ if (result.status === 201) {
129
+ const repo = result.data;
130
+ if (json) {
131
+ console.log(JSON.stringify(repo));
132
+ }
133
+ else {
134
+ console.log(`Registered: ${repo.id} → ${repo.path}`);
135
+ }
136
+ }
137
+ else {
138
+ const error = result.data?.error ?? `Status ${result.status}`;
139
+ console.error(error);
140
+ process.exit(1);
141
+ }
142
+ }
143
+ async function cmdStopServer(args) {
144
+ const baseUrl = getFlag(args, "--url") ?? DEFAULT_URL;
145
+ const json = hasFlag(args, "--json");
146
+ try {
147
+ const response = await fetch(`${baseUrl}/api/server/stop`, { method: "POST" });
148
+ if (response.status === 204) {
149
+ if (json) {
150
+ console.log(JSON.stringify({ stopped: true }));
151
+ }
152
+ else {
153
+ console.log("Server stopped.");
154
+ }
155
+ }
156
+ else {
157
+ console.error(`Unexpected response: ${response.status}`);
158
+ process.exit(1);
159
+ }
160
+ }
161
+ catch {
162
+ console.error("Connection failed — is the server running?");
163
+ process.exit(1);
164
+ }
165
+ }
166
+ async function cmdRemoveRepo(args) {
167
+ const repoId = args[0];
168
+ const baseUrl = getFlag(args, "--url") ?? DEFAULT_URL;
169
+ const json = hasFlag(args, "--json");
170
+ const dryRun = hasFlag(args, "--dry-run");
171
+ if (!repoId || repoId.startsWith("-")) {
172
+ console.error("Usage: crloop remove-repo <repoId> [--url URL] [--json] [--dry-run]");
173
+ process.exit(1);
174
+ }
175
+ if (dryRun) {
176
+ if (json) {
177
+ console.log(JSON.stringify({ dryRun: true, id: repoId }));
178
+ }
179
+ else {
180
+ console.log(`Would remove: ${repoId}`);
181
+ }
182
+ return;
183
+ }
184
+ let result;
185
+ try {
186
+ result = await apiFetch(`${baseUrl}/api/repos/${encodeURIComponent(repoId)}`, "DELETE");
187
+ }
188
+ catch {
189
+ console.error("Connection failed");
190
+ process.exit(1);
191
+ }
192
+ if (result.status === 204) {
193
+ if (json) {
194
+ console.log(JSON.stringify({ id: repoId }));
195
+ }
196
+ else {
197
+ console.log(`Removed: ${repoId}`);
198
+ }
199
+ }
200
+ else {
201
+ const error = result.data?.error ?? `Status ${result.status}`;
202
+ console.error(error);
203
+ process.exit(1);
204
+ }
205
+ }
206
+ function cmdSchema(args) {
207
+ const command = args[0];
208
+ const schema = {
209
+ serve: {
210
+ description: "Start the review server (default when no command given)",
211
+ options: {
212
+ "--repo": { type: "string", multiple: true, description: "Add a repository. Use name:/path for explicit ID." },
213
+ "--port": { type: "number", default: 3000, description: "Server port (must be > 0)" },
214
+ },
215
+ },
216
+ "stop-server": {
217
+ description: "Stop the running server",
218
+ options: {
219
+ "--url": { type: "string", default: "http://localhost:3000", description: "Server URL" },
220
+ "--json": { type: "boolean", description: "Output JSON: {stopped: true}" },
221
+ },
222
+ },
223
+ repos: {
224
+ description: "List repos registered with the running server",
225
+ options: {
226
+ "--url": { type: "string", default: "http://localhost:3000", description: "Server URL" },
227
+ "--json": { type: "boolean", description: "Output JSON array of {id, path} objects" },
228
+ },
229
+ },
230
+ "add-repo": {
231
+ description: "Register a repo with the running server at runtime",
232
+ args: [{ name: "path", required: true, description: "Filesystem path to the repository" }],
233
+ options: {
234
+ "--id": { type: "string", description: "Explicit repo ID (derived from basename if omitted)" },
235
+ "--url": { type: "string", default: "http://localhost:3000", description: "Server URL" },
236
+ "--json": { type: "boolean", description: "Output JSON {id, path} on success" },
237
+ "--dry-run": { type: "boolean", description: "Validate and preview without registering" },
238
+ },
239
+ },
240
+ "remove-repo": {
241
+ description: "Unregister a repo from the running server",
242
+ args: [{ name: "repoId", required: true, description: "ID of the repo to remove" }],
243
+ options: {
244
+ "--url": { type: "string", default: "http://localhost:3000", description: "Server URL" },
245
+ "--json": { type: "boolean", description: "Output JSON {id} on success" },
246
+ "--dry-run": { type: "boolean", description: "Preview without removing" },
247
+ },
248
+ },
249
+ schema: {
250
+ description: "Print machine-readable schema for all commands or a single command",
251
+ args: [{ name: "command", required: false, description: "Command name to describe (omit for all)" }],
252
+ },
253
+ };
254
+ if (command && command in schema) {
255
+ console.log(JSON.stringify(schema[command], null, 2));
256
+ }
257
+ else {
258
+ console.log(JSON.stringify(schema, null, 2));
259
+ }
260
+ }
261
+ function printHelp() {
262
+ console.log(`crloop — agentic code review loop
263
+
264
+ Usage:
265
+ crloop serve [--repo <path>] [--repo name:<path>] [--port <number>]
266
+ crloop stop-server [--url URL] [--json]
267
+ crloop repos [--url URL] [--json]
268
+ crloop add-repo <path> [--id <repoId>] [--url URL] [--json] [--dry-run]
269
+ crloop remove-repo <repoId> [--url URL] [--json] [--dry-run]
270
+ crloop schema [command]
271
+
272
+ Commands:
273
+ serve Start the review server (default when no command given)
274
+ stop-server Stop the running server
275
+ repos List repos registered with the running server
276
+ add-repo Register a repo with the running server at runtime
277
+ remove-repo Unregister a repo from the running server
278
+ schema Print machine-readable JSON schema for commands
279
+
280
+ Flags available on most commands:
281
+ --json Output machine-readable JSON instead of human text
282
+ --dry-run (add-repo, remove-repo) Preview action without executing it
283
+
284
+ Repo ID derivation:
285
+ Directory basename lowercased, non-alphanumeric chars replaced with "-"
286
+ Override with name:/path syntax: --repo fe:/path/to/frontend
287
+
288
+ Examples:
289
+ crloop serve --repo /path/to/project
290
+ crloop serve --repo fe:/path/to/frontend --repo be:/path/to/backend
291
+ crloop add-repo /path/to/repo --id my-api
292
+ crloop add-repo /path/to/repo --dry-run
293
+ crloop repos --url http://localhost:4000
294
+ crloop repos --json
295
+ crloop remove-repo my-api
296
+ crloop schema add-repo
297
+
298
+ ID derivation examples:
299
+ my_frontend → my-frontend (${deriveRepoId("my_frontend")})
300
+ Backend.API → backend-api (${deriveRepoId("Backend.API")})
301
+ `);
302
+ }
303
+ async function main() {
304
+ const args = process.argv.slice(2);
305
+ const command = args[0];
306
+ if (command === "--help" || command === "-h") {
307
+ printHelp();
308
+ }
309
+ else if (command === "--version" || command === "-v") {
310
+ console.log(getCurrentVersion());
311
+ }
312
+ else if (command === "serve" || !command || command.startsWith("-")) {
313
+ const argv = command === "serve" ? args.slice(1) : args;
314
+ if (process.env["CRLOOP_DAEMON"] === "1") {
315
+ // Running as daemon — start server and keep process alive
316
+ await runServer({ argv });
317
+ }
318
+ else {
319
+ // Validate args in the foreground so errors surface before spawning
320
+ parseServerOptions(argv);
321
+ // Spawn detached daemon and exit
322
+ const child = spawn(process.execPath, [process.argv[1], "serve", ...argv], {
323
+ detached: true,
324
+ stdio: "ignore",
325
+ env: { ...process.env, CRLOOP_DAEMON: "1" },
326
+ });
327
+ child.unref();
328
+ // Wait briefly to let the server bind, then confirm
329
+ await new Promise((resolve) => setTimeout(resolve, 500));
330
+ const portFlag = argv.indexOf("--port");
331
+ const port = portFlag !== -1 ? argv[portFlag + 1] : "3000";
332
+ console.log(`Server started (pid ${child.pid}) on http://localhost:${port}`);
333
+ checkForUpdate().then((notice) => { if (notice)
334
+ console.log(notice); }).catch(() => { });
335
+ }
336
+ }
337
+ else if (command === "schema") {
338
+ cmdSchema(args.slice(1));
339
+ }
340
+ else if (command === "stop-server") {
341
+ const subArgs = args.slice(1);
342
+ const updateCheck = checkForUpdate();
343
+ await cmdStopServer(subArgs);
344
+ const notice = await updateCheck;
345
+ if (notice && !hasFlag(subArgs, "--json"))
346
+ console.log(notice);
347
+ }
348
+ else if (command === "repos") {
349
+ const subArgs = args.slice(1);
350
+ const updateCheck = checkForUpdate();
351
+ await cmdRepos(subArgs);
352
+ const notice = await updateCheck;
353
+ if (notice && !hasFlag(subArgs, "--json"))
354
+ console.log(notice);
355
+ }
356
+ else if (command === "add-repo") {
357
+ const subArgs = args.slice(1);
358
+ const updateCheck = checkForUpdate();
359
+ await cmdAddRepo(subArgs);
360
+ const notice = await updateCheck;
361
+ if (notice && !hasFlag(subArgs, "--json"))
362
+ console.log(notice);
363
+ }
364
+ else if (command === "remove-repo") {
365
+ const subArgs = args.slice(1);
366
+ const updateCheck = checkForUpdate();
367
+ await cmdRemoveRepo(subArgs);
368
+ const notice = await updateCheck;
369
+ if (notice && !hasFlag(subArgs, "--json"))
370
+ console.log(notice);
371
+ }
372
+ else {
373
+ console.error(`Unknown command: ${command}\nRun "crloop --help" for usage.`);
374
+ process.exit(1);
375
+ }
376
+ }
377
+ main().catch(logFatalError);