@ian2018cs/agenthub 0.1.75 → 0.1.77
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/dist/assets/index-C9030Hra.js +197 -0
- package/dist/assets/index-DkNpDSsg.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/server/claude-sdk.js +7 -152
- package/server/index.js +108 -12
- package/server/routes/agents.js +333 -5
- package/server/services/builtin-tools/background-task-pool.js +316 -0
- package/server/services/builtin-tools/background-task.js +231 -0
- package/server/services/builtin-tools/index.js +146 -0
- package/server/services/builtin-tools/share-project-template.js +124 -0
- package/server/services/system-agent-repo.js +43 -26
- package/server/services/user-directories.js +1 -0
- package/shared/brand.js +4 -0
- package/dist/assets/index-B5imuFpg.js +0 -192
- package/dist/assets/index-oUz7uC99.css +0 -32
package/server/routes/agents.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'path';
|
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import AdmZip from 'adm-zip';
|
|
7
7
|
import { getUserPaths, getPublicPaths } from '../services/user-directories.js';
|
|
8
|
-
import { scanAgents, ensureAgentRepo, incrementPatchVersion, publishAgentToRepo
|
|
8
|
+
import { scanAgents, ensureAgentRepo, incrementPatchVersion, publishAgentToRepo } from '../services/system-agent-repo.js';
|
|
9
9
|
import { ensureSystemRepo, SYSTEM_REPO_URL } from '../services/system-repo.js';
|
|
10
10
|
import { ensureSystemMcpRepo, SYSTEM_MCP_REPO_URL } from '../services/system-mcp-repo.js';
|
|
11
11
|
import { addProjectManually, loadProjectConfig, saveProjectConfig } from '../projects.js';
|
|
@@ -159,6 +159,217 @@ function parseYamlMcps(yamlContent) {
|
|
|
159
159
|
return mcps;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Parse git_repos list from agent.yaml content string.
|
|
164
|
+
* Returns array of { name, repo, branch }.
|
|
165
|
+
*/
|
|
166
|
+
function parseYamlGitRepos(yamlContent) {
|
|
167
|
+
const repos = [];
|
|
168
|
+
const section = yamlContent.match(/^git_repos:\s*\n((?:[ \t]+.+\n?)*)/m);
|
|
169
|
+
if (!section) return repos;
|
|
170
|
+
const lines = section[1].split('\n');
|
|
171
|
+
let current = null;
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const nameMatch = line.match(/^\s+-\s+name:\s*["']?(.+?)["']?\s*$/);
|
|
174
|
+
if (nameMatch) {
|
|
175
|
+
if (current) repos.push(current);
|
|
176
|
+
current = { name: nameMatch[1].trim(), repo: '', branch: 'main' };
|
|
177
|
+
}
|
|
178
|
+
const repoMatch = line.match(/^\s+repo:\s*["']?(.+?)["']?\s*$/);
|
|
179
|
+
if (repoMatch && current) current.repo = repoMatch[1].trim();
|
|
180
|
+
const branchMatch = line.match(/^\s+branch:\s*["']?(.+?)["']?\s*$/);
|
|
181
|
+
if (branchMatch && current) current.branch = branchMatch[1].trim();
|
|
182
|
+
}
|
|
183
|
+
if (current) repos.push(current);
|
|
184
|
+
return repos;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Run a git command and return stdout (for reading git info).
|
|
189
|
+
*/
|
|
190
|
+
function runGitOutput(args, cwd) {
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
const opts = { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } };
|
|
193
|
+
if (cwd) opts.cwd = cwd;
|
|
194
|
+
const proc = spawn('git', args, opts);
|
|
195
|
+
let stdout = '';
|
|
196
|
+
let stderr = '';
|
|
197
|
+
proc.stdout.on('data', d => { stdout += d.toString(); });
|
|
198
|
+
proc.stderr.on('data', d => { stderr += d.toString(); });
|
|
199
|
+
proc.on('close', code => {
|
|
200
|
+
if (code === 0) resolve(stdout.trim());
|
|
201
|
+
else reject(new Error(stderr || `git ${args[0]} failed with code ${code}`));
|
|
202
|
+
});
|
|
203
|
+
proc.on('error', err => reject(err));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Scan immediate subdirectories of a project for git repositories.
|
|
209
|
+
* Returns array of { name, repo, branch }.
|
|
210
|
+
*/
|
|
211
|
+
async function scanGitRepos(projectPath) {
|
|
212
|
+
const results = [];
|
|
213
|
+
const SKIP_DIRS = new Set(['node_modules', '.claude', '.git', '.vscode', '__pycache__', '.next', 'dist', 'build']);
|
|
214
|
+
|
|
215
|
+
let entries;
|
|
216
|
+
try {
|
|
217
|
+
entries = await fs.readdir(projectPath, { withFileTypes: true });
|
|
218
|
+
} catch {
|
|
219
|
+
return results;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
224
|
+
if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue;
|
|
225
|
+
|
|
226
|
+
const subdirPath = path.join(projectPath, entry.name);
|
|
227
|
+
|
|
228
|
+
// Check if this is a git repo (has .git directory)
|
|
229
|
+
try {
|
|
230
|
+
await fs.access(path.join(subdirPath, '.git'));
|
|
231
|
+
} catch {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Get remote URL
|
|
236
|
+
let repoUrl;
|
|
237
|
+
try {
|
|
238
|
+
repoUrl = await runGitOutput(['remote', 'get-url', 'origin'], subdirPath);
|
|
239
|
+
} catch {
|
|
240
|
+
continue; // Skip repos without remote
|
|
241
|
+
}
|
|
242
|
+
if (!repoUrl) continue;
|
|
243
|
+
|
|
244
|
+
// Get current branch
|
|
245
|
+
let branch;
|
|
246
|
+
try {
|
|
247
|
+
branch = await runGitOutput(['branch', '--show-current'], subdirPath);
|
|
248
|
+
} catch {
|
|
249
|
+
try {
|
|
250
|
+
branch = await runGitOutput(['rev-parse', '--abbrev-ref', 'HEAD'], subdirPath);
|
|
251
|
+
} catch {
|
|
252
|
+
branch = 'main';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!branch) branch = 'main';
|
|
256
|
+
|
|
257
|
+
results.push({ name: entry.name, repo: repoUrl, branch });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return results;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get the shared git repo path for a given repo URL and branch.
|
|
265
|
+
* Path structure: data/git-repo/{owner}/{repo}/{branch}/
|
|
266
|
+
*/
|
|
267
|
+
function getSharedGitRepoPath(repoUrl, branch) {
|
|
268
|
+
const parsed = parseGitUrl(repoUrl);
|
|
269
|
+
if (!parsed) return null;
|
|
270
|
+
const publicPaths = getPublicPaths();
|
|
271
|
+
return path.join(publicPaths.gitRepoDir, parsed.owner, parsed.repo, branch);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Ensure a shared git repo exists at data/git-repo/{owner}/{repo}/{branch}/.
|
|
276
|
+
* - If target exists: fetch + checkout + pull (reset on conflict)
|
|
277
|
+
* - If target doesn't exist but same repo other branch exists: copy + fetch + checkout
|
|
278
|
+
* - If completely new: clone + checkout
|
|
279
|
+
* Returns the local path to the shared repo.
|
|
280
|
+
*/
|
|
281
|
+
async function ensureSharedGitRepo(repoUrl, branch) {
|
|
282
|
+
const parsed = parseGitUrl(repoUrl);
|
|
283
|
+
if (!parsed) throw new Error(`Cannot parse git URL: ${repoUrl}`);
|
|
284
|
+
|
|
285
|
+
const publicPaths = getPublicPaths();
|
|
286
|
+
const targetPath = path.join(publicPaths.gitRepoDir, parsed.owner, parsed.repo, branch);
|
|
287
|
+
const repoParentDir = path.join(publicPaths.gitRepoDir, parsed.owner, parsed.repo);
|
|
288
|
+
|
|
289
|
+
// Case 1: Target path already exists — update it
|
|
290
|
+
try {
|
|
291
|
+
await fs.access(targetPath);
|
|
292
|
+
console.log(`[GitRepo] Updating existing shared repo at ${targetPath}`);
|
|
293
|
+
try {
|
|
294
|
+
await runGit(['fetch', 'origin'], targetPath);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.warn(`[GitRepo] fetch failed: ${e.message}`);
|
|
297
|
+
}
|
|
298
|
+
await runGit(['checkout', branch], targetPath);
|
|
299
|
+
try {
|
|
300
|
+
await runGit(['pull', 'origin', branch], targetPath);
|
|
301
|
+
} catch {
|
|
302
|
+
// Pull failed (conflict) — reset and retry
|
|
303
|
+
console.warn(`[GitRepo] pull conflict, resetting...`);
|
|
304
|
+
await runGit(['checkout', '.'], targetPath);
|
|
305
|
+
await runGit(['clean', '-fd'], targetPath);
|
|
306
|
+
await runGit(['pull', 'origin', branch], targetPath);
|
|
307
|
+
}
|
|
308
|
+
return targetPath;
|
|
309
|
+
} catch {
|
|
310
|
+
// Target doesn't exist — continue to Case 2/3
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Case 2: Same repo, different branch exists — copy + fetch + checkout
|
|
314
|
+
try {
|
|
315
|
+
await fs.access(repoParentDir);
|
|
316
|
+
const branches = await fs.readdir(repoParentDir, { withFileTypes: true });
|
|
317
|
+
const existingBranch = branches.find(b => b.isDirectory());
|
|
318
|
+
if (existingBranch) {
|
|
319
|
+
const existingPath = path.join(repoParentDir, existingBranch.name);
|
|
320
|
+
console.log(`[GitRepo] Copying from existing branch "${existingBranch.name}" to "${branch}"`);
|
|
321
|
+
await fs.mkdir(targetPath, { recursive: true });
|
|
322
|
+
await fs.cp(existingPath, targetPath, { recursive: true });
|
|
323
|
+
try {
|
|
324
|
+
await runGit(['fetch', 'origin'], targetPath);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
console.warn(`[GitRepo] fetch after copy failed: ${e.message}`);
|
|
327
|
+
}
|
|
328
|
+
await runGit(['checkout', branch], targetPath);
|
|
329
|
+
try {
|
|
330
|
+
await runGit(['pull', 'origin', branch], targetPath);
|
|
331
|
+
} catch {
|
|
332
|
+
await runGit(['checkout', '.'], targetPath);
|
|
333
|
+
await runGit(['clean', '-fd'], targetPath);
|
|
334
|
+
await runGit(['pull', 'origin', branch], targetPath);
|
|
335
|
+
}
|
|
336
|
+
return targetPath;
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
// No existing branches — continue to Case 3
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Case 3: Fresh clone
|
|
343
|
+
console.log(`[GitRepo] Cloning ${repoUrl} branch ${branch} to ${targetPath}`);
|
|
344
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
345
|
+
await runGit(['clone', repoUrl, targetPath]);
|
|
346
|
+
await runGit(['checkout', branch], targetPath);
|
|
347
|
+
return targetPath;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Create a symlink from a project subdirectory to a shared git repo.
|
|
352
|
+
* Removes existing directory or symlink at the target location.
|
|
353
|
+
*/
|
|
354
|
+
async function linkGitRepoToProject(projectDir, subdirName, sharedRepoPath) {
|
|
355
|
+
const linkPath = path.join(projectDir, subdirName);
|
|
356
|
+
|
|
357
|
+
// Remove existing directory or symlink
|
|
358
|
+
try {
|
|
359
|
+
const stat = await fs.lstat(linkPath);
|
|
360
|
+
if (stat.isSymbolicLink()) {
|
|
361
|
+
await fs.unlink(linkPath);
|
|
362
|
+
} else if (stat.isDirectory()) {
|
|
363
|
+
await fs.rm(linkPath, { recursive: true, force: true });
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
// Doesn't exist — fine
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await fs.symlink(sharedRepoPath, linkPath);
|
|
370
|
+
console.log(`[GitRepo] Created symlink: ${linkPath} → ${sharedRepoPath}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
162
373
|
/**
|
|
163
374
|
* Ensure a skill repo is available for the user. Clone if needed.
|
|
164
375
|
* Returns the local path to the skill directory inside the repo.
|
|
@@ -586,6 +797,20 @@ router.post('/install', async (req, res) => {
|
|
|
586
797
|
}
|
|
587
798
|
}
|
|
588
799
|
|
|
800
|
+
// Install git repo dependencies (clone/update shared repo + create symlink)
|
|
801
|
+
const gitRepoResults = [];
|
|
802
|
+
for (const gitRepo of agent.git_repos || []) {
|
|
803
|
+
if (!gitRepo.name || !gitRepo.repo) continue;
|
|
804
|
+
try {
|
|
805
|
+
const sharedPath = await ensureSharedGitRepo(gitRepo.repo, gitRepo.branch || 'main');
|
|
806
|
+
await linkGitRepoToProject(projectDir, gitRepo.name, sharedPath);
|
|
807
|
+
gitRepoResults.push({ name: gitRepo.name, success: true });
|
|
808
|
+
} catch (err) {
|
|
809
|
+
console.error(`[AgentInstall] Failed to install git repo "${gitRepo.name}":`, err.message);
|
|
810
|
+
gitRepoResults.push({ name: gitRepo.name, success: false, error: err.message });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
589
814
|
// Mark project as agent in project-config.json
|
|
590
815
|
const config = await loadProjectConfig(userUuid);
|
|
591
816
|
const projectKey = projectDir.replace(/\//g, '-');
|
|
@@ -597,7 +822,10 @@ router.post('/install', async (req, res) => {
|
|
|
597
822
|
installedAt: new Date().toISOString(),
|
|
598
823
|
isAgent: true,
|
|
599
824
|
claudeMdHash,
|
|
600
|
-
installedFiles
|
|
825
|
+
installedFiles,
|
|
826
|
+
gitRepos: (agent.git_repos || []).filter(g => g.name && g.repo).map(g => ({
|
|
827
|
+
name: g.name, repo: g.repo, branch: g.branch || 'main'
|
|
828
|
+
}))
|
|
601
829
|
}
|
|
602
830
|
};
|
|
603
831
|
await saveProjectConfig(config, userUuid);
|
|
@@ -606,7 +834,8 @@ router.post('/install', async (req, res) => {
|
|
|
606
834
|
success: true,
|
|
607
835
|
project: { ...project, agentInfo: config[projectKey].agentInfo },
|
|
608
836
|
skills: skillResults,
|
|
609
|
-
mcps: mcpResults
|
|
837
|
+
mcps: mcpResults,
|
|
838
|
+
gitRepos: gitRepoResults
|
|
610
839
|
});
|
|
611
840
|
} catch (error) {
|
|
612
841
|
console.error('Error installing agent:', error);
|
|
@@ -685,7 +914,10 @@ router.get('/preview', async (req, res) => {
|
|
|
685
914
|
// Scan CLAUDE.md for referenced local files
|
|
686
915
|
const refFiles = await scanClaudeMdRefs(projectPath);
|
|
687
916
|
|
|
688
|
-
|
|
917
|
+
// Scan subdirectories for git repositories
|
|
918
|
+
const gitRepos = await scanGitRepos(projectPath);
|
|
919
|
+
|
|
920
|
+
res.json({ hasClaudeMd, skills, mcps, refFiles, gitRepos });
|
|
689
921
|
} catch (error) {
|
|
690
922
|
console.error('Error getting agent preview:', error);
|
|
691
923
|
res.status(500).json({ error: 'Failed to get preview', details: error.message });
|
|
@@ -737,6 +969,11 @@ router.get('/project-files', async (req, res) => {
|
|
|
737
969
|
|
|
738
970
|
if (entry.isDirectory()) {
|
|
739
971
|
if (AGENT_SKIP_DIRS.has(entry.name)) continue;
|
|
972
|
+
// Skip directories that are git repositories (handled as git_repos dependencies)
|
|
973
|
+
try {
|
|
974
|
+
await fs.access(path.join(fullPath, '.git'));
|
|
975
|
+
continue; // Has .git → skip entire subtree
|
|
976
|
+
} catch {}
|
|
740
977
|
await walk(fullPath, depth + 1);
|
|
741
978
|
} else if (entry.isFile()) {
|
|
742
979
|
if (entry.name === 'CLAUDE.md') continue;
|
|
@@ -1287,6 +1524,49 @@ router.post('/submissions/:id/approve', async (req, res) => {
|
|
|
1287
1524
|
}
|
|
1288
1525
|
}
|
|
1289
1526
|
|
|
1527
|
+
// === C. Handle git repo dependencies ===
|
|
1528
|
+
const yamlGitRepos = agentYamlContent ? parseYamlGitRepos(agentYamlContent) : [];
|
|
1529
|
+
if (yamlGitRepos.length > 0) {
|
|
1530
|
+
for (const gitRepo of yamlGitRepos) {
|
|
1531
|
+
if (!gitRepo.name || !gitRepo.repo) continue;
|
|
1532
|
+
try {
|
|
1533
|
+
// Ensure shared git repo exists
|
|
1534
|
+
const sharedPath = await ensureSharedGitRepo(gitRepo.repo, gitRepo.branch || 'main');
|
|
1535
|
+
console.log(`[AgentApprove] Ensured shared git repo "${gitRepo.name}" at ${sharedPath}`);
|
|
1536
|
+
|
|
1537
|
+
// Replace submitter's original subdirectory with symlink to shared repo
|
|
1538
|
+
if (submitterUuid) {
|
|
1539
|
+
const submitterProjectDir = path.join(getUserPaths(submitterUuid).projectsDir, submission.agent_name);
|
|
1540
|
+
try {
|
|
1541
|
+
await linkGitRepoToProject(submitterProjectDir, gitRepo.name, sharedPath);
|
|
1542
|
+
console.log(`[AgentApprove] Linked git repo "${gitRepo.name}" for submitter`);
|
|
1543
|
+
} catch (e) {
|
|
1544
|
+
console.warn(`[AgentApprove] Could not link git repo "${gitRepo.name}" for submitter:`, e.message);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
} catch (e) {
|
|
1548
|
+
console.error(`[AgentApprove] Failed to setup shared git repo "${gitRepo.name}":`, e.message);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Update submitter's agentInfo with git repos
|
|
1553
|
+
if (submitterUuid) {
|
|
1554
|
+
try {
|
|
1555
|
+
const submitterConfig = await loadProjectConfig(submitterUuid);
|
|
1556
|
+
const submitterProjectDir = path.join(getUserPaths(submitterUuid).projectsDir, submission.agent_name);
|
|
1557
|
+
const submitterKey = submitterProjectDir.replace(/\//g, '-');
|
|
1558
|
+
if (submitterConfig[submitterKey]?.agentInfo) {
|
|
1559
|
+
submitterConfig[submitterKey].agentInfo.gitRepos = yamlGitRepos.filter(g => g.name && g.repo).map(g => ({
|
|
1560
|
+
name: g.name, repo: g.repo, branch: g.branch || 'main'
|
|
1561
|
+
}));
|
|
1562
|
+
await saveProjectConfig(submitterConfig, submitterUuid);
|
|
1563
|
+
}
|
|
1564
|
+
} catch (e) {
|
|
1565
|
+
console.warn('[AgentApprove] Could not update submitter gitRepos config:', e.message);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1290
1570
|
// Remove temporary migration directories from extractDir before publishing to agent repo
|
|
1291
1571
|
await fs.rm(path.join(extractDir, '_skill_files'), { recursive: true, force: true });
|
|
1292
1572
|
await fs.rm(path.join(extractDir, '_mcp_configs'), { recursive: true, force: true });
|
|
@@ -1310,7 +1590,7 @@ router.post('/submissions/:id/approve', async (req, res) => {
|
|
|
1310
1590
|
if (updateAllUsers) {
|
|
1311
1591
|
// Force-update all users who have this agent installed
|
|
1312
1592
|
try {
|
|
1313
|
-
const freshAgents = await scanAgents(
|
|
1593
|
+
const freshAgents = await scanAgents();
|
|
1314
1594
|
const publishedAgent = freshAgents.find(a =>
|
|
1315
1595
|
a.dirName === submission.agent_name || a.name === submission.agent_name
|
|
1316
1596
|
);
|
|
@@ -1390,6 +1670,54 @@ router.post('/submissions/:id/approve', async (req, res) => {
|
|
|
1390
1670
|
entry.agentInfo.installedAt = new Date().toISOString();
|
|
1391
1671
|
entry.agentInfo.claudeMdHash = newClaudeMdHash;
|
|
1392
1672
|
entry.agentInfo.installedFiles = newInstalledFiles;
|
|
1673
|
+
|
|
1674
|
+
// Update git repo dependencies
|
|
1675
|
+
if (publishedAgent) {
|
|
1676
|
+
const newGitRepos = (publishedAgent.git_repos || []).filter(g => g.name && g.repo);
|
|
1677
|
+
const oldGitRepos = entry.agentInfo.gitRepos || [];
|
|
1678
|
+
|
|
1679
|
+
// Build lookup maps
|
|
1680
|
+
const oldByName = new Map(oldGitRepos.map(g => [g.name, g]));
|
|
1681
|
+
const newByName = new Map(newGitRepos.map(g => [g.name, g]));
|
|
1682
|
+
|
|
1683
|
+
// Remove symlinks for deleted git repos
|
|
1684
|
+
for (const old of oldGitRepos) {
|
|
1685
|
+
if (!newByName.has(old.name)) {
|
|
1686
|
+
try {
|
|
1687
|
+
const linkPath = path.join(projectDir, old.name);
|
|
1688
|
+
await fs.unlink(linkPath).catch(() => {});
|
|
1689
|
+
console.log(`[AgentApprove] Removed git repo symlink "${old.name}" for user ${user.uuid}`);
|
|
1690
|
+
} catch {}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// Add/update git repos
|
|
1695
|
+
for (const gitRepo of newGitRepos) {
|
|
1696
|
+
const old = oldByName.get(gitRepo.name);
|
|
1697
|
+
const branchChanged = !old || old.branch !== gitRepo.branch;
|
|
1698
|
+
const repoChanged = !old || old.repo !== gitRepo.repo;
|
|
1699
|
+
|
|
1700
|
+
if (!old || branchChanged || repoChanged) {
|
|
1701
|
+
try {
|
|
1702
|
+
const sharedPath = await ensureSharedGitRepo(gitRepo.repo, gitRepo.branch || 'main');
|
|
1703
|
+
await linkGitRepoToProject(projectDir, gitRepo.name, sharedPath);
|
|
1704
|
+
console.log(`[AgentApprove] Updated git repo "${gitRepo.name}" for user ${user.uuid}`);
|
|
1705
|
+
} catch (e) {
|
|
1706
|
+
console.warn(`[AgentApprove] Failed to update git repo "${gitRepo.name}" for user ${user.uuid}:`, e.message);
|
|
1707
|
+
}
|
|
1708
|
+
} else {
|
|
1709
|
+
// Same repo + branch — just ensure shared repo is up to date
|
|
1710
|
+
try {
|
|
1711
|
+
await ensureSharedGitRepo(gitRepo.repo, gitRepo.branch || 'main');
|
|
1712
|
+
} catch {}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
entry.agentInfo.gitRepos = newGitRepos.map(g => ({
|
|
1717
|
+
name: g.name, repo: g.repo, branch: g.branch || 'main'
|
|
1718
|
+
}));
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1393
1721
|
userUpdated = true;
|
|
1394
1722
|
}
|
|
1395
1723
|
|