Refactor code structure for improved readability and maintainability

This commit is contained in:
catlog22
2025-12-04 17:22:25 +08:00
parent efaa8b6620
commit 39df995e37
17 changed files with 7887 additions and 55 deletions

View File

@@ -6,6 +6,9 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Bundled template paths
const UNIFIED_TEMPLATE = join(__dirname, '../templates/dashboard.html');
const CSS_FILE = join(__dirname, '../templates/dashboard.css');
const JS_FILE = join(__dirname, '../templates/dashboard.js');
const WORKFLOW_TEMPLATE = join(__dirname, '../templates/workflow-dashboard.html');
const REVIEW_TEMPLATE = join(__dirname, '../templates/review-cycle-dashboard.html');
@@ -16,15 +19,68 @@ const REVIEW_TEMPLATE = join(__dirname, '../templates/review-cycle-dashboard.htm
* @returns {Promise<string>} - Generated HTML
*/
export async function generateDashboard(data) {
// Use bundled workflow template
// Use new unified template (with sidebar layout)
if (existsSync(UNIFIED_TEMPLATE)) {
return generateFromUnifiedTemplate(data);
}
// Fallback to legacy workflow template
if (existsSync(WORKFLOW_TEMPLATE)) {
return generateFromBundledTemplate(data, WORKFLOW_TEMPLATE);
}
// Fallback to inline dashboard if template missing
// Fallback to inline dashboard if templates missing
return generateInlineDashboard(data);
}
/**
* Generate dashboard using unified template (new sidebar layout)
* @param {Object} data - Dashboard data
* @returns {string} - Generated HTML
*/
function generateFromUnifiedTemplate(data) {
let html = readFileSync(UNIFIED_TEMPLATE, 'utf8');
// Read CSS and JS files
const cssContent = existsSync(CSS_FILE) ? readFileSync(CSS_FILE, 'utf8') : '';
let jsContent = existsSync(JS_FILE) ? readFileSync(JS_FILE, 'utf8') : '';
// Prepare complete workflow data
const workflowData = {
generatedAt: data.generatedAt || new Date().toISOString(),
activeSessions: data.activeSessions || [],
archivedSessions: data.archivedSessions || [],
liteTasks: data.liteTasks || { litePlan: [], liteFix: [] },
reviewData: data.reviewData || { dimensions: {} },
statistics: data.statistics || {
totalSessions: 0,
activeSessions: 0,
totalTasks: 0,
completedTasks: 0,
litePlanCount: 0,
liteFixCount: 0
}
};
// Get project path and recent paths
const projectPath = data.projectPath || process.cwd();
const recentPaths = data.recentPaths || [projectPath];
// Replace JS placeholders with actual data
jsContent = jsContent.replace('{{WORKFLOW_DATA}}', JSON.stringify(workflowData, null, 2));
jsContent = jsContent.replace(/\{\{PROJECT_PATH\}\}/g, projectPath.replace(/\\/g, '/'));
jsContent = jsContent.replace('{{RECENT_PATHS}}', JSON.stringify(recentPaths));
// Inject CSS and JS into HTML template
html = html.replace('{{CSS_CONTENT}}', cssContent);
html = html.replace('{{JS_CONTENT}}', jsContent);
// Also replace any remaining placeholders in HTML
html = html.replace(/\{\{PROJECT_PATH\}\}/g, projectPath.replace(/\\/g, '/'));
return html;
}
/**
* Generate dashboard using bundled template
* @param {Object} data - Dashboard data

View File

@@ -1,6 +1,7 @@
import { glob } from 'glob';
import { readFileSync, existsSync } from 'fs';
import { join, basename } from 'path';
import { scanLiteTasks } from './lite-scanner.js';
/**
* Aggregate all data for dashboard rendering
@@ -13,13 +14,19 @@ export async function aggregateData(sessions, workflowDir) {
generatedAt: new Date().toISOString(),
activeSessions: [],
archivedSessions: [],
liteTasks: {
litePlan: [],
liteFix: []
},
reviewData: null,
statistics: {
totalSessions: 0,
activeSessions: 0,
totalTasks: 0,
completedTasks: 0,
reviewFindings: 0
reviewFindings: 0,
litePlanCount: 0,
liteFixCount: 0
}
};
@@ -48,6 +55,16 @@ export async function aggregateData(sessions, workflowDir) {
data.statistics.totalSessions = sessions.active.length + sessions.archived.length;
data.statistics.activeSessions = sessions.active.length;
// Scan and include lite tasks
try {
const liteTasks = await scanLiteTasks(workflowDir);
data.liteTasks = liteTasks;
data.statistics.litePlanCount = liteTasks.litePlan.length;
data.statistics.liteFixCount = liteTasks.liteFix.length;
} catch (err) {
console.error('Error scanning lite tasks:', err.message);
}
return data;
}
@@ -62,8 +79,10 @@ async function processSession(session, isActive) {
session_id: session.session_id,
project: session.project || session.session_id,
status: session.status || (isActive ? 'active' : 'archived'),
created_at: formatDate(session.created_at),
archived_at: formatDate(session.archived_at),
type: session.type || 'workflow', // Session type (workflow, review, test, docs)
workflow_type: session.workflow_type || null, // Original workflow_type for reference
created_at: session.created_at || null, // Raw ISO string - let frontend format
archived_at: session.archived_at || null, // Raw ISO string - let frontend format
path: session.path,
tasks: [],
taskCount: 0,
@@ -249,26 +268,8 @@ async function safeGlob(pattern, cwd) {
}
}
/**
* Format date for display
* @param {string|null} dateStr - ISO date string
* @returns {string}
*/
function formatDate(dateStr) {
if (!dateStr) return 'N/A';
try {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return dateStr;
}
}
// formatDate removed - dates are now passed as raw ISO strings
// Frontend (dashboard.js) handles all date formatting
/**
* Sort task IDs numerically (IMPL-1, IMPL-2, IMPL-1.1, etc.)

View File

@@ -0,0 +1,290 @@
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
import { join } from 'path';
/**
* Scan lite-plan and lite-fix directories for task sessions
* @param {string} workflowDir - Path to .workflow directory
* @returns {Promise<Object>} - Lite tasks data
*/
export async function scanLiteTasks(workflowDir) {
const litePlanDir = join(workflowDir, '.lite-plan');
const liteFixDir = join(workflowDir, '.lite-fix');
return {
litePlan: scanLiteDir(litePlanDir, 'lite-plan'),
liteFix: scanLiteDir(liteFixDir, 'lite-fix')
};
}
/**
* Scan a lite task directory
* @param {string} dir - Directory path
* @param {string} type - Task type ('lite-plan' or 'lite-fix')
* @returns {Array} - Array of lite task sessions
*/
function scanLiteDir(dir, type) {
if (!existsSync(dir)) return [];
try {
const sessions = readdirSync(dir, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => {
const sessionPath = join(dir, d.name);
const session = {
id: d.name,
type,
path: sessionPath,
createdAt: getCreatedTime(sessionPath),
plan: loadPlanJson(sessionPath),
tasks: loadTaskJsons(sessionPath)
};
// Calculate progress
session.progress = calculateProgress(session.tasks);
return session;
})
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
return sessions;
} catch (err) {
console.error(`Error scanning ${dir}:`, err.message);
return [];
}
}
/**
* Load plan.json from session directory
* @param {string} sessionPath - Session directory path
* @returns {Object|null} - Plan data or null
*/
function loadPlanJson(sessionPath) {
const planPath = join(sessionPath, 'plan.json');
if (!existsSync(planPath)) return null;
try {
const content = readFileSync(planPath, 'utf8');
return JSON.parse(content);
} catch {
return null;
}
}
/**
* Load all task JSON files from session directory
* Supports multiple task formats:
* 1. .task/IMPL-*.json files
* 2. tasks array in plan.json
* 3. task-*.json files in session root
* @param {string} sessionPath - Session directory path
* @returns {Array} - Array of task objects
*/
function loadTaskJsons(sessionPath) {
let tasks = [];
// Method 1: Check .task/IMPL-*.json files
const taskDir = join(sessionPath, '.task');
if (existsSync(taskDir)) {
try {
const implTasks = readdirSync(taskDir)
.filter(f => f.endsWith('.json') && (
f.startsWith('IMPL-') ||
f.startsWith('TASK-') ||
f.startsWith('task-') ||
/^T\d+\.json$/i.test(f)
))
.map(f => {
const taskPath = join(taskDir, f);
try {
const content = readFileSync(taskPath, 'utf8');
return normalizeTask(JSON.parse(content));
} catch {
return null;
}
})
.filter(Boolean);
tasks = tasks.concat(implTasks);
} catch {
// Continue to other methods
}
}
// Method 2: Check plan.json for embedded tasks array
if (tasks.length === 0) {
const planPath = join(sessionPath, 'plan.json');
if (existsSync(planPath)) {
try {
const plan = JSON.parse(readFileSync(planPath, 'utf8'));
if (Array.isArray(plan.tasks)) {
tasks = plan.tasks.map(t => normalizeTask(t));
}
} catch {
// Continue to other methods
}
}
}
// Method 3: Check for task-*.json files in session root
if (tasks.length === 0) {
try {
const rootTasks = readdirSync(sessionPath)
.filter(f => f.endsWith('.json') && (
f.startsWith('task-') ||
f.startsWith('TASK-') ||
/^T\d+\.json$/i.test(f)
))
.map(f => {
const taskPath = join(sessionPath, f);
try {
const content = readFileSync(taskPath, 'utf8');
return normalizeTask(JSON.parse(content));
} catch {
return null;
}
})
.filter(Boolean);
tasks = tasks.concat(rootTasks);
} catch {
// No tasks found
}
}
// Sort tasks by ID
return tasks.sort((a, b) => {
const aNum = parseInt(a.id?.replace(/\D/g, '') || '0');
const bNum = parseInt(b.id?.replace(/\D/g, '') || '0');
return aNum - bNum;
});
}
/**
* Normalize task object to consistent structure
* @param {Object} task - Raw task object
* @returns {Object} - Normalized task
*/
function normalizeTask(task) {
if (!task) return null;
// Determine status - support various status formats
let status = task.status || 'pending';
if (typeof status === 'object') {
status = status.state || status.value || 'pending';
}
return {
id: task.id || task.task_id || 'unknown',
title: task.title || task.name || task.summary || 'Untitled Task',
status: status.toLowerCase(),
// Preserve original fields for flexible rendering
meta: task.meta || {
type: task.type || task.action || 'task',
agent: task.agent || null,
scope: task.scope || null,
module: task.module || null
},
context: task.context || {
requirements: task.requirements || task.description ? [task.description] : [],
focus_paths: task.focus_paths || task.modification_points?.map(m => m.file) || [],
acceptance: task.acceptance || [],
depends_on: task.depends_on || []
},
flow_control: task.flow_control || {
implementation_approach: task.implementation?.map((step, i) => ({
step: `Step ${i + 1}`,
action: step
})) || []
},
// Keep all original fields for raw JSON view
_raw: task
};
}
/**
* Get directory creation time
* @param {string} dirPath - Directory path
* @returns {string} - ISO date string
*/
function getCreatedTime(dirPath) {
try {
const stat = statSync(dirPath);
return stat.birthtime.toISOString();
} catch {
return new Date().toISOString();
}
}
/**
* Calculate progress from tasks
* @param {Array} tasks - Array of task objects
* @returns {Object} - Progress info
*/
function calculateProgress(tasks) {
if (!tasks || tasks.length === 0) {
return { total: 0, completed: 0, percentage: 0 };
}
const total = tasks.length;
const completed = tasks.filter(t => t.status === 'completed').length;
const percentage = Math.round((completed / total) * 100);
return { total, completed, percentage };
}
/**
* Get detailed lite task info
* @param {string} workflowDir - Workflow directory
* @param {string} type - 'lite-plan' or 'lite-fix'
* @param {string} sessionId - Session ID
* @returns {Object|null} - Detailed task info
*/
export function getLiteTaskDetail(workflowDir, type, sessionId) {
const dir = type === 'lite-plan'
? join(workflowDir, '.lite-plan', sessionId)
: join(workflowDir, '.lite-fix', sessionId);
if (!existsSync(dir)) return null;
return {
id: sessionId,
type,
path: dir,
plan: loadPlanJson(dir),
tasks: loadTaskJsons(dir),
explorations: loadExplorations(dir),
clarifications: loadClarifications(dir)
};
}
/**
* Load exploration results
* @param {string} sessionPath - Session directory path
* @returns {Array} - Exploration results
*/
function loadExplorations(sessionPath) {
const explorePath = join(sessionPath, 'explorations.json');
if (!existsSync(explorePath)) return [];
try {
const content = readFileSync(explorePath, 'utf8');
return JSON.parse(content);
} catch {
return [];
}
}
/**
* Load clarification data
* @param {string} sessionPath - Session directory path
* @returns {Object|null} - Clarification data
*/
function loadClarifications(sessionPath) {
const clarifyPath = join(sessionPath, 'clarifications.json');
if (!existsSync(clarifyPath)) return null;
try {
const content = readFileSync(clarifyPath, 'utf8');
return JSON.parse(content);
} catch {
return null;
}
}

328
ccw/src/core/server.js Normal file
View File

@@ -0,0 +1,328 @@
import http from 'http';
import { URL } from 'url';
import { readFileSync, existsSync, readdirSync } from 'fs';
import { join } from 'path';
import { scanSessions } from './session-scanner.js';
import { aggregateData } from './data-aggregator.js';
import { resolvePath, getRecentPaths, trackRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js';
const TEMPLATE_PATH = join(import.meta.dirname, '../templates/dashboard.html');
const CSS_FILE = join(import.meta.dirname, '../templates/dashboard.css');
const JS_FILE = join(import.meta.dirname, '../templates/dashboard.js');
/**
* Create and start the dashboard server
* @param {Object} options - Server options
* @param {number} options.port - Port to listen on (default: 3456)
* @param {string} options.initialPath - Initial project path
* @returns {Promise<http.Server>}
*/
export async function startServer(options = {}) {
const port = options.port || 3456;
const initialPath = options.initialPath || process.cwd();
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://localhost:${port}`);
const pathname = url.pathname;
// CORS headers for API requests
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
try {
// API: Get workflow data for a path
if (pathname === '/api/data') {
const projectPath = url.searchParams.get('path') || initialPath;
const data = await getWorkflowData(projectPath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
return;
}
// API: Get recent paths
if (pathname === '/api/recent-paths') {
const paths = getRecentPaths();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ paths }));
return;
}
// API: Get session detail data (context, summaries, impl-plan, review)
if (pathname === '/api/session-detail') {
const sessionPath = url.searchParams.get('path');
const dataType = url.searchParams.get('type') || 'all';
if (!sessionPath) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Session path is required' }));
return;
}
const detail = await getSessionDetailData(sessionPath, dataType);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(detail));
return;
}
// Serve dashboard HTML
if (pathname === '/' || pathname === '/index.html') {
const html = generateServerDashboard(initialPath);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
return;
}
// 404
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
} catch (error) {
console.error('Server error:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: error.message }));
}
});
return new Promise((resolve, reject) => {
server.listen(port, () => {
console.log(`Dashboard server running at http://localhost:${port}`);
resolve(server);
});
server.on('error', reject);
});
}
/**
* Get workflow data for a project path
* @param {string} projectPath
* @returns {Promise<Object>}
*/
async function getWorkflowData(projectPath) {
const resolvedPath = resolvePath(projectPath);
const workflowDir = join(resolvedPath, '.workflow');
// Track this path
trackRecentPath(resolvedPath);
// Check if .workflow exists
if (!existsSync(workflowDir)) {
return {
generatedAt: new Date().toISOString(),
activeSessions: [],
archivedSessions: [],
liteTasks: { litePlan: [], liteFix: [] },
reviewData: { dimensions: {} },
statistics: {
totalSessions: 0,
activeSessions: 0,
totalTasks: 0,
completedTasks: 0,
reviewFindings: 0,
litePlanCount: 0,
liteFixCount: 0
},
projectPath: normalizePathForDisplay(resolvedPath),
recentPaths: getRecentPaths()
};
}
// Scan and aggregate data
const sessions = await scanSessions(workflowDir);
const data = await aggregateData(sessions, workflowDir);
data.projectPath = normalizePathForDisplay(resolvedPath);
data.recentPaths = getRecentPaths();
return data;
}
/**
* Get session detail data (context, summaries, impl-plan, review)
* @param {string} sessionPath - Path to session directory
* @param {string} dataType - Type of data to load: context, summary, impl-plan, review, or all
* @returns {Promise<Object>}
*/
async function getSessionDetailData(sessionPath, dataType) {
const result = {};
// Normalize path
const normalizedPath = sessionPath.replace(/\\/g, '/');
try {
// Load context-package.json (in .process/ subfolder)
if (dataType === 'context' || dataType === 'all') {
// Try .process/context-package.json first (common location)
let contextFile = join(normalizedPath, '.process', 'context-package.json');
if (!existsSync(contextFile)) {
// Fallback to session root
contextFile = join(normalizedPath, 'context-package.json');
}
if (existsSync(contextFile)) {
try {
result.context = JSON.parse(readFileSync(contextFile, 'utf8'));
} catch (e) {
result.context = null;
}
}
}
// Load task JSONs from .task/ folder
if (dataType === 'tasks' || dataType === 'all') {
const taskDir = join(normalizedPath, '.task');
result.tasks = [];
if (existsSync(taskDir)) {
const files = readdirSync(taskDir).filter(f => f.endsWith('.json') && f.startsWith('IMPL-'));
for (const file of files) {
try {
const content = JSON.parse(readFileSync(join(taskDir, file), 'utf8'));
result.tasks.push({
filename: file,
task_id: file.replace('.json', ''),
...content
});
} catch (e) {
// Skip unreadable files
}
}
// Sort by task ID
result.tasks.sort((a, b) => a.task_id.localeCompare(b.task_id));
}
}
// Load summaries from .summaries/
if (dataType === 'summary' || dataType === 'all') {
const summariesDir = join(normalizedPath, '.summaries');
result.summaries = [];
if (existsSync(summariesDir)) {
const files = readdirSync(summariesDir).filter(f => f.endsWith('.md'));
for (const file of files) {
try {
const content = readFileSync(join(summariesDir, file), 'utf8');
result.summaries.push({ name: file.replace('.md', ''), content });
} catch (e) {
// Skip unreadable files
}
}
}
}
// Load IMPL_PLAN.md
if (dataType === 'impl-plan' || dataType === 'all') {
const implPlanFile = join(normalizedPath, 'IMPL_PLAN.md');
if (existsSync(implPlanFile)) {
try {
result.implPlan = readFileSync(implPlanFile, 'utf8');
} catch (e) {
result.implPlan = null;
}
}
}
// Load review data from .review/
if (dataType === 'review' || dataType === 'all') {
const reviewDir = join(normalizedPath, '.review');
result.review = { dimensions: {} };
if (existsSync(reviewDir)) {
const dimensionsDir = join(reviewDir, 'dimensions');
if (existsSync(dimensionsDir)) {
const files = readdirSync(dimensionsDir).filter(f => f.endsWith('.json'));
for (const file of files) {
try {
const dimName = file.replace('.json', '');
const content = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8'));
result.review.dimensions[dimName] = content.findings || content;
} catch (e) {
// Skip unreadable files
}
}
}
}
}
} catch (error) {
console.error('Error loading session detail:', error);
result.error = error.message;
}
return result;
}
/**
* Generate dashboard HTML for server mode
* @param {string} initialPath
* @returns {string}
*/
function generateServerDashboard(initialPath) {
let html = readFileSync(TEMPLATE_PATH, 'utf8');
// Read CSS and JS files
const cssContent = existsSync(CSS_FILE) ? readFileSync(CSS_FILE, 'utf8') : '';
let jsContent = existsSync(JS_FILE) ? readFileSync(JS_FILE, 'utf8') : '';
// Inject CSS content
html = html.replace('{{CSS_CONTENT}}', cssContent);
// Prepare JS content with empty initial data (will be loaded dynamically)
const emptyData = {
generatedAt: new Date().toISOString(),
activeSessions: [],
archivedSessions: [],
liteTasks: { litePlan: [], liteFix: [] },
reviewData: { dimensions: {} },
statistics: { totalSessions: 0, activeSessions: 0, totalTasks: 0, completedTasks: 0, reviewFindings: 0, litePlanCount: 0, liteFixCount: 0 }
};
// Replace JS placeholders
jsContent = jsContent.replace('{{WORKFLOW_DATA}}', JSON.stringify(emptyData, null, 2));
jsContent = jsContent.replace(/\{\{PROJECT_PATH\}\}/g, normalizePathForDisplay(initialPath).replace(/\\/g, '/'));
jsContent = jsContent.replace('{{RECENT_PATHS}}', JSON.stringify(getRecentPaths()));
// Add server mode flag and dynamic loading functions at the start of JS
const serverModeScript = `
// Server mode - load data dynamically
window.SERVER_MODE = true;
window.INITIAL_PATH = '${normalizePathForDisplay(initialPath).replace(/\\/g, '/')}';
async function loadDashboardData(path) {
try {
const res = await fetch('/api/data?path=' + encodeURIComponent(path));
if (!res.ok) throw new Error('Failed to load data');
return await res.json();
} catch (err) {
console.error('Error loading data:', err);
return null;
}
}
async function loadRecentPaths() {
try {
const res = await fetch('/api/recent-paths');
if (!res.ok) return [];
const data = await res.json();
return data.paths || [];
} catch (err) {
return [];
}
}
`;
// Prepend server mode script to JS content
jsContent = serverModeScript + jsContent;
// Inject JS content
html = html.replace('{{JS_CONTENT}}', jsContent);
// Replace any remaining placeholders in HTML
html = html.replace(/\{\{PROJECT_PATH\}\}/g, normalizePathForDisplay(initialPath).replace(/\\/g, '/'));
return html;
}

View File

@@ -90,6 +90,55 @@ async function findWfsSessions(dir) {
}
}
/**
* Parse timestamp from session name
* Supports formats: WFS-xxx-20251128172537 or WFS-xxx-20251120-170640
* @param {string} sessionName - Session directory name
* @returns {string|null} - ISO date string or null
*/
function parseTimestampFromName(sessionName) {
// Format: 14-digit timestamp (YYYYMMDDHHmmss)
const match14 = sessionName.match(/(\d{14})$/);
if (match14) {
const ts = match14[1];
return `${ts.slice(0,4)}-${ts.slice(4,6)}-${ts.slice(6,8)}T${ts.slice(8,10)}:${ts.slice(10,12)}:${ts.slice(12,14)}Z`;
}
// Format: 8-digit date + 6-digit time separated by hyphen (YYYYMMDD-HHmmss)
const match8_6 = sessionName.match(/(\d{8})-(\d{6})$/);
if (match8_6) {
const d = match8_6[1];
const t = match8_6[2];
return `${d.slice(0,4)}-${d.slice(4,6)}-${d.slice(6,8)}T${t.slice(0,2)}:${t.slice(2,4)}:${t.slice(4,6)}Z`;
}
return null;
}
/**
* Infer session type from session name pattern
* @param {string} sessionName - Session directory name
* @returns {string} - Inferred type
*/
function inferTypeFromName(sessionName) {
const name = sessionName.toLowerCase();
if (name.includes('-review-') || name.includes('-code-review-')) {
return 'review';
}
if (name.includes('-test-')) {
return 'test';
}
if (name.includes('-docs-')) {
return 'docs';
}
if (name.includes('-tdd-')) {
return 'tdd';
}
return 'workflow';
}
/**
* Read session data from workflow-session.json or create minimal from directory
* @param {string} sessionPath - Path to session directory
@@ -97,17 +146,27 @@ async function findWfsSessions(dir) {
*/
function readSessionData(sessionPath) {
const sessionFile = join(sessionPath, 'workflow-session.json');
const sessionName = basename(sessionPath);
if (existsSync(sessionFile)) {
try {
const data = JSON.parse(readFileSync(sessionFile, 'utf8'));
// Multi-level type detection: JSON type > workflow_type > infer from name
let type = data.type || data.workflow_type || inferTypeFromName(sessionName);
// Normalize workflow_type values
if (type === 'test_session') type = 'test';
if (type === 'implementation') type = 'workflow';
return {
session_id: data.session_id || basename(sessionPath),
session_id: data.session_id || sessionName,
project: data.project || data.description || '',
status: data.status || 'active',
created_at: data.created_at || data.initialized_at || null,
created_at: data.created_at || data.initialized_at || data.timestamp || null,
archived_at: data.archived_at || null,
type: data.type || 'workflow'
type: type,
workflow_type: data.workflow_type || null // Keep original for reference
};
} catch {
// Fall through to minimal session
@@ -115,17 +174,34 @@ function readSessionData(sessionPath) {
}
// Fallback: create minimal session from directory info
// Try to extract timestamp from session name first
const timestampFromName = parseTimestampFromName(sessionName);
const inferredType = inferTypeFromName(sessionName);
try {
const stats = statSync(sessionPath);
return {
session_id: basename(sessionPath),
session_id: sessionName,
project: '',
status: 'unknown',
created_at: stats.birthtime.toISOString(),
created_at: timestampFromName || stats.birthtime.toISOString(),
archived_at: null,
type: 'workflow'
type: inferredType,
workflow_type: null
};
} catch {
// Even if stat fails, return with name-extracted data
if (timestampFromName) {
return {
session_id: sessionName,
project: '',
status: 'unknown',
created_at: timestampFromName,
archived_at: null,
type: inferredType,
workflow_type: null
};
}
return null;
}
}