mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-06 01:54:11 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5114a942dc | ||
|
|
edef937822 | ||
|
|
faa86eded0 | ||
|
|
44fa6e0a42 | ||
|
|
be9a1c76d4 | ||
|
|
fcc811d6a1 | ||
|
|
906404f075 | ||
|
|
1267c8d0f4 | ||
|
|
eb1093128e | ||
|
|
4ddeb6551e | ||
|
|
7252c2ff3d |
@@ -67,7 +67,9 @@ Phase 4: Execution Strategy & Task Execution
|
||||
├─ Get next in_progress task from TodoWrite
|
||||
├─ Lazy load task JSON
|
||||
├─ Launch agent with task context
|
||||
├─ Mark task completed
|
||||
├─ Mark task completed (update IMPL-*.json status)
|
||||
│ # Quick fix: Update task status for ccw dashboard
|
||||
│ # TS=$(date -Iseconds) && jq --arg ts "$TS" '.status="completed" | .status_history=(.status_history // [])+[{"from":"in_progress","to":"completed","changed_at":$ts}]' IMPL-X.json > tmp.json && mv tmp.json IMPL-X.json
|
||||
└─ Advance to next task
|
||||
|
||||
Phase 5: Completion
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
name: command-guide
|
||||
description: Workflow command guide for Claude DMS3 (78 commands). Search/browse commands, get next-step recommendations, view documentation, report issues. Triggers "CCW-help", "CCW-issue", "ccw-help", "ccw-issue", "ccw"
|
||||
description: Workflow command guide for Claude Code Workflow (78 commands). Search/browse commands, get next-step recommendations, view documentation, report issues. Triggers "CCW-help", "CCW-issue", "ccw-help", "ccw-issue", "ccw"
|
||||
allowed-tools: Read, Grep, Glob, AskUserQuestion
|
||||
version: 5.8.0
|
||||
---
|
||||
|
||||
# Command Guide Skill
|
||||
|
||||
Comprehensive command guide for Claude DMS3 workflow system covering 78 commands across 5 categories (workflow, cli, memory, task, general).
|
||||
Comprehensive command guide for Claude Code Workflow (CCW) system covering 78 commands across 5 categories (workflow, cli, memory, task, general).
|
||||
|
||||
## 🆕 What's New in v5.8.0
|
||||
|
||||
@@ -385,4 +385,4 @@ This SKILL documentation is kept in sync with command implementations through a
|
||||
- 4 issue templates for standardized problem reporting
|
||||
- CLI-assisted complex query analysis with gemini/qwen integration
|
||||
|
||||
**Maintainer**: Claude DMS3 Team
|
||||
**Maintainer**: CCW Team
|
||||
|
||||
@@ -12,79 +12,68 @@
|
||||
|
||||
## ⚡ CCW edit_file Tool (AI-Powered Editing)
|
||||
|
||||
**When to Use**: Edit tool fails 2+ times on same file
|
||||
**When to Use**: Edit tool fails 1+ times on same file
|
||||
|
||||
### update Mode (Default)
|
||||
### Usage
|
||||
|
||||
**Best for**: Code block replacements, function rewrites, multi-line changes
|
||||
|
||||
```bash
|
||||
ccw tool exec edit_file '{
|
||||
"path": "file.py",
|
||||
"oldText": "def old():\n pass",
|
||||
"newText": "def new():\n return True"
|
||||
}'
|
||||
ccw tool exec edit_file --path "file.py" --old "def old():
|
||||
pass" --new "def new():
|
||||
return True"
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `--path`: File path to edit
|
||||
- `--old`: Text to find and replace
|
||||
- `--new`: New text to insert
|
||||
|
||||
**Features**:
|
||||
- ✅ Exact text matching (precise and predictable)
|
||||
- ✅ Auto line ending adaptation (CRLF/LF)
|
||||
- ✅ Simple `oldText` → `newText` replacement
|
||||
- ✅ No special markers needed
|
||||
|
||||
### line Mode (Precise Line Operations)
|
||||
|
||||
**Best for**: Config files, line insertions/deletions, precise line number control
|
||||
|
||||
```bash
|
||||
# Insert after specific line
|
||||
ccw tool exec edit_file '{
|
||||
"path": "config.txt",
|
||||
"mode": "line",
|
||||
"operation": "insert_after",
|
||||
"line": 10,
|
||||
"text": "new config line"
|
||||
}'
|
||||
|
||||
# Delete line range
|
||||
ccw tool exec edit_file '{
|
||||
"path": "log.txt",
|
||||
"mode": "line",
|
||||
"operation": "delete",
|
||||
"line": 5,
|
||||
"end_line": 8
|
||||
}'
|
||||
|
||||
# Replace specific line
|
||||
ccw tool exec edit_file '{
|
||||
"path": "script.sh",
|
||||
"mode": "line",
|
||||
"operation": "replace",
|
||||
"line": 3,
|
||||
"text": "#!/bin/bash"
|
||||
}'
|
||||
```
|
||||
|
||||
**Operations**:
|
||||
- `insert_before`: Insert text before specified line
|
||||
- `insert_after`: Insert text after specified line
|
||||
- `replace`: Replace line or line range
|
||||
- `delete`: Delete line or line range
|
||||
|
||||
### Mode Selection Guide
|
||||
|
||||
| Scenario | Mode | Reason |
|
||||
|----------|------|--------|
|
||||
| Code refactoring | update | Content-driven replacement |
|
||||
| Function rewrite | update | Simple oldText/newText |
|
||||
| Config line change | line | Precise line number control |
|
||||
| Insert at specific position | line | Exact line number needed |
|
||||
| Delete line range | line | Line-based operation |
|
||||
- ✅ No JSON escaping issues
|
||||
- ✅ Multi-line text supported with quotes
|
||||
|
||||
### Fallback Strategy
|
||||
|
||||
1. **Edit fails 1+ times** → Use `ccw tool exec edit_file` (update mode)
|
||||
2. **update mode fails** → Try line mode with precise line numbers
|
||||
3. **All fails** → Use Write to recreate file
|
||||
1. **Edit fails 1+ times** → Use `ccw tool exec edit_file`
|
||||
2. **Still fails** → Use Write to recreate file
|
||||
|
||||
**Default mode**: update (exact matching with line ending adaptation)
|
||||
## ⚡ sed Line Operations (Line Mode Alternative)
|
||||
|
||||
**When to Use**: Precise line number control (insert, delete, replace specific lines)
|
||||
|
||||
### Common Operations
|
||||
|
||||
```bash
|
||||
# Insert after line 10
|
||||
sed -i '10a\new line content' file.txt
|
||||
|
||||
# Insert before line 5
|
||||
sed -i '5i\new line content' file.txt
|
||||
|
||||
# Delete line 3
|
||||
sed -i '3d' file.txt
|
||||
|
||||
# Delete lines 5-8
|
||||
sed -i '5,8d' file.txt
|
||||
|
||||
# Replace line 3 content
|
||||
sed -i '3c\replacement line' file.txt
|
||||
|
||||
# Replace lines 3-5 content
|
||||
sed -i '3,5c\single replacement line' file.txt
|
||||
```
|
||||
|
||||
### Operation Reference
|
||||
|
||||
| Operation | Command | Example |
|
||||
|-----------|---------|---------|
|
||||
| Insert after | `Na\text` | `sed -i '10a\new' file` |
|
||||
| Insert before | `Ni\text` | `sed -i '5i\new' file` |
|
||||
| Delete line | `Nd` | `sed -i '3d' file` |
|
||||
| Delete range | `N,Md` | `sed -i '5,8d' file` |
|
||||
| Replace line | `Nc\text` | `sed -i '3c\new' file` |
|
||||
|
||||
**Note**: Use `sed -i` for in-place file modification (works in Git Bash on Windows)
|
||||
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -5,6 +5,33 @@ All notable changes to Claude Code Workflow (CCW) will be documented in this fil
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [6.1.3] - 2025-12-09
|
||||
|
||||
### 🔧 CLI Tool Simplification
|
||||
|
||||
This release simplifies the `ccw tool exec edit_file` command for better usability.
|
||||
|
||||
#### 🔄 Changed
|
||||
- **Simplified edit_file**: Removed JSON input support, now uses parameter-based input only (`--path`, `--old`, `--new`)
|
||||
- **Removed line mode**: Line operations now recommended via `sed` command
|
||||
- **Updated tool-strategy.md**: Added sed as line operation alternative with usage examples
|
||||
|
||||
#### Usage
|
||||
```bash
|
||||
ccw tool exec edit_file --path "file.txt" --old "old text" --new "new text"
|
||||
```
|
||||
|
||||
## [6.1.2] - 2025-12-09
|
||||
|
||||
### 🔔 Dashboard Update Notification & Bug Fixes
|
||||
|
||||
#### ✨ Added
|
||||
- **Version Update Notification**: Dashboard now checks npm registry for updates and displays upgrade banner
|
||||
- **Version Check API**: New `/api/version-check` endpoint with 1-hour cache
|
||||
|
||||
#### 🐛 Fixed
|
||||
- **Hook Manager**: Fixed button click event handling for edit/delete operations (changed `e.target` to `e.currentTarget`)
|
||||
|
||||
## [5.9.6] - 2025-11-28
|
||||
|
||||
### 🚀 Review Cycle & Dashboard Enhancement
|
||||
|
||||
25
README.md
25
README.md
@@ -1,8 +1,11 @@
|
||||
# 🚀 Claude Code Workflow (CCW)
|
||||
|
||||
[](https://smithery.ai/skills?ns=catlog22&utm_source=github&utm_medium=badge)
|
||||
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/catlog22/Claude-Code-Workflow/releases)
|
||||
[](https://github.com/catlog22/Claude-Code-Workflow/releases)
|
||||
[](https://www.npmjs.com/package/claude-code-workflow)
|
||||
[](LICENSE)
|
||||
[]()
|
||||
@@ -15,13 +18,11 @@
|
||||
|
||||
**Claude Code Workflow (CCW)** is a JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution. It transforms AI development from simple prompt chaining into a powerful orchestration system.
|
||||
|
||||
> **🎉 Version 6.1.0: Dashboard Icon Unification & CCW Tool System**
|
||||
> **🎉 Version 6.1.3: CLI Tool Simplification**
|
||||
>
|
||||
> **Core Improvements**:
|
||||
> - 🎨 **Dashboard Icon Unification**: Complete migration to Lucide Icons library across all views
|
||||
> - 🛠️ **CCW Tool Exec System**: New `ccw tool exec` command for executing tools with JSON parameters
|
||||
> - 🚀 **Explorer Enhancements**: Async task execution, CLI selector improvements, WebSocket frame handling
|
||||
> - ✨ **Smart Server Recognition**: Intelligent workspace switching and MCP multi-source configuration
|
||||
> - 🔧 **Simplified edit_file**: Parameter-based input only (`--path`, `--old`, `--new`)
|
||||
> - 📝 **Updated tool-strategy.md**: Added sed as line operation alternative
|
||||
>
|
||||
> See [CHANGELOG.md](CHANGELOG.md) for complete details.
|
||||
|
||||
@@ -63,18 +64,6 @@ ccw install -m Global
|
||||
ccw install -m Path -p /path/to/project
|
||||
```
|
||||
|
||||
### **🚀 Alternative: One-Click Script Install**
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
Invoke-Expression (Invoke-WebRequest -Uri "https://raw.githubusercontent.com/catlog22/Claude-Code-Workflow/main/install-remote.ps1" -UseBasicParsing).Content
|
||||
```
|
||||
|
||||
**Linux/macOS (Bash/Zsh):**
|
||||
```bash
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/catlog22/Claude-Code-Workflow/main/install-remote.sh)
|
||||
```
|
||||
|
||||
### **✅ Verify Installation**
|
||||
After installation, open **Claude Code** and verify that workflow commands are available by running:
|
||||
```bash
|
||||
|
||||
@@ -108,9 +108,12 @@ export function run(argv) {
|
||||
|
||||
// Tool command
|
||||
program
|
||||
.command('tool [subcommand] [args] [json]')
|
||||
.command('tool [subcommand] [args]')
|
||||
.description('Execute CCW tools')
|
||||
.action((subcommand, args, json) => toolCommand(subcommand, args, { json }));
|
||||
.option('--path <path>', 'File path (for edit_file)')
|
||||
.option('--old <text>', 'Old text to replace (for edit_file)')
|
||||
.option('--new <text>', 'New text (for edit_file)')
|
||||
.action((subcommand, args, options) => toolCommand(subcommand, args, options));
|
||||
|
||||
program.parse(argv);
|
||||
}
|
||||
|
||||
@@ -66,80 +66,13 @@ async function schemaAction(options) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from stdin if available
|
||||
*/
|
||||
async function readStdin() {
|
||||
// Check if stdin is a TTY (interactive terminal)
|
||||
if (process.stdin.isTTY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = process.stdin.read()) !== null) {
|
||||
data += chunk;
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
resolve(data.trim() || null);
|
||||
});
|
||||
|
||||
process.stdin.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart JSON parser with Windows path handling
|
||||
*/
|
||||
function parseJsonWithPathFix(jsonString) {
|
||||
try {
|
||||
// Try normal parse first
|
||||
return JSON.parse(jsonString);
|
||||
} catch (firstError) {
|
||||
// If parsing fails, try to fix Windows paths
|
||||
try {
|
||||
// Pattern: "path": "X:\..." or "path":"X:\..."
|
||||
const fixedJson = jsonString.replace(
|
||||
/("(?:path|file|target|source|dest|destination)":\s*")([A-Za-z]:[^"]+)"/g,
|
||||
(match, prefix, path) => {
|
||||
// Convert backslashes to forward slashes (universal)
|
||||
const fixedPath = path.replace(/\\/g, '/');
|
||||
return `${prefix}${fixedPath}"`;
|
||||
}
|
||||
);
|
||||
|
||||
return JSON.parse(fixedJson);
|
||||
} catch (secondError) {
|
||||
// If still fails, throw original error with helpful message
|
||||
const errorMsg = firstError.message;
|
||||
const hint = errorMsg.includes('escaped character') || errorMsg.includes('position')
|
||||
? '\n\n' + chalk.yellow('Hint: Windows paths in JSON need forward slashes or double backslashes:') +
|
||||
'\n ' + chalk.green('✓ "D:/Claude_dms3/file.md"') +
|
||||
'\n ' + chalk.green('✓ "D:\\\\Claude_dms3\\\\file.md"') +
|
||||
'\n ' + chalk.red('✗ "D:\\Claude_dms3\\file.md"')
|
||||
: '';
|
||||
|
||||
throw new Error(errorMsg + hint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a tool with given parameters
|
||||
*/
|
||||
async function execAction(toolName, jsonInput, options) {
|
||||
async function execAction(toolName, options) {
|
||||
if (!toolName) {
|
||||
console.error(chalk.red('Tool name is required'));
|
||||
console.error(chalk.gray('Usage: ccw tool exec <tool-name> \'{"param": "value"}\''));
|
||||
console.error(chalk.gray('Usage: ccw tool exec edit_file --path file.txt --old "old" --new "new"'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -150,34 +83,22 @@ async function execAction(toolName, jsonInput, options) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse JSON input (default format)
|
||||
let params = {};
|
||||
// Build params from CLI options
|
||||
const params = {};
|
||||
|
||||
if (jsonInput) {
|
||||
try {
|
||||
params = parseJsonWithPathFix(jsonInput);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Invalid JSON: ${error.message}`));
|
||||
if (toolName === 'edit_file') {
|
||||
if (!options.path || !options.old || !options.new) {
|
||||
console.error(chalk.red('edit_file requires --path, --old, and --new parameters'));
|
||||
console.error(chalk.gray('Usage: ccw tool exec edit_file --path file.txt --old "old text" --new "new text"'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for stdin input (for piped commands)
|
||||
const stdinData = await readStdin();
|
||||
if (stdinData) {
|
||||
// If tool has an 'input' parameter, use it
|
||||
// Otherwise, try to parse stdin as JSON and merge with params
|
||||
if (tool.parameters?.properties?.input) {
|
||||
params.input = stdinData;
|
||||
} else {
|
||||
try {
|
||||
const stdinJson = JSON.parse(stdinData);
|
||||
params = { ...stdinJson, ...params };
|
||||
} catch {
|
||||
// If not JSON, store as 'input' anyway
|
||||
params.input = stdinData;
|
||||
}
|
||||
}
|
||||
params.path = options.path;
|
||||
params.oldText = options.old;
|
||||
params.newText = options.new;
|
||||
} else {
|
||||
console.error(chalk.red(`Tool "${toolName}" is not supported via CLI parameters`));
|
||||
console.error(chalk.gray('Currently only edit_file is supported'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Execute tool
|
||||
@@ -200,7 +121,7 @@ export async function toolCommand(subcommand, args, options) {
|
||||
await schemaAction({ name: args });
|
||||
break;
|
||||
case 'exec':
|
||||
await execAction(args, options.json, options);
|
||||
await execAction(args, options);
|
||||
break;
|
||||
default:
|
||||
console.log(chalk.bold.cyan('\nCCW Tool System\n'));
|
||||
@@ -209,9 +130,9 @@ export async function toolCommand(subcommand, args, options) {
|
||||
console.log(chalk.gray(' schema [name] Show tool schema (JSON)'));
|
||||
console.log(chalk.gray(' exec <name> Execute a tool'));
|
||||
console.log();
|
||||
console.log('Examples:');
|
||||
console.log('Usage:');
|
||||
console.log(chalk.gray(' ccw tool list'));
|
||||
console.log(chalk.gray(' ccw tool schema edit_file'));
|
||||
console.log(chalk.gray(' ccw tool exec edit_file \'{"path":"file.txt","oldText":"old","newText":"new"}\''));
|
||||
console.log(chalk.gray(' ccw tool exec edit_file --path file.txt --old "old text" --new "new text"'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ function scanLiteDir(dir, type) {
|
||||
tasks: loadTaskJsons(sessionPath)
|
||||
};
|
||||
|
||||
// For lite-fix sessions, also load diagnoses separately
|
||||
if (type === 'lite-fix') {
|
||||
session.diagnoses = loadDiagnoses(sessionPath);
|
||||
}
|
||||
|
||||
// Calculate progress
|
||||
session.progress = calculateProgress(session.tasks);
|
||||
|
||||
@@ -268,7 +273,7 @@ export function getLiteTaskDetail(workflowDir, type, sessionId) {
|
||||
|
||||
if (!existsSync(dir)) return null;
|
||||
|
||||
return {
|
||||
const detail = {
|
||||
id: sessionId,
|
||||
type,
|
||||
path: dir,
|
||||
@@ -277,6 +282,13 @@ export function getLiteTaskDetail(workflowDir, type, sessionId) {
|
||||
explorations: loadExplorations(dir),
|
||||
clarifications: loadClarifications(dir)
|
||||
};
|
||||
|
||||
// For lite-fix sessions, also load diagnoses
|
||||
if (type === 'lite-fix') {
|
||||
detail.diagnoses = loadDiagnoses(dir);
|
||||
}
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,3 +324,50 @@ function loadClarifications(sessionPath) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load diagnosis files for lite-fix sessions
|
||||
* Loads diagnosis-*.json files from session root directory
|
||||
* @param {string} sessionPath - Session directory path
|
||||
* @returns {Object} - Diagnoses data with manifest and items
|
||||
*/
|
||||
function loadDiagnoses(sessionPath) {
|
||||
const result = {
|
||||
manifest: null,
|
||||
items: []
|
||||
};
|
||||
|
||||
// Try to load diagnoses-manifest.json first
|
||||
const manifestPath = join(sessionPath, 'diagnoses-manifest.json');
|
||||
if (existsSync(manifestPath)) {
|
||||
try {
|
||||
result.manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||
} catch {
|
||||
// Continue without manifest
|
||||
}
|
||||
}
|
||||
|
||||
// Load all diagnosis-*.json files from session root
|
||||
try {
|
||||
const diagnosisFiles = readdirSync(sessionPath)
|
||||
.filter(f => f.startsWith('diagnosis-') && f.endsWith('.json'));
|
||||
|
||||
for (const file of diagnosisFiles) {
|
||||
const filePath = join(sessionPath, file);
|
||||
try {
|
||||
const content = JSON.parse(readFileSync(filePath, 'utf8'));
|
||||
result.items.push({
|
||||
id: file.replace('diagnosis-', '').replace('.json', ''),
|
||||
filename: file,
|
||||
...content
|
||||
});
|
||||
} catch {
|
||||
// Skip invalid files
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Return empty items if directory read fails
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ const MODULE_FILES = [
|
||||
'components/carousel.js',
|
||||
'components/notifications.js',
|
||||
'components/global-notifications.js',
|
||||
'components/version-check.js',
|
||||
'components/mcp-manager.js',
|
||||
'components/hook-manager.js',
|
||||
'components/_exp_helpers.js',
|
||||
@@ -191,6 +192,15 @@ export async function startServer(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Version check (check for npm updates)
|
||||
if (pathname === '/api/version-check') {
|
||||
const versionData = await checkNpmVersion();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(versionData));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// API: Shutdown server (for ccw stop command)
|
||||
if (pathname === '/api/shutdown' && req.method === 'POST') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
@@ -1946,3 +1956,108 @@ async function triggerUpdateClaudeMd(targetPath, tool, strategy) {
|
||||
}, 300000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// Version Check Functions
|
||||
// ========================================
|
||||
|
||||
// Package name on npm registry
|
||||
const NPM_PACKAGE_NAME = 'claude-code-workflow';
|
||||
|
||||
// Cache for version check (avoid too frequent requests)
|
||||
let versionCheckCache = null;
|
||||
let versionCheckTime = 0;
|
||||
const VERSION_CHECK_CACHE_TTL = 3600000; // 1 hour
|
||||
|
||||
/**
|
||||
* Get current package version from package.json
|
||||
* @returns {string}
|
||||
*/
|
||||
function getCurrentVersion() {
|
||||
try {
|
||||
const packageJsonPath = join(import.meta.dirname, '../../../package.json');
|
||||
if (existsSync(packageJsonPath)) {
|
||||
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||
return pkg.version || '0.0.0';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error reading package.json:', e);
|
||||
}
|
||||
return '0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check npm registry for latest version
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function checkNpmVersion() {
|
||||
// Return cached result if still valid
|
||||
const now = Date.now();
|
||||
if (versionCheckCache && (now - versionCheckTime) < VERSION_CHECK_CACHE_TTL) {
|
||||
return versionCheckCache;
|
||||
}
|
||||
|
||||
const currentVersion = getCurrentVersion();
|
||||
|
||||
try {
|
||||
// Fetch latest version from npm registry
|
||||
const npmUrl = 'https://registry.npmjs.org/' + encodeURIComponent(NPM_PACKAGE_NAME) + '/latest';
|
||||
const response = await fetch(npmUrl, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const latestVersion = data.version;
|
||||
|
||||
// Compare versions
|
||||
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
|
||||
|
||||
const result = {
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
hasUpdate,
|
||||
packageName: NPM_PACKAGE_NAME,
|
||||
updateCommand: 'npm update -g ' + NPM_PACKAGE_NAME,
|
||||
checkedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Cache the result
|
||||
versionCheckCache = result;
|
||||
versionCheckTime = now;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Version check failed:', error.message);
|
||||
return {
|
||||
currentVersion,
|
||||
latestVersion: null,
|
||||
hasUpdate: false,
|
||||
error: error.message,
|
||||
checkedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two semver versions
|
||||
* @param {string} v1
|
||||
* @param {string} v2
|
||||
* @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
||||
*/
|
||||
function compareVersions(v1, v2) {
|
||||
const parts1 = v1.split('.').map(Number);
|
||||
const parts2 = v2.split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const p1 = parts1[i] || 0;
|
||||
const p2 = parts2[i] || 0;
|
||||
if (p1 > p2) return 1;
|
||||
if (p1 < p2) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -159,3 +159,133 @@ body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
/* ===================================
|
||||
Version Update Banner
|
||||
=================================== */
|
||||
|
||||
.version-update-banner {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: linear-gradient(135deg, hsl(var(--primary) / 0.1), hsl(var(--accent) / 0.1));
|
||||
border-bottom: 1px solid hsl(var(--primary) / 0.3);
|
||||
padding: 0.75rem 1rem;
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.version-update-banner.show {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.version-banner-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.version-banner-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.version-banner-text {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.version-banner-text code {
|
||||
background: hsl(var(--muted));
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.version-banner-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--primary-foreground));
|
||||
background: hsl(var(--primary));
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.1s;
|
||||
}
|
||||
|
||||
.version-banner-btn:hover {
|
||||
background: hsl(var(--primary) / 0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.version-banner-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.version-banner-btn.secondary {
|
||||
background: hsl(var(--secondary));
|
||||
color: hsl(var(--secondary-foreground));
|
||||
}
|
||||
|
||||
.version-banner-btn.secondary:hover {
|
||||
background: hsl(var(--secondary) / 0.8);
|
||||
}
|
||||
|
||||
.version-banner-close {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.version-banner-close:hover {
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
/* Mobile responsiveness for banner */
|
||||
@media (max-width: 640px) {
|
||||
.version-banner-content {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.version-banner-text {
|
||||
width: 100%;
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.version-banner-btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.version-banner-close {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.version-update-banner {
|
||||
position: relative;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,3 +841,331 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Fix Plan Enhanced Styles
|
||||
=================================== */
|
||||
|
||||
.plan-root-cause-text,
|
||||
.plan-strategy-text,
|
||||
.plan-requirements-text {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
border-left: 3px solid hsl(var(--primary));
|
||||
}
|
||||
|
||||
.severity-badge,
|
||||
.risk-badge {
|
||||
display: inline-block;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.severity-badge.critical,
|
||||
.risk-badge.high {
|
||||
background: hsl(0 70% 50% / 0.15);
|
||||
color: hsl(0 70% 45%);
|
||||
}
|
||||
|
||||
.severity-badge.high,
|
||||
.risk-badge.medium {
|
||||
background: hsl(30 90% 50% / 0.15);
|
||||
color: hsl(30 90% 40%);
|
||||
}
|
||||
|
||||
.severity-badge.medium {
|
||||
background: hsl(45 90% 50% / 0.15);
|
||||
color: hsl(45 80% 35%);
|
||||
}
|
||||
|
||||
.severity-badge.low,
|
||||
.risk-badge.low {
|
||||
background: hsl(142 70% 50% / 0.15);
|
||||
color: hsl(142 70% 35%);
|
||||
}
|
||||
|
||||
.fix-tasks-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.fix-task-summary-item {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fix-task-summary-item .collapsible-header {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.task-num {
|
||||
font-weight: 600;
|
||||
color: hsl(var(--primary));
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.task-title-brief {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--foreground));
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-scope-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.task-detail-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.task-detail-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.task-detail-section strong {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mod-points-list,
|
||||
.verify-list {
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.mod-points-list li,
|
||||
.verify-list li {
|
||||
margin-bottom: 0.375rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.mod-points-list code {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 0.25rem;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.func-name {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.change-type {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.7rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Diagnoses Tab Styles
|
||||
=================================== */
|
||||
|
||||
.diagnoses-tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.diagnoses-section-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.diagnoses-manifest-section {
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.manifest-meta-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.diagnoses-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.diagnosis-card {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diagnosis-header {
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
}
|
||||
|
||||
.diagnosis-id {
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.diag-section {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border) / 0.5);
|
||||
}
|
||||
|
||||
.diag-section:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.diag-section strong {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.diag-section p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.issues-list {
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.issue-item {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.issue-title {
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.issue-location {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.issue-location code {
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 0.25rem;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.contracts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.contract-item {
|
||||
padding: 0.75rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.contract-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.contract-endpoint {
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.contract-method {
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: hsl(var(--primary) / 0.15);
|
||||
color: hsl(var(--primary));
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.contract-desc {
|
||||
font-size: 0.8rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.contract-issues {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(0 70% 50%);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.dataflow-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.df-item {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.df-label {
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.df-transforms {
|
||||
margin: 0.25rem 0 0 1rem;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.recommendations-list {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.recommendations-list li {
|
||||
margin-bottom: 0.375rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
167
ccw/src/templates/dashboard-js/components/version-check.js
Normal file
167
ccw/src/templates/dashboard-js/components/version-check.js
Normal file
@@ -0,0 +1,167 @@
|
||||
// ==========================================
|
||||
// VERSION CHECK COMPONENT
|
||||
// ==========================================
|
||||
// Checks for npm package updates and displays upgrade notification
|
||||
|
||||
// State
|
||||
let versionCheckData = null;
|
||||
let versionBannerDismissed = false;
|
||||
|
||||
/**
|
||||
* Initialize version check on page load
|
||||
*/
|
||||
async function initVersionCheck() {
|
||||
// Check version after a short delay to not block initial render
|
||||
setTimeout(async () => {
|
||||
await checkForUpdates();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for package updates
|
||||
*/
|
||||
async function checkForUpdates() {
|
||||
try {
|
||||
const res = await fetch('/api/version-check');
|
||||
if (!res.ok) return;
|
||||
|
||||
versionCheckData = await res.json();
|
||||
|
||||
if (versionCheckData.hasUpdate && !versionBannerDismissed) {
|
||||
showUpdateBanner(versionCheckData);
|
||||
addGlobalNotification(
|
||||
'info',
|
||||
'Update Available',
|
||||
'Version ' + versionCheckData.latestVersion + ' is now available. Current: ' + versionCheckData.currentVersion,
|
||||
'system'
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Version check skipped:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show update banner at top of page
|
||||
*/
|
||||
function showUpdateBanner(data) {
|
||||
// Remove existing banner if any
|
||||
const existing = document.getElementById('versionUpdateBanner');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const banner = document.createElement('div');
|
||||
banner.id = 'versionUpdateBanner';
|
||||
banner.className = 'version-update-banner';
|
||||
banner.innerHTML = '\
|
||||
<div class="version-banner-content">\
|
||||
<span class="version-banner-icon">🚀</span>\
|
||||
<span class="version-banner-text">\
|
||||
<strong>Update Available!</strong> \
|
||||
Version <code>' + escapeHtml(data.latestVersion) + '</code> is available \
|
||||
(you have <code>' + escapeHtml(data.currentVersion) + '</code>)\
|
||||
</span>\
|
||||
<button class="version-banner-btn" onclick="copyUpdateCommand()">\
|
||||
<span>📋</span> Copy Command\
|
||||
</button>\
|
||||
<button class="version-banner-btn secondary" onclick="showUpdateModal()">\
|
||||
<span>ℹ️</span> Details\
|
||||
</button>\
|
||||
<button class="version-banner-close" onclick="dismissUpdateBanner()" title="Dismiss">\
|
||||
×\
|
||||
</button>\
|
||||
</div>\
|
||||
';
|
||||
|
||||
// Insert at top of main content
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
if (mainContent) {
|
||||
mainContent.insertBefore(banner, mainContent.firstChild);
|
||||
} else {
|
||||
document.body.insertBefore(banner, document.body.firstChild);
|
||||
}
|
||||
|
||||
// Animate in
|
||||
requestAnimationFrame(() => banner.classList.add('show'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss update banner
|
||||
*/
|
||||
function dismissUpdateBanner() {
|
||||
versionBannerDismissed = true;
|
||||
const banner = document.getElementById('versionUpdateBanner');
|
||||
if (banner) {
|
||||
banner.classList.remove('show');
|
||||
setTimeout(() => banner.remove(), 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy update command to clipboard
|
||||
*/
|
||||
async function copyUpdateCommand() {
|
||||
if (!versionCheckData) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(versionCheckData.updateCommand);
|
||||
addGlobalNotification('success', 'Command copied to clipboard', versionCheckData.updateCommand, 'version-check');
|
||||
} catch (err) {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = versionCheckData.updateCommand;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
addGlobalNotification('success', 'Command copied to clipboard', null, 'version-check');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show update details modal
|
||||
*/
|
||||
function showUpdateModal() {
|
||||
if (!versionCheckData) return;
|
||||
|
||||
const content = '\
|
||||
# Update Available\n\
|
||||
\n\
|
||||
A new version of Claude Code Workflow is available!\n\
|
||||
\n\
|
||||
| Property | Value |\n\
|
||||
|----------|-------|\n\
|
||||
| Current Version | `' + versionCheckData.currentVersion + '` |\n\
|
||||
| Latest Version | `' + versionCheckData.latestVersion + '` |\n\
|
||||
| Package | `' + versionCheckData.packageName + '` |\n\
|
||||
\n\
|
||||
## Update Command\n\
|
||||
\n\
|
||||
```bash\n\
|
||||
' + versionCheckData.updateCommand + '\n\
|
||||
```\n\
|
||||
\n\
|
||||
## Alternative Methods\n\
|
||||
\n\
|
||||
### Using ccw upgrade command\n\
|
||||
```bash\n\
|
||||
ccw upgrade\n\
|
||||
```\n\
|
||||
\n\
|
||||
### Fresh install\n\
|
||||
```bash\n\
|
||||
npm install -g ' + versionCheckData.packageName + '@latest\n\
|
||||
```\n\
|
||||
\n\
|
||||
---\n\
|
||||
*Checked at: ' + new Date(versionCheckData.checkedAt).toLocaleString() + '*\n\
|
||||
';
|
||||
|
||||
showMarkdownModal(content, 'Update Available - v' + versionCheckData.latestVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current version info (for display in UI)
|
||||
*/
|
||||
function getVersionInfo() {
|
||||
return versionCheckData;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
try { initMcpManager(); } catch (e) { console.error('MCP Manager init failed:', e); }
|
||||
try { initHookManager(); } catch (e) { console.error('Hook Manager init failed:', e); }
|
||||
try { initGlobalNotifications(); } catch (e) { console.error('Global notifications init failed:', e); }
|
||||
try { initVersionCheck(); } catch (e) { console.error('Version check init failed:', e); }
|
||||
|
||||
// Initialize real-time features (WebSocket + auto-refresh)
|
||||
try { initWebSocket(); } catch (e) { console.log('WebSocket not available:', e.message); }
|
||||
|
||||
@@ -330,9 +330,10 @@ function attachHookEventListeners() {
|
||||
// Edit buttons
|
||||
document.querySelectorAll('.hook-card button[data-action="edit"]').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const scope = e.target.dataset.scope;
|
||||
const event = e.target.dataset.event;
|
||||
const index = parseInt(e.target.dataset.index);
|
||||
const button = e.currentTarget;
|
||||
const scope = button.dataset.scope;
|
||||
const event = button.dataset.event;
|
||||
const index = parseInt(button.dataset.index);
|
||||
|
||||
const hooks = scope === 'global' ? hookConfig.global.hooks : hookConfig.project.hooks;
|
||||
const hookList = Array.isArray(hooks[event]) ? hooks[event] : [hooks[event]];
|
||||
@@ -354,9 +355,10 @@ function attachHookEventListeners() {
|
||||
// Delete buttons
|
||||
document.querySelectorAll('.hook-card button[data-action="delete"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const scope = e.target.dataset.scope;
|
||||
const event = e.target.dataset.event;
|
||||
const index = parseInt(e.target.dataset.index);
|
||||
const button = e.currentTarget;
|
||||
const scope = button.dataset.scope;
|
||||
const event = button.dataset.event;
|
||||
const index = parseInt(button.dataset.index);
|
||||
|
||||
if (confirm(`Remove this ${event} hook?`)) {
|
||||
await removeHook(scope, event, index);
|
||||
@@ -367,7 +369,7 @@ function attachHookEventListeners() {
|
||||
// Install project buttons
|
||||
document.querySelectorAll('button[data-action="install-project"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const templateId = e.target.dataset.template;
|
||||
const templateId = e.currentTarget.dataset.template;
|
||||
await installHookTemplate(templateId, 'project');
|
||||
});
|
||||
});
|
||||
@@ -375,7 +377,7 @@ function attachHookEventListeners() {
|
||||
// Install global buttons
|
||||
document.querySelectorAll('button[data-action="install-global"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const templateId = e.target.dataset.template;
|
||||
const templateId = e.currentTarget.dataset.template;
|
||||
await installHookTemplate(templateId, 'global');
|
||||
});
|
||||
});
|
||||
@@ -383,7 +385,7 @@ function attachHookEventListeners() {
|
||||
// Uninstall buttons
|
||||
document.querySelectorAll('button[data-action="uninstall"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const templateId = e.target.dataset.template;
|
||||
const templateId = e.currentTarget.dataset.template;
|
||||
await uninstallHookTemplate(templateId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,6 +136,13 @@ function showLiteTaskDetailPage(sessionKey) {
|
||||
<span class="tab-icon"><i data-lucide="ruler" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Plan</span>
|
||||
</button>
|
||||
${session.type === 'lite-fix' ? `
|
||||
<button class="detail-tab" data-tab="diagnoses" onclick="switchLiteDetailTab('diagnoses')">
|
||||
<span class="tab-icon"><i data-lucide="stethoscope" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Diagnoses</span>
|
||||
${session.diagnoses?.items?.length ? `<span class="tab-count">${session.diagnoses.items.length}</span>` : ''}
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="detail-tab" data-tab="context" onclick="switchLiteDetailTab('context')">
|
||||
<span class="tab-icon"><i data-lucide="package" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Context</span>
|
||||
@@ -196,6 +203,17 @@ function switchLiteDetailTab(tabName) {
|
||||
break;
|
||||
case 'plan':
|
||||
contentArea.innerHTML = renderLitePlanTab(session);
|
||||
// Re-initialize collapsible sections for plan tab
|
||||
setTimeout(() => {
|
||||
initCollapsibleSections(contentArea);
|
||||
}, 50);
|
||||
break;
|
||||
case 'diagnoses':
|
||||
contentArea.innerHTML = renderDiagnosesTab(session);
|
||||
// Re-initialize collapsible sections for diagnoses tab
|
||||
setTimeout(() => {
|
||||
initCollapsibleSections(contentArea);
|
||||
}, 50);
|
||||
break;
|
||||
case 'context':
|
||||
loadAndRenderLiteContextTab(session, contentArea);
|
||||
@@ -287,13 +305,14 @@ function openTaskDrawerForLite(sessionId, taskId) {
|
||||
|
||||
function renderLitePlanTab(session) {
|
||||
const plan = session.plan;
|
||||
const isFixPlan = session.type === 'lite-fix';
|
||||
|
||||
if (!plan) {
|
||||
return `
|
||||
<div class="tab-empty-state">
|
||||
<div class="empty-icon"><i data-lucide="ruler" class="w-12 h-12"></i></div>
|
||||
<div class="empty-title">No Plan Data</div>
|
||||
<div class="empty-text">No plan.json found for this session.</div>
|
||||
<div class="empty-text">No ${isFixPlan ? 'fix-plan.json' : 'plan.json'} found for this session.</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -308,6 +327,22 @@ function renderLitePlanTab(session) {
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Root Cause (fix-plan specific) -->
|
||||
${plan.root_cause ? `
|
||||
<div class="plan-section">
|
||||
<h4 class="plan-section-title"><i data-lucide="search" class="w-4 h-4 inline mr-1"></i> Root Cause</h4>
|
||||
<p class="plan-root-cause-text">${escapeHtml(plan.root_cause)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Strategy (fix-plan specific) -->
|
||||
${plan.strategy ? `
|
||||
<div class="plan-section">
|
||||
<h4 class="plan-section-title"><i data-lucide="route" class="w-4 h-4 inline mr-1"></i> Fix Strategy</h4>
|
||||
<p class="plan-strategy-text">${escapeHtml(plan.strategy)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Approach -->
|
||||
${plan.approach ? `
|
||||
<div class="plan-section">
|
||||
@@ -316,6 +351,14 @@ function renderLitePlanTab(session) {
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- User Requirements (fix-plan specific) -->
|
||||
${plan.user_requirements ? `
|
||||
<div class="plan-section">
|
||||
<h4 class="plan-section-title"><i data-lucide="user" class="w-4 h-4 inline mr-1"></i> User Requirements</h4>
|
||||
<p class="plan-requirements-text">${escapeHtml(plan.user_requirements)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Focus Paths -->
|
||||
${plan.focus_paths?.length ? `
|
||||
<div class="plan-section">
|
||||
@@ -330,16 +373,74 @@ function renderLitePlanTab(session) {
|
||||
<div class="plan-section">
|
||||
<h4 class="plan-section-title"><i data-lucide="info" class="w-4 h-4 inline mr-1"></i> Metadata</h4>
|
||||
<div class="plan-meta-grid">
|
||||
${plan.severity ? `<div class="meta-item"><span class="meta-label">Severity:</span> <span class="severity-badge ${escapeHtml(plan.severity)}">${escapeHtml(plan.severity)}</span></div>` : ''}
|
||||
${plan.risk_level ? `<div class="meta-item"><span class="meta-label">Risk Level:</span> <span class="risk-badge ${escapeHtml(plan.risk_level)}">${escapeHtml(plan.risk_level)}</span></div>` : ''}
|
||||
${plan.estimated_time ? `<div class="meta-item"><span class="meta-label">Estimated Time:</span> ${escapeHtml(plan.estimated_time)}</div>` : ''}
|
||||
${plan.complexity ? `<div class="meta-item"><span class="meta-label">Complexity:</span> ${escapeHtml(plan.complexity)}</div>` : ''}
|
||||
${plan.recommended_execution ? `<div class="meta-item"><span class="meta-label">Execution:</span> ${escapeHtml(plan.recommended_execution)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fix Tasks Summary (fix-plan specific) -->
|
||||
${plan.tasks?.length ? `
|
||||
<div class="plan-section">
|
||||
<h4 class="plan-section-title"><i data-lucide="list-checks" class="w-4 h-4 inline mr-1"></i> Fix Tasks (${plan.tasks.length})</h4>
|
||||
<div class="fix-tasks-summary">
|
||||
${plan.tasks.map((task, idx) => `
|
||||
<div class="fix-task-summary-item collapsible-section">
|
||||
<div class="collapsible-header">
|
||||
<span class="collapse-icon">▶</span>
|
||||
<span class="task-num">#${idx + 1}</span>
|
||||
<span class="task-title-brief">${escapeHtml(task.title || task.summary || 'Untitled')}</span>
|
||||
${task.scope ? `<span class="task-scope-badge">${escapeHtml(task.scope)}</span>` : ''}
|
||||
</div>
|
||||
<div class="collapsible-content collapsed">
|
||||
${task.modification_points?.length ? `
|
||||
<div class="task-detail-section">
|
||||
<strong>Modification Points:</strong>
|
||||
<ul class="mod-points-list">
|
||||
${task.modification_points.map(mp => `
|
||||
<li>
|
||||
<code>${escapeHtml(mp.file || '')}</code>
|
||||
${mp.function_name ? `<span class="func-name">→ ${escapeHtml(mp.function_name)}</span>` : ''}
|
||||
${mp.change_type ? `<span class="change-type">(${escapeHtml(mp.change_type)})</span>` : ''}
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
${task.implementation?.length ? `
|
||||
<div class="task-detail-section">
|
||||
<strong>Implementation Steps:</strong>
|
||||
<ol class="impl-steps-list">
|
||||
${task.implementation.map(step => `<li>${escapeHtml(step)}</li>`).join('')}
|
||||
</ol>
|
||||
</div>
|
||||
` : ''}
|
||||
${task.verification?.length ? `
|
||||
<div class="task-detail-section">
|
||||
<strong>Verification:</strong>
|
||||
<ul class="verify-list">
|
||||
${task.verification.map(v => `<li>${escapeHtml(v)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Raw JSON -->
|
||||
<div class="plan-section">
|
||||
<h4 class="plan-section-title">{ } Raw JSON</h4>
|
||||
<pre class="json-content">${escapeHtml(JSON.stringify(plan, null, 2))}</pre>
|
||||
<div class="plan-section collapsible-section">
|
||||
<div class="collapsible-header">
|
||||
<span class="collapse-icon">▶</span>
|
||||
<span class="section-label">{ } Raw JSON</span>
|
||||
</div>
|
||||
<div class="collapsible-content collapsed">
|
||||
<pre class="json-content">${escapeHtml(JSON.stringify(plan, null, 2))}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -393,3 +494,192 @@ async function loadAndRenderLiteSummaryTab(session, contentArea) {
|
||||
contentArea.innerHTML = `<div class="tab-error">Failed to load summaries: ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DIAGNOSES TAB RENDERING (lite-fix specific)
|
||||
// ============================================
|
||||
|
||||
function renderDiagnosesTab(session) {
|
||||
const diagnoses = session.diagnoses;
|
||||
|
||||
if (!diagnoses || (!diagnoses.manifest && diagnoses.items?.length === 0)) {
|
||||
return `
|
||||
<div class="tab-empty-state">
|
||||
<div class="empty-icon"><i data-lucide="stethoscope" class="w-12 h-12"></i></div>
|
||||
<div class="empty-title">No Diagnoses</div>
|
||||
<div class="empty-text">No diagnosis-*.json files found for this session.</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let sections = [];
|
||||
|
||||
// Manifest summary (if available)
|
||||
if (diagnoses.manifest) {
|
||||
sections.push(`
|
||||
<div class="diagnoses-manifest-section">
|
||||
<h4 class="diagnoses-section-title"><i data-lucide="clipboard-check" class="w-4 h-4 inline mr-1"></i> Diagnosis Summary</h4>
|
||||
<div class="manifest-meta-grid">
|
||||
${diagnoses.manifest.total_diagnoses ? `<div class="meta-item"><span class="meta-label">Total Diagnoses:</span> ${diagnoses.manifest.total_diagnoses}</div>` : ''}
|
||||
${diagnoses.manifest.diagnosis_angles ? `<div class="meta-item"><span class="meta-label">Angles:</span> ${diagnoses.manifest.diagnosis_angles.join(', ')}</div>` : ''}
|
||||
${diagnoses.manifest.created_at ? `<div class="meta-item"><span class="meta-label">Created:</span> ${formatDate(diagnoses.manifest.created_at)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Individual diagnosis items
|
||||
if (diagnoses.items && diagnoses.items.length > 0) {
|
||||
const diagnosisCards = diagnoses.items.map(diag => renderDiagnosisCard(diag)).join('');
|
||||
sections.push(`
|
||||
<div class="diagnoses-items-section">
|
||||
<h4 class="diagnoses-section-title"><i data-lucide="search" class="w-4 h-4 inline mr-1"></i> Diagnosis Details (${diagnoses.items.length})</h4>
|
||||
<div class="diagnoses-grid">
|
||||
${diagnosisCards}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return `<div class="diagnoses-tab-content">${sections.join('')}</div>`;
|
||||
}
|
||||
|
||||
function renderDiagnosisCard(diag) {
|
||||
const diagJsonId = `diag-json-${diag.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
taskJsonStore[diagJsonId] = diag;
|
||||
|
||||
return `
|
||||
<div class="diagnosis-card collapsible-section">
|
||||
<div class="collapsible-header diagnosis-header">
|
||||
<span class="collapse-icon">▶</span>
|
||||
<span class="diagnosis-id"><i data-lucide="file-search" class="w-4 h-4 inline mr-1"></i>${escapeHtml(diag.id)}</span>
|
||||
<button class="btn-view-json" onclick="event.stopPropagation(); showJsonModal('${diagJsonId}', '${escapeHtml(diag.id)}')">{ } JSON</button>
|
||||
</div>
|
||||
<div class="collapsible-content collapsed">
|
||||
${renderDiagnosisContent(diag)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDiagnosisContent(diag) {
|
||||
let content = [];
|
||||
|
||||
// Summary/Overview
|
||||
if (diag.summary || diag.overview) {
|
||||
content.push(`
|
||||
<div class="diag-section">
|
||||
<strong>Summary:</strong>
|
||||
<p>${escapeHtml(diag.summary || diag.overview)}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Root Cause Analysis
|
||||
if (diag.root_cause || diag.root_cause_analysis) {
|
||||
content.push(`
|
||||
<div class="diag-section">
|
||||
<strong>Root Cause:</strong>
|
||||
<p>${escapeHtml(diag.root_cause || diag.root_cause_analysis)}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Issues/Findings
|
||||
if (diag.issues && Array.isArray(diag.issues)) {
|
||||
content.push(`
|
||||
<div class="diag-section">
|
||||
<strong>Issues Found (${diag.issues.length}):</strong>
|
||||
<ul class="issues-list">
|
||||
${diag.issues.map(issue => `
|
||||
<li class="issue-item">
|
||||
${typeof issue === 'string' ? escapeHtml(issue) : `
|
||||
<div class="issue-title">${escapeHtml(issue.title || issue.description || 'Unknown')}</div>
|
||||
${issue.location ? `<div class="issue-location"><code>${escapeHtml(issue.location)}</code></div>` : ''}
|
||||
${issue.severity ? `<span class="severity-badge ${issue.severity}">${escapeHtml(issue.severity)}</span>` : ''}
|
||||
`}
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Affected Files
|
||||
if (diag.affected_files && Array.isArray(diag.affected_files)) {
|
||||
content.push(`
|
||||
<div class="diag-section">
|
||||
<strong>Affected Files:</strong>
|
||||
<div class="path-tags">
|
||||
${diag.affected_files.map(f => `<span class="path-tag">${escapeHtml(typeof f === 'string' ? f : f.path || f.file)}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// API Contracts (for api-contracts diagnosis)
|
||||
if (diag.contracts && Array.isArray(diag.contracts)) {
|
||||
content.push(`
|
||||
<div class="diag-section">
|
||||
<strong>API Contracts (${diag.contracts.length}):</strong>
|
||||
<div class="contracts-list">
|
||||
${diag.contracts.map(contract => `
|
||||
<div class="contract-item">
|
||||
<div class="contract-header">
|
||||
<span class="contract-endpoint">${escapeHtml(contract.endpoint || contract.name || 'Unknown')}</span>
|
||||
${contract.method ? `<span class="contract-method">${escapeHtml(contract.method)}</span>` : ''}
|
||||
</div>
|
||||
${contract.description ? `<div class="contract-desc">${escapeHtml(contract.description)}</div>` : ''}
|
||||
${contract.issues?.length ? `<div class="contract-issues">${contract.issues.length} issue(s)</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Dataflow Analysis (for dataflow diagnosis)
|
||||
if (diag.dataflow || diag.data_flow) {
|
||||
const df = diag.dataflow || diag.data_flow;
|
||||
content.push(`
|
||||
<div class="diag-section">
|
||||
<strong>Data Flow Analysis:</strong>
|
||||
${typeof df === 'string' ? `<p>${escapeHtml(df)}</p>` : `
|
||||
<div class="dataflow-details">
|
||||
${df.source ? `<div class="df-item"><span class="df-label">Source:</span> ${escapeHtml(df.source)}</div>` : ''}
|
||||
${df.sink ? `<div class="df-item"><span class="df-label">Sink:</span> ${escapeHtml(df.sink)}</div>` : ''}
|
||||
${df.transformations?.length ? `
|
||||
<div class="df-item">
|
||||
<span class="df-label">Transformations:</span>
|
||||
<ol class="df-transforms">${df.transformations.map(t => `<li>${escapeHtml(t)}</li>`).join('')}</ol>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Recommendations
|
||||
if (diag.recommendations && Array.isArray(diag.recommendations)) {
|
||||
content.push(`
|
||||
<div class="diag-section">
|
||||
<strong>Recommendations:</strong>
|
||||
<ol class="recommendations-list">
|
||||
${diag.recommendations.map(rec => `<li>${escapeHtml(typeof rec === 'string' ? rec : rec.description || rec.action)}</li>`).join('')}
|
||||
</ol>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// If no specific content was rendered, show raw JSON preview
|
||||
if (content.length === 0) {
|
||||
content.push(`
|
||||
<div class="diag-section">
|
||||
<pre class="json-content">${escapeHtml(JSON.stringify(diag, null, 2))}</pre>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return content.join('');
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-code-workflow",
|
||||
"version": "6.0.5",
|
||||
"version": "6.1.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-code-workflow",
|
||||
"version": "6.0.5",
|
||||
"version": "6.1.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"boxen": "^7.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-code-workflow",
|
||||
"version": "6.1.1",
|
||||
"version": "6.1.4",
|
||||
"description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution",
|
||||
"type": "module",
|
||||
"main": "ccw/src/index.js",
|
||||
|
||||
Reference in New Issue
Block a user