/** * Daemon project helpers -- Worktree tracking, project filtering, config handlers. * * Extracted from daemon.js to keep module sizes manageable. */ var fs = require("fs"); var { scanWorktreesResult, isWorktree } = require("./worktree"); var { daemonSync } = require("./daemon-sync"); var logContext = require("./project-log-context"); var daemonProjectAccess = require("./daemon-project-access"); var worktreePreferences = require("./worktree-preferences"); var saveConfig = require("./config").saveConfig; // --- Worktree tracking state --- var worktreeRegistry = {}; // parentSlug -> [wtSlug, ...] var worktreeScanning = {}; // parentSlug -> boolean (mutex) function getWorktreeSyncKey(parentSlug) { return "worktrees:" + parentSlug; } function isWorktreeSlug(slug) { return slug.indexOf("--") !== -1; } /** * Scan a parent project for worktrees and register them with the relay server. * @param {object} relay - the relay server instance (has addProject, removeProject, etc.) * @param {string} parentPath - absolute path to parent project * @param {string} parentSlug - slug of parent project * @param {string} parentIcon - icon of parent project * @param {string} parentOwnerId - owner user ID */ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, projectKnowledgeId, osUserInfo, config, getConfig, getOsUserInfo) { var savedParent = config && (config.projects || []).find(function (item) { return item.slug === parentSlug; }); if (savedParent && (savedParent.lifecycleState || worktreePreferences.pendingRemoval(config, parentSlug))) return; if (isWorktree(parentPath, osUserInfo)) return; var scan = scanWorktreesResult(parentPath, osUserInfo); if (!scan.ok) return; var worktrees = scan.worktrees; var currentSlugs = worktrees.map(function (worktree) { return parentSlug + "--" + worktree.dirName; }); if (config && daemonProjectAccess.reconcileWorktreeGrants(config, parentSlug, currentSlugs)) saveConfig(config); if (worktrees.length === 0) return; if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = []; for (var i = 0; i < worktrees.length; i++) { var wt = worktrees[i]; var wtSlug = parentSlug + "--" + wt.dirName; var alreadyRegistered = false; for (var j = 0; j < worktreeRegistry[parentSlug].length; j++) { if (worktreeRegistry[parentSlug][j] === wtSlug) { alreadyRegistered = true; break; } } if (alreadyRegistered) continue; var context = logContext.resolveWorktreeContext(wt.path, projectKnowledgeId, { branch: wt.branch || wt.dirName }, osUserInfo); var wtMeta = Object.assign({ parentSlug: parentSlug, external: wt.external, projectKnowledgeId: projectKnowledgeId }, context); if (worktreePreferences.blocked(config || {}, parentSlug, wtSlug, wt.path, context.changeSetId)) continue; relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, worktreePreferences.icon(config || {}, parentSlug, wtSlug, wt.path, context.changeSetId), parentOwnerId, wtMeta); worktreeRegistry[parentSlug].push(wtSlug); console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.external ? "(external)" : "(inside project folder)"); } daemonSync.register(getWorktreeSyncKey(parentSlug), function () { var currentConfig = getConfig ? getConfig() : config; var current = currentConfig && (currentConfig.projects || []).find(function (item) { return item.slug === parentSlug; }); if (currentConfig && !current) return; rescanWorktrees(relay, current ? current.path : parentPath, parentSlug, current ? current.icon : parentIcon, current ? current.ownerId : parentOwnerId, currentConfig, current ? current.projectKnowledgeId : projectKnowledgeId, current && getOsUserInfo ? getOsUserInfo(current) : osUserInfo); }); } /** * Rescan worktrees for a parent project, adding new and removing stale ones. * @param {object} relay - the relay server instance * @param {string} parentPath - absolute path to parent project * @param {string} parentSlug - slug of parent project * @param {string} parentIcon - icon of parent project * @param {string} parentOwnerId - owner user ID * @param {object} [config] - daemon config (optional, for broadcasting project count) */ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, config, projectKnowledgeId, osUserInfo) { var liveParent = config && (config.projects || []).find(function (item) { return item.slug === parentSlug; }); if (liveParent && (liveParent.lifecycleState || worktreePreferences.pendingRemoval(config, parentSlug))) return; if (worktreeScanning[parentSlug]) return; worktreeScanning[parentSlug] = true; try { var scan = scanWorktreesResult(parentPath, osUserInfo); if (!scan.ok) return; var discovered = scan.worktrees; var changed = false; var discoveredSlugs = discovered.map(function (worktree) { return parentSlug + "--" + worktree.dirName; }); if (config && daemonProjectAccess.reconcileWorktreeGrants(config, parentSlug, discoveredSlugs)) { saveConfig(config); changed = true; } var existingSlugs = worktreeRegistry[parentSlug] || []; var discoveredNames = {}; for (var i = 0; i < discovered.length; i++) { discoveredNames[discovered[i].dirName] = discovered[i]; } for (var di = 0; di < discovered.length; di++) { var wt = discovered[di]; var wtSlug = parentSlug + "--" + wt.dirName; var found = false; for (var ei = 0; ei < existingSlugs.length; ei++) { if (existingSlugs[ei] === wtSlug) { found = true; break; } } if (!found) { var context = logContext.resolveWorktreeContext(wt.path, projectKnowledgeId, { branch: wt.branch || wt.dirName }, osUserInfo); var wtMeta = Object.assign({ parentSlug: parentSlug, external: wt.external, projectKnowledgeId: projectKnowledgeId }, context); if (worktreePreferences.blocked(config || {}, parentSlug, wtSlug, wt.path, context.changeSetId)) continue; relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, worktreePreferences.icon(config || {}, parentSlug, wtSlug, wt.path, context.changeSetId), parentOwnerId, wtMeta); if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = []; worktreeRegistry[parentSlug].push(wtSlug); console.log("[daemon] Rescan: added worktree:", wtSlug); changed = true; } } for (var si = existingSlugs.length - 1; si >= 0; si--) { var sSlug = existingSlugs[si]; var dirName = sSlug.split("--").slice(1).join("--"); if (!discoveredNames[dirName]) { if (relay.setProjectLogContextState) relay.setProjectLogContextState(sSlug, "archived"); relay.removeProject(sSlug); if (config && daemonProjectAccess.clearWorktreeGrant(config, sSlug)) saveConfig(config); existingSlugs.splice(si, 1); console.log("[daemon] Rescan: removed stale worktree:", sSlug); changed = true; } } if (changed) { relay.broadcastAll({ type: "projects_updated", projects: relay.getProjects(), projectCount: config ? config.projects.length : 0, }); } } finally { worktreeScanning[parentSlug] = false; } } function cleanupWorktreesForParent(relay, parentSlug) { var wtSlugs = worktreeRegistry[parentSlug] || []; for (var i = 0; i < wtSlugs.length; i++) { if (relay.setProjectLogContextState) relay.setProjectLogContextState(wtSlugs[i], "archived"); relay.removeProject(wtSlugs[i]); console.log("[daemon] Cascade removed worktree:", wtSlugs[i]); } delete worktreeRegistry[parentSlug]; daemonSync.unregister(getWorktreeSyncKey(parentSlug)); } /** * Filter removed projects by userId and existence. * @param {object} config - daemon config with removedProjects array * @param {string|null} userId - user ID to filter by (null for single-user mode) */ function getFilteredRemovedProjects(config, userId) { if (!config.removedProjects || config.removedProjects.length === 0) return []; return config.removedProjects.filter(function (rp) { if (userId && rp.userId !== userId) return false; if (!userId && rp.userId) return false; return fs.existsSync(rp.path); }); } /** * Register a worktree slug under a parent slug. * Used by daemon.js when creating worktrees directly. */ function registerWorktreeSlug(parentSlug, wtSlug) { if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = []; worktreeRegistry[parentSlug].push(wtSlug); } /** * Unregister a worktree slug from its parent. * Used by daemon.js when removing worktree projects directly. */ function unregisterWorktreeSlug(parentSlug, wtSlug) { if (worktreeRegistry[parentSlug]) { worktreeRegistry[parentSlug] = worktreeRegistry[parentSlug].filter(function (s) { return s !== wtSlug; }); } } module.exports = { isWorktreeSlug: isWorktreeSlug, scanAndRegisterWorktrees: scanAndRegisterWorktrees, rescanWorktrees: rescanWorktrees, cleanupWorktreesForParent: cleanupWorktreesForParent, getFilteredRemovedProjects: getFilteredRemovedProjects, registerWorktreeSlug: registerWorktreeSlug, unregisterWorktreeSlug: unregisterWorktreeSlug, };