groove-dev 0.16.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@groove-dev/daemon/src/api.js +122 -1
- package/node_modules/@groove-dev/daemon/src/mimetypes.js +43 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-Dxg9hdf3.js → index-BQSznoq0.js} +36 -36
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/src/components/FileTree.jsx +322 -23
- package/node_modules/@groove-dev/gui/src/components/MediaViewer.jsx +104 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +131 -2
- package/node_modules/@groove-dev/gui/src/views/FileEditor.jsx +11 -4
- package/package.json +1 -1
- package/packages/daemon/src/api.js +122 -1
- package/packages/daemon/src/mimetypes.js +43 -0
- package/packages/gui/dist/assets/{index-Dxg9hdf3.js → index-BQSznoq0.js} +36 -36
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/src/components/FileTree.jsx +322 -23
- package/packages/gui/src/components/MediaViewer.jsx +104 -0
- package/packages/gui/src/stores/groove.js +131 -2
- package/packages/gui/src/views/FileEditor.jsx +11 -4
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import express from 'express';
|
|
5
5
|
import { resolve, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
-
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream } from 'fs';
|
|
8
|
+
import { lookup as mimeLookup } from './mimetypes.js';
|
|
8
9
|
import { listProviders } from './providers/index.js';
|
|
9
10
|
import { validateAgentConfig } from './validate.js';
|
|
10
11
|
|
|
@@ -648,6 +649,126 @@ export function createApi(app, daemon) {
|
|
|
648
649
|
}
|
|
649
650
|
});
|
|
650
651
|
|
|
652
|
+
// Create a new file
|
|
653
|
+
app.post('/api/files/create', (req, res) => {
|
|
654
|
+
const { path: relPath, content = '' } = req.body;
|
|
655
|
+
const result = validateFilePath(relPath, daemon.projectDir);
|
|
656
|
+
if (result.error) return res.status(400).json({ error: result.error });
|
|
657
|
+
|
|
658
|
+
if (existsSync(result.fullPath)) {
|
|
659
|
+
return res.status(409).json({ error: 'File already exists' });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
try {
|
|
663
|
+
// Ensure parent directory exists
|
|
664
|
+
const parentDir = resolve(result.fullPath, '..');
|
|
665
|
+
if (!parentDir.startsWith(daemon.projectDir)) {
|
|
666
|
+
return res.status(400).json({ error: 'Path outside project' });
|
|
667
|
+
}
|
|
668
|
+
mkdirSync(parentDir, { recursive: true });
|
|
669
|
+
writeFileSync(result.fullPath, content, 'utf8');
|
|
670
|
+
daemon.audit.log('file.create', { path: relPath });
|
|
671
|
+
res.status(201).json({ ok: true, path: relPath });
|
|
672
|
+
} catch (err) {
|
|
673
|
+
res.status(500).json({ error: err.message });
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// Create a new directory
|
|
678
|
+
app.post('/api/files/mkdir', (req, res) => {
|
|
679
|
+
const { path: relPath } = req.body;
|
|
680
|
+
const result = validateFilePath(relPath, daemon.projectDir);
|
|
681
|
+
if (result.error) return res.status(400).json({ error: result.error });
|
|
682
|
+
|
|
683
|
+
if (existsSync(result.fullPath)) {
|
|
684
|
+
return res.status(409).json({ error: 'Directory already exists' });
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
mkdirSync(result.fullPath, { recursive: true });
|
|
689
|
+
daemon.audit.log('file.mkdir', { path: relPath });
|
|
690
|
+
res.status(201).json({ ok: true, path: relPath });
|
|
691
|
+
} catch (err) {
|
|
692
|
+
res.status(500).json({ error: err.message });
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// Delete a file or directory
|
|
697
|
+
app.delete('/api/files/delete', (req, res) => {
|
|
698
|
+
const relPath = req.query.path || req.body?.path;
|
|
699
|
+
const result = validateFilePath(relPath, daemon.projectDir);
|
|
700
|
+
if (result.error) return res.status(400).json({ error: result.error });
|
|
701
|
+
|
|
702
|
+
if (!existsSync(result.fullPath)) {
|
|
703
|
+
return res.status(404).json({ error: 'Not found' });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
try {
|
|
707
|
+
const stat = statSync(result.fullPath);
|
|
708
|
+
if (stat.isDirectory()) {
|
|
709
|
+
rmSync(result.fullPath, { recursive: true });
|
|
710
|
+
} else {
|
|
711
|
+
unlinkSync(result.fullPath);
|
|
712
|
+
}
|
|
713
|
+
daemon.audit.log('file.delete', { path: relPath });
|
|
714
|
+
res.json({ ok: true });
|
|
715
|
+
} catch (err) {
|
|
716
|
+
res.status(500).json({ error: err.message });
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Rename / move a file or directory
|
|
721
|
+
app.post('/api/files/rename', (req, res) => {
|
|
722
|
+
const { oldPath, newPath } = req.body;
|
|
723
|
+
const oldResult = validateFilePath(oldPath, daemon.projectDir);
|
|
724
|
+
if (oldResult.error) return res.status(400).json({ error: oldResult.error });
|
|
725
|
+
const newResult = validateFilePath(newPath, daemon.projectDir);
|
|
726
|
+
if (newResult.error) return res.status(400).json({ error: newResult.error });
|
|
727
|
+
|
|
728
|
+
if (!existsSync(oldResult.fullPath)) {
|
|
729
|
+
return res.status(404).json({ error: 'Source not found' });
|
|
730
|
+
}
|
|
731
|
+
if (existsSync(newResult.fullPath)) {
|
|
732
|
+
return res.status(409).json({ error: 'Destination already exists' });
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
// Ensure parent of new path exists
|
|
737
|
+
const parentDir = resolve(newResult.fullPath, '..');
|
|
738
|
+
mkdirSync(parentDir, { recursive: true });
|
|
739
|
+
renameSync(oldResult.fullPath, newResult.fullPath);
|
|
740
|
+
daemon.audit.log('file.rename', { oldPath, newPath });
|
|
741
|
+
res.json({ ok: true, oldPath, newPath });
|
|
742
|
+
} catch (err) {
|
|
743
|
+
res.status(500).json({ error: err.message });
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// Serve raw file (images, video, etc.)
|
|
748
|
+
app.get('/api/files/raw', (req, res) => {
|
|
749
|
+
const result = validateFilePath(req.query.path, daemon.projectDir);
|
|
750
|
+
if (result.error) return res.status(400).json({ error: result.error });
|
|
751
|
+
|
|
752
|
+
if (!existsSync(result.fullPath)) {
|
|
753
|
+
return res.status(404).json({ error: 'File not found' });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
try {
|
|
757
|
+
const stat = statSync(result.fullPath);
|
|
758
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
759
|
+
return res.status(400).json({ error: 'File too large (>50MB)' });
|
|
760
|
+
}
|
|
761
|
+
const filename = req.query.path.split('/').pop();
|
|
762
|
+
const contentType = mimeLookup(filename);
|
|
763
|
+
res.setHeader('Content-Type', contentType);
|
|
764
|
+
res.setHeader('Content-Length', stat.size);
|
|
765
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
766
|
+
createReadStream(result.fullPath).pipe(res);
|
|
767
|
+
} catch (err) {
|
|
768
|
+
res.status(500).json({ error: err.message });
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
651
772
|
// --- Codebase Indexer ---
|
|
652
773
|
|
|
653
774
|
app.get('/api/indexer', (req, res) => {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// GROOVE — Lightweight MIME type lookup (no deps)
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
const TYPES = {
|
|
5
|
+
// Images
|
|
6
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
|
|
7
|
+
svg: 'image/svg+xml', webp: 'image/webp', ico: 'image/x-icon', bmp: 'image/bmp',
|
|
8
|
+
avif: 'image/avif',
|
|
9
|
+
// Video
|
|
10
|
+
mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', avi: 'video/x-msvideo',
|
|
11
|
+
mkv: 'video/x-matroska', ogv: 'video/ogg',
|
|
12
|
+
// Audio
|
|
13
|
+
mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', flac: 'audio/flac',
|
|
14
|
+
// Text/code
|
|
15
|
+
js: 'text/javascript', mjs: 'text/javascript', jsx: 'text/javascript',
|
|
16
|
+
ts: 'text/typescript', tsx: 'text/typescript',
|
|
17
|
+
css: 'text/css', html: 'text/html', json: 'application/json',
|
|
18
|
+
md: 'text/markdown', txt: 'text/plain', xml: 'application/xml',
|
|
19
|
+
yaml: 'text/yaml', yml: 'text/yaml', toml: 'text/plain',
|
|
20
|
+
py: 'text/x-python', rs: 'text/x-rust', go: 'text/x-go',
|
|
21
|
+
sh: 'text/x-sh', sql: 'text/x-sql',
|
|
22
|
+
// Other
|
|
23
|
+
pdf: 'application/pdf', zip: 'application/zip', wasm: 'application/wasm',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function lookup(filename) {
|
|
27
|
+
const ext = filename.split('.').pop()?.toLowerCase();
|
|
28
|
+
return TYPES[ext] || 'application/octet-stream';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isImage(filename) {
|
|
32
|
+
const ext = filename.split('.').pop()?.toLowerCase();
|
|
33
|
+
return ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp', 'avif'].includes(ext);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isVideo(filename) {
|
|
37
|
+
const ext = filename.split('.').pop()?.toLowerCase();
|
|
38
|
+
return ['mp4', 'webm', 'mov', 'avi', 'mkv', 'ogv'].includes(ext);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isMedia(filename) {
|
|
42
|
+
return isImage(filename) || isVideo(filename);
|
|
43
|
+
}
|