Ctrl K
ComunidadGuías…

lib/daemon-projects.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * 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,
};