@mmmbuto/nexuscrew 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -23
- package/frontend/dist/assets/index-D2TrRtWQ.js +90 -0
- package/frontend/dist/assets/index-DrEuy6A6.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/fleet-service.js +8 -3
- package/lib/fleet/builtin.js +139 -22
- package/lib/fleet/definitions.js +20 -0
- package/lib/fleet/index.js +42 -1
- package/lib/fleet/managed.js +15 -2
- package/lib/fleet/provider.js +12 -11
- package/lib/fleet/routes.js +11 -2
- package/lib/nodes/peering.js +85 -7
- package/lib/settings/routes.js +214 -61
- package/package.json +1 -1
- package/frontend/dist/assets/index-UAVxf6SE.js +0 -90
- package/frontend/dist/assets/index-U_e8auCM.css +0 -32
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
.nc-icon{display:block;flex:0 0 auto}.nc-sheet-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:50;background:#0000008c;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}.nc-launch-overlay{z-index:70}.nc-sheet{width:100%;max-width:380px;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:14px;padding:16px;display:flex;flex-direction:column;gap:12px;box-shadow:0 12px 40px #00000080}.nc-power-sheet{max-width:min(620px,calc(100vw - 32px));max-height:calc(100dvh - 32px);overflow-y:auto;box-sizing:border-box;min-width:0}.nc-launch-sheet{max-width:min(520px,calc(100vw - 32px));width:100%;min-width:0}.nc-launch-fields{display:flex;flex-direction:column;gap:10px;min-width:0}.nc-launch-fields .nc-field,.nc-launch-fields .nc-field input,.nc-launch-fields .nc-field select{width:100%;min-width:0;max-width:100%;box-sizing:border-box}.nc-sheet-head{display:flex;align-items:center;justify-content:space-between}.nc-sheet-head b{font-size:16px;color:#d8ffe8}.nc-sheet-state{font-size:12px;opacity:.6}.nc-sheet-label{font-size:12px;opacity:.7;text-transform:uppercase;letter-spacing:.5px}.nc-power-config{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 10px;padding:10px;border:1px solid #243;border-radius:8px;background:#0a0e0a;min-width:0;overflow-wrap:anywhere}.nc-power-config span{opacity:.7}.nc-power-config small{grid-column:1 / -1;opacity:.65}.nc-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}.nc-check input{accent-color:#2e7d32}.nc-note{font-size:11px;color:#e0b020;opacity:.9}.nc-field{display:flex;flex-direction:column;gap:4px;font-size:12px;opacity:.8}.nc-field input,.nc-field select{padding:9px 10px;font:14px/1.2 inherit;background:#0a0e0a;color:#cfe;border:1px solid #243;border-radius:8px;width:100%;min-width:0;max-width:100%;box-sizing:border-box}.nc-field input:focus,.nc-field select:focus{outline:none;border-color:#2e7d32}.nc-err{color:#f88;font-size:12px}.nc-sheet-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;min-width:0}.nc-btn{border:none;border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer}.nc-btn.ghost{background:none;border:1px solid #243;color:#9fd}.nc-btn.primary{background:#2e7d32;color:#fff}.nc-btn.primary:active{background:#255028}.nc-btn:disabled{opacity:.5;cursor:default}@media(max-width:600px){.nc-power-sheet{max-width:380px}}.nc-home{display:flex;flex-direction:column;gap:12px;padding:18px 14px 10px;color:#cfe;max-width:560px;margin:0 auto;min-height:100dvh;box-sizing:border-box}.nc-home-head{position:relative}.nc-wordmark{font-size:26px;font-weight:700;letter-spacing:.5px;color:#d8ffe8}.nc-cursor{display:inline-block;width:12px;height:22px;margin-left:6px;background:#0c6;vertical-align:-2px;animation:nc-blink 1.1s steps(1) infinite}@keyframes nc-blink{50%{opacity:0}}@media(prefers-reduced-motion:reduce){.nc-cursor{animation:none}}.nc-home-sub{margin-top:4px;font-size:13px;opacity:.65}.nc-head-actions{position:absolute;top:0;right:0;display:inline-flex;gap:6px}.nc-refresh{display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid #243;color:#9fd;border-radius:8px;min-width:40px;min-height:40px}.nc-filter{padding:10px 12px;font:16px/1.2 inherit;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:8px}.nc-filter:focus{outline:none;border-color:#2e7d32}.nc-session-list{display:flex;flex-direction:column;gap:8px}.nc-session{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid #1c241c;background:#0e140e;color:#cfe;border-radius:10px;text-align:left;min-height:56px}.nc-session:active{background:#14200f;border-color:#2e7d32}.nc-session .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-session .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-session-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.nc-session-main b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}.nc-session-main small{opacity:.55;font-size:12px}.nc-badge{flex:0 0 auto;background:#2e7d32;color:#fff;border-radius:10px;padding:1px 8px;font-size:12px;line-height:18px}.nc-err{color:#f88;font-size:13px;padding:4px 2px}.nc-empty{opacity:.5;padding:18px 4px;font-size:13px}.nc-home-foot{margin-top:auto;padding-top:14px;display:flex;justify-content:space-between;gap:8px;font-size:11px;opacity:.4}.nc-group{display:flex;flex-direction:column;gap:8px}.nc-group-title{font-size:11px;opacity:.55;text-transform:uppercase;letter-spacing:.5px;padding:6px 2px 2px}.nc-mcard{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid #1c241c;background:#0e140e;color:#cfe;border-radius:12px;min-height:56px}.nc-mcard-main{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:12px;background:none;border:none;color:inherit;cursor:pointer;padding:0;text-align:left}.nc-mcard-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.nc-mcard-text b{font-size:14px;color:#d8ffe8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nc-mcard-text small{font-size:12px;opacity:.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dot.warn{background:#e0b020;box-shadow:0 0 6px #e0b02088}.nc-group-title.nc-node-title{display:flex;align-items:center;gap:6px}.nc-group-title.nc-node-title .dot{flex:0 0 auto;width:8px;height:8px;border-radius:50%;background:#355}.nc-group-title.nc-node-title .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-mcard .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-mcard .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-rel{flex:0 0 auto;font-size:11px;opacity:.45}.nc-power,.nc-menu{flex:0 0 auto;background:none;border:1px solid #243;color:#9fd;border-radius:8px;min-width:38px;min-height:38px;font-size:17px;cursor:pointer}.nc-power:active,.nc-menu:active{border-color:#2e7d32}.nc-fab{position:fixed;right:18px;bottom:18px;z-index:20;width:54px;height:54px;border-radius:50%;background:#2e7d32;color:#fff;border:none;font-size:28px;line-height:1;cursor:pointer;box-shadow:0 6px 18px #00000073}.nc-fab:active{background:#255028}.nc-lang{display:inline-flex;align-items:center;gap:2px}.nc-act{width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:#ffffff0d;border:1px solid rgba(255,255,255,.22);color:#9aa89a;cursor:pointer;flex:0 0 auto;font-size:15px;line-height:1}.nc-act.power.on{color:#0c6;border-color:#00cc668c;background:#00cc661a}.nc-act.power.warn{color:#e8c547;border-color:#e8c5478c;background:#e8c5471a}.nc-act.pin.on{color:#e8c547;border-color:#e8c5478c;background:#e8c54714}.nc-mcard{gap:8px}/**
|
|
2
|
+
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
3
|
+
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
4
|
+
* https://github.com/chjj/term.js
|
|
5
|
+
* @license MIT
|
|
6
|
+
*
|
|
7
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
* in the Software without restriction, including without limitation the rights
|
|
10
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
* furnished to do so, subject to the following conditions:
|
|
13
|
+
*
|
|
14
|
+
* The above copyright notice and this permission notice shall be included in
|
|
15
|
+
* all copies or substantial portions of the Software.
|
|
16
|
+
*
|
|
17
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
* THE SOFTWARE.
|
|
24
|
+
*
|
|
25
|
+
* Originally forked from (with the author's permission):
|
|
26
|
+
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
27
|
+
* http://bellard.org/jslinux/
|
|
28
|
+
* Copyright (c) 2011 Fabrice Bellard
|
|
29
|
+
* The original design remains. The terminal itself
|
|
30
|
+
* has been extended to include xterm CSI codes, among
|
|
31
|
+
* other features.
|
|
32
|
+
*/.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{font-family:monospace;-webkit-user-select:text;user-select:text;white-space:pre}.xterm .xterm-accessibility-tree>div{transform-origin:left;width:fit-content}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;background:#0000;transition:opacity .1s linear;z-index:11}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{position:absolute;display:none}.xterm .xterm-scrollable-element>.shadow.top{display:block;top:0;left:3px;height:3px;width:100%;box-shadow:var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.left{display:block;top:3px;left:0;height:100%;width:3px;box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.top-left-corner{display:block;top:0;left:0;height:3px;width:3px}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}.nc-terminal{position:absolute;top:0;right:0;bottom:0;left:0;background:#0a0e0a}.nc-terminal-host{position:absolute;top:0;right:0;bottom:0;left:0;padding:4px}.nc-terminal .xterm{height:100%}.nc-terminal-host,.nc-terminal .xterm,.nc-terminal .xterm-viewport{touch-action:none}.nc-terminal.selecting{box-shadow:inset 0 0 0 2px #d6a600}.nc-selection-tools{position:absolute;z-index:9;right:10px;top:10px;display:flex;gap:6px;align-items:center;background:#0e140ef0;border:1px solid #586b2a;border-radius:8px;padding:5px 7px;color:#edda8a;font-size:11px}.nc-selection-tools button{background:#172417;border:1px solid #486044;color:#d8ffe8;border-radius:6px;padding:5px 9px}.nc-selection-tools textarea{width:180px;height:48px;background:#071007;color:#d8ffe8}.nc-keybar{background:#0a0e0a;border-top:1px solid #1c241c;position:relative}.nc-keybar.termux{display:flex;flex-direction:column;padding:2px 0}.nc-keybar .row{display:flex}.nc-keybar .row button{flex:1 1 0;min-width:0;padding:10px 2px;background:none;border:none;border-radius:0;color:#e6ffe6;font-size:14px;font-weight:600;letter-spacing:.5px;font-family:inherit}.nc-keybar .row button:active{background:#1d2a1d}.nc-keybar .row button.armed{color:#021;background:#0c6}.nc-keymenu{position:absolute;bottom:100%;left:8px;right:8px;z-index:30;display:flex;flex-wrap:wrap;gap:6px;padding:8px;background:#101810;border:1px solid #2a3a2a;border-radius:10px 10px 0 0}.nc-keymenu button{flex:1 0 30%;padding:10px 6px;border-radius:8px;border:1px solid #243;background:#122;color:#9fd;font-size:13px}.nc-keymenu button:active{background:#0c6;color:#021}.nc-keybar.copy{display:flex;gap:6px;overflow-x:auto;padding:8px;background:#14210f}.nc-keybar.copy button{flex:0 0 auto;padding:10px 12px;min-width:48px;border-radius:8px;border:1px solid #243;background:#122;color:#9fd;font-size:13px}.nc-keybar.copy button:active{background:#0c6;color:#021}.nc-keybar .tag{flex:0 0 auto;align-self:center;padding:0 8px;color:#8e8;font-size:12px}@media(pointer:coarse){.nc-keybar .row button{padding:12px 2px;font-size:15px}}.nc-files{position:fixed;top:0;right:0;bottom:0;width:min(340px,90vw);background:var(--bg, #111511);border-left:1px solid #2a332a;display:flex;flex-direction:column;z-index:30}.nc-files header{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-bottom:1px solid #2a332a}.nc-files nav{display:flex;gap:6px;padding:8px 10px}.nc-files nav button{flex:0 0 auto;padding:4px 10px;border-radius:6px}.nc-files nav button.on{background:#2e7d32;color:#fff}.nc-files nav .up{margin-left:auto}.nc-files ul{list-style:none;margin:0;padding:0;overflow-y:auto;flex:1}.nc-files li{display:flex;align-items:center;gap:8px;padding:8px 10px;border-bottom:1px solid #1c231c}.nc-files li .name{flex:1;cursor:pointer;overflow-wrap:anywhere}.nc-files li.empty{opacity:.5;justify-content:center}.nc-busy{padding:4px 10px;font-size:12px;color:#e0a030}@media(pointer:coarse){.nc-files nav button,.nc-files li{min-height:42px}}.nc-files header button,.nc-files nav button,.nc-files li button{display:inline-flex;align-items:center;justify-content:center;gap:6px}.nc-composer{border-top:1px solid #2a332a;padding:6px 8px;background:var(--bg, #111511)}.nc-composer-row{display:flex;gap:6px;align-items:flex-end}.nc-composer textarea{flex:1;resize:none;background:#0a0e0a;color:#d8e0d8;border:1px solid #2a332a;border-radius:8px;padding:6px 8px;font:13px/1.4 inherit}.nc-composer .mic.on{background:#b02a2a;color:#fff}.nc-composer-err{font-size:12px;color:#e0a030;padding-bottom:4px}@media(pointer:coarse){.nc-composer .mic,.nc-composer .go,.nc-composer .attach{min-width:46px;min-height:46px;font-size:20px}.nc-composer textarea{font-size:16px}.nc-attach-menu button{min-height:48px}}.nc-composer .mic,.nc-composer .go,.nc-composer .attach{display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid #243;color:#9fd;border-radius:8px;cursor:pointer}.nc-composer .go{color:#7ee787;border-color:#2e7d32}.nc-composer .attach.busy{opacity:.5;animation:nc-attach-pulse 1s ease-in-out infinite}@keyframes nc-attach-pulse{50%{opacity:.9}}.nc-attach-menu{position:fixed;z-index:1000;min-width:180px;display:flex;flex-direction:column;padding:4px;background:#0e130e;border:1px solid #2e7d32;border-radius:10px;box-shadow:0 6px 24px #0000008c}.nc-attach-menu button{display:flex;align-items:center;gap:10px;text-align:left;min-height:40px;padding:8px 12px;border:0;border-radius:7px;background:none;color:#d8e0d8;font:14px/1 inherit;cursor:pointer}.nc-attach-menu button:hover,.nc-attach-menu button:focus-visible{background:#1a241a;color:#7ee787}.nc-sidebar{display:flex;flex-direction:column;gap:8px;padding:12px 10px;color:#cfe;min-height:0;position:relative;overflow-y:auto;box-sizing:border-box;border-right:1px solid #1c241c;background:#0a0e0a}.nc-sidebar.mini{padding:12px 4px;align-items:center;gap:6px}.nc-side-resize{position:absolute;top:0;right:0;bottom:0;width:6px;cursor:col-resize;z-index:5}.nc-side-resize:hover{background:#1c3a1c}.nc-collapse-btn{flex:0 0 auto;background:none;border:1px solid #243;color:#9fd;border-radius:8px;min-width:26px;min-height:26px;cursor:pointer;font-size:13px;line-height:1}.nc-collapse-btn:hover{border-color:#2e7d32;color:#d8ffe8}.nc-side-head{display:flex;align-items:center;gap:6px;justify-content:space-between;padding:2px 4px 6px}.nc-side-head.mini{padding:2px 0 6px;justify-content:center}.nc-side-group.mini{align-items:center;gap:6px}.nc-mini-dot,.nc-mini-init{display:flex;align-items:center;justify-content:center;width:34px;height:34px;box-sizing:border-box;border:1px solid #1c241c;background:#0e140e;border-radius:9px;cursor:pointer;-webkit-user-select:none;user-select:none;color:#d8ffe8;padding:0}.nc-mini-init{font-size:13px;font-weight:700}.nc-mini-dot:hover,.nc-mini-init:hover{border-color:#244}.nc-mini-dot.active,.nc-mini-init.active{border-color:#2e7d32;background:#14200f}.nc-side-title{font-size:13px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;opacity:.8}.nc-new-btn{background:#2e7d32;color:#fff;border:none;border-radius:8px;padding:4px 10px;font-size:12px;cursor:pointer}.nc-new-btn:active{background:#255028}.nc-side-group{display:flex;flex-direction:column;gap:6px}.nc-side-group-title{font-size:11px;opacity:.55;text-transform:uppercase;letter-spacing:.5px;padding:10px 4px 2px}.nc-node-title{display:flex;align-items:center;gap:6px;opacity:.8}.nc-node-title .nc-dot{width:7px;height:7px;flex:0 0 auto}.nc-node-title b{font-weight:700}.nc-node-title small{text-transform:none;letter-spacing:0;opacity:.8}.nc-cell{display:flex;align-items:center;gap:8px;padding:9px 10px;border:1px solid #1c241c;background:#0e140e;border-radius:10px}.nc-cell-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}.nc-cell-main b{font-size:13px;color:#d8ffe8}.nc-cell-main small{font-size:11px;opacity:.6}.nc-power{flex:0 0 auto;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:#ffffff0d;border:1px solid rgba(255,255,255,.22);color:#9aa89a;border-radius:50%;cursor:pointer}.nc-power.on{color:#0c6;border-color:#00cc668c;background:#00cc661a}.nc-power.warn{color:#e8c547;border-color:#e8c5478c;background:#e8c5471a}.nc-power:hover{filter:brightness(1.25)}.nc-dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-dot.warn{background:#e0b020;box-shadow:0 0 6px #e0b02088}.nc-side-card{display:flex;align-items:center;gap:8px;padding:9px 10px;border:1px solid #1c241c;background:#0e140e;border-radius:10px;cursor:grab;-webkit-user-select:none;user-select:none}.nc-side-card:hover{border-color:#244}.nc-side-card.active{border-color:#2e7d32;background:#14200f}.nc-side-card:active{cursor:grabbing}.nc-card-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}.nc-card-main b{font-size:13px;color:#d8ffe8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nc-card-main small{font-size:11px;opacity:.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nc-rel{flex:0 0 auto;font-size:10px;opacity:.45}.nc-menu{flex:0 0 auto;width:26px;height:26px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:none;border:none;color:#9fd;cursor:pointer;font-size:15px;line-height:1;border-radius:50%}.nc-menu:active{color:#f88}.nc-empty{opacity:.4;padding:8px 4px;font-size:12px}.nc-side-gear{display:flex;align-items:center;gap:8px;background:none;border:1px solid #1c241c;border-radius:10px;color:#9fd;font-size:12px;padding:7px 10px;cursor:pointer;margin-top:6px}.nc-side-gear:hover{border-color:#2e7d32;color:#d8ffe8}.nc-side-gear.mini{justify-content:center;width:34px;height:34px;box-sizing:border-box;padding:0;border-radius:9px;margin:6px auto 0}.nc-side-lang{margin-top:auto;padding:10px 4px 4px;display:flex;align-items:center;gap:2px;font-size:11px;opacity:.6}.nc-lang-btn{background:none;border:none;color:inherit;cursor:pointer;font-size:11px;padding:0 1px;opacity:.5;text-transform:uppercase}.nc-lang-btn.on{opacity:1;font-weight:700;color:#d8ffe8}.nc-cell.live{cursor:pointer}.nc-cell.live:hover{background:#ffffff0f}.nc-cell.active{outline:1px solid rgba(0,204,102,.5)}.nc-side-presets{display:flex;gap:4px;padding:2px 8px 6px}.nc-side-presets button{background:none;border:1px solid rgba(255,255,255,.15);border-radius:4px;color:inherit;cursor:pointer;padding:2px 8px}.nc-side-presets button:hover{background:#ffffff14}.nc-pin{flex:0 0 auto;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:#ffffff0d;border:1px solid rgba(255,255,255,.22);color:#9aa89a;cursor:pointer;border-radius:50%;font-size:14px;line-height:1}.nc-pin:hover{filter:brightness(1.25)}.nc-pin.on{color:#e8c547;border-color:#e8c5478c;background:#e8c54714}.nc-mini-tip{position:fixed;left:56px;transform:translateY(-50%);background:#161b16;color:#e6ffe6;border:1px solid rgba(0,204,102,.4);padding:3px 10px;border-radius:4px;white-space:nowrap;z-index:1000;pointer-events:none;font-size:12px}.nc-tile{display:flex;flex-direction:column;min-height:0;min-width:0;position:relative;background:#0a0e0a;border:1px solid #1c241c;border-radius:8px;overflow:hidden}.nc-tile.focused{border-color:#2e7d32;box-shadow:0 0 0 1px #2e7d3244 inset}.nc-tile-head{flex:0 0 auto;display:flex;align-items:center;gap:6px;padding:3px 6px;background:#0e140e;border-bottom:1px solid #1c241c;height:28px}.nc-tile-name{flex:1 1 auto;min-width:0;display:inline-flex;align-items:center;gap:6px;background:none;border:none;color:#cfe;cursor:pointer;padding:0}.nc-tile-name b{font-size:12px;font-weight:600;color:#d8ffe8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nc-tile-node{flex:0 0 auto;font-size:10px;color:#7fb89a;border:1px solid #2c4a3a;border-radius:4px;padding:0 4px;line-height:14px}.nc-tile .nc-dot{width:7px;height:7px;flex:0 0 auto}.nc-tile-actions{flex:0 0 auto;display:inline-flex;gap:2px}.nc-tile-actions button{background:none;border:none;color:#9fd;cursor:pointer;min-width:22px;height:22px;font-size:13px;border-radius:4px;padding:0 3px}.nc-tile-actions button:hover{color:#d8ffe8}.nc-tile-actions .nc-tile-close:hover{color:#f88}.nc-tile-body{flex:1 1 auto;position:relative;min-height:0}.nc-tile-body .nc-terminal{width:100%;height:100%}.nc-tile-composer{flex:0 0 auto;border-top:1px solid #1c241c}.nc-tile-files{position:absolute;top:0;right:0;bottom:0;left:0;z-index:5;background:#0e140e}.nc-tile-head{cursor:grab}.nc-tile-head:active{cursor:grabbing}.nc-grid{flex:1 1 auto;min-width:0;min-height:0;display:flex;flex-direction:row;padding:6px;gap:0;position:relative;background:#070a07}.nc-col{display:flex;flex-direction:column;min-width:120px;min-height:0;position:relative}.nc-col.drop-newcol:before{content:"";position:absolute;left:-3px;top:0;bottom:0;width:3px;background:#0c6;border-radius:2px;z-index:6}.nc-tile-slot{display:flex;flex-direction:column;min-height:60px;min-width:0;position:relative}.nc-tile-slot>.nc-tile{flex:1 1 auto;min-height:0}.nc-tile-slot.drop-left:after,.nc-tile-slot.drop-right:after,.nc-tile-slot.drop-top:after,.nc-tile-slot.drop-bottom:after{content:"";position:absolute;z-index:7;pointer-events:none;background:#00cc6647;border:2px solid #00cc66;border-radius:4px}.nc-tile-slot.drop-left:after{left:0;top:0;bottom:0;width:50%}.nc-tile-slot.drop-right:after{right:0;top:0;bottom:0;width:50%}.nc-tile-slot.drop-top:after{left:0;right:0;top:0;height:50%}.nc-tile-slot.drop-bottom:after{left:0;right:0;bottom:0;height:50%}.nc-grid-toolbar{position:absolute;top:10px;right:12px;z-index:8;display:flex;gap:4px;background:#0a100ad1;border:1px solid #1c241c;border-radius:8px;padding:3px}.nc-grid-toolbar button{background:none;border:none;color:#9fd;cursor:pointer;font-size:15px;line-height:1;padding:4px 7px;border-radius:6px}.nc-grid-toolbar button:hover{background:#1c3a1c;color:#d8ffe8}.nc-divider-v{flex:0 0 6px;cursor:col-resize;margin:0 1px;border-radius:2px}.nc-divider-v:hover{background:#1c3a1c}.nc-divider-h{flex:0 0 6px;cursor:row-resize;margin:1px 0;border-radius:2px}.nc-divider-h:hover{background:#1c3a1c}.nc-drop-line-v{flex:0 0 3px;align-self:stretch;background:#0c6;border-radius:2px}.nc-drop-line-end{margin-left:3px}.nc-grid-empty{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;color:#cfe;opacity:.4;font-size:13px;text-align:center;padding:20px}.nc-deckbar{display:flex;align-items:center;gap:8px;flex:0 0 auto;min-width:0;padding:5px 8px;background:#0e140e;border-bottom:1px solid #1c2a1c;overflow-x:auto;white-space:nowrap}.nc-deckbar-label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.5;color:#9fd;flex:0 0 auto}.nc-deck-chips{display:flex;align-items:center;gap:6px;flex:0 1 auto;min-width:0}.nc-deck-chip{display:inline-flex;align-items:center;border:1px solid #243;border-radius:6px;overflow:hidden;background:#0a0e0a}.nc-deck-chip.current{border-color:#7aa903}.nc-deck-open{background:none;border:none;color:#cfe;padding:3px 8px;font:inherit;font-size:12px;cursor:pointer;max-width:180px;overflow:hidden;text-overflow:ellipsis}.nc-deck-chip.current .nc-deck-open{color:#d9f7c7}.nc-deck-open:hover{background:#132013}.nc-deck-mini{background:none;border:none;border-left:1px solid #1c2a1c;color:#7a9;padding:3px 6px;font:inherit;font-size:11px;cursor:pointer}.nc-deck-mini:hover{background:#132013;color:#9fd}.nc-deck-del:hover{color:#f99}.nc-deck-newbtn{background:none;border:1px dashed #2a3a2a;color:#9fd;border-radius:6px;padding:3px 10px;font:inherit;font-size:12px;cursor:pointer;flex:0 0 auto}.nc-deck-newbtn:hover{border-color:#7aa903}.nc-deck-add{display:inline-flex;align-items:center;gap:5px;flex:0 0 auto}.nc-deck-add input{padding:3px 8px;background:#122;border:1px solid #243;color:#cfe;border-radius:6px;font:inherit;font-size:12px;width:130px}.nc-deck-add input.invalid{border-color:#a44}.nc-deck-slug{color:#7a9;font-size:10px;max-width:130px;overflow:hidden;text-overflow:ellipsis}.nc-deck-ok,.nc-deck-cancel{background:#122;border:1px solid #243;color:#9fd;border-radius:6px;padding:3px 9px;font:inherit;font-size:12px;cursor:pointer}.nc-deck-ok:disabled{opacity:.4;cursor:default}.nc-deck-state{font-size:10px;color:#7a9;min-width:64px}.nc-deck-state.error,.nc-deck-error{color:#f88;font-size:10px}.nc-deck-fallback{color:#fd8;font-size:11px}.nc-tile-deck{background:#0a0e0a;border:1px solid #243;color:#9fd;border-radius:5px;font:inherit;font-size:11px;padding:1px 3px;max-width:92px}.nc-side-show{position:fixed;left:8px;top:8px;z-index:30;background:#0e140e;border:1px solid #243;color:#9fd;border-radius:6px;padding:5px 9px;font:inherit;font-size:13px;cursor:pointer;opacity:.8}.nc-side-show:hover{opacity:1;border-color:#7aa903}.nc-pair-card{border:1px solid #2f5;border-radius:8px;box-shadow:0 0 0 1px #22ff5514}.nc-pair-input{width:100%;min-width:0;box-sizing:border-box;font-size:15px;padding:10px}.nc-pair-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.nc-pair-status{min-height:1em}.nc-pair-progress{color:#9f9;animation:nc-pair-pulse 1.2s ease-in-out infinite}@keyframes nc-pair-pulse{0%,to{opacity:1}50%{opacity:.45}}.nc-pair-hint{opacity:.85;margin-top:4px;font-size:12px}.nc-pair-notice{display:block}.nc-pair-advanced-toggle{align-self:flex-start}.nc-pair-advanced{display:flex;flex-direction:column;gap:8px;min-width:0}.nc-qr-overlay{z-index:80}.nc-qr-modal{max-width:min(480px,calc(100vw - 32px));width:100%}.nc-qr-video{width:100%;max-height:60dvh;border-radius:6px;background:#000;object-fit:cover}.nc-qr-upload{display:inline-flex;align-items:center;cursor:pointer}.nc-set-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:60;background:#0000008c;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}.nc-set-panel{width:100%;max-width:560px;max-height:90vh;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:14px;padding:16px;display:flex;flex-direction:column;gap:12px;box-shadow:0 12px 40px #00000080;overflow:hidden}@media(max-width:1023px){.nc-set-overlay{padding:0}.nc-set-panel{max-width:none;max-height:none;height:100%;border-radius:0;border:none}}.nc-set-head{display:flex;align-items:center;gap:8px}.nc-set-head b{font-size:16px;color:#d8ffe8;flex:1}.nc-set-close{background:none;border:none;color:#9fd;cursor:pointer;padding:4px}.nc-set-readonly{font-size:12px;color:#e0b020;border:1px solid #4a3a10;border-radius:8px;padding:8px 10px;background:#171307}.nc-set-tabs{display:flex;gap:6px;border-bottom:1px solid #1c241c;padding-bottom:8px}.nc-set-tabbtn{background:none;border:1px solid #1c241c;border-radius:8px;color:#9fd;font-size:12px;padding:6px 12px;cursor:pointer;text-transform:uppercase;letter-spacing:.5px}.nc-set-tabbtn.on{border-color:#2e7d32;background:#14200f;color:#d8ffe8}.nc-set-body{overflow-y:auto;flex:1;min-height:120px}.nc-set-tab{display:flex;flex-direction:column;gap:12px}.nc-set-tab .nc-check span{display:flex;flex-direction:column;gap:2px}.nc-set-tab .nc-check small{opacity:.6;font-size:11px}.nc-set-info{font-size:12px;opacity:.7;line-height:1.6}.nc-set-note{font-size:12px;color:#9fd}.nc-set-hint{font-size:11px;opacity:.7}.nc-set-form{display:flex;flex-direction:column;gap:8px}.nc-set-form .nc-field{display:flex;flex-direction:column;gap:4px;font-size:12px;opacity:.82}.nc-set-form .nc-field small{font-size:10px;opacity:.7;line-height:1.3}.nc-set-ports{display:grid;grid-template-columns:1fr 1fr;gap:8px}@media(max-width:520px){.nc-set-ports{grid-template-columns:1fr}}.nc-set-form input:not([type=checkbox]):not([type=file]){padding:9px 10px;font:14px/1.2 inherit;background:#0a0e0a;color:#cfe;border:1px solid #243;border-radius:8px;width:100%;min-width:0;max-width:100%;box-sizing:border-box}.nc-set-form input:focus{outline:none;border-color:#2e7d32}.nc-set-form input:disabled{opacity:.5}.nc-set-form textarea,.nc-set-form select{padding:9px 10px;font:13px/1.3 inherit;background:#0a0e0a;color:#cfe;border:1px solid #243;border-radius:8px;min-height:36px;width:100%;min-width:0;max-width:100%;box-sizing:border-box}.nc-set-form textarea{min-height:72px;resize:vertical}.nc-fleet-editor{gap:10px}.nc-fleet-section-head,.nc-fleet-item{display:flex;align-items:center;justify-content:space-between;gap:8px}.nc-fleet-section-head{margin-top:4px;border-bottom:1px solid #243;padding-bottom:6px}.nc-fleet-item{border:1px solid #1c241c;border-radius:8px;padding:8px}.nc-fleet-item>span{display:flex;gap:6px;align-items:center;min-width:0}.nc-fleet-item>span:first-child{flex-direction:column;align-items:flex-start;min-width:0}.nc-fleet-item>span:last-child{flex-wrap:wrap;justify-content:flex-end}.nc-fleet-item small{opacity:.6;overflow-wrap:anywhere}.nc-fleet-form{border:1px solid #2e7d32;border-radius:10px;padding:10px}.nc-fleet-pair,.nc-fleet-env{display:flex;gap:6px}.nc-fleet-pair input,.nc-fleet-pair select,.nc-fleet-env input{flex:1;min-width:0}@media(max-width:520px){.nc-fleet-item{align-items:flex-start;flex-direction:column}.nc-fleet-item>span:last-child{width:100%;justify-content:flex-start}.nc-fleet-pair{flex-direction:column}}.nc-set-node{border:1px solid #1c241c;border-radius:10px;padding:10px;display:flex;flex-direction:column;gap:8px;background:#0a0e0a}.nc-set-node-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.nc-set-node-head b{color:#d8ffe8}.nc-set-node-head small{opacity:.6;font-size:11px}.nc-set-tunnel{margin-left:auto;font-size:11px;color:#f88}.nc-set-tunnel.up{color:#6c6}.nc-set-node-actions{display:flex;gap:6px;flex-wrap:wrap}.nc-set-node-actions .nc-btn{padding:5px 10px;font-size:12px}.nc-set-test{font-size:11px}.nc-set-test.ok{color:#6c6}.nc-set-test.ko{color:#f88}.nc-set-copyline{display:flex;gap:8px;align-items:center}.nc-set-copyline code{flex:1;font-size:10px;word-break:break-all;-webkit-user-select:all;user-select:all;background:#0a0e0a;border:1px solid #1c241c;border-radius:6px;padding:6px;max-height:72px;overflow-y:auto}.nc-set-akeys{display:flex;flex-direction:column;gap:6px}.nc-set-row{display:flex;gap:8px;flex-wrap:wrap}.nc-set-confirm{border:1px solid #4a3a10;border-radius:10px;padding:10px;background:#171307;font-size:12px;color:#e8d8a0;display:flex;flex-direction:column;gap:10px}.nc-set-panel .nc-check{display:flex;align-items:flex-start;gap:8px;font-size:13px;cursor:pointer}.nc-set-panel .nc-check input{accent-color:#2e7d32;margin-top:2px}.nc-set-panel .nc-btn{border:none;border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer}.nc-set-panel .nc-btn.ghost{background:none;border:1px solid #243;color:#9fd}.nc-set-panel .nc-btn.primary{background:#2e7d32;color:#fff}.nc-set-panel .nc-btn.danger{background:none;border:1px solid #532;color:#f88}.nc-set-panel .nc-btn:disabled{opacity:.5;cursor:default}.nc-set-panel .nc-sheet-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap}.nc-set-panel .nc-sheet-label{font-size:12px;opacity:.7;text-transform:uppercase;letter-spacing:.5px}.nc-set-panel .nc-err{color:#f88;font-size:12px}.nc-set-panel .nc-empty{opacity:.5;font-size:12px}.nc-set-panel .nc-dot{width:8px;height:8px;border-radius:50%;background:#333;display:inline-block;flex:0 0 8px}.nc-set-panel .nc-dot.on{background:#2e7d32}.nc-wiz-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:70;background:#000000b3;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}.nc-wiz{width:100%;max-width:420px;max-height:90vh;overflow-y:auto;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:14px;padding:18px;display:flex;flex-direction:column;gap:14px;box-shadow:0 12px 40px #00000080}.nc-wiz-head{display:flex;flex-direction:column;gap:4px}.nc-wiz-head b{font-size:17px;color:#d8ffe8}.nc-wiz-head small{font-size:12px;opacity:.65;text-transform:uppercase;letter-spacing:.5px}.nc-wiz-body{display:flex;flex-direction:column;gap:10px}.nc-wiz-body input{padding:9px 10px;font:14px/1.2 inherit;background:#0a0e0a;color:#cfe;border:1px solid #243;border-radius:8px}.nc-wiz-body input:focus{outline:none;border-color:#2e7d32}.nc-wiz-body .nc-field{display:flex;flex-direction:column;gap:5px;font-size:12px;color:#bfe8cd}.nc-wiz-body .nc-field small{font-size:10px;opacity:.58;line-height:1.3}.nc-wiz-help{margin:0;font-size:11px;line-height:1.4;opacity:.7}.nc-wiz-ports{display:grid;grid-template-columns:1fr 1fr;gap:10px}@media(max-width:420px){.nc-wiz-ports{grid-template-columns:1fr}}.nc-wiz-done{font-size:13px;opacity:.85}.nc-wiz .nc-check{display:flex;align-items:flex-start;gap:8px;font-size:13px;cursor:pointer}.nc-wiz .nc-check input{accent-color:#2e7d32;margin-top:2px}.nc-wiz .nc-check span{display:flex;flex-direction:column;gap:2px}.nc-wiz .nc-check small{opacity:.6;font-size:11px}.nc-wiz .nc-btn{border:none;border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer}.nc-wiz .nc-btn.ghost{background:none;border:1px solid #243;color:#9fd}.nc-wiz .nc-btn.primary{background:#2e7d32;color:#fff}.nc-wiz .nc-btn:disabled{opacity:.5;cursor:default}.nc-wiz .nc-sheet-actions{display:flex;justify-content:flex-end;gap:8px}.nc-wiz .nc-err{color:#f88;font-size:12px}.nc-wiz-skip{background:none;border:none;color:#9fd;opacity:.6;font-size:12px;cursor:pointer;text-decoration:underline;align-self:center;padding:4px}.nc-wiz-skip:hover{opacity:1}.nc-ntf-toasts{position:fixed;top:10px;right:10px;z-index:2100;display:flex;flex-direction:column;gap:8px;max-width:min(360px,calc(100vw - 20px))}.nc-ntf-toast{display:flex;align-items:flex-start;gap:8px;background:var(--bg-secondary);color:var(--text-primary);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:10px;padding:8px 8px 8px 12px;font-size:13px;box-shadow:0 2px 12px #0000008c}.nc-ntf-toast.high{border-left-color:var(--warning)}.nc-ntf-toast-txt{display:flex;flex-direction:column;gap:2px;min-width:0}.nc-ntf-toast-txt b{word-break:break-word}.nc-ntf-toast-txt small{color:var(--text-secondary);word-break:break-word}.nc-ntf-from{color:var(--text-tertiary);font-size:11px;font-family:var(--font-mono)}.nc-ntf-x{margin-left:auto;background:none;border:none;color:var(--text-tertiary);cursor:pointer;padding:2px;display:inline-flex;border-radius:6px}.nc-ntf-x:hover{color:var(--text-primary);background:var(--bg-hover)}.nc-ask-badge{position:fixed;bottom:64px;right:12px;z-index:2100;display:inline-flex;align-items:center;gap:6px;background:var(--accent);color:#fff;border:none;border-radius:999px;padding:8px 14px;font-size:14px;font-weight:700;cursor:pointer;box-shadow:0 2px 12px #0000008c}.nc-ask-badge:hover{background:var(--accent-hover)}.nc-ask-count{background:#00000059;border-radius:999px;padding:0 7px;font-size:12px;line-height:18px}.nc-ask-panel{position:fixed;bottom:10px;right:10px;z-index:2100;width:min(420px,calc(100vw - 20px));max-height:min(70vh,560px);display:flex;flex-direction:column;background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;box-shadow:0 4px 20px #0009}.nc-ask-panel-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--border);font-size:13px}.nc-ask-panel-head .nc-ask-count{background:var(--accent)}.nc-ask-panel-body{overflow-y:auto;padding:10px 12px;display:flex;flex-direction:column;gap:10px}.nc-ask-card{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:10px;padding:10px;display:flex;flex-direction:column;gap:8px}.nc-ask-head{display:flex;align-items:center;gap:8px}.nc-ask-id{color:var(--text-tertiary);font-size:11px;margin-left:auto}.nc-ask-q{font-size:13px;white-space:pre-wrap;word-break:break-word}.nc-ask-opts{display:flex;flex-wrap:wrap;gap:6px}.nc-ask-reply{display:flex;gap:6px;align-items:flex-end}.nc-ask-reply textarea{flex:1;resize:vertical;min-height:34px;background:var(--bg-primary);color:var(--text-primary);border:1px solid var(--border);border-radius:8px;padding:6px 8px;font:inherit;font-size:13px}.nc-ask-card .nc-err{color:var(--error);font-size:12px}:root{color-scheme:dark}body,#root{margin:0;height:100vh;height:100dvh;background:#0a0e0a;font-family:ui-monospace,monospace}.nc-app{display:flex;flex-direction:column;height:100vh;height:100dvh;min-height:0}.nc-keybar{flex:0 0 auto}.nc-bar{display:flex;gap:10px;align-items:center;padding:8px;color:#cfe;background:#0e140e}.nc-bar button{background:none;border:1px solid #243;color:#9fd;border-radius:6px;padding:4px 10px}.nc-termwrap{position:relative;flex:1}.nc-auth{color:#cfe;padding:20px;display:flex;flex-direction:column;gap:10px;max-width:420px}.nc-auth input[type=text],.nc-auth input:not([type]){padding:10px;background:#122;border:1px solid #243;color:#cfe;border-radius:6px}.nc-auth button{padding:10px;background:#122;border:1px solid #243;color:#9fd;border-radius:6px}.nc-bar-right{margin-left:auto;display:flex;gap:4px}@media(pointer:coarse){.nc-bar button,.nc-bar-right button{min-height:42px;min-width:42px;font-size:20px}}.nc-bar button,.nc-bar-right button{display:inline-flex;align-items:center;justify-content:center;gap:6px}.nc-bar>button{flex:0 0 auto}.nc-bar>b{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:600}.nc-bar-right{flex:0 0 auto}@media(max-width:480px){.nc-bar-label{display:none}}.nc-workspace{display:flex;flex-direction:row;height:100vh;height:100dvh;min-height:0;background:#0a0e0a;color:#cfe}.nc-workspace-main{flex:1 1 auto;min-width:0;min-height:0;display:flex;flex-direction:column}.nc-single-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:40;background:#0a0e0a}.nc-bar-single .nc-bar-center{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;line-height:1.15}.nc-bar-single .nc-bar-center b{font-size:13px;font-weight:600;color:#d8ffe8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}.nc-bar-sub{font-size:10px;opacity:.55}.nc-app{animation:nc-slide-in .2s ease-out}@keyframes nc-slide-in{0%{transform:translate(12%);opacity:0}to{transform:translate(0);opacity:1}}@media(prefers-reduced-motion:reduce){.nc-app{animation:none}}.nc-stale{position:fixed;top:8px;left:50%;transform:translate(-50%);z-index:2000;cursor:pointer;background:#1a2410;color:#d9f7c7;border:1px solid #7aa903;border-radius:8px;padding:6px 14px;font-size:13px;box-shadow:0 2px 10px #00000080}.nc-update{position:fixed;bottom:8px;left:50%;transform:translate(-50%);z-index:2000;display:flex;align-items:center;gap:10px;background:#0f1a0f;color:#d9f7c7;border:1px solid #2e7d32;border-radius:10px;padding:6px 8px 6px 14px;font-size:13px;box-shadow:0 2px 12px #0000008c}.nc-update-msg{white-space:nowrap}.nc-update-btn{display:inline-flex;align-items:center;justify-content:center;background:#2e7d32;border:1px solid #2e7d32;color:#06120a;border-radius:7px;padding:4px 12px;font:inherit;font-size:13px;cursor:pointer}.nc-update-btn:hover{background:#3a9d42}@media(prefers-reduced-motion:reduce){.nc-update{transition:none}}:root{--bg-primary: #0d1117;--bg-secondary: #161b22;--bg-tertiary: #21262d;--bg-hover: #30363d;--text-primary: #e6edf3;--text-secondary: #8b949e;--text-tertiary: #6e7681;--border: #30363d;--accent: #6c5ce7;--accent-hover: #7c6cf7;--success: #3fb950;--warning: #d29922;--error: #f85149;--user-bg: #1c2333;--assistant-bg: transparent;--font-mono: "SF Mono", monospace;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif}*{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;width:100%;overflow:hidden}body{font-family:var(--font-sans);background:var(--bg-primary);color:var(--text-primary);-webkit-font-smoothing:antialiased}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--bg-tertiary);border-radius:3px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.spinning{animation:spin 1s linear infinite}
|
package/frontend/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-D2TrRtWQ.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DrEuy6A6.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.8.
|
|
1
|
+
{"version":"0.8.7"}
|
package/lib/cli/fleet-service.js
CHANGED
|
@@ -185,8 +185,13 @@ function selectProviderModeSync(cfg = {}) {
|
|
|
185
185
|
|
|
186
186
|
// external: binario fidato (regular file, exec, non world-writable, no symlink).
|
|
187
187
|
// lazy-require: niente costo/accoppiamento all'import del modulo (usato solo dal companion).
|
|
188
|
-
const { binTrusted } = require('../fleet/index.js'); // sync (audit F3)
|
|
189
|
-
|
|
188
|
+
const { binTrusted, externalFleetCandidates } = require('../fleet/index.js'); // sync (audit F3)
|
|
189
|
+
// Deve usare lo stesso insieme di path del resolver runtime asincrono. In
|
|
190
|
+
// particolare Termux installa normalmente `fleet` in $PREFIX/bin: se init
|
|
191
|
+
// guardasse soltanto cfg.fleetBin potrebbe installare il companion builtin e
|
|
192
|
+
// il runtime scegliere poi l'external, creando due proprietari del boot.
|
|
193
|
+
const trustedExternal = externalFleetCandidates(cfg).find((candidate) => binTrusted(candidate)) || null;
|
|
194
|
+
const extTrusted = !!trustedExternal;
|
|
190
195
|
|
|
191
196
|
// builtin: fleet.json valido (loadDefinitions e' sync; garbage -> null = fail-closed).
|
|
192
197
|
const { loadDefinitions } = require('../fleet/definitions.js');
|
|
@@ -209,7 +214,7 @@ function selectProviderModeSync(cfg = {}) {
|
|
|
209
214
|
if (forced) return { mode: 'disabled', reason: `fail-closed: provider forzato "${forced}" non riconosciuto` };
|
|
210
215
|
|
|
211
216
|
// auto: external fidato vince (no-doppio-boot), poi builtin, poi disabled.
|
|
212
|
-
if (extTrusted) return { mode: 'external', reason:
|
|
217
|
+
if (extTrusted) return { mode: 'external', reason: `auto: external fidato configurato (${trustedExternal}; skip companion)` };
|
|
213
218
|
if (builtinAvail) return { mode: 'builtin', reason: 'auto: builtin (fleet.json valido)' };
|
|
214
219
|
return { mode: 'disabled', reason: 'auto: nessun provider disponibile (no external fidato, no fleet.json)' };
|
|
215
220
|
}
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -26,7 +26,7 @@ const path = require('node:path');
|
|
|
26
26
|
const { execFile } = require('node:child_process');
|
|
27
27
|
const {
|
|
28
28
|
parseDefinitions, validateCommandTrust, resolveCwd,
|
|
29
|
-
loadDefinitions, atomicWrite, CAPS,
|
|
29
|
+
loadDefinitions, atomicWrite, CAPS, validTmuxName,
|
|
30
30
|
} = require('./definitions.js');
|
|
31
31
|
const {
|
|
32
32
|
publicCatalog, describeManaged, resolveManagedEngine, discoverOllamaModels, discoverPiModels,
|
|
@@ -193,24 +193,36 @@ function draftFrom(defs) {
|
|
|
193
193
|
...(e.env ? { env: { ...e.env } } : {}),
|
|
194
194
|
...(e.model ? { model: { ...e.model } } : {}),
|
|
195
195
|
})),
|
|
196
|
-
cells: defs.cells.map((c) => ({
|
|
196
|
+
cells: defs.cells.map((c) => ({
|
|
197
|
+
...c,
|
|
198
|
+
...(c.models ? { models: { ...c.models } } : {}),
|
|
199
|
+
...(c.permissionPolicies ? { permissionPolicies: { ...c.permissionPolicies } } : {}),
|
|
200
|
+
})),
|
|
197
201
|
};
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
// Applica engine + modello come un'unica transizione. Ogni engine ricorda
|
|
201
|
-
// proprio ultimo modello; passando a un altro engine
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
// Applica engine + modello + policy come un'unica transizione. Ogni engine ricorda
|
|
205
|
+
// il proprio ultimo modello E l'ultima policy; passando a un altro engine né
|
|
206
|
+
// l'uno né l'altra attraversano il confine. La policy e' PER-CELL PER-ENGINE:
|
|
207
|
+
// mai si tocca engine.managed.permissionPolicy (globale su tutte le celle che
|
|
208
|
+
// usano quell'engine). Valori ammessi solo 'standard' | 'unsafe'.
|
|
209
|
+
function applyCellTransition(target, engineId, { model, hasModel, policy, hasPolicy } = {}) {
|
|
210
|
+
const rememberedModels = { ...(target.models || {}) };
|
|
211
|
+
const rememberedPolicies = { ...(target.permissionPolicies || {}) };
|
|
205
212
|
target.engine = engineId;
|
|
206
213
|
if (hasModel) {
|
|
207
|
-
if (typeof model === 'string' && model)
|
|
208
|
-
else delete
|
|
214
|
+
if (typeof model === 'string' && model) rememberedModels[engineId] = model;
|
|
215
|
+
else delete rememberedModels[engineId];
|
|
216
|
+
}
|
|
217
|
+
if (hasPolicy) {
|
|
218
|
+
if (policy === 'standard' || policy === 'unsafe') rememberedPolicies[engineId] = policy;
|
|
219
|
+
else delete rememberedPolicies[engineId];
|
|
209
220
|
}
|
|
221
|
+
if (Object.keys(rememberedModels).length) target.models = rememberedModels; else delete target.models;
|
|
222
|
+
if (Object.keys(rememberedPolicies).length) target.permissionPolicies = rememberedPolicies; else delete target.permissionPolicies;
|
|
210
223
|
const selected = hasModel
|
|
211
224
|
? (typeof model === 'string' ? model : '')
|
|
212
|
-
: (
|
|
213
|
-
if (Object.keys(remembered).length) target.models = remembered; else delete target.models;
|
|
225
|
+
: (rememberedModels[engineId] || '');
|
|
214
226
|
if (selected) target.model = selected; else delete target.model;
|
|
215
227
|
}
|
|
216
228
|
|
|
@@ -264,9 +276,21 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
264
276
|
const sessions = cache.sessions;
|
|
265
277
|
const cells = cache.defs.cells.map((c) => {
|
|
266
278
|
const alive = sessions.has(c.tmuxSession);
|
|
279
|
+
// Effective policy per cella: override remembered per-engine, altrimenti il
|
|
280
|
+
// default dell'engine gestito. Esposta SOLO come policy effettiva (mai segreti).
|
|
281
|
+
const engineDef = findEngine(cache.defs, c.engine);
|
|
282
|
+
const engineDefault = engineDef && engineDef.managed ? engineDef.managed.permissionPolicy : '';
|
|
283
|
+
const remembered = c.permissionPolicies
|
|
284
|
+
&& (c.permissionPolicies[c.engine] === 'standard' || c.permissionPolicies[c.engine] === 'unsafe')
|
|
285
|
+
? c.permissionPolicies[c.engine] : null;
|
|
286
|
+
const effectivePolicy = engineDef?.managed?.client === 'pi'
|
|
287
|
+
? 'standard'
|
|
288
|
+
: (remembered || engineDefault || '');
|
|
267
289
|
return {
|
|
268
290
|
cell: c.id, tmuxSession: c.tmuxSession, engine: c.engine,
|
|
269
291
|
model: c.model || '', models: { ...(c.models || {}) },
|
|
292
|
+
permissionPolicy: effectivePolicy,
|
|
293
|
+
permissionPolicies: { ...(c.permissionPolicies || {}) },
|
|
270
294
|
active: alive, boot: c.boot, tmux: alive,
|
|
271
295
|
rc: '', key: '', degraded: false, // builtin: cella "up" <=> sessione tmux viva
|
|
272
296
|
};
|
|
@@ -391,8 +415,20 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
391
415
|
if (readonly()) throw httpError(403, 'READONLY: engine bloccato');
|
|
392
416
|
const defs = reloadDefs();
|
|
393
417
|
if (!findCell(defs, cellId)) throw httpError(400, `cella sconosciuta: ${cellId}`);
|
|
394
|
-
|
|
395
|
-
|
|
418
|
+
const selectedEngine = findEngine(defs, engId);
|
|
419
|
+
if (!selectedEngine) throw httpError(400, `engine non valido: ${engId}`);
|
|
420
|
+
const hasModel = typeof opts.model === 'string';
|
|
421
|
+
const hasPolicy = Object.prototype.hasOwnProperty.call(opts, 'permissionPolicy') && typeof opts.permissionPolicy === 'string';
|
|
422
|
+
if (hasPolicy && opts.permissionPolicy !== 'standard' && opts.permissionPolicy !== 'unsafe') {
|
|
423
|
+
throw httpError(400, 'permissionPolicy non valida (standard|unsafe)');
|
|
424
|
+
}
|
|
425
|
+
if (hasPolicy && selectedEngine.managed?.client === 'pi' && opts.permissionPolicy !== 'standard') {
|
|
426
|
+
throw httpError(400, 'Pi supporta solo permissionPolicy standard');
|
|
427
|
+
}
|
|
428
|
+
await mutate(defs, (d) => applyCellTransition(findCell(d, cellId), engId, {
|
|
429
|
+
model: opts.model, hasModel,
|
|
430
|
+
policy: hasPolicy ? opts.permissionPolicy : null, hasPolicy,
|
|
431
|
+
}));
|
|
396
432
|
const saved = reloadDefs(); const target = findCell(saved, cellId);
|
|
397
433
|
return { ok: true, engine: engId, model: target.model || '' };
|
|
398
434
|
}
|
|
@@ -450,9 +486,14 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
450
486
|
await mutate(defs, (d) => {
|
|
451
487
|
d.engines = d.engines.filter((e) => e.id !== id);
|
|
452
488
|
for (const cell of d.cells) {
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
|
|
489
|
+
if (cell.models && Object.prototype.hasOwnProperty.call(cell.models, id)) {
|
|
490
|
+
delete cell.models[id];
|
|
491
|
+
if (!Object.keys(cell.models).length) delete cell.models;
|
|
492
|
+
}
|
|
493
|
+
if (cell.permissionPolicies && Object.prototype.hasOwnProperty.call(cell.permissionPolicies, id)) {
|
|
494
|
+
delete cell.permissionPolicies[id];
|
|
495
|
+
if (!Object.keys(cell.permissionPolicies).length) delete cell.permissionPolicies;
|
|
496
|
+
}
|
|
456
497
|
}
|
|
457
498
|
});
|
|
458
499
|
return { ok: true };
|
|
@@ -469,19 +510,33 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
469
510
|
if (readonly()) throw httpError(403, 'READONLY: edit-cell bloccato');
|
|
470
511
|
if (!id) throw httpError(400, 'id cell mancante');
|
|
471
512
|
const defs = reloadDefs();
|
|
472
|
-
|
|
513
|
+
const current = findCell(defs, id);
|
|
514
|
+
if (!current) throw httpError(400, `cell inesistente: ${id}`);
|
|
473
515
|
if (patch && (Object.prototype.hasOwnProperty.call(patch, 'id') || Object.prototype.hasOwnProperty.call(patch, 'tmuxSession'))) {
|
|
474
516
|
throw httpError(400, 'id e tmuxSession sono immutabili');
|
|
475
517
|
}
|
|
518
|
+
const nextEngine = findEngine(defs, typeof patch?.engine === 'string' ? patch.engine : current.engine);
|
|
519
|
+
if (Object.prototype.hasOwnProperty.call(patch || {}, 'permissionPolicy')
|
|
520
|
+
&& patch.permissionPolicy === 'unsafe' && nextEngine?.managed?.client === 'pi') {
|
|
521
|
+
throw httpError(400, 'Pi supporta solo permissionPolicy standard');
|
|
522
|
+
}
|
|
476
523
|
await mutate(defs, (d) => {
|
|
477
524
|
const target = findCell(d, id);
|
|
478
525
|
const hasEngine = typeof patch?.engine === 'string';
|
|
479
526
|
const hasModel = Object.prototype.hasOwnProperty.call(patch || {}, 'model');
|
|
527
|
+
const hasPolicy = Object.prototype.hasOwnProperty.call(patch || {}, 'permissionPolicy');
|
|
528
|
+
if (hasPolicy && patch.permissionPolicy !== 'standard' && patch.permissionPolicy !== 'unsafe' && patch.permissionPolicy !== null) {
|
|
529
|
+
throw httpError(400, 'permissionPolicy non valida (standard|unsafe)');
|
|
530
|
+
}
|
|
480
531
|
for (const [key, value] of Object.entries(patch || {})) {
|
|
481
|
-
if (key === 'engine' || key === 'model') continue;
|
|
532
|
+
if (key === 'engine' || key === 'model' || key === 'permissionPolicy') continue;
|
|
482
533
|
if (value === null) delete target[key]; else target[key] = value;
|
|
483
534
|
}
|
|
484
|
-
if (hasEngine || hasModel)
|
|
535
|
+
if (hasEngine || hasModel || hasPolicy) applyCellTransition(target, hasEngine ? patch.engine : target.engine, {
|
|
536
|
+
model: patch?.model, hasModel,
|
|
537
|
+
policy: hasPolicy ? patch.permissionPolicy : null,
|
|
538
|
+
hasPolicy,
|
|
539
|
+
});
|
|
485
540
|
});
|
|
486
541
|
const sessions = await refreshSessions();
|
|
487
542
|
return { ok: true, active: sessions.has(findCell(defs, id).tmuxSession) };
|
|
@@ -501,6 +556,63 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
501
556
|
return { ok: true };
|
|
502
557
|
}
|
|
503
558
|
|
|
559
|
+
// importCell: riconcilia una sessione tmux esistente (es. cella Fleet legacy
|
|
560
|
+
// rimasta orfana su installazione pulita, tipo "jarvis") in una cella GESTITA
|
|
561
|
+
// di fleet.json. Design §reconciliation:
|
|
562
|
+
// - idempotente: se una cella gestita possiede gia' questa tmuxSession -> no-op.
|
|
563
|
+
// - NESSUNA invenzione di engine/provider/model: l'engine DEV'essere gia'
|
|
564
|
+
// dichiarato in fleet.json (l'operatore lo sceglie). model/policy restano i
|
|
565
|
+
// default dell'engine scelto (modificabili dopo dal pannello di lancio).
|
|
566
|
+
// - tmuxSession non canonica (es. "jarvis") è ammessa e round-trip; una
|
|
567
|
+
// sessione legacy canonica cloud-X viene adottata come cella X.
|
|
568
|
+
// - cwd obbligatoria (default = home); la validazione strict (parseCell, sotto
|
|
569
|
+
// home, no cloud-* alias incoerenti) avviene in atomicWrite -> errore chiaro.
|
|
570
|
+
// Dopo l'import la sessione sparisce da "unmanaged" e compare una sola volta in
|
|
571
|
+
// Fleet con gear+power. Elimina/terminate resta solo nelle Impostazioni.
|
|
572
|
+
const CELL_ID_RE = /^[A-Za-z0-9._-]{1,32}$/;
|
|
573
|
+
function deriveCellId(session, cells) {
|
|
574
|
+
const raw = String(session || '');
|
|
575
|
+
// Una sessione legacy canonica cloud-X si adotta come cella X. Usare
|
|
576
|
+
// direttamente cloud-X come id produrrebbe cloud-cloud-X e fallirebbe la
|
|
577
|
+
// validazione; un id esplicito diverso continua invece a essere rifiutato.
|
|
578
|
+
const canonicalSuffix = raw.startsWith('cloud-') ? raw.slice('cloud-'.length) : '';
|
|
579
|
+
let base = (canonicalSuffix && CELL_ID_RE.test(canonicalSuffix) ? canonicalSuffix : raw)
|
|
580
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32) || 'cell';
|
|
581
|
+
const used = new Set(cells.map((c) => c.id));
|
|
582
|
+
if (!used.has(base)) return base;
|
|
583
|
+
if (canonicalSuffix) return base; // il caller produrrà un 409 esplicito
|
|
584
|
+
for (let i = 2; i < 100; i += 1) {
|
|
585
|
+
const candidate = `${base.slice(0, Math.max(1, 32 - String(i).length - 1))}-${i}`;
|
|
586
|
+
if (!used.has(candidate)) return candidate;
|
|
587
|
+
}
|
|
588
|
+
return base;
|
|
589
|
+
}
|
|
590
|
+
async function importCell(b = {}) {
|
|
591
|
+
if (readonly()) throw httpError(403, 'READONLY: import bloccato');
|
|
592
|
+
const tmuxSession = typeof b.tmuxSession === 'string' ? b.tmuxSession.trim() : '';
|
|
593
|
+
if (!tmuxSession || !validTmuxName(tmuxSession)) throw httpError(400, 'tmuxSession non valida');
|
|
594
|
+
const defs = reloadDefs();
|
|
595
|
+
const existing = defs.cells.find((c) => c.tmuxSession === tmuxSession);
|
|
596
|
+
if (existing) return { ok: true, id: existing.id, tmuxSession, idempotent: true };
|
|
597
|
+
const sessions = await refreshSessions();
|
|
598
|
+
if (!sessions.has(tmuxSession)) {
|
|
599
|
+
throw httpError(404, `sessione tmux non trovata: ${tmuxSession}`);
|
|
600
|
+
}
|
|
601
|
+
const engineId = typeof b.engine === 'string' ? b.engine.trim() : '';
|
|
602
|
+
if (!engineId || !findEngine(defs, engineId)) {
|
|
603
|
+
throw httpError(400, `engine non dichiarato: "${engineId || '(manca)'}" — crea prima l'engine in Impostazioni`);
|
|
604
|
+
}
|
|
605
|
+
let id = typeof b.id === 'string' && b.id.trim() ? b.id.trim() : deriveCellId(tmuxSession, defs.cells);
|
|
606
|
+
if (!CELL_ID_RE.test(id)) throw httpError(400, 'id non valido (A-Za-z0-9._- max 32)');
|
|
607
|
+
if (defs.cells.some((c) => c.id === id)) throw httpError(409, `id cella già usato: ${id}`);
|
|
608
|
+
const cwd = typeof b.cwd === 'string' && b.cwd.trim() ? b.cwd.trim() : home;
|
|
609
|
+
const cellDef = { id, cwd, engine: engineId, boot: b.boot === true, tmuxSession };
|
|
610
|
+
try {
|
|
611
|
+
await mutate(defs, (d) => { d.cells.push(cellDef); });
|
|
612
|
+
} catch (e) { throw httpError(400, `import non valido: ${e.message}`); }
|
|
613
|
+
return { ok: true, id, tmuxSession, imported: true };
|
|
614
|
+
}
|
|
615
|
+
|
|
504
616
|
// Vista editabile ma secret-safe: gli env values restano write-only.
|
|
505
617
|
function definitions() {
|
|
506
618
|
const defs = reloadDefs();
|
|
@@ -512,7 +624,11 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
512
624
|
if (e.managed) out.managedInfo = describeManaged(e.managed, { ...cfg, home });
|
|
513
625
|
return out;
|
|
514
626
|
}),
|
|
515
|
-
cells: defs.cells.map((c) => ({
|
|
627
|
+
cells: defs.cells.map((c) => ({
|
|
628
|
+
...c,
|
|
629
|
+
...(c.models ? { models: { ...c.models } } : {}),
|
|
630
|
+
...(c.permissionPolicies ? { permissionPolicies: { ...c.permissionPolicies } } : {}),
|
|
631
|
+
})),
|
|
516
632
|
managedCatalog: publicCatalog(),
|
|
517
633
|
managedConfig: { providerSecretsPath: cfg.providerSecretsPath || path.join(home, '.nexuscrew', 'providers.env') },
|
|
518
634
|
};
|
|
@@ -581,13 +697,14 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
581
697
|
boot: { type: 'boolean', required: false, default: false },
|
|
582
698
|
model: { type: 'string', required: false, max: CAPS.MAX_MODEL_VAL_LEN },
|
|
583
699
|
models: { type: 'object', required: false, keyRef: 'engine.id', valueMax: CAPS.MAX_MODEL_VAL_LEN },
|
|
700
|
+
permissionPolicies: { type: 'object', required: false, keyRef: 'engine.id', valueEnum: ['standard', 'unsafe'] },
|
|
584
701
|
prompt: { type: 'string', required: false, max: CAPS.MAX_PROMPT_LEN },
|
|
585
702
|
},
|
|
586
703
|
};
|
|
587
704
|
}
|
|
588
705
|
|
|
589
706
|
function capabilities() {
|
|
590
|
-
return ['status', 'up', 'down', 'restart', 'engine', 'boot', 'define', 'edit', 'remove', 'schema', 'definitions'];
|
|
707
|
+
return ['status', 'up', 'down', 'restart', 'engine', 'boot', 'define', 'edit', 'remove', 'import', 'schema', 'definitions'];
|
|
591
708
|
}
|
|
592
709
|
|
|
593
710
|
return {
|
|
@@ -595,7 +712,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
595
712
|
provider: 'builtin',
|
|
596
713
|
status, up, down, restart, engine: setEngine, boot: setBoot, isCellSession,
|
|
597
714
|
defineEngine, editEngine, removeEngine,
|
|
598
|
-
defineCell, editCell, removeCell,
|
|
715
|
+
defineCell, editCell, removeCell, importCell,
|
|
599
716
|
schema, definitions, capabilities,
|
|
600
717
|
};
|
|
601
718
|
}
|
package/lib/fleet/definitions.js
CHANGED
|
@@ -254,6 +254,24 @@ function parseCell(c, engineIds) {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// permissionPolicies: scelta PER-CELL PER-ENGINE (override del default engine).
|
|
258
|
+
// Stesso confine di `models`: ricorda l'ultima policy usata con ogni engine, così
|
|
259
|
+
// tornando a un provider si ritrova la scelta precedente senza trascinarla altrove
|
|
260
|
+
// e SENZA toccare engine.managed.permissionPolicy (globale: cambierebbe ogni cella
|
|
261
|
+
// che usa quell'engine). Valori ammessi solo 'standard' | 'unsafe'.
|
|
262
|
+
let permissionPolicies;
|
|
263
|
+
if (c.permissionPolicies !== undefined) {
|
|
264
|
+
if (!c.permissionPolicies || typeof c.permissionPolicies !== 'object' || Array.isArray(c.permissionPolicies)) return null;
|
|
265
|
+
const entries = Object.entries(c.permissionPolicies);
|
|
266
|
+
if (entries.length > MAX_ENGINES) return null;
|
|
267
|
+
permissionPolicies = {};
|
|
268
|
+
for (const [engineId, value] of entries) {
|
|
269
|
+
if (!engineIds.has(engineId)) return null;
|
|
270
|
+
if (value !== 'standard' && value !== 'unsafe') return null;
|
|
271
|
+
permissionPolicies[engineId] = value;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
257
275
|
// prompt (opzionale, cap)
|
|
258
276
|
let prompt;
|
|
259
277
|
if (c.prompt !== undefined) {
|
|
@@ -280,6 +298,7 @@ function parseCell(c, engineIds) {
|
|
|
280
298
|
const out = { id: c.id, cwd: c.cwd, engine: c.engine, boot, tmuxSession };
|
|
281
299
|
if (model !== undefined) out.model = model;
|
|
282
300
|
if (Object.keys(models).length) out.models = models;
|
|
301
|
+
if (permissionPolicies) out.permissionPolicies = permissionPolicies;
|
|
283
302
|
if (prompt !== undefined) out.prompt = prompt;
|
|
284
303
|
return out;
|
|
285
304
|
}
|
|
@@ -403,6 +422,7 @@ module.exports = {
|
|
|
403
422
|
resolveCwd,
|
|
404
423
|
loadDefinitions,
|
|
405
424
|
atomicWrite,
|
|
425
|
+
validTmuxName,
|
|
406
426
|
CAPS,
|
|
407
427
|
// Costanti esposte anche piatte (comode per la UI/schema e i test)
|
|
408
428
|
SCHEMA_VERSION, MAX_ENGINES, MAX_CELLS, MAX_ARGS, MAX_ARG_LEN,
|
package/lib/fleet/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
3
5
|
const { createFleetExec } = require('./exec.js');
|
|
4
6
|
|
|
5
7
|
const STATUS_TTL_MS = 2000;
|
|
@@ -135,4 +137,43 @@ async function createFleet(cfg = {}) {
|
|
|
135
137
|
};
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
// Discovery del binario fleet legacy cross-platform. In precedenza si cercava
|
|
141
|
+
// solo cfg.fleetBin (~/.local/bin/fleet di default); su Termux un fleet può stare
|
|
142
|
+
// in $PREFIX/bin/fleet. Ordine candidati: (1) cfg.fleetBin (config/env esplicita),
|
|
143
|
+
// (2) $PREFIX/bin/fleet (Termux), (3) ~/.local/bin/fleet (default storico).
|
|
144
|
+
// Dedup + skip vuoti. La validità (regular/trusted + status --json schema valido)
|
|
145
|
+
// la verifica il chiamante (resolveExternalFleet) via binTrusted + createFleet.
|
|
146
|
+
function fleetCandidates(cfg = {}) {
|
|
147
|
+
const home = cfg.home || os.homedir();
|
|
148
|
+
const envPrefix = (cfg.env && cfg.env.PREFIX) || process.env.PREFIX;
|
|
149
|
+
const out = [];
|
|
150
|
+
const push = (p) => { if (typeof p === 'string' && p && !out.includes(p)) out.push(p); };
|
|
151
|
+
push(cfg.fleetBin);
|
|
152
|
+
if (envPrefix) push(path.join(envPrefix, 'bin', 'fleet'));
|
|
153
|
+
push(path.join(home, '.local', 'bin', 'fleet'));
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// In auto mode i fallback cross-platform sono intenzionali. In forced external,
|
|
158
|
+
// invece, un fleetBin fornito è un pin: se non è valido si fallisce chiuso e non
|
|
159
|
+
// si cambia eseguibile alle spalle dell'operatore.
|
|
160
|
+
function externalFleetCandidates(cfg = {}) {
|
|
161
|
+
const forced = (cfg.fleetProvider || process.env.NEXUSCREW_FLEET_PROVIDER || '').toLowerCase();
|
|
162
|
+
if (forced === 'external' && typeof cfg.fleetBin === 'string' && cfg.fleetBin) return [cfg.fleetBin];
|
|
163
|
+
return fleetCandidates(cfg);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Prova i candidati in ordine e ritorna il primo external FIDATO (binTrusted) E
|
|
167
|
+
// rispondente al contratto (createFleet -> available, che fa status --json e
|
|
168
|
+
// valida lo schema ai-fleet). Null se nessuno valido. Così un external reale
|
|
169
|
+
// resta external (non cade sul builtin e non doppia il boot).
|
|
170
|
+
async function resolveExternalFleet(cfg = {}) {
|
|
171
|
+
for (const bin of externalFleetCandidates(cfg)) {
|
|
172
|
+
if (!binTrusted(bin)) continue;
|
|
173
|
+
const f = await createFleet({ ...cfg, fleetBin: bin });
|
|
174
|
+
if (f.available) return { fleet: f, bin, reason: `fidato + risponde al contratto (${bin})` };
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { createFleet, parseStatus, binTrusted, fleetCandidates, externalFleetCandidates, resolveExternalFleet };
|
package/lib/fleet/managed.js
CHANGED
|
@@ -333,7 +333,18 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
333
333
|
const profile = profileFor(spec.client, spec.provider, spec.credentialProfile || '');
|
|
334
334
|
const cred = credential(profile, spec, cfg, home);
|
|
335
335
|
const env = {}; const args = []; const model = cell?.model || spec.model;
|
|
336
|
-
|
|
336
|
+
// Effective permission policy: override PER-CELL PER-ENGINE (remembered) vince sul
|
|
337
|
+
// default dell'engine. Mai si mutationa engine.managed.permissionPolicy (globale).
|
|
338
|
+
// Pi resta sempre 'standard' (lo spec normalized rifiuta gia' unsafe per pi).
|
|
339
|
+
const engineId = engine && typeof engine.id === 'string' ? engine.id : '';
|
|
340
|
+
const override = cell && cell.permissionPolicies && Object.prototype.hasOwnProperty.call(cell.permissionPolicies, engineId)
|
|
341
|
+
? cell.permissionPolicies[engineId] : null;
|
|
342
|
+
let effectivePolicy = (override === 'standard' || override === 'unsafe') ? override : spec.permissionPolicy;
|
|
343
|
+
// Pi resta sempre 'standard': normalizeManagedSpec rifiuta gia' unsafe nello spec
|
|
344
|
+
// dell'engine, ma l'override PER-CELL bypasserebbe quel check -> clamp esplicito.
|
|
345
|
+
if (spec.client === 'pi') effectivePolicy = 'standard';
|
|
346
|
+
info.permissionPolicy = effectivePolicy;
|
|
347
|
+
if (effectivePolicy === 'unsafe') {
|
|
337
348
|
if (spec.client === 'claude') args.push('--dangerously-skip-permissions');
|
|
338
349
|
if (spec.client === 'codex' || spec.client === 'codex-vl') args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
339
350
|
}
|
|
@@ -345,12 +356,14 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
345
356
|
} else {
|
|
346
357
|
const endpoint = spec.baseUrl || profile.endpoint;
|
|
347
358
|
const token = profile.auth === 'none' ? 'ollama' : cred.value;
|
|
359
|
+
const contextWindow = OLLAMA_CONTEXT[model] || (spec.provider === 'zai' ? 1000000 : 200000);
|
|
348
360
|
Object.assign(env, {
|
|
349
361
|
ANTHROPIC_BASE_URL: endpoint, ANTHROPIC_AUTH_TOKEN: token, ANTHROPIC_API_KEY: '',
|
|
350
362
|
ANTHROPIC_MODEL: model, ANTHROPIC_DEFAULT_OPUS_MODEL: model,
|
|
351
363
|
ANTHROPIC_DEFAULT_SONNET_MODEL: model, ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
|
|
352
364
|
CLAUDE_CODE_SUBAGENT_MODEL: model, API_TIMEOUT_MS: '3000000',
|
|
353
|
-
|
|
365
|
+
CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(contextWindow),
|
|
366
|
+
CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(contextWindow),
|
|
354
367
|
});
|
|
355
368
|
}
|
|
356
369
|
if (model) args.push('--model', model);
|
package/lib/fleet/provider.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// Il drift a runtime NON si risolve qui: e' una scelta one-shot. Se l'external
|
|
15
15
|
// diventa invalido DOPO lo startup, lo status risultante sara' degraded/unavailable
|
|
16
16
|
// (mai fall-through silenzioso al built-in) — gestito dal layer di status.
|
|
17
|
-
const {
|
|
17
|
+
const { binTrusted, externalFleetCandidates, resolveExternalFleet } = require('./index.js');
|
|
18
18
|
const { createBuiltinFleet } = require('./builtin.js');
|
|
19
19
|
|
|
20
20
|
const DISABLED_FLEET = Object.freeze({
|
|
@@ -37,19 +37,20 @@ async function selectProvider(cfg = {}) {
|
|
|
37
37
|
|
|
38
38
|
const forced = (cfg.fleetProvider || process.env.NEXUSCREW_FLEET_PROVIDER || '').toLowerCase() || null;
|
|
39
39
|
|
|
40
|
-
// External:
|
|
40
|
+
// External: scopre il binario fleet legacy cross-platform (cfg.fleetBin,
|
|
41
|
+
// $PREFIX/bin/fleet su Termux, ~/.local/bin/fleet) e accetta il primo fidato
|
|
42
|
+
// (binTrusted) che risponde al contratto (status --json con schema valido).
|
|
41
43
|
let extFleet = null;
|
|
42
44
|
let extReason = null;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
else extReason = 'binario fidato ma non risponde al contratto (status invalido)';
|
|
48
|
-
} else {
|
|
49
|
-
extReason = 'binario non fidato (symlink/world-writable/non exec)';
|
|
50
|
-
}
|
|
45
|
+
const resolvedExternal = await resolveExternalFleet(cfg);
|
|
46
|
+
if (resolvedExternal) {
|
|
47
|
+
extFleet = resolvedExternal.fleet;
|
|
48
|
+
extReason = resolvedExternal.reason;
|
|
51
49
|
} else {
|
|
52
|
-
|
|
50
|
+
// Diagnosi: perché nessun candidato external è valido (fidato + contratto).
|
|
51
|
+
const cands = externalFleetCandidates(cfg);
|
|
52
|
+
if (!cands.length) extReason = 'nessun fleetBin esterno configurato';
|
|
53
|
+
else extReason = `nessun candidato external valido: ${cands.map((b) => (binTrusted(b) ? `${b} (non risponde al contratto)` : `${b} (non fidato)`)).join('; ')}`;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
// Builtin: abilitato + fleet.json valido.
|
package/lib/fleet/routes.js
CHANGED
|
@@ -78,7 +78,9 @@ function fleetRoutes(fleetP, cfg = {}) {
|
|
|
78
78
|
r.post('/up', guard(async (f, b) => {
|
|
79
79
|
const cell = String(b.cell || '');
|
|
80
80
|
if (b.engine && capList(f).includes('edit')) {
|
|
81
|
-
|
|
81
|
+
const opts = { model: typeof b.model === 'string' ? b.model : '' };
|
|
82
|
+
if (typeof b.permissionPolicy === 'string') opts.permissionPolicy = b.permissionPolicy;
|
|
83
|
+
await f.engine(cell, String(b.engine), opts);
|
|
82
84
|
}
|
|
83
85
|
if (Object.prototype.hasOwnProperty.call(b, 'boot') && capList(f).includes('edit') && capList(f).includes('boot')) {
|
|
84
86
|
await f.boot(cell, b.boot === true);
|
|
@@ -91,7 +93,11 @@ function fleetRoutes(fleetP, cfg = {}) {
|
|
|
91
93
|
return f.down(cell, { boot: !!b.boot });
|
|
92
94
|
}, { mutate: true }));
|
|
93
95
|
r.post('/restart', guard((f, b) => { requireCap(f, 'restart'); return f.restart(String(b.cell || '')); }, { mutate: true }));
|
|
94
|
-
r.post('/engine', guard((f, b) =>
|
|
96
|
+
r.post('/engine', guard((f, b) => {
|
|
97
|
+
const opts = { model: typeof b.model === 'string' ? b.model : '' };
|
|
98
|
+
if (typeof b.permissionPolicy === 'string') opts.permissionPolicy = b.permissionPolicy;
|
|
99
|
+
return f.engine(String(b.cell || ''), String(b.engine || ''), opts);
|
|
100
|
+
}, { mutate: true }));
|
|
95
101
|
r.post('/boot', guard((f, b) => f.boot(String(b.cell || ''), b.enabled === true), { mutate: true }));
|
|
96
102
|
|
|
97
103
|
// --- Estensione B4.2: schema + define/edit/remove (engine e cell) ---
|
|
@@ -104,6 +110,9 @@ function fleetRoutes(fleetP, cfg = {}) {
|
|
|
104
110
|
r.post('/define-cell', guard((f, b) => { requireCap(f, 'define'); return f.defineCell(b.def); }, { mutate: true }));
|
|
105
111
|
r.post('/edit-cell', guard((f, b) => { requireCap(f, 'edit'); return f.editCell(b.id, b.patch); }, { mutate: true }));
|
|
106
112
|
r.post('/remove-cell', guard((f, b) => { requireCap(f, 'remove'); return f.removeCell(b.id, { stop: b.stop === true }); }, { mutate: true }));
|
|
113
|
+
// Riconciliazione sessione tmux esistente (cella Fleet legacy orfana) in cella
|
|
114
|
+
// gestita fleet.json. Capability 'define' (solo builtin): external legacy -> 501.
|
|
115
|
+
r.post('/import-cell', guard(async (f, b) => { requireCap(f, 'import'); return f.importCell(b || {}); }, { mutate: true }));
|
|
107
116
|
|
|
108
117
|
return r;
|
|
109
118
|
}
|