coxpit 5.26.3 → 5.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/cockpit.ts +29 -2
- package/src/files.ts +19 -0
- package/src/server.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coxpit",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.27.0",
|
|
4
4
|
"description": "Self-hosted cockpit for running a fleet of AI coding agents across your own machines — parallel worktree runs, live board, compare & merge, web terminal, design capture.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@fastify/websocket": "^11",
|
|
46
46
|
"@libsql/client": "^0.14",
|
|
47
|
+
"@xterm/addon-clipboard": "^0.1.0",
|
|
47
48
|
"@xterm/addon-fit": "^0.10.0",
|
|
48
49
|
"@xterm/addon-search": "^0.15.0",
|
|
49
50
|
"@xterm/addon-unicode11": "^0.8.0",
|
package/src/cockpit.ts
CHANGED
|
@@ -552,6 +552,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
552
552
|
<script src="/vendor/addon-unicode11.js"></script>
|
|
553
553
|
<script src="/vendor/addon-web-links.js"></script>
|
|
554
554
|
<script src="/vendor/addon-search.js"></script>
|
|
555
|
+
<script src="/vendor/addon-clipboard.js"></script>
|
|
555
556
|
<script src="/vendor/marked.js"></script>
|
|
556
557
|
<script>
|
|
557
558
|
// 모바일 = 터미널 우선을 유지하되 좁은 화면에 맞춤(드로어 트리 + 단일 터미널 + IME 입력바).
|
|
@@ -770,6 +771,8 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
770
771
|
}
|
|
771
772
|
cb(links.length?links:undefined);
|
|
772
773
|
}}); }catch(e){}
|
|
774
|
+
// OSC 52 클립보드 — 터미널 안 앱(claude 등)이 "복사"하면 실제 시스템 클립보드로. (앱은 됐다는데 안 붙던 원인)
|
|
775
|
+
try{ term.loadAddon(new window.ClipboardAddon.ClipboardAddon()); }catch(e){}
|
|
773
776
|
// 스크롤백 검색(⌘F) — SearchAddon. t.search 에 보관, 검색바가 findNext/Previous 호출.
|
|
774
777
|
var search=null; try{ search=new window.SearchAddon.SearchAddon(); term.loadAddon(search);
|
|
775
778
|
search.onDidChangeResults(function(e){ if($('termFind') && !$('termFind').hidden && focusedRunId()===runId){ var c=$('findCount'); if(c){ var n=(e&&e.resultCount)||0; c.textContent = n ? (((e.resultIndex>=0?e.resultIndex+1:0))+'/'+n) : '0'; } } });
|
|
@@ -1179,14 +1182,38 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1179
1182
|
if (e.target.closest('.leaf-h .x')){ closeSlot(leaf.dataset.leaf); return; }
|
|
1180
1183
|
setLeafFocus(leaf.dataset.leaf);
|
|
1181
1184
|
});
|
|
1182
|
-
|
|
1185
|
+
function dragHasFiles(e){ try{ return e.dataTransfer && Array.prototype.indexOf.call(e.dataTransfer.types||[],'Files')>=0; }catch(_){ return false; } }
|
|
1186
|
+
$('panes').addEventListener('dragover', function(e){ var body=e.target.closest('[data-leafbody]'); if(body && (dragRunId!=null || dragHasFiles(e))){ e.preventDefault(); if(dragHasFiles(e)) try{ e.dataTransfer.dropEffect='copy'; }catch(_){}; body.classList.add('drop'); } });
|
|
1183
1187
|
$('panes').addEventListener('dragleave', function(e){ var body=e.target.closest('[data-leafbody]'); if(body) body.classList.remove('drop'); });
|
|
1184
1188
|
$('panes').addEventListener('drop', function(e){
|
|
1185
|
-
var body=e.target.closest('[data-leafbody]'); if(!body
|
|
1189
|
+
var body=e.target.closest('[data-leafbody]'); if(!body) return;
|
|
1190
|
+
if(dragHasFiles(e)){ e.preventDefault(); body.classList.remove('drop'); var lf=findLeaf(body.dataset.leafbody);
|
|
1191
|
+
if(lf && lf.tab!=null && e.dataTransfer.files && e.dataTransfer.files.length) uploadToTab(lf.tab, e.dataTransfer.files); else toast('업로드할 터미널 페인이 아닙니다'); return; }
|
|
1192
|
+
if(dragRunId==null) return; e.preventDefault(); body.classList.remove('drop');
|
|
1186
1193
|
var l=findLeaf(body.dataset.leafbody); if(!l) return;
|
|
1187
1194
|
var rid=dragRunId; eachLeaf(layout,function(x){ if(x.tab===rid) x.tab=null; });
|
|
1188
1195
|
ensureTab(rid); l.tab=rid; focusLeaf=l.id; render(); renderTree();
|
|
1189
1196
|
});
|
|
1197
|
+
// 파일 첨부 — 로컬 파일을 그 페인 run 의 작업폴더(cwd)로 업로드하고 경로를 터미널에 삽입.
|
|
1198
|
+
function uploadToTab(tabId, files){
|
|
1199
|
+
var t=tabs[tabId]; if(!t || t.kind==='viewer' || !t.ws){ toast('터미널 페인에만 첨부할 수 있어요'); return; }
|
|
1200
|
+
var r=runById[tabId]; var cwd=r&&r.worktreePath; if(!cwd){ toast('이 페인의 작업 폴더를 몰라 첨부할 수 없어요'); return; }
|
|
1201
|
+
Array.prototype.forEach.call(files, function(file){
|
|
1202
|
+
if(file.size > 25*1024*1024){ toast('너무 큼(25MB 초과): '+file.name); return; }
|
|
1203
|
+
var reader=new FileReader();
|
|
1204
|
+
reader.onload=function(){
|
|
1205
|
+
var b64=String(reader.result||'').split(',')[1]||'';
|
|
1206
|
+
fetch('/api/fs/upload',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({path:cwd, name:file.name, dataB64:b64})})
|
|
1207
|
+
.then(function(x){return x.json();}).then(function(d){
|
|
1208
|
+
if(d.error){ toast('첨부 실패: '+d.error); return; }
|
|
1209
|
+
var p=d.path; var ins=/[ "'\\\\]/.test(p) ? ('"'+p.replace(/(["\\\\])/g,'\\\\$1')+'"') : p;
|
|
1210
|
+
if(t.ws && t.ws.readyState===1) t.ws.send(JSON.stringify({t:'i', d:ins+' '}));
|
|
1211
|
+
toast('첨부됨 · '+d.name+' → 경로 삽입');
|
|
1212
|
+
}).catch(function(){ toast('첨부 실패'); });
|
|
1213
|
+
};
|
|
1214
|
+
reader.readAsDataURL(file);
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1190
1217
|
$('panes').addEventListener('mousedown', function(e){
|
|
1191
1218
|
var g=e.target.closest('[data-gutter]'); if(!g) return; e.preventDefault();
|
|
1192
1219
|
var node=findSplit(g.dataset.gutter); if(!node) return;
|
package/src/files.ts
CHANGED
|
@@ -186,6 +186,25 @@ export async function readRaw(input?: string) {
|
|
|
186
186
|
return { path, name: pbasename(path), mime: mimeFor(path), buf: await readFile(path) };
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
const MAX_UPLOAD = 25 * 1024 * 1024; // 25MB
|
|
190
|
+
// Save an uploaded file into a directory (within root). Returns the final path (dedup, never clobbers).
|
|
191
|
+
export async function uploadFile(dir: string, name: string, buf: Buffer) {
|
|
192
|
+
if (buf.length > MAX_UPLOAD) throw new Error('파일이 너무 큽니다 (25MB 초과)');
|
|
193
|
+
const base = (name || 'file').replace(/[\/\x00]/g, '_').replace(/^\.+/, '').slice(0, 200) || 'file';
|
|
194
|
+
const dot = base.lastIndexOf('.');
|
|
195
|
+
const stem = dot > 0 ? base.slice(0, dot) : base;
|
|
196
|
+
const ext = dot > 0 ? base.slice(dot) : '';
|
|
197
|
+
const cleanDir = (dir || '').replace(/\/+$/, '');
|
|
198
|
+
let path = await jail(cleanDir + '/' + base); // jail validates within root
|
|
199
|
+
for (let i = 1; i < 100; i++) {
|
|
200
|
+
const st = await stat(path).catch(() => null);
|
|
201
|
+
if (!st) break; // free name
|
|
202
|
+
path = await jail(cleanDir + '/' + stem + '-' + i + ext);
|
|
203
|
+
}
|
|
204
|
+
await writeFile(path, buf);
|
|
205
|
+
return { path, name: pbasename(path), size: buf.length };
|
|
206
|
+
}
|
|
207
|
+
|
|
189
208
|
export async function writeText(input: string, content: string) {
|
|
190
209
|
let path: string;
|
|
191
210
|
try { path = await jail(input); } catch (e) { throw friendly(e); }
|
package/src/server.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { getProvider, listProviders } from './providers';
|
|
|
26
26
|
import { remoteState, setServe, setFunnel } from './remote';
|
|
27
27
|
import { BOARD_HTML } from './board';
|
|
28
28
|
import { COCKPIT_HTML } from './cockpit';
|
|
29
|
-
import { listDir as fsListDir, readForView as fsReadForView, readRaw as fsReadRaw, writeText as fsWriteText, findFiles as fsFindFiles } from './files';
|
|
29
|
+
import { listDir as fsListDir, readForView as fsReadForView, readRaw as fsReadRaw, writeText as fsWriteText, findFiles as fsFindFiles, uploadFile as fsUploadFile } from './files';
|
|
30
30
|
|
|
31
31
|
const require_ = createRequire(import.meta.url);
|
|
32
32
|
|
|
@@ -38,6 +38,7 @@ const VENDOR: Record<string, { pkg: string; rel: string; type: string }> = {
|
|
|
38
38
|
'addon-unicode11.js': { pkg: '@xterm/addon-unicode11/package.json', rel: 'lib/addon-unicode11.js', type: 'text/javascript' },
|
|
39
39
|
'addon-web-links.js': { pkg: '@xterm/addon-web-links/package.json', rel: 'lib/addon-web-links.js', type: 'text/javascript' },
|
|
40
40
|
'addon-search.js': { pkg: '@xterm/addon-search/package.json', rel: 'lib/addon-search.js', type: 'text/javascript' },
|
|
41
|
+
'addon-clipboard.js': { pkg: '@xterm/addon-clipboard/package.json', rel: 'lib/addon-clipboard.js', type: 'text/javascript' },
|
|
41
42
|
'marked.js': { pkg: 'marked/package.json', rel: 'marked.min.js', type: 'text/javascript' },
|
|
42
43
|
};
|
|
43
44
|
|
|
@@ -784,6 +785,13 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
784
785
|
.send(buf);
|
|
785
786
|
} catch (e: any) { return reply.code(400).send({ error: String(e?.message || e) }); }
|
|
786
787
|
});
|
|
788
|
+
// 파일 첨부 — 로컬 파일을 세션 폴더로 업로드(base64). 경로 삽입은 클라가 함. 32MB 바디 허용.
|
|
789
|
+
app.post('/api/fs/upload', { bodyLimit: 32 * 1024 * 1024 }, async (req, reply) => {
|
|
790
|
+
const b = (req.body ?? {}) as { path?: string; name?: string; dataB64?: string };
|
|
791
|
+
if (!b.path || !b.name || typeof b.dataB64 !== 'string') return reply.code(400).send({ error: 'path, name, dataB64 required' });
|
|
792
|
+
try { const buf = Buffer.from(b.dataB64, 'base64'); return await fsUploadFile(b.path, b.name, buf); }
|
|
793
|
+
catch (e: any) { return reply.code(400).send({ error: String(e?.message || e) }); }
|
|
794
|
+
});
|
|
787
795
|
app.post('/api/fs/write', async (req, reply) => {
|
|
788
796
|
const b = (req.body ?? {}) as { path?: string; content?: string };
|
|
789
797
|
if (!b.path || typeof b.content !== 'string') return reply.code(400).send({ error: 'path and content required' });
|