Ctrl K
ComunidadGuías…

lib/project-log-context.js

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

Archivo original ↗Preguntar a KiBot
// Stable project knowledge and Git worktree change-set identities.
//
// Existing projects freeze their current path-derived scope so every opaque
// log ref remains valid. New projects receive a path-independent id. A linked
// worktree stores its generated change-set marker in Git's administrative
// directory, which survives daemon restarts and branch renames but disappears
// with the worktree itself; the durable log records retain the id afterward.

var crypto = require("crypto");
var fs = require("fs");
var path = require("path");
var { runGitSync } = require("./git-cli");
var { fsAsUser } = require("./os-users");
var logsRoot = require("./project-logs-root");
var utils = require("./utils");

var ID_PATTERN = /^[A-Za-z0-9_-]{1,120}$/;

function newId(prefix) {
  return prefix + "_" + crypto.randomBytes(16).toString("base64url");
}

function validId(value, prefix) {
  return typeof value === "string" && ID_PATTERN.test(value) && (!prefix || value.indexOf(prefix + "_") === 0);
}

function legacyKnowledgeId(cwd) {
  return utils.encodeCwd(logsRoot.resolveProjectRoot(cwd));
}

function ensureProjectKnowledgeId(project) {
  if (!project || typeof project !== "object") return false;
  if (validId(project.projectKnowledgeId)) return false;
  project.projectKnowledgeId = project.path ? legacyKnowledgeId(project.path) : newId("pk");
  return true;
}

function initializeProjectKnowledge(config) {
  var changed = false;
  var projects = config && Array.isArray(config.projects) ? config.projects : [];
  for (var i = 0; i < projects.length; i++) {
    if (ensureProjectKnowledgeId(projects[i])) changed = true;
  }
  return changed;
}

function createProjectKnowledgeId() {
  return newId("pk");
}

function git(cwd, args, osUserInfo) {
  return runGitSync(cwd, args, null, osUserInfo).trim();
}

function commit(cwd, ref, osUserInfo) {
  try { return git(cwd, ["rev-parse", ref || "HEAD"], osUserInfo); }
  catch (e) { return null; }
}

function markerPath(cwd, osUserInfo) {
  try {
    var gitDir = git(cwd, ["rev-parse", "--git-dir"], osUserInfo);
    return path.join(path.resolve(cwd, gitDir), "clay-change-set.json");
  } catch (e) {
    return null;
  }
}

function readMarker(filePath, knowledgeId) {
  if (!filePath) return null;
  try {
    var parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
    if (!parsed || parsed.projectKnowledgeId !== knowledgeId || !validId(parsed.changeSetId, "cs")) return null;
    return parsed;
  } catch (e) {
    return null;
  }
}

function deterministicFallback(knowledgeId, cwd) {
  var digest = crypto.createHash("sha256").update(knowledgeId + "\u0000" + logsRoot.canonicalPath(cwd)).digest("base64url");
  return "cs_" + digest.substring(0, 22);
}

function writeMarker(filePath, marker, osUserInfo) {
  var content = JSON.stringify(marker, null, 2) + "\n";
  if (osUserInfo) {
    fsAsUser("write", { file: filePath, content: content, flag: "wx", mode: 0o600 }, osUserInfo);
    return;
  }
  fs.writeFileSync(filePath, content, { flag: "wx", mode: 0o600 });
}

function resolveWorktreeContext(cwd, knowledgeId, details, osUserInfo) {
  var input = details || {};
  var filePath = markerPath(cwd, osUserInfo);
  var marker = readMarker(filePath, knowledgeId);
  if (!marker) {
    marker = {
      projectKnowledgeId: knowledgeId,
      changeSetId: newId("cs"),
      baseCommit: input.baseCommit || null,
      createdAt: Date.now(),
    };
    if (filePath) {
      try {
        writeMarker(filePath, marker, osUserInfo);
      } catch (e) {
        marker = readMarker(filePath, knowledgeId) || marker;
      }
    }
  }
  if (!filePath || !readMarker(filePath, knowledgeId)) {
    marker.changeSetId = deterministicFallback(knowledgeId, cwd);
  }
  return {
    kind: "worktree",
    changeSetId: marker.changeSetId,
    branch: input.branch || null,
    baseCommit: marker.baseCommit || input.baseCommit || null,
    headCommit: commit(cwd, "HEAD", osUserInfo),
    status: "active",
  };
}

function isMerged(parentCwd, worktreeCwd, osUserInfo) {
  var head = commit(worktreeCwd, "HEAD", osUserInfo);
  if (!head) return false;
  try {
    runGitSync(parentCwd, ["merge-base", "--is-ancestor", head, "HEAD"], null, osUserInfo);
    return true;
  } catch (e) {
    return false;
  }
}

function cleanCommit(value) {
  return typeof value === "string" && /^[0-9a-f]{7,64}$/i.test(value) ? value : null;
}

function cleanBranch(value) {
  if (typeof value !== "string") return null;
  var cleaned = value.trim().substring(0, 200);
  return cleaned && cleaned.indexOf("\n") === -1 && cleaned.indexOf("\r") === -1 ? cleaned : null;
}

function normalizeRecordContext(value) {
  if (!value || value.kind !== "worktree" || !validId(value.changeSetId, "cs")) {
    return { kind: "project", changeSetId: null, branch: null, baseCommit: null, headCommit: null, status: "active" };
  }
  return {
    kind: "worktree",
    changeSetId: value.changeSetId,
    branch: cleanBranch(value.branch),
    baseCommit: cleanCommit(value.baseCommit),
    headCommit: cleanCommit(value.headCommit),
    status: value.status === "merged" || value.status === "archived" ? value.status : "active",
  };
}

module.exports = {
  ID_PATTERN: ID_PATTERN,
  validId: validId,
  legacyKnowledgeId: legacyKnowledgeId,
  ensureProjectKnowledgeId: ensureProjectKnowledgeId,
  initializeProjectKnowledge: initializeProjectKnowledge,
  createProjectKnowledgeId: createProjectKnowledgeId,
  resolveWorktreeContext: resolveWorktreeContext,
  commit: commit,
  isMerged: isMerged,
  normalizeRecordContext: normalizeRecordContext,
};