Add internationalization support for help view and implement help rendering logic

- Introduced `help-i18n.js` for managing translations in Chinese and English for the help view.
- Created `help.js` to render the help view, including command categories, workflow diagrams, and CodexLens quick-start.
- Implemented search functionality with debounce for command filtering.
- Added workflow diagram rendering with Cytoscape.js integration.
- Developed tests for write-file verification, ensuring proper handling of small and large JSON files.
This commit is contained in:
catlog22
2025-12-16 22:24:29 +08:00
parent b702791c2c
commit 154a9283b5
21 changed files with 2003 additions and 303 deletions

View File

@@ -0,0 +1,308 @@
// @ts-nocheck
/**
* Help Routes Module
* Handles all Help-related API endpoints for command guide and CodexLens docs
*/
import type { IncomingMessage, ServerResponse } from 'http';
import { readFileSync, existsSync, watch } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
export interface RouteContext {
pathname: string;
url: URL;
req: IncomingMessage;
res: ServerResponse;
initialPath: string;
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
broadcastToClients: (data: unknown) => void;
}
// ========== In-Memory Cache ==========
interface CacheEntry {
data: any;
timestamp: number;
}
const cache = new Map<string, CacheEntry>();
const CACHE_TTL = 300000; // 5 minutes
/**
* Get cached data or load from file
*/
function getCachedData(key: string, filePath: string): any {
const now = Date.now();
const cached = cache.get(key);
// Return cached data if valid
if (cached && (now - cached.timestamp) < CACHE_TTL) {
return cached.data;
}
// Load fresh data
try {
if (!existsSync(filePath)) {
console.error(`Help data file not found: ${filePath}`);
return null;
}
const content = readFileSync(filePath, 'utf8');
const data = JSON.parse(content);
// Update cache
cache.set(key, { data, timestamp: now });
return data;
} catch (error) {
console.error(`Failed to load help data from ${filePath}:`, error);
return null;
}
}
/**
* Invalidate cache for a specific key
*/
function invalidateCache(key: string): void {
cache.delete(key);
console.log(`Cache invalidated: ${key}`);
}
// ========== File Watchers ==========
let watchersInitialized = false;
/**
* Initialize file watchers for JSON indexes
*/
function initializeFileWatchers(): void {
if (watchersInitialized) return;
const indexDir = join(homedir(), '.claude', 'skills', 'command-guide', 'index');
if (!existsSync(indexDir)) {
console.warn(`Command guide index directory not found: ${indexDir}`);
return;
}
try {
// Watch all JSON files in index directory
const watcher = watch(indexDir, { recursive: false }, (eventType, filename) => {
if (!filename || !filename.endsWith('.json')) return;
console.log(`File change detected: ${filename} (${eventType})`);
// Invalidate relevant cache entries
if (filename === 'all-commands.json') {
invalidateCache('all-commands');
} else if (filename === 'command-relationships.json') {
invalidateCache('command-relationships');
} else if (filename === 'by-category.json') {
invalidateCache('by-category');
}
});
watchersInitialized = true;
console.log(`File watchers initialized for: ${indexDir}`);
} catch (error) {
console.error('Failed to initialize file watchers:', error);
}
}
// ========== Helper Functions ==========
/**
* Filter commands by search query
*/
function filterCommands(commands: any[], query: string): any[] {
if (!query) return commands;
const lowerQuery = query.toLowerCase();
return commands.filter(cmd =>
cmd.name?.toLowerCase().includes(lowerQuery) ||
cmd.command?.toLowerCase().includes(lowerQuery) ||
cmd.description?.toLowerCase().includes(lowerQuery) ||
cmd.category?.toLowerCase().includes(lowerQuery)
);
}
/**
* Group commands by category with subcategories
*/
function groupCommandsByCategory(commands: any[]): any {
const grouped: any = {};
for (const cmd of commands) {
const category = cmd.category || 'general';
const subcategory = cmd.subcategory || null;
if (!grouped[category]) {
grouped[category] = {
name: category,
commands: [],
subcategories: {}
};
}
if (subcategory) {
if (!grouped[category].subcategories[subcategory]) {
grouped[category].subcategories[subcategory] = [];
}
grouped[category].subcategories[subcategory].push(cmd);
} else {
grouped[category].commands.push(cmd);
}
}
return grouped;
}
// ========== API Routes ==========
/**
* Handle Help routes
* @returns true if route was handled, false otherwise
*/
export async function handleHelpRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, url, req, res } = ctx;
// Initialize file watchers on first request
initializeFileWatchers();
const indexDir = join(homedir(), '.claude', 'skills', 'command-guide', 'index');
// API: Get all commands with optional search
if (pathname === '/api/help/commands') {
const searchQuery = url.searchParams.get('q') || '';
const filePath = join(indexDir, 'all-commands.json');
let commands = getCachedData('all-commands', filePath);
if (!commands) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Commands data not found' }));
return true;
}
// Filter by search query if provided
if (searchQuery) {
commands = filterCommands(commands, searchQuery);
}
// Group by category
const grouped = groupCommandsByCategory(commands);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
commands: commands,
grouped: grouped,
total: commands.length
}));
return true;
}
// API: Get workflow command relationships
if (pathname === '/api/help/workflows') {
const filePath = join(indexDir, 'command-relationships.json');
const relationships = getCachedData('command-relationships', filePath);
if (!relationships) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Workflow relationships not found' }));
return true;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(relationships));
return true;
}
// API: Get commands by category
if (pathname === '/api/help/commands/by-category') {
const filePath = join(indexDir, 'by-category.json');
const byCategory = getCachedData('by-category', filePath);
if (!byCategory) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Category data not found' }));
return true;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(byCategory));
return true;
}
// API: Get CodexLens documentation metadata
if (pathname === '/api/help/codexlens') {
// Return CodexLens quick-start guide data
const codexLensData = {
title: 'CodexLens Quick Start',
description: 'Fast code indexing and semantic search for large codebases',
sections: [
{
title: 'Key Concepts',
items: [
{
name: 'Indexing',
description: 'CodexLens builds a semantic index of your codebase for fast retrieval',
command: 'codex_lens(action="init", path=".")'
},
{
name: 'Search Modes',
description: 'Text search for exact matches, semantic search for concept-based queries',
command: 'codex_lens(action="search", query="authentication logic", mode="semantic")'
},
{
name: 'Symbol Navigation',
description: 'Extract and navigate code symbols (functions, classes, interfaces)',
command: 'codex_lens(action="symbol", file="path/to/file.py")'
}
]
},
{
title: 'Common Commands',
items: [
{
name: 'Initialize Index',
command: 'codex_lens(action="init", path=".")',
description: 'Index the current directory'
},
{
name: 'Text Search',
command: 'codex_lens(action="search", query="function name", path=".")',
description: 'Search for exact text matches'
},
{
name: 'Semantic Search',
command: 'codex_lens(action="search", query="user authentication", mode="semantic")',
description: 'Search by concept or meaning'
},
{
name: 'Check Status',
command: 'codex_lens(action="status")',
description: 'View indexing status for all projects'
}
]
},
{
title: 'Best Practices',
items: [
{ description: 'Index large codebases (>500 files) for optimal performance' },
{ description: 'Use semantic search for exploratory tasks' },
{ description: 'Combine with smart_search for medium-sized projects' },
{ description: 'Re-index after major code changes' }
]
}
],
links: [
{ text: 'Full Documentation', url: 'https://github.com/yourusername/codex-lens' },
{ text: 'Tool Selection Guide', url: '/.claude/rules/tool-selection.md' }
]
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(codexLensData));
return true;
}
return false;
}

View File

@@ -20,6 +20,7 @@ import { handleRulesRoutes } from './routes/rules-routes.js';
import { handleSessionRoutes } from './routes/session-routes.js';
import { handleCcwRoutes } from './routes/ccw-routes.js';
import { handleClaudeRoutes } from './routes/claude-routes.js';
import { handleHelpRoutes } from './routes/help-routes.js';
// Import WebSocket handling
import { handleWebSocketUpgrade, broadcastToClients } from './websocket.js';
@@ -65,12 +66,14 @@ const MODULE_CSS_FILES = [
'12-skills-rules.css',
'13-claude-manager.css',
'14-graph-explorer.css',
'15-mcp-manager.css'
'15-mcp-manager.css',
'16-help.css'
];
// Modular JS files in dependency order
const MODULE_FILES = [
'i18n.js', // Must be loaded first for translations
'help-i18n.js', // Help page translations
'utils.js',
'state.js',
'api.js',
@@ -113,6 +116,7 @@ const MODULE_FILES = [
'views/rules-manager.js',
'views/claude-manager.js',
'views/graph-explorer.js',
'views/help.js',
'main.js'
];
@@ -300,6 +304,11 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
if (await handleRulesRoutes(routeContext)) return;
}
// Help routes (/api/help/*)
if (pathname.startsWith('/api/help/')) {
if (await handleHelpRoutes(routeContext)) return;
}
// Session routes (/api/session-detail, /api/update-task-status, /api/bulk-update-task-status)
if (pathname.includes('session') || pathname.includes('task-status')) {
if (await handleSessionRoutes(routeContext)) return;

View File

@@ -0,0 +1,264 @@
/* ==========================================
HELP VIEW - TAB, ACCORDION, SEARCH STYLES
========================================== */
/* ==========================================
TAB TRANSITIONS
========================================== */
.help-main-tab {
position: relative;
transition: all 0.2s ease;
}
.help-main-tab:hover {
background-color: hsl(var(--muted));
}
.help-main-tab.bg-primary {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.help-main-tab::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: hsl(var(--primary));
opacity: 0;
transition: opacity 0.2s ease;
}
.help-main-tab.bg-primary::after {
opacity: 1;
}
/* ==========================================
ACCORDION ANIMATIONS
========================================== */
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.accordion-content:not(.hidden) {
max-height: 5000px;
transition: max-height 0.5s ease-in;
}
.accordion-icon {
transition: transform 0.2s ease;
}
.accordion-header:hover {
background-color: hsl(var(--muted) / 0.8);
}
/* ==========================================
SEARCH HIGHLIGHTING
========================================== */
#helpSearchInput {
transition: all 0.2s ease;
}
#helpSearchInput:focus {
border-color: hsl(var(--primary));
box-shadow: 0 0 0 3px hsl(var(--primary) / 0.1);
}
/* Highlight matching text in search results */
mark,
.search-highlight {
background-color: hsl(var(--primary) / 0.2);
color: hsl(var(--foreground));
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
}
::selection {
background-color: hsl(var(--primary) / 0.3);
color: hsl(var(--foreground));
}
/* ==========================================
CYTOSCAPE CONTAINER
========================================== */
#cytoscapeContainer {
min-height: 500px;
background-color: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
overflow: hidden;
}
#cytoscapeContainer::-webkit-scrollbar {
width: 8px;
height: 8px;
}
#cytoscapeContainer::-webkit-scrollbar-track {
background: hsl(var(--muted));
border-radius: 4px;
}
#cytoscapeContainer::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.4);
border-radius: 4px;
}
#cytoscapeContainer::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground) / 0.6);
}
/* ==========================================
WORKFLOW DIAGRAM BUTTONS
========================================== */
.workflow-diagram-btn {
transition: all 0.2s ease;
}
.workflow-diagram-btn.bg-primary {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.workflow-diagram-btn.bg-muted {
background-color: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
.workflow-diagram-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px hsl(var(--primary) / 0.15);
}
/* ==========================================
COMMAND CARDS
========================================== */
.help-view-container .bg-background {
background-color: hsl(var(--background));
}
.help-view-container .border-border {
border-color: hsl(var(--border));
}
.help-view-container .border-primary {
border-color: hsl(var(--primary));
}
.help-view-container code {
font-family: 'Fira Code', 'Courier New', monospace;
}
/* Command card hover effect */
.help-view-container .bg-background:hover {
border-color: hsl(var(--primary));
box-shadow: 0 2px 8px hsl(var(--primary) / 0.1);
}
/* ==========================================
RESPONSIVE DESIGN
========================================== */
@media (max-width: 768px) {
.help-main-tab {
padding: 0.5rem;
font-size: 0.75rem;
}
#cytoscapeContainer {
min-height: 400px;
}
.workflow-diagram-btn {
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
}
}
/* ==========================================
CODEXLENS QUICK-START SECTION
========================================== */
.codexlens-quickstart h3,
.codexlens-quickstart h4,
.codexlens-quickstart h5 {
color: hsl(var(--foreground));
}
.codexlens-quickstart p {
color: hsl(var(--muted-foreground));
}
.codexlens-quickstart .bg-muted {
background-color: hsl(var(--muted));
}
.codexlens-quickstart code {
background-color: hsl(var(--muted));
color: hsl(var(--foreground));
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
}
/* ==========================================
DARK MODE SUPPORT
========================================== */
.dark .help-main-tab:hover {
background-color: hsl(var(--muted) / 0.8);
}
.dark #helpSearchInput {
background-color: hsl(var(--background));
border-color: hsl(var(--border));
color: hsl(var(--foreground));
}
.dark #cytoscapeContainer {
background-color: hsl(var(--background));
border-color: hsl(var(--border));
}
.dark mark,
.dark .search-highlight {
background-color: hsl(var(--primary) / 0.3);
color: hsl(var(--foreground));
}
/* ==========================================
LOADING STATES
========================================== */
.help-view-container .loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 0;
color: hsl(var(--muted-foreground));
}
.help-view-container .loading-spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -135,6 +135,8 @@ function initNavigation() {
renderClaudeManager();
} else if (currentView === 'graph-explorer') {
renderGraphExplorer();
} else if (currentView === 'help') {
renderHelpView();
}
});
});
@@ -171,6 +173,8 @@ function updateContentTitle() {
titleEl.textContent = t('title.claudeManager');
} else if (currentView === 'graph-explorer') {
titleEl.textContent = t('title.graphExplorer');
} else if (currentView === 'help') {
titleEl.textContent = t('title.helpGuide');
} else if (currentView === 'liteTasks') {
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');

View File

@@ -0,0 +1,152 @@
// ==========================================
// HELP VIEW I18N
// Internationalization for help page (Chinese translations)
// ==========================================
var helpI18n = {
zh: {
// Page Headers
'help.title': '帮助与指南',
'help.subtitle': '全面的命令参考、工作流程图和 CodexLens 快速入门指南',
// Search
'help.search.placeholder': '按名称、类别或描述搜索命令...',
'help.search.results': '找到 {count} 个匹配 "{query}" 的命令',
'help.search.noResults': '没有找到匹配您搜索的命令',
// Tabs
'help.tab.cli': 'CLI 命令',
'help.tab.memory': '内存命令',
'help.tab.workflow': '工作流命令',
'help.tab.task': '任务命令',
'help.tab.diagrams': '工作流程',
'help.tab.codexlens': 'CodexLens',
// Command Card
'help.command.arguments': '参数',
'help.command.difficulty.beginner': '初级',
'help.command.difficulty.intermediate': '中级',
'help.command.difficulty.advanced': '高级',
// Workflow Diagrams
'help.diagrams.title': '常见工作流场景',
'help.diagrams.tdd': 'TDD 开发',
'help.diagrams.feature': '功能开发',
'help.diagrams.bugfix': 'Bug 调查',
'help.diagrams.review': '代码审查',
'help.diagrams.fit': '适应视图',
'help.diagrams.zoomIn': '放大',
'help.diagrams.zoomOut': '缩小',
'help.diagrams.legend': '图例',
'help.diagrams.legend.prerequisites': '前置条件',
'help.diagrams.legend.nextSteps': '下一步',
'help.diagrams.legend.alternatives': '替代方案',
'help.diagrams.notLoaded': 'Cytoscape.js 未加载',
// CodexLens
'help.codexlens.title': 'CodexLens 快速入门',
'help.codexlens.subtitle': '强大的代码索引和语义搜索工具',
'help.codexlens.concepts': '核心概念',
'help.codexlens.concept.indexing': '索引',
'help.codexlens.concept.indexing.desc': '为快速检索构建代码库索引',
'help.codexlens.concept.search': '搜索模式',
'help.codexlens.concept.search.desc': '文本、语义和符号导航',
'help.codexlens.concept.symbols': '符号导航',
'help.codexlens.concept.symbols.desc': '跳转到定义、查找引用',
'help.codexlens.commands': '常用命令',
'help.codexlens.practices': '最佳实践',
'help.codexlens.practice.1': '初次使用前先运行索引',
'help.codexlens.practice.2': '使用语义搜索查找概念代码',
'help.codexlens.practice.3': '利用符号导航探索大型代码库',
'help.codexlens.practice.4': '代码更改后定期重新索引',
'help.codexlens.resources': '资源',
'help.codexlens.fullDocs': '完整文档',
'help.codexlens.apiRef': 'API 参考',
'help.codexlens.examples': '示例',
// Empty States
'help.empty.noCommands': '此类别中没有命令',
'help.empty.loadFailed': '加载帮助数据失败'
},
en: {
// Page Headers
'help.title': 'Help & Guide',
'help.subtitle': 'Comprehensive command reference, workflow diagrams, and CodexLens quick-start guide',
// Search
'help.search.placeholder': 'Search commands by name, category, or description...',
'help.search.results': 'Found {count} commands matching "{query}"',
'help.search.noResults': 'No commands found matching your search',
// Tabs
'help.tab.cli': 'CLI Commands',
'help.tab.memory': 'Memory Commands',
'help.tab.workflow': 'Workflow Commands',
'help.tab.task': 'Task Commands',
'help.tab.diagrams': 'Workflows',
'help.tab.codexlens': 'CodexLens',
// Command Card
'help.command.arguments': 'Arguments',
'help.command.difficulty.beginner': 'Beginner',
'help.command.difficulty.intermediate': 'Intermediate',
'help.command.difficulty.advanced': 'Advanced',
// Workflow Diagrams
'help.diagrams.title': 'Common Workflow Scenarios',
'help.diagrams.tdd': 'TDD Development',
'help.diagrams.feature': 'Feature Development',
'help.diagrams.bugfix': 'Bug Investigation',
'help.diagrams.review': 'Code Review',
'help.diagrams.fit': 'Fit to View',
'help.diagrams.zoomIn': 'Zoom In',
'help.diagrams.zoomOut': 'Zoom Out',
'help.diagrams.legend': 'Legend',
'help.diagrams.legend.prerequisites': 'Prerequisites',
'help.diagrams.legend.nextSteps': 'Next Steps',
'help.diagrams.legend.alternatives': 'Alternatives',
'help.diagrams.notLoaded': 'Cytoscape.js not loaded',
// CodexLens
'help.codexlens.title': 'CodexLens Quick Start',
'help.codexlens.subtitle': 'Powerful code indexing and semantic search tool',
'help.codexlens.concepts': 'Key Concepts',
'help.codexlens.concept.indexing': 'Indexing',
'help.codexlens.concept.indexing.desc': 'Build codebase index for fast retrieval',
'help.codexlens.concept.search': 'Search Modes',
'help.codexlens.concept.search.desc': 'Text, semantic, and symbol navigation',
'help.codexlens.concept.symbols': 'Symbol Navigation',
'help.codexlens.concept.symbols.desc': 'Jump to definition, find references',
'help.codexlens.commands': 'Common Commands',
'help.codexlens.practices': 'Best Practices',
'help.codexlens.practice.1': 'Run index before first use',
'help.codexlens.practice.2': 'Use semantic search to find conceptual code',
'help.codexlens.practice.3': 'Leverage symbol navigation for large codebases',
'help.codexlens.practice.4': 'Re-index periodically after code changes',
'help.codexlens.resources': 'Resources',
'help.codexlens.fullDocs': 'Full Documentation',
'help.codexlens.apiRef': 'API Reference',
'help.codexlens.examples': 'Examples',
// Empty States
'help.empty.noCommands': 'No commands in this category',
'help.empty.loadFailed': 'Failed to load help data'
}
};
// Helper function to get help translation
function ht(key, replacements) {
var lang = typeof currentLanguage !== 'undefined' ? currentLanguage : 'en';
var translations = helpI18n[lang] || helpI18n.en;
var text = translations[key] || helpI18n.en[key] || key;
// Replace placeholders like {count}, {query}
if (replacements) {
Object.keys(replacements).forEach(function(placeholder) {
text = text.replace('{' + placeholder + '}', replacements[placeholder]);
});
}
return text;
}

View File

@@ -1035,6 +1035,10 @@ const i18n = {
'graph.noSearchData': 'No search process data available.',
'graph.center': 'Center',
'graph.resetFilters': 'Reset Filters',
// Help & Guide
'nav.help': 'Help',
'title.helpGuide': 'Help & Guide',
'graph.cytoscapeNotLoaded': 'Graph library not loaded',
'graph.impactAnalysisError': 'Failed to load impact analysis',
'graph.searchProcessDesc': 'Visualize how search queries flow through the system',
@@ -2118,6 +2122,10 @@ const i18n = {
'graph.noSearchData': '无搜索过程数据。',
'graph.center': '居中',
'graph.resetFilters': '重置筛选',
// Help & Guide
'nav.help': '帮助',
'title.helpGuide': '帮助与指南',
'graph.cytoscapeNotLoaded': '图谱库未加载',
'graph.impactAnalysisError': '加载影响分析失败',
'graph.searchProcessDesc': '可视化搜索查询在系统中的流转过程',

View File

@@ -0,0 +1,730 @@
// ==========================================
// HELP VIEW
// Command guide with categories, workflow diagrams, and CodexLens quick-start
// ==========================================
// State variables
var helpData = {
commands: [],
grouped: {},
workflows: {},
codexlens: {}
};
var activeHelpTab = 'cli';
var helpSearchQuery = '';
var helpSearchTimeout = null;
var cytoscapeInstance = null;
var activeWorkflowDiagram = 'tdd';
// ========== Main Render Function ==========
async function renderHelpView() {
hideStatsAndCarousel();
var container = document.getElementById('mainContent');
if (!container) return;
// Show loading state
container.innerHTML = '<div class="flex items-center justify-center py-16"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>';
if (typeof lucide !== 'undefined') lucide.createIcons();
// Load help data
await loadHelpData();
// Render layout
container.innerHTML = renderHelpLayout();
// Initialize event handlers
initializeHelpEventHandlers();
// Render initial tab
renderCommandsTab(activeHelpTab);
if (typeof lucide !== 'undefined') lucide.createIcons();
}
// ========== Data Loading ==========
async function loadHelpData() {
try {
// Load all commands with grouping
var commandsResp = await fetch('/api/help/commands');
if (commandsResp.ok) {
var data = await commandsResp.json();
helpData.commands = data.commands || [];
helpData.grouped = data.grouped || {};
}
// Load workflow relationships
var workflowsResp = await fetch('/api/help/workflows');
if (workflowsResp.ok) {
helpData.workflows = await workflowsResp.json();
}
// Load CodexLens data
var codexResp = await fetch('/api/help/codexlens');
if (codexResp.ok) {
helpData.codexlens = await codexResp.json();
}
} catch (err) {
console.error('Failed to load help data:', err);
}
}
// ========== Layout Rendering ==========
function renderHelpLayout() {
return `
<div class="help-view-container">
<!-- Page Header -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h2 class="text-2xl font-bold text-foreground mb-2 flex items-center gap-2">
<i data-lucide="help-circle" class="w-6 h-6"></i>
${ht('help.title')}
</h2>
<p class="text-muted-foreground">
${ht('help.subtitle')}
</p>
</div>
<!-- Search Bar -->
<div class="bg-card border border-border rounded-lg p-4 mb-6">
<div class="relative">
<i data-lucide="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground"></i>
<input
type="text"
id="helpSearchInput"
class="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="${ht('help.search.placeholder')}"
value="${escapeHtml(helpSearchQuery)}"
/>
</div>
</div>
<!-- Main Tab Navigation -->
<div class="bg-card border border-border rounded-lg overflow-hidden">
<div class="flex border-b border-border">
<button class="help-main-tab flex-1 px-6 py-3 text-sm font-medium transition-colors" data-tab="cli">
${ht('help.tab.cli')}
</button>
<button class="help-main-tab flex-1 px-6 py-3 text-sm font-medium transition-colors" data-tab="memory">
${ht('help.tab.memory')}
</button>
<button class="help-main-tab flex-1 px-6 py-3 text-sm font-medium transition-colors" data-tab="workflow">
${ht('help.tab.workflow')}
</button>
<button class="help-main-tab flex-1 px-6 py-3 text-sm font-medium transition-colors" data-tab="task">
${ht('help.tab.task')}
</button>
<button class="help-main-tab flex-1 px-6 py-3 text-sm font-medium transition-colors" data-tab="diagrams">
<i data-lucide="git-branch" class="w-4 h-4 inline-block mr-1"></i>
${ht('help.tab.diagrams')}
</button>
<button class="help-main-tab flex-1 px-6 py-3 text-sm font-medium transition-colors" data-tab="codexlens">
<i data-lucide="zap" class="w-4 h-4 inline-block mr-1"></i>
${ht('help.tab.codexlens')}
</button>
</div>
<!-- Tab Content Container -->
<div id="helpTabContent" class="p-6">
<!-- Content will be dynamically rendered -->
</div>
</div>
</div>
`;
}
// ========== Event Handlers ==========
function initializeHelpEventHandlers() {
// Tab switching
var tabs = document.querySelectorAll('.help-main-tab');
tabs.forEach(function(tab) {
tab.addEventListener('click', function() {
var tabName = this.dataset.tab;
switchHelpTab(tabName);
});
});
// Update active tab styles
updateActiveTab(activeHelpTab);
// Search input with debounce
var searchInput = document.getElementById('helpSearchInput');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
clearTimeout(helpSearchTimeout);
helpSearchTimeout = setTimeout(function() {
helpSearchQuery = e.target.value;
performHelpSearch();
}, 300);
});
}
}
function switchHelpTab(tabName) {
activeHelpTab = tabName;
updateActiveTab(tabName);
if (tabName === 'diagrams') {
renderWorkflowDiagrams();
} else if (tabName === 'codexlens') {
renderCodexLensQuickStart();
} else {
renderCommandsTab(tabName);
}
}
function updateActiveTab(activeTab) {
var tabs = document.querySelectorAll('.help-main-tab');
tabs.forEach(function(tab) {
if (tab.dataset.tab === activeTab) {
tab.classList.add('bg-primary', 'text-primary-foreground');
tab.classList.remove('bg-transparent', 'text-muted-foreground', 'hover:bg-muted');
} else {
tab.classList.remove('bg-primary', 'text-primary-foreground');
tab.classList.add('bg-transparent', 'text-muted-foreground', 'hover:bg-muted');
}
});
}
// ========== Command Rendering ==========
function renderCommandsTab(category) {
var container = document.getElementById('helpTabContent');
if (!container) return;
var categoryData = helpData.grouped[category];
if (!categoryData) {
container.innerHTML = `
<div class="text-center py-8 text-muted-foreground">
<i data-lucide="inbox" class="w-12 h-12 mx-auto mb-2"></i>
<p>No commands found for this category</p>
</div>
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
return;
}
var filteredCommands = helpSearchQuery
? filterCommandsBySearch(categoryData.commands, helpSearchQuery)
: categoryData.commands;
var html = '';
// Show search results count
if (helpSearchQuery) {
html += `
<div class="mb-4 p-3 bg-muted rounded-lg text-sm text-muted-foreground">
Found ${filteredCommands.length} commands matching "${escapeHtml(helpSearchQuery)}"
</div>
`;
}
// Render direct commands
if (filteredCommands.length > 0) {
html += '<div class="space-y-3">';
filteredCommands.forEach(function(cmd) {
html += renderCommandCard(cmd);
});
html += '</div>';
}
// Render subcategories as accordions
var subcategories = categoryData.subcategories || {};
var subcategoryKeys = Object.keys(subcategories);
if (subcategoryKeys.length > 0) {
html += '<div class="mt-6 space-y-3">';
subcategoryKeys.forEach(function(subcat) {
var subcatCommands = helpSearchQuery
? filterCommandsBySearch(subcategories[subcat], helpSearchQuery)
: subcategories[subcat];
if (subcatCommands.length > 0) {
html += renderSubcategoryAccordion(subcat, subcatCommands);
}
});
html += '</div>';
}
if (filteredCommands.length === 0 && subcategoryKeys.length === 0) {
html = `
<div class="text-center py-8 text-muted-foreground">
<i data-lucide="search-x" class="w-12 h-12 mx-auto mb-2"></i>
<p>No commands found matching your search</p>
</div>
`;
}
container.innerHTML = html;
if (typeof lucide !== 'undefined') lucide.createIcons();
// Initialize accordion handlers
initializeAccordions();
}
function renderCommandCard(cmd) {
var difficultyColor = {
'Beginner': 'bg-success-light text-success',
'Intermediate': 'bg-warning-light text-warning',
'Advanced': 'bg-error-light text-error'
}[cmd.difficulty] || 'bg-muted text-muted-foreground';
return `
<div class="bg-background border border-border rounded-lg p-4 hover:border-primary transition-colors">
<div class="flex items-start justify-between mb-2">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<code class="text-sm font-mono text-primary font-semibold">${escapeHtml(cmd.command)}</code>
<span class="text-xs px-2 py-0.5 rounded ${difficultyColor}">${escapeHtml(cmd.difficulty)}</span>
</div>
<p class="text-sm text-muted-foreground">${escapeHtml(cmd.description)}</p>
</div>
</div>
${cmd.arguments ? `
<div class="mt-2 text-xs">
<span class="text-muted-foreground">Arguments:</span>
<code class="ml-2 text-foreground">${escapeHtml(cmd.arguments)}</code>
</div>
` : ''}
</div>
`;
}
function renderSubcategoryAccordion(subcatName, commands) {
var accordionId = 'accordion-' + subcatName.replace(/\s+/g, '-').toLowerCase();
return `
<div class="border border-border rounded-lg overflow-hidden">
<button
class="accordion-header w-full px-4 py-3 bg-muted hover:bg-muted/80 text-left flex items-center justify-between transition-colors"
data-accordion="${accordionId}"
>
<div class="flex items-center gap-2">
<i data-lucide="chevron-right" class="accordion-icon w-4 h-4 transition-transform"></i>
<span class="font-medium text-foreground">${escapeHtml(subcatName)}</span>
<span class="text-xs text-muted-foreground ml-2">(${commands.length} commands)</span>
</div>
</button>
<div class="accordion-content hidden">
<div class="p-4 space-y-3 bg-card">
${commands.map(cmd => renderCommandCard(cmd)).join('')}
</div>
</div>
</div>
`;
}
function initializeAccordions() {
var headers = document.querySelectorAll('.accordion-header');
headers.forEach(function(header) {
header.addEventListener('click', function() {
var content = this.nextElementSibling;
var icon = this.querySelector('.accordion-icon');
if (content.classList.contains('hidden')) {
content.classList.remove('hidden');
icon.style.transform = 'rotate(90deg)';
} else {
content.classList.add('hidden');
icon.style.transform = 'rotate(0deg)';
}
});
});
}
// ========== Search Functions ==========
function filterCommandsBySearch(commands, query) {
if (!query) return commands;
var lowerQuery = query.toLowerCase();
return commands.filter(function(cmd) {
return (cmd.name && cmd.name.toLowerCase().includes(lowerQuery)) ||
(cmd.command && cmd.command.toLowerCase().includes(lowerQuery)) ||
(cmd.description && cmd.description.toLowerCase().includes(lowerQuery)) ||
(cmd.category && cmd.category.toLowerCase().includes(lowerQuery));
});
}
async function performHelpSearch() {
// Reload data with search query
try {
var url = '/api/help/commands' + (helpSearchQuery ? '?q=' + encodeURIComponent(helpSearchQuery) : '');
var resp = await fetch(url);
if (resp.ok) {
var data = await resp.json();
helpData.commands = data.commands || [];
helpData.grouped = data.grouped || {};
}
} catch (err) {
console.error('Search failed:', err);
}
// Re-render current tab
if (activeHelpTab !== 'diagrams' && activeHelpTab !== 'codexlens') {
renderCommandsTab(activeHelpTab);
}
}
// ========== Workflow Diagrams ==========
function renderWorkflowDiagrams() {
var container = document.getElementById('helpTabContent');
if (!container) return;
container.innerHTML = `
<div class="workflow-diagrams-section">
<div class="mb-4">
<h3 class="text-lg font-semibold text-foreground mb-3">${ht('help.diagrams.title')}</h3>
<div class="flex gap-2 flex-wrap">
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="tdd">
${ht('help.diagrams.tdd')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="feature">
${ht('help.diagrams.feature')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="bugfix">
${ht('help.diagrams.bugfix')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="review">
${ht('help.diagrams.review')}
</button>
</div>
</div>
<!-- Cytoscape Container -->
<div id="cytoscapeContainer" class="bg-background border border-border rounded-lg" style="height: 600px; min-height: 500px;"></div>
<!-- Diagram Controls -->
<div class="mt-4 flex gap-2">
<button id="fitDiagramBtn" class="px-3 py-2 bg-muted hover:bg-muted/80 rounded-lg text-sm flex items-center gap-2">
<i data-lucide="maximize-2" class="w-4 h-4"></i>
${ht('help.diagrams.fit')}
</button>
<button id="zoomInBtn" class="px-3 py-2 bg-muted hover:bg-muted/80 rounded-lg text-sm flex items-center gap-2">
<i data-lucide="zoom-in" class="w-4 h-4"></i>
${ht('help.diagrams.zoomIn')}
</button>
<button id="zoomOutBtn" class="px-3 py-2 bg-muted hover:bg-muted/80 rounded-lg text-sm flex items-center gap-2">
<i data-lucide="zoom-out" class="w-4 h-4"></i>
${ht('help.diagrams.zoomOut')}
</button>
</div>
<!-- Legend -->
<div class="mt-4 p-4 bg-muted rounded-lg">
<h4 class="text-sm font-semibold text-foreground mb-2">${ht('help.diagrams.legend')}</h4>
<div class="flex gap-4 flex-wrap text-xs">
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-primary"></div>
<span>${ht('help.diagrams.legend.prerequisites')}</span>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-success"></div>
<span>${ht('help.diagrams.legend.nextSteps')}</span>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-warning"></div>
<span>${ht('help.diagrams.legend.alternatives')}</span>
</div>
</div>
</div>
</div>
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
// Initialize workflow diagram buttons
var diagramBtns = document.querySelectorAll('.workflow-diagram-btn');
diagramBtns.forEach(function(btn) {
btn.addEventListener('click', function() {
activeWorkflowDiagram = this.dataset.workflow;
updateActiveWorkflowBtn(activeWorkflowDiagram);
initializeCytoscapeDiagram(activeWorkflowDiagram);
});
});
// Initialize control buttons
var fitBtn = document.getElementById('fitDiagramBtn');
if (fitBtn) {
fitBtn.addEventListener('click', function() {
if (cytoscapeInstance) cytoscapeInstance.fit();
});
}
var zoomInBtn = document.getElementById('zoomInBtn');
if (zoomInBtn) {
zoomInBtn.addEventListener('click', function() {
if (cytoscapeInstance) cytoscapeInstance.zoom(cytoscapeInstance.zoom() * 1.2);
});
}
var zoomOutBtn = document.getElementById('zoomOutBtn');
if (zoomOutBtn) {
zoomOutBtn.addEventListener('click', function() {
if (cytoscapeInstance) cytoscapeInstance.zoom(cytoscapeInstance.zoom() * 0.8);
});
}
// Update active button
updateActiveWorkflowBtn(activeWorkflowDiagram);
// Initialize Cytoscape diagram
setTimeout(function() {
initializeCytoscapeDiagram(activeWorkflowDiagram);
}, 100);
}
function updateActiveWorkflowBtn(workflow) {
var btns = document.querySelectorAll('.workflow-diagram-btn');
btns.forEach(function(btn) {
if (btn.dataset.workflow === workflow) {
btn.classList.add('bg-primary', 'text-primary-foreground');
btn.classList.remove('bg-muted', 'text-muted-foreground');
} else {
btn.classList.remove('bg-primary', 'text-primary-foreground');
btn.classList.add('bg-muted', 'text-muted-foreground');
}
});
}
function initializeCytoscapeDiagram(workflow) {
var container = document.getElementById('cytoscapeContainer');
if (!container) return;
// Destroy previous instance
if (cytoscapeInstance) {
cytoscapeInstance.destroy();
cytoscapeInstance = null;
}
// Get workflow data
var graphData = getWorkflowGraphData(workflow);
// Check if cytoscape is available
if (typeof cytoscape === 'undefined') {
container.innerHTML = '<div class="flex items-center justify-center h-full text-muted-foreground">' + ht('help.diagrams.notLoaded') + '</div>';
return;
}
// Initialize Cytoscape
cytoscapeInstance = cytoscape({
container: container,
elements: graphData,
style: [
{
selector: 'node',
style: {
'background-color': 'hsl(var(--primary))',
'label': 'data(label)',
'color': 'hsl(var(--foreground))',
'text-valign': 'center',
'text-halign': 'center',
'font-size': '12px',
'width': '80px',
'height': '80px',
'text-wrap': 'wrap',
'text-max-width': '70px'
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': 'hsl(var(--muted-foreground))',
'target-arrow-color': 'hsl(var(--muted-foreground))',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
'label': 'data(label)',
'font-size': '10px',
'color': 'hsl(var(--muted-foreground))'
}
},
{
selector: 'edge.prerequisite',
style: {
'line-color': 'hsl(var(--primary))',
'target-arrow-color': 'hsl(var(--primary))'
}
},
{
selector: 'edge.next-step',
style: {
'line-color': '#10B981',
'target-arrow-color': '#10B981'
}
},
{
selector: 'edge.alternative',
style: {
'line-color': '#F59E0B',
'target-arrow-color': '#F59E0B',
'line-style': 'dashed'
}
}
],
layout: {
name: 'dagre',
rankDir: 'TB',
nodeSep: 50,
rankSep: 80
}
});
// Add click handler for nodes
cytoscapeInstance.on('tap', 'node', function(evt) {
var node = evt.target;
var commandName = node.data('id');
showCommandTooltip(commandName, node);
});
// Fit to viewport
cytoscapeInstance.fit();
}
function getWorkflowGraphData(workflow) {
var nodes = [];
var edges = [];
var workflows = {
'tdd': ['workflow:tdd-plan', 'workflow:execute', 'workflow:tdd-verify'],
'feature': ['workflow:plan', 'workflow:action-plan-verify', 'workflow:execute', 'workflow:review'],
'bugfix': ['workflow:lite-fix', 'workflow:lite-execute', 'workflow:test-cycle-execute'],
'review': ['workflow:review-session-cycle', 'workflow:review-fix', 'workflow:test-cycle-execute']
};
var workflowCommands = workflows[workflow] || workflows['tdd'];
console.log('Building workflow diagram for:', workflow);
console.log('Commands:', workflowCommands);
console.log('Available workflows data:', helpData.workflows ? Object.keys(helpData.workflows).length + ' commands' : 'no data');
// Build graph from workflow relationships
workflowCommands.forEach(function(cmd) {
nodes.push({ data: { id: cmd, label: cmd.replace('workflow:', '').replace('task:', '') } });
var relationships = helpData.workflows ? helpData.workflows[cmd] : null;
if (relationships) {
// Add prerequisites
if (relationships.prerequisites) {
relationships.prerequisites.forEach(function(prereq) {
if (!nodes.find(n => n.data.id === prereq)) {
nodes.push({ data: { id: prereq, label: prereq.replace('workflow:', '').replace('task:', '') } });
}
edges.push({
data: { source: prereq, target: cmd, label: 'requires' },
classes: 'prerequisite'
});
});
}
// Add next steps
if (relationships.next_steps) {
relationships.next_steps.forEach(function(next) {
if (!nodes.find(n => n.data.id === next)) {
nodes.push({ data: { id: next, label: next.replace('workflow:', '').replace('task:', '') } });
}
edges.push({
data: { source: cmd, target: next, label: 'then' },
classes: 'next-step'
});
});
}
// Add alternatives
if (relationships.alternatives) {
relationships.alternatives.forEach(function(alt) {
if (!nodes.find(n => n.data.id === alt)) {
nodes.push({ data: { id: alt, label: alt.replace('workflow:', '').replace('task:', '') } });
}
edges.push({
data: { source: cmd, target: alt, label: 'or' },
classes: 'alternative'
});
});
}
}
});
console.log('Generated graph:', nodes.length, 'nodes,', edges.length, 'edges');
// If no edges but we have nodes, create a simple chain
if (edges.length === 0 && nodes.length > 1) {
console.log('No relationships found, creating simple chain');
for (var i = 0; i < nodes.length - 1; i++) {
edges.push({
data: { source: nodes[i].data.id, target: nodes[i + 1].data.id },
classes: 'next-step'
});
}
}
return nodes.concat(edges);
}
function showCommandTooltip(commandName, node) {
// Find command in helpData
var command = helpData.commands.find(function(cmd) {
return cmd.command === '/' + commandName;
});
if (command) {
alert(command.command + '\n\n' + command.description);
}
}
// ========== CodexLens Quick Start ==========
function renderCodexLensQuickStart() {
var container = document.getElementById('helpTabContent');
if (!container) return;
var data = helpData.codexlens;
var html = `
<div class="codexlens-quickstart">
<div class="mb-6">
<h3 class="text-xl font-bold text-foreground mb-2">${ht('help.codexlens.title')}</h3>
<p class="text-muted-foreground">${ht('help.codexlens.subtitle')}</p>
</div>
${data.sections ? data.sections.map(function(section) {
return `
<div class="mb-8">
<h4 class="text-lg font-semibold text-foreground mb-4">${escapeHtml(section.title)}</h4>
<div class="space-y-4">
${section.items.map(function(item) {
return `
<div class="bg-background border border-border rounded-lg p-4">
${item.name ? `<h5 class="font-medium text-foreground mb-2">${escapeHtml(item.name)}</h5>` : ''}
<p class="text-sm text-muted-foreground mb-2">${escapeHtml(item.description)}</p>
${item.command ? `
<div class="bg-muted rounded p-3 mt-2">
<code class="text-xs font-mono text-foreground">${escapeHtml(item.command)}</code>
</div>
` : ''}
</div>
`;
}).join('')}
</div>
</div>
`;
}).join('') : ''}
${data.links && data.links.length > 0 ? `
<div class="mt-8 p-4 bg-muted rounded-lg">
<h4 class="text-sm font-semibold text-foreground mb-3">Additional Resources</h4>
<div class="space-y-2">
${data.links.map(function(link) {
return `
<a href="${escapeHtml(link.url)}" class="block text-sm text-primary hover:underline">
<i data-lucide="external-link" class="w-3 h-3 inline-block mr-1"></i>
${escapeHtml(link.text)}
</a>
`;
}).join('')}
</div>
</div>
` : ''}
</div>
`;
container.innerHTML = html;
if (typeof lucide !== 'undefined') lucide.createIcons();
}

View File

@@ -443,6 +443,10 @@
<span class="nav-text flex-1" data-i18n="nav.claudeManager">CLAUDE.md</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeClaude">0</span>
</li>
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="help" data-tooltip="Help & Guide">
<i data-lucide="help-circle" class="nav-icon"></i>
<span class="nav-text flex-1" data-i18n="nav.help">Help</span>
</li>
</ul>
</div>

View File

@@ -10,7 +10,7 @@
import { z } from 'zod';
import type { ToolSchema, ToolResult } from '../types/tool.js';
import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync } from 'fs';
import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync, statSync } from 'fs';
import { resolve, isAbsolute, dirname, basename } from 'path';
// Define Zod schema for validation
@@ -66,6 +66,39 @@ function createBackup(filePath: string): string | null {
}
}
/**
* Verify file write operation completed successfully
* @param filePath - Path to written file
* @param expectedBytes - Expected file size in bytes
* @param encoding - File encoding used
* @returns Error message if verification fails, null if successful
*/
function verifyFileWrite(filePath: string, expectedBytes: number, encoding: BufferEncoding): string | null {
// Check 1: File exists
if (!existsSync(filePath)) {
return `File verification failed: file does not exist at ${filePath}`;
}
try {
// Check 2: File size matches expected bytes
const stats = statSync(filePath);
if (stats.size !== expectedBytes) {
return `File verification failed: size mismatch (expected ${expectedBytes}B, actual ${stats.size}B)`;
}
// Check 3: File is readable (for long JSON files)
const readContent = readFileSync(filePath, { encoding });
const actualBytes = Buffer.byteLength(readContent, encoding);
if (actualBytes !== expectedBytes) {
return `File verification failed: content size mismatch after read (expected ${expectedBytes}B, read ${actualBytes}B)`;
}
return null; // Verification passed
} catch (error) {
return `File verification failed: ${(error as Error).message}`;
}
}
// Tool schema for MCP
export const schema: ToolSchema = {
name: 'write_file',
@@ -152,14 +185,23 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
writeFileSync(resolvedPath, content, { encoding });
const bytes = Buffer.byteLength(content, encoding);
// Verify write operation completed successfully
const verificationError = verifyFileWrite(resolvedPath, bytes, encoding as BufferEncoding);
if (verificationError) {
return {
success: false,
error: verificationError,
};
}
// Build compact message
let message: string;
if (fileExists) {
message = backupPath
? `Overwrote (${bytes}B, backup: ${basename(backupPath)})`
: `Overwrote (${bytes}B)`;
? `Overwrote (${bytes}B, backup: ${basename(backupPath)}) - verified`
: `Overwrote (${bytes}B) - verified`;
} else {
message = `Created (${bytes}B)`;
message = `Created (${bytes}B) - verified`;
}
return {