groove-dev 0.27.112 → 0.27.115
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/CENTRAL_COMMAND_REBUILD.md +689 -0
- package/EMBEDDING_DIAGNOSTIC.md +197 -0
- package/TRAINING_DATA_v4.md +3 -0
- package/moe-training/client/parsers/codex.js +3 -3
- package/moe-training/client/parsers/gemini.js +2 -2
- package/moe-training/client/step-classifier.js +2 -2
- package/moe-training/test/client/step-classifier.test.js +63 -7
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/team.js +43 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +75 -15
- package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
- package/node_modules/@groove-dev/daemon/src/index.js +36 -10
- package/node_modules/@groove-dev/daemon/src/teams.js +100 -6
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +75 -43
- package/node_modules/@groove-dev/gui/dist/assets/{index-CHu5w3i3.js → index-BKCiOUDb.js} +593 -593
- package/node_modules/@groove-dev/gui/dist/assets/index-D4Q72afD.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +0 -22
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +43 -45
- package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +3 -1
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -8
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +31 -3
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +106 -3
- package/node_modules/moe-training/client/parsers/codex.js +3 -3
- package/node_modules/moe-training/client/parsers/gemini.js +2 -2
- package/node_modules/moe-training/client/step-classifier.js +2 -2
- package/node_modules/moe-training/test/client/step-classifier.test.js +63 -7
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/team.js +43 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +75 -15
- package/packages/daemon/src/filewatcher.js +45 -0
- package/packages/daemon/src/index.js +36 -10
- package/packages/daemon/src/teams.js +100 -6
- package/packages/daemon/src/tunnel-manager.js +75 -43
- package/packages/gui/dist/assets/{index-CHu5w3i3.js → index-BKCiOUDb.js} +593 -593
- package/packages/gui/dist/assets/index-D4Q72afD.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/workspace-mode.jsx +0 -22
- package/packages/gui/src/components/layout/status-bar.jsx +43 -45
- package/packages/gui/src/components/preview/preview-workspace.jsx +3 -1
- package/packages/gui/src/components/settings/quick-connect.jsx +2 -1
- package/packages/gui/src/stores/groove.js +57 -8
- package/packages/gui/src/views/agents.jsx +31 -3
- package/packages/gui/src/views/editor.jsx +1 -20
- package/packages/gui/src/views/teams.jsx +106 -3
- package/TRAINING_DATA_v2.md +0 -9
- package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
- package/packages/gui/dist/assets/index-DAlSbVyK.css +0 -1
|
@@ -1100,6 +1100,30 @@ export function createApi(app, daemon) {
|
|
|
1100
1100
|
}
|
|
1101
1101
|
});
|
|
1102
1102
|
|
|
1103
|
+
app.get('/api/teams/archived', (req, res) => {
|
|
1104
|
+
res.json({ archived: daemon.teams.listArchived() });
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
app.post('/api/teams/archived/:id/restore', (req, res) => {
|
|
1108
|
+
try {
|
|
1109
|
+
const team = daemon.teams.restore(req.params.id);
|
|
1110
|
+
daemon.audit.log('team.restore', { archivedId: req.params.id, newId: team.id, name: team.name });
|
|
1111
|
+
res.json(team);
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
res.status(400).json({ error: err.message });
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
app.delete('/api/teams/archived/:id', (req, res) => {
|
|
1118
|
+
try {
|
|
1119
|
+
daemon.teams.purge(req.params.id);
|
|
1120
|
+
daemon.audit.log('team.purge', { archivedId: req.params.id });
|
|
1121
|
+
res.json({ ok: true });
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
res.status(400).json({ error: err.message });
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1103
1127
|
app.patch('/api/teams/:id', (req, res) => {
|
|
1104
1128
|
try {
|
|
1105
1129
|
if (req.body.name) daemon.teams.rename(req.params.id, req.body.name);
|
|
@@ -3855,6 +3879,48 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3855
3879
|
// --- Preview Proxy (same-origin iframe support) ---
|
|
3856
3880
|
// Forwards HTTP requests to the dev server so the GUI can iframe the preview
|
|
3857
3881
|
// without cross-origin issues. WebSocket upgrade for HMR is handled in index.js.
|
|
3882
|
+
|
|
3883
|
+
function rewriteAbsoluteUrls(body, proxyBase) {
|
|
3884
|
+
let out = body;
|
|
3885
|
+
// HTML attributes: src, href, action, poster
|
|
3886
|
+
out = out.replace(/((?:src|href|action|poster)\s*=\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3887
|
+
// JS imports: from '/' and import('/')
|
|
3888
|
+
out = out.replace(/(from\s+(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3889
|
+
out = out.replace(/(import\s*\(\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3890
|
+
// CSS url()
|
|
3891
|
+
out = out.replace(/(url\s*\(\s*(["']?))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3892
|
+
return out;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
const REWRITABLE_TYPES = ['text/html', 'application/javascript', 'text/javascript', 'text/css'];
|
|
3896
|
+
|
|
3897
|
+
function handleProxyResponse(proxyRes, res, proxyBase) {
|
|
3898
|
+
const fwdHeaders = { ...proxyRes.headers };
|
|
3899
|
+
delete fwdHeaders['content-security-policy'];
|
|
3900
|
+
delete fwdHeaders['x-frame-options'];
|
|
3901
|
+
|
|
3902
|
+
const ct = (fwdHeaders['content-type'] || '').toLowerCase();
|
|
3903
|
+
const shouldRewrite = REWRITABLE_TYPES.some((t) => ct.includes(t));
|
|
3904
|
+
|
|
3905
|
+
if (!shouldRewrite) {
|
|
3906
|
+
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3907
|
+
proxyRes.pipe(res);
|
|
3908
|
+
return;
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
const chunks = [];
|
|
3912
|
+
proxyRes.on('data', (c) => chunks.push(c));
|
|
3913
|
+
proxyRes.on('end', () => {
|
|
3914
|
+
let body = Buffer.concat(chunks).toString('utf8');
|
|
3915
|
+
body = rewriteAbsoluteUrls(body, proxyBase);
|
|
3916
|
+
const buf = Buffer.from(body, 'utf8');
|
|
3917
|
+
fwdHeaders['content-length'] = buf.length;
|
|
3918
|
+
delete fwdHeaders['transfer-encoding'];
|
|
3919
|
+
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3920
|
+
res.end(buf);
|
|
3921
|
+
});
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3858
3924
|
app.all('/api/preview/:teamId/proxy/*', (req, res) => {
|
|
3859
3925
|
const entry = daemon.preview?.get(req.params.teamId);
|
|
3860
3926
|
if (!entry || !entry.url) return res.status(404).json({ error: 'No active preview for this team' });
|
|
@@ -3865,9 +3931,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3865
3931
|
const proxyPath = req.params[0] || '';
|
|
3866
3932
|
const search = req.url.includes('?') ? '?' + req.url.split('?').slice(1).join('?') : '';
|
|
3867
3933
|
const fullPath = '/' + proxyPath + search;
|
|
3934
|
+
const proxyBase = `/api/preview/${req.params.teamId}/proxy`;
|
|
3868
3935
|
|
|
3869
3936
|
const headers = { ...req.headers };
|
|
3870
3937
|
delete headers.host;
|
|
3938
|
+
delete headers['accept-encoding'];
|
|
3871
3939
|
headers.host = targetUrl.host;
|
|
3872
3940
|
|
|
3873
3941
|
const proxyReq = httpRequest({
|
|
@@ -3876,13 +3944,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3876
3944
|
path: fullPath,
|
|
3877
3945
|
method: req.method,
|
|
3878
3946
|
headers,
|
|
3879
|
-
}, (proxyRes) =>
|
|
3880
|
-
const fwdHeaders = { ...proxyRes.headers };
|
|
3881
|
-
delete fwdHeaders['content-security-policy'];
|
|
3882
|
-
delete fwdHeaders['x-frame-options'];
|
|
3883
|
-
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3884
|
-
proxyRes.pipe(res);
|
|
3885
|
-
});
|
|
3947
|
+
}, (proxyRes) => handleProxyResponse(proxyRes, res, proxyBase));
|
|
3886
3948
|
|
|
3887
3949
|
proxyReq.on('error', (err) => {
|
|
3888
3950
|
if (!res.headersSent) res.status(502).json({ error: `Proxy error: ${err.message}` });
|
|
@@ -3899,9 +3961,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3899
3961
|
try { targetUrl = new URL(entry.devUrl || entry.url); } catch { return res.status(500).json({ error: 'Invalid preview URL' }); }
|
|
3900
3962
|
|
|
3901
3963
|
const search = req.url.includes('?') ? '?' + req.url.split('?').slice(1).join('?') : '';
|
|
3964
|
+
const proxyBase = `/api/preview/${req.params.teamId}/proxy`;
|
|
3902
3965
|
|
|
3903
3966
|
const headers = { ...req.headers };
|
|
3904
3967
|
delete headers.host;
|
|
3968
|
+
delete headers['accept-encoding'];
|
|
3905
3969
|
headers.host = targetUrl.host;
|
|
3906
3970
|
|
|
3907
3971
|
const proxyReq = httpRequest({
|
|
@@ -3910,13 +3974,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3910
3974
|
path: '/' + search,
|
|
3911
3975
|
method: req.method,
|
|
3912
3976
|
headers,
|
|
3913
|
-
}, (proxyRes) =>
|
|
3914
|
-
const fwdHeaders = { ...proxyRes.headers };
|
|
3915
|
-
delete fwdHeaders['content-security-policy'];
|
|
3916
|
-
delete fwdHeaders['x-frame-options'];
|
|
3917
|
-
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3918
|
-
proxyRes.pipe(res);
|
|
3919
|
-
});
|
|
3977
|
+
}, (proxyRes) => handleProxyResponse(proxyRes, res, proxyBase));
|
|
3920
3978
|
|
|
3921
3979
|
proxyReq.on('error', (err) => {
|
|
3922
3980
|
if (!res.headersSent) res.status(502).json({ error: `Proxy error: ${err.message}` });
|
|
@@ -6183,13 +6241,15 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
6183
6241
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
6184
6242
|
env: tagEnv,
|
|
6185
6243
|
});
|
|
6244
|
+
daemon._networkCheckProc = proc;
|
|
6186
6245
|
let stdout = '';
|
|
6187
6246
|
let stderr = '';
|
|
6188
6247
|
proc.stdout.on('data', (c) => { stdout += c.toString(); });
|
|
6189
6248
|
proc.stderr.on('data', (c) => { stderr += c.toString(); });
|
|
6190
6249
|
const timeout = setTimeout(() => { safeKill(proc, 'SIGTERM'); }, 10_000);
|
|
6191
|
-
proc.on('error', () => { clearTimeout(timeout); resolvePromise(null); });
|
|
6250
|
+
proc.on('error', () => { clearTimeout(timeout); daemon._networkCheckProc = null; resolvePromise(null); });
|
|
6192
6251
|
proc.on('close', (code) => {
|
|
6252
|
+
daemon._networkCheckProc = null;
|
|
6193
6253
|
clearTimeout(timeout);
|
|
6194
6254
|
if (code !== 0) return resolvePromise(null);
|
|
6195
6255
|
const tags = [];
|
|
@@ -8,6 +8,7 @@ export class FileWatcher {
|
|
|
8
8
|
constructor(daemon) {
|
|
9
9
|
this.daemon = daemon;
|
|
10
10
|
this.watchers = new Map(); // relPath → { watcher, timer }
|
|
11
|
+
this.dirWatchers = new Map(); // relPath → { watcher, timer }
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
watch(relPath) {
|
|
@@ -51,9 +52,53 @@ export class FileWatcher {
|
|
|
51
52
|
this.watchers.delete(relPath);
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
watchDir(relPath) {
|
|
56
|
+
if (typeof relPath !== 'string') return;
|
|
57
|
+
if (relPath && relPath.includes('..')) return;
|
|
58
|
+
if (this.dirWatchers.has(relPath)) return;
|
|
59
|
+
|
|
60
|
+
const fullPath = relPath ? resolve(this.daemon.projectDir, relPath) : this.daemon.projectDir;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const watcher = watch(fullPath, () => {
|
|
64
|
+
const entry = this.dirWatchers.get(relPath);
|
|
65
|
+
if (!entry) return;
|
|
66
|
+
|
|
67
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
68
|
+
entry.timer = setTimeout(() => {
|
|
69
|
+
this.daemon.broadcast({
|
|
70
|
+
type: 'file:tree-changed',
|
|
71
|
+
path: relPath,
|
|
72
|
+
timestamp: Date.now(),
|
|
73
|
+
});
|
|
74
|
+
}, 300);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
watcher.on('error', () => {
|
|
78
|
+
this.unwatchDir(relPath);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.dirWatchers.set(relPath, { watcher, timer: null });
|
|
82
|
+
} catch {
|
|
83
|
+
// Directory doesn't exist or not watchable — ignore
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
unwatchDir(relPath) {
|
|
88
|
+
const entry = this.dirWatchers.get(relPath);
|
|
89
|
+
if (!entry) return;
|
|
90
|
+
|
|
91
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
92
|
+
try { entry.watcher.close(); } catch { /* already closed */ }
|
|
93
|
+
this.dirWatchers.delete(relPath);
|
|
94
|
+
}
|
|
95
|
+
|
|
54
96
|
unwatchAll() {
|
|
55
97
|
for (const [relPath] of this.watchers) {
|
|
56
98
|
this.unwatch(relPath);
|
|
57
99
|
}
|
|
100
|
+
for (const [relPath] of this.dirWatchers) {
|
|
101
|
+
this.unwatchDir(relPath);
|
|
102
|
+
}
|
|
58
103
|
}
|
|
59
104
|
}
|
|
@@ -290,11 +290,11 @@ export class Daemon {
|
|
|
290
290
|
});
|
|
291
291
|
|
|
292
292
|
// Debounced file I/O for registry changes (at most once per 2s)
|
|
293
|
-
|
|
293
|
+
this._registryIoTimer = null;
|
|
294
294
|
const _debouncedRegistryIo = () => {
|
|
295
|
-
if (_registryIoTimer) return;
|
|
296
|
-
_registryIoTimer = setTimeout(() => {
|
|
297
|
-
_registryIoTimer = null;
|
|
295
|
+
if (this._registryIoTimer) return;
|
|
296
|
+
this._registryIoTimer = setTimeout(() => {
|
|
297
|
+
this._registryIoTimer = null;
|
|
298
298
|
this.introducer.writeRegistryFile(this.projectDir);
|
|
299
299
|
this.introducer.injectGrooveSection(this.projectDir);
|
|
300
300
|
}, 2000);
|
|
@@ -325,8 +325,9 @@ export class Daemon {
|
|
|
325
325
|
data: enrichAgents(this.registry.getAll()),
|
|
326
326
|
}));
|
|
327
327
|
|
|
328
|
-
// Track which files this client is watching (for cleanup on disconnect)
|
|
328
|
+
// Track which files/dirs this client is watching (for cleanup on disconnect)
|
|
329
329
|
const watchedFiles = new Set();
|
|
330
|
+
const watchedDirs = new Set();
|
|
330
331
|
|
|
331
332
|
ws.on('message', (raw) => {
|
|
332
333
|
try {
|
|
@@ -335,7 +336,7 @@ export class Daemon {
|
|
|
335
336
|
// Validate message type against whitelist
|
|
336
337
|
const VALID_WS_TYPES = new Set([
|
|
337
338
|
'terminal:spawn', 'terminal:resize', 'terminal:input', 'terminal:close', 'terminal:kill', 'terminal:rename',
|
|
338
|
-
'editor:watch', 'editor:unwatch', 'editor:save',
|
|
339
|
+
'editor:watch', 'editor:unwatch', 'editor:save', 'editor:watchdir', 'editor:unwatchdir',
|
|
339
340
|
'ping'
|
|
340
341
|
]);
|
|
341
342
|
if (!msg || typeof msg !== 'object' || !VALID_WS_TYPES.has(msg.type)) return;
|
|
@@ -351,6 +352,14 @@ export class Daemon {
|
|
|
351
352
|
case 'editor:unwatch':
|
|
352
353
|
if (msg.path) { this.fileWatcher.unwatch(msg.path); watchedFiles.delete(msg.path); }
|
|
353
354
|
break;
|
|
355
|
+
case 'editor:watchdir':
|
|
356
|
+
if (typeof msg.path === 'string' && !msg.path.includes('..')) {
|
|
357
|
+
this.fileWatcher.watchDir(msg.path); watchedDirs.add(msg.path);
|
|
358
|
+
}
|
|
359
|
+
break;
|
|
360
|
+
case 'editor:unwatchdir':
|
|
361
|
+
if (typeof msg.path === 'string') { this.fileWatcher.unwatchDir(msg.path); watchedDirs.delete(msg.path); }
|
|
362
|
+
break;
|
|
354
363
|
// Terminal
|
|
355
364
|
case 'terminal:spawn': {
|
|
356
365
|
if (msg.cwd !== undefined && (typeof msg.cwd !== 'string' || msg.cwd.includes('..'))) break;
|
|
@@ -389,6 +398,9 @@ export class Daemon {
|
|
|
389
398
|
for (const path of watchedFiles) {
|
|
390
399
|
this.fileWatcher.unwatch(path);
|
|
391
400
|
}
|
|
401
|
+
for (const path of watchedDirs) {
|
|
402
|
+
this.fileWatcher.unwatchDir(path);
|
|
403
|
+
}
|
|
392
404
|
this.terminalManager.cleanupClient(ws);
|
|
393
405
|
});
|
|
394
406
|
});
|
|
@@ -779,6 +791,11 @@ export class Daemon {
|
|
|
779
791
|
if (this._stateSaveInterval) clearInterval(this._stateSaveInterval);
|
|
780
792
|
if (this._classifierInterval) clearInterval(this._classifierInterval);
|
|
781
793
|
if (this._subscriptionPollInterval) clearInterval(this._subscriptionPollInterval);
|
|
794
|
+
if (this._registryIoTimer) clearTimeout(this._registryIoTimer);
|
|
795
|
+
if (this._networkCheckProc) {
|
|
796
|
+
try { this._networkCheckProc.kill(); } catch { /* already exited */ }
|
|
797
|
+
this._networkCheckProc = null;
|
|
798
|
+
}
|
|
782
799
|
|
|
783
800
|
// Clean up file watchers and terminal sessions
|
|
784
801
|
this.fileWatcher.unwatchAll();
|
|
@@ -823,10 +840,19 @@ export class Daemon {
|
|
|
823
840
|
|
|
824
841
|
// Close server
|
|
825
842
|
return new Promise((resolvePromise) => {
|
|
826
|
-
this.
|
|
827
|
-
this.
|
|
828
|
-
|
|
829
|
-
|
|
843
|
+
this.federationWss.close(() => {
|
|
844
|
+
this.wss.close(() => {
|
|
845
|
+
this.server.close(() => {
|
|
846
|
+
// Unref lingering handles (idle fetch/undici TLS pool connections,
|
|
847
|
+
// closed servers) so they don't prevent process exit in tests.
|
|
848
|
+
for (const h of process._getActiveHandles()) {
|
|
849
|
+
if (typeof h.unref === 'function' && h !== process.stdout && h !== process.stderr) {
|
|
850
|
+
h.unref();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
console.log('GROOVE daemon stopped.');
|
|
854
|
+
resolvePromise();
|
|
855
|
+
});
|
|
830
856
|
});
|
|
831
857
|
});
|
|
832
858
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// GROOVE — Teams (Live Agent Groups)
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync } from 'fs';
|
|
5
|
-
import { resolve } from 'path';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync, readdirSync, cpSync } from 'fs';
|
|
5
|
+
import { resolve, basename } from 'path';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import { validateTeamName } from './validate.js';
|
|
8
8
|
|
|
@@ -162,17 +162,39 @@ export class Teams {
|
|
|
162
162
|
this.daemon.registry.remove(agent.id);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
//
|
|
166
|
-
// (legacy default teams that were never migrated point there).
|
|
165
|
+
// Archive the team's working directory instead of deleting it
|
|
167
166
|
if (
|
|
168
167
|
team.workingDir &&
|
|
169
168
|
team.workingDir !== this.daemon.projectDir &&
|
|
170
169
|
existsSync(team.workingDir)
|
|
171
170
|
) {
|
|
172
171
|
try {
|
|
173
|
-
|
|
172
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
173
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
174
|
+
const slug = basename(team.workingDir);
|
|
175
|
+
const archiveName = `${slug}-${Date.now()}`;
|
|
176
|
+
const archivePath = resolve(archiveDir, archiveName);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
renameSync(team.workingDir, archivePath);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err.code === 'EXDEV') {
|
|
182
|
+
cpSync(team.workingDir, archivePath, { recursive: true });
|
|
183
|
+
rmSync(team.workingDir, { recursive: true, force: true });
|
|
184
|
+
} else {
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const metadata = {
|
|
190
|
+
originalName: team.name,
|
|
191
|
+
originalId: team.id,
|
|
192
|
+
deletedAt: new Date().toISOString(),
|
|
193
|
+
agentCount: agents.length,
|
|
194
|
+
};
|
|
195
|
+
writeFileSync(resolve(archivePath, 'metadata.json'), JSON.stringify(metadata, null, 2));
|
|
174
196
|
} catch (err) {
|
|
175
|
-
console.log(`[Groove:Teams] Failed to
|
|
197
|
+
console.log(`[Groove:Teams] Failed to archive directory: ${err.message}`);
|
|
176
198
|
}
|
|
177
199
|
}
|
|
178
200
|
|
|
@@ -193,6 +215,78 @@ export class Teams {
|
|
|
193
215
|
return true;
|
|
194
216
|
}
|
|
195
217
|
|
|
218
|
+
listArchived() {
|
|
219
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
220
|
+
if (!existsSync(archiveDir)) return [];
|
|
221
|
+
const entries = readdirSync(archiveDir, { withFileTypes: true });
|
|
222
|
+
const result = [];
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
if (!entry.isDirectory()) continue;
|
|
225
|
+
const metaPath = resolve(archiveDir, entry.name, 'metadata.json');
|
|
226
|
+
try {
|
|
227
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
228
|
+
result.push({ id: entry.name, ...meta });
|
|
229
|
+
} catch {
|
|
230
|
+
result.push({ id: entry.name, originalName: entry.name, deletedAt: null, agentCount: 0 });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
restore(archivedId) {
|
|
237
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
238
|
+
const archivePath = resolve(archiveDir, archivedId);
|
|
239
|
+
if (!existsSync(archivePath)) throw new Error('Archived team not found');
|
|
240
|
+
|
|
241
|
+
let meta = {};
|
|
242
|
+
const metaPath = resolve(archivePath, 'metadata.json');
|
|
243
|
+
try { meta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* use defaults */ }
|
|
244
|
+
|
|
245
|
+
const name = meta.originalName || archivedId;
|
|
246
|
+
const dirName = slugify(name);
|
|
247
|
+
let workingDir = resolve(this.daemon.projectDir, dirName);
|
|
248
|
+
|
|
249
|
+
if (existsSync(workingDir)) {
|
|
250
|
+
workingDir = resolve(this.daemon.projectDir, `${dirName}-${Date.now()}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
renameSync(archivePath, workingDir);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
if (err.code === 'EXDEV') {
|
|
257
|
+
cpSync(archivePath, workingDir, { recursive: true });
|
|
258
|
+
rmSync(archivePath, { recursive: true, force: true });
|
|
259
|
+
} else {
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Remove the metadata file from the restored directory
|
|
265
|
+
const restoredMetaPath = resolve(workingDir, 'metadata.json');
|
|
266
|
+
try { rmSync(restoredMetaPath); } catch { /* may not exist */ }
|
|
267
|
+
|
|
268
|
+
const id = randomUUID().slice(0, 8);
|
|
269
|
+
const team = {
|
|
270
|
+
id,
|
|
271
|
+
name,
|
|
272
|
+
isDefault: false,
|
|
273
|
+
workingDir,
|
|
274
|
+
createdAt: new Date().toISOString(),
|
|
275
|
+
};
|
|
276
|
+
this.teams.set(id, team);
|
|
277
|
+
this._save();
|
|
278
|
+
this.daemon.broadcast({ type: 'team:created', team });
|
|
279
|
+
return team;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
purge(archivedId) {
|
|
283
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
284
|
+
const archivePath = resolve(archiveDir, archivedId);
|
|
285
|
+
if (!existsSync(archivePath)) throw new Error('Archived team not found');
|
|
286
|
+
rmSync(archivePath, { recursive: true, force: true });
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
196
290
|
// Migrate old agents (teamName but no teamId) to default team
|
|
197
291
|
migrateAgents() {
|
|
198
292
|
const defaultTeam = this.getDefault();
|