@pimzino/claude-code-spec-workflow
Version:
Automated workflows for Claude Code. Includes spec-driven development (Requirements → Design → Tasks → Implementation) with intelligent orchestration, optional steering documents and streamlined bug fix workflow (Report → Analyze → Fix → Verify). We have
235 lines • 9.96 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProjectDiscovery = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const os_1 = require("os");
const child_process_1 = require("child_process");
const util_1 = require("util");
const simple_git_1 = require("simple-git");
const logger_1 = require("./logger");
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class ProjectDiscovery {
constructor() {
this.searchPaths = [];
// Common project directories
this.searchPaths = [
(0, path_1.join)((0, os_1.homedir)(), 'Projects'),
(0, path_1.join)((0, os_1.homedir)(), 'Documents'),
(0, path_1.join)((0, os_1.homedir)(), 'Development'),
(0, path_1.join)((0, os_1.homedir)(), 'Code'),
(0, path_1.join)((0, os_1.homedir)(), 'repos'),
(0, path_1.join)((0, os_1.homedir)(), 'workspace'),
(0, path_1.join)((0, os_1.homedir)(), 'src'),
];
}
async discoverProjects() {
const projects = [];
const activeClaudes = await this.getActiveClaudeSessions();
// Search for .claude directories
for (const searchPath of this.searchPaths) {
try {
await fs_1.promises.access(searchPath);
const found = await this.searchDirectory(searchPath, activeClaudes);
projects.push(...found);
}
catch {
// Directory doesn't exist, skip it
}
}
// Sort by last activity
projects.sort((a, b) => {
const dateA = a.lastActivity?.getTime() || 0;
const dateB = b.lastActivity?.getTime() || 0;
return dateB - dateA;
});
return projects;
}
async searchDirectory(dir, activeSessions, depth = 0) {
if (depth > 3)
return []; // Don't go too deep
const projects = [];
try {
const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory())
continue;
if (entry.name.startsWith('.') && entry.name !== '.claude')
continue;
if (entry.name === 'node_modules' || entry.name === 'venv' || entry.name === '__pycache__')
continue;
const fullPath = (0, path_1.join)(dir, entry.name);
// Check if this directory has a .claude folder
const claudePath = (0, path_1.join)(fullPath, '.claude');
try {
const claudeStat = await fs_1.promises.stat(claudePath);
if (claudeStat.isDirectory()) {
const project = await this.analyzeProject(fullPath, claudePath, activeSessions);
projects.push(project);
}
}
catch {
// No .claude directory, check subdirectories
if (depth < 3) {
const subProjects = await this.searchDirectory(fullPath, activeSessions, depth + 1);
projects.push(...subProjects);
}
}
}
}
catch (error) {
console.error(`Error searching directory ${dir}:`, error);
}
return projects;
}
async analyzeProject(projectPath, claudePath, activeSessions) {
(0, logger_1.debug)(`Analyzing project: ${projectPath}`);
const name = projectPath.split('/').pop() || 'Unknown';
// Check if any active Claude session is in this project directory
const hasActiveSession = activeSessions.some((session) => {
// Normalize paths for comparison
const normalizedSession = session.replace(/\/$/, '');
const normalizedProject = projectPath.replace(/\/$/, '');
const isMatch = normalizedSession === normalizedProject || normalizedSession.startsWith(normalizedProject + '/');
if (isMatch) {
(0, logger_1.debug)(`Found active session match: ${session} matches ${projectPath}`);
}
return isMatch;
});
if (!hasActiveSession && name === 'beejax') {
(0, logger_1.debug)(`No active session found for beejax. Active sessions: ${activeSessions.join(', ')}, Project path: ${projectPath}`);
}
// Get git info (do this outside the try block so it always runs)
const gitInfo = await this.getGitInfo(projectPath);
// Get last activity by checking file modification times
let lastActivity;
let specCount = 0;
let bugCount = 0;
try {
const specsPath = (0, path_1.join)(claudePath, 'specs');
const specDirs = await fs_1.promises.readdir(specsPath);
let mostRecent = 0;
for (const specDir of specDirs) {
if (specDir.startsWith('.'))
continue;
const specPath = (0, path_1.join)(specsPath, specDir);
const stat = await fs_1.promises.stat(specPath);
if (stat.mtime.getTime() > mostRecent) {
mostRecent = stat.mtime.getTime();
}
}
if (mostRecent > 0) {
lastActivity = new Date(mostRecent);
}
specCount = specDirs.filter((d) => !d.startsWith('.')).length;
}
catch {
// Error reading specs directory, but continue with git info
(0, logger_1.debug)(`Could not read specs for ${projectPath}, but continuing with git info`);
}
// Count bugs
try {
const bugsPath = (0, path_1.join)(claudePath, 'bugs');
const bugDirs = await fs_1.promises.readdir(bugsPath);
bugCount = bugDirs.filter((d) => !d.startsWith('.')).length;
// Check bug modification times for last activity
for (const bugDir of bugDirs) {
if (bugDir.startsWith('.'))
continue;
const bugPath = (0, path_1.join)(bugsPath, bugDir);
try {
const stat = await fs_1.promises.stat(bugPath);
if (!lastActivity || stat.mtime > lastActivity) {
lastActivity = stat.mtime;
}
}
catch {
// Error reading bug directory
}
}
}
catch {
// No bugs directory or error reading it
(0, logger_1.debug)(`Could not read bugs for ${projectPath}`);
}
const result = {
path: projectPath,
name,
hasActiveSession,
lastActivity,
specCount,
bugCount,
...gitInfo,
};
(0, logger_1.debug)(`Returning project ${name} with result:`, {
hasGitInfo: !!(result.gitBranch || result.gitCommit),
gitBranch: result.gitBranch,
gitCommit: result.gitCommit
});
return result;
}
async getActiveClaudeSessions() {
try {
// Get Claude processes with their working directories
const { stdout } = await execAsync('ps aux | grep "claude" | grep -v grep | grep -v claude-code-spec');
const lines = stdout
.trim()
.split('\n')
.filter((line) => line.length > 0);
// Try to get working directories for each Claude process
const sessions = [];
for (const line of lines) {
const parts = line.split(/\s+/);
const pid = parts[1];
try {
// On macOS, we can try to get the current working directory
const { stdout: cwd } = await execAsync(`lsof -p ${pid} | grep cwd | awk '{print $NF}'`);
if (cwd.trim()) {
sessions.push(cwd.trim());
}
}
catch {
// Can't get CWD, that's okay
}
}
(0, logger_1.debug)(`Found ${sessions.length} active Claude sessions:`, sessions);
return sessions;
}
catch {
return [];
}
}
async getGitInfo(projectPath) {
(0, logger_1.debug)(`Getting git info for: ${projectPath}`);
try {
const git = (0, simple_git_1.simpleGit)(projectPath);
// Check if it's a git repository
const isRepo = await git.checkIsRepo();
if (!isRepo) {
(0, logger_1.debug)(`${projectPath} is not a git repository`);
return {};
}
// Get current branch
const branchSummary = await git.branchLocal();
const branch = branchSummary.current;
// Get current commit hash (short version)
const log = await git.log({ maxCount: 1 });
const commit = log.latest?.hash.substring(0, 7);
const gitInfo = {
gitBranch: branch || undefined,
gitCommit: commit || undefined,
};
if (gitInfo.gitBranch || gitInfo.gitCommit) {
(0, logger_1.debug)(`Git info for ${projectPath}: branch=${gitInfo.gitBranch}, commit=${gitInfo.gitCommit}`);
}
return gitInfo;
}
catch (error) {
// Not a git repo or git command failed
(0, logger_1.debug)(`Failed to get git info for ${projectPath}:`, error instanceof Error ? error.message : String(error));
return {};
}
}
}
exports.ProjectDiscovery = ProjectDiscovery;
//# sourceMappingURL=project-discovery.js.map