mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-03 23:53:29 +08:00
176 lines
4.8 KiB
JavaScript
176 lines
4.8 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const { spawnSync } = require('child_process');
|
|
const { getClaudeDir, ensureDir, sanitizeSessionId } = require('./utils');
|
|
|
|
function getHomunculusDir() {
|
|
return path.join(getClaudeDir(), 'homunculus');
|
|
}
|
|
|
|
function getProjectsDir() {
|
|
return path.join(getHomunculusDir(), 'projects');
|
|
}
|
|
|
|
function getProjectRegistryPath() {
|
|
return path.join(getHomunculusDir(), 'projects.json');
|
|
}
|
|
|
|
function readProjectRegistry() {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(getProjectRegistryPath(), 'utf8'));
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function runGit(args, cwd) {
|
|
const result = spawnSync('git', args, {
|
|
cwd,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
});
|
|
if (result.status !== 0) return '';
|
|
return (result.stdout || '').trim();
|
|
}
|
|
|
|
function stripRemoteCredentials(remoteUrl) {
|
|
if (!remoteUrl) return '';
|
|
return String(remoteUrl).replace(/:\/\/[^@]+@/, '://');
|
|
}
|
|
|
|
function resolveProjectRoot(cwd = process.cwd()) {
|
|
const envRoot = process.env.CLAUDE_PROJECT_DIR;
|
|
if (envRoot && fs.existsSync(envRoot)) {
|
|
return path.resolve(envRoot);
|
|
}
|
|
|
|
const gitRoot = runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
if (gitRoot) return path.resolve(gitRoot);
|
|
|
|
return '';
|
|
}
|
|
|
|
function computeProjectId(projectRoot) {
|
|
const remoteUrl = stripRemoteCredentials(runGit(['remote', 'get-url', 'origin'], projectRoot));
|
|
return crypto.createHash('sha256').update(remoteUrl || projectRoot).digest('hex').slice(0, 12);
|
|
}
|
|
|
|
function resolveProjectContext(cwd = process.cwd()) {
|
|
const projectRoot = resolveProjectRoot(cwd);
|
|
if (!projectRoot) {
|
|
const projectDir = getHomunculusDir();
|
|
ensureDir(projectDir);
|
|
return { projectId: 'global', projectRoot: '', projectDir, isGlobal: true };
|
|
}
|
|
|
|
const registry = readProjectRegistry();
|
|
const registryEntry = Object.values(registry).find(entry => entry && path.resolve(entry.root || '') === projectRoot);
|
|
const projectId = registryEntry?.id || computeProjectId(projectRoot);
|
|
const projectDir = path.join(getProjectsDir(), projectId);
|
|
ensureDir(projectDir);
|
|
|
|
return { projectId, projectRoot, projectDir, isGlobal: false };
|
|
}
|
|
|
|
function getObserverPidFile(context) {
|
|
return path.join(context.projectDir, '.observer.pid');
|
|
}
|
|
|
|
function getObserverSignalCounterFile(context) {
|
|
return path.join(context.projectDir, '.observer-signal-counter');
|
|
}
|
|
|
|
function getObserverActivityFile(context) {
|
|
return path.join(context.projectDir, '.observer-last-activity');
|
|
}
|
|
|
|
function getSessionLeaseDir(context) {
|
|
return path.join(context.projectDir, '.observer-sessions');
|
|
}
|
|
|
|
function resolveSessionId(rawSessionId = process.env.CLAUDE_SESSION_ID) {
|
|
return sanitizeSessionId(rawSessionId || '') || '';
|
|
}
|
|
|
|
function getSessionLeaseFile(context, rawSessionId = process.env.CLAUDE_SESSION_ID) {
|
|
const sessionId = resolveSessionId(rawSessionId);
|
|
if (!sessionId) return '';
|
|
return path.join(getSessionLeaseDir(context), `${sessionId}.json`);
|
|
}
|
|
|
|
function writeSessionLease(context, rawSessionId = process.env.CLAUDE_SESSION_ID, extra = {}) {
|
|
const leaseFile = getSessionLeaseFile(context, rawSessionId);
|
|
if (!leaseFile) return '';
|
|
|
|
ensureDir(getSessionLeaseDir(context));
|
|
const payload = {
|
|
sessionId: resolveSessionId(rawSessionId),
|
|
cwd: process.cwd(),
|
|
pid: process.pid,
|
|
updatedAt: new Date().toISOString(),
|
|
...extra
|
|
};
|
|
fs.writeFileSync(leaseFile, JSON.stringify(payload, null, 2) + '\n');
|
|
return leaseFile;
|
|
}
|
|
|
|
function removeSessionLease(context, rawSessionId = process.env.CLAUDE_SESSION_ID) {
|
|
const leaseFile = getSessionLeaseFile(context, rawSessionId);
|
|
if (!leaseFile) return false;
|
|
try {
|
|
fs.rmSync(leaseFile, { force: true });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function listSessionLeases(context) {
|
|
const leaseDir = getSessionLeaseDir(context);
|
|
if (!fs.existsSync(leaseDir)) return [];
|
|
return fs.readdirSync(leaseDir)
|
|
.filter(name => name.endsWith('.json'))
|
|
.map(name => path.join(leaseDir, name));
|
|
}
|
|
|
|
function stopObserverForContext(context) {
|
|
const pidFile = getObserverPidFile(context);
|
|
if (!fs.existsSync(pidFile)) return false;
|
|
|
|
const pid = (fs.readFileSync(pidFile, 'utf8') || '').trim();
|
|
if (!/^[0-9]+$/.test(pid) || pid === '0' || pid === '1') {
|
|
fs.rmSync(pidFile, { force: true });
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
process.kill(Number(pid), 0);
|
|
} catch {
|
|
fs.rmSync(pidFile, { force: true });
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
process.kill(Number(pid), 'SIGTERM');
|
|
} catch {
|
|
return false;
|
|
}
|
|
|
|
fs.rmSync(pidFile, { force: true });
|
|
fs.rmSync(getObserverSignalCounterFile(context), { force: true });
|
|
return true;
|
|
}
|
|
|
|
module.exports = {
|
|
resolveProjectContext,
|
|
getObserverActivityFile,
|
|
getObserverPidFile,
|
|
getSessionLeaseDir,
|
|
writeSessionLease,
|
|
removeSessionLease,
|
|
listSessionLeases,
|
|
stopObserverForContext,
|
|
resolveSessionId
|
|
};
|