mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-15 22:43:28 +08:00
fix: harden dashboard terminal launch helpers
This commit is contained in:
@@ -8,8 +8,11 @@ import tkinter as tk
|
|||||||
from tkinter import ttk, scrolledtext, messagebox
|
from tkinter import ttk, scrolledtext, messagebox
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import subprocess
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from scripts.lib.ecc_dashboard_runtime import build_terminal_launch, maximize_window
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# DATA LOADERS - Load ECC data from the project
|
# DATA LOADERS - Load ECC data from the project
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -18,6 +21,7 @@ def get_project_path() -> str:
|
|||||||
"""Get the ECC project path - assumes this script is run from the project dir"""
|
"""Get the ECC project path - assumes this script is run from the project dir"""
|
||||||
return os.path.dirname(os.path.abspath(__file__))
|
return os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
def load_agents(project_path: str) -> List[Dict]:
|
def load_agents(project_path: str) -> List[Dict]:
|
||||||
"""Load agents from AGENTS.md"""
|
"""Load agents from AGENTS.md"""
|
||||||
agents_file = os.path.join(project_path, "AGENTS.md")
|
agents_file = os.path.join(project_path, "AGENTS.md")
|
||||||
@@ -257,7 +261,7 @@ class ECCDashboard(tk.Tk):
|
|||||||
self.project_path = get_project_path()
|
self.project_path = get_project_path()
|
||||||
self.title("ECC Dashboard - Everything Claude Code")
|
self.title("ECC Dashboard - Everything Claude Code")
|
||||||
|
|
||||||
self.state('zoomed')
|
maximize_window(self)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.icon_image = tk.PhotoImage(file='assets/images/ecc-logo.png')
|
self.icon_image = tk.PhotoImage(file='assets/images/ecc-logo.png')
|
||||||
@@ -789,14 +793,9 @@ Project: github.com/affaan-m/everything-claude-code"""
|
|||||||
|
|
||||||
def open_terminal(self):
|
def open_terminal(self):
|
||||||
"""Open terminal at project path"""
|
"""Open terminal at project path"""
|
||||||
import subprocess
|
|
||||||
path = self.path_entry.get()
|
path = self.path_entry.get()
|
||||||
if os.name == 'nt': # Windows
|
argv, kwargs = build_terminal_launch(path)
|
||||||
subprocess.Popen(['cmd', '/c', 'start', 'cmd', '/k', f'cd /d "{path}"'])
|
subprocess.Popen(argv, **kwargs)
|
||||||
elif os.uname().sysname == 'Darwin': # macOS
|
|
||||||
subprocess.Popen(['open', '-a', 'Terminal', path])
|
|
||||||
else: # Linux
|
|
||||||
subprocess.Popen(['x-terminal-emulator', '-e', f'cd {path}'])
|
|
||||||
|
|
||||||
def open_readme(self):
|
def open_readme(self):
|
||||||
"""Open README in default browser/reader"""
|
"""Open README in default browser/reader"""
|
||||||
|
|||||||
61
scripts/lib/ecc_dashboard_runtime.py
Normal file
61
scripts/lib/ecc_dashboard_runtime.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Runtime helpers for ecc_dashboard.py that do not depend on tkinter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
from typing import Optional, Tuple, Dict, List
|
||||||
|
|
||||||
|
|
||||||
|
def maximize_window(window) -> None:
|
||||||
|
"""Maximize the dashboard window using the safest supported method."""
|
||||||
|
try:
|
||||||
|
window.state('zoomed')
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
system_name = platform.system()
|
||||||
|
if system_name == 'Linux':
|
||||||
|
try:
|
||||||
|
window.attributes('-zoomed', True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif system_name == 'Darwin':
|
||||||
|
try:
|
||||||
|
window.attributes('-fullscreen', True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def build_terminal_launch(
|
||||||
|
path: str,
|
||||||
|
*,
|
||||||
|
os_name: Optional[str] = None,
|
||||||
|
system_name: Optional[str] = None,
|
||||||
|
) -> Tuple[List[str], Dict[str, object]]:
|
||||||
|
"""Return safe argv/kwargs for opening a terminal rooted at the requested path."""
|
||||||
|
resolved_os_name = os_name or os.name
|
||||||
|
resolved_system_name = system_name or platform.system()
|
||||||
|
|
||||||
|
if resolved_os_name == 'nt':
|
||||||
|
creationflags = getattr(subprocess, 'CREATE_NEW_CONSOLE', 0)
|
||||||
|
return (
|
||||||
|
['cmd.exe', '/k', 'cd', '/d', path],
|
||||||
|
{
|
||||||
|
'cwd': path,
|
||||||
|
'creationflags': creationflags,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if resolved_system_name == 'Darwin':
|
||||||
|
return (['open', '-a', 'Terminal', path], {})
|
||||||
|
|
||||||
|
return (
|
||||||
|
['x-terminal-emulator', '-e', 'bash', '-lc', 'cd -- "$1"; exec bash', 'bash', path],
|
||||||
|
{},
|
||||||
|
)
|
||||||
128
tests/scripts/ecc-dashboard.test.js
Normal file
128
tests/scripts/ecc-dashboard.test.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* Behavioral tests for ecc_dashboard.py helper functions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
const repoRoot = path.join(__dirname, '..', '..');
|
||||||
|
const runtimeHelpersPath = path.join(repoRoot, 'scripts', 'lib', 'ecc_dashboard_runtime.py');
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(` ✓ ${name}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ✗ ${name}`);
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPython(source) {
|
||||||
|
const candidates = process.platform === 'win32' ? ['python', 'python3'] : ['python3', 'python'];
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
for (const command of candidates) {
|
||||||
|
const result = spawnSync(command, ['-c', source], {
|
||||||
|
cwd: repoRoot,
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error && result.error.code === 'ENOENT') {
|
||||||
|
lastError = result.error;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error((result.stderr || result.stdout || '').trim() || `${command} exited ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError || new Error('No Python interpreter available');
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
console.log('\n=== Testing ecc_dashboard.py ===\n');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
if (test('build_terminal_launch keeps Linux path separate from shell command text', () => {
|
||||||
|
const output = runPython(`
|
||||||
|
import importlib.util, json
|
||||||
|
spec = importlib.util.spec_from_file_location("ecc_dashboard_runtime", r"""${runtimeHelpersPath}""")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
argv, kwargs = module.build_terminal_launch('/tmp/proj; rm -rf ~', os_name='posix', system_name='Linux')
|
||||||
|
print(json.dumps({'argv': argv, 'kwargs': kwargs}))
|
||||||
|
`);
|
||||||
|
const parsed = JSON.parse(output);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
parsed.argv,
|
||||||
|
['x-terminal-emulator', '-e', 'bash', '-lc', 'cd -- "$1"; exec bash', 'bash', '/tmp/proj; rm -rf ~']
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(parsed.kwargs, {});
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('build_terminal_launch uses cwd + CREATE_NEW_CONSOLE style launch on Windows', () => {
|
||||||
|
const output = runPython(`
|
||||||
|
import importlib.util, json
|
||||||
|
spec = importlib.util.spec_from_file_location("ecc_dashboard_runtime", r"""${runtimeHelpersPath}""")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
argv, kwargs = module.build_terminal_launch(r'C:\\\\Users\\\\user\\\\proj & del C:\\\\*', os_name='nt', system_name='Windows')
|
||||||
|
print(json.dumps({'argv': argv, 'kwargs': kwargs}))
|
||||||
|
`);
|
||||||
|
const parsed = JSON.parse(output);
|
||||||
|
assert.deepStrictEqual(parsed.argv.slice(0, 4), ['cmd.exe', '/k', 'cd', '/d']);
|
||||||
|
assert.strictEqual(parsed.argv[4], parsed.kwargs.cwd);
|
||||||
|
assert.ok(parsed.argv[4].includes('proj & del'), 'path should remain a literal argv entry');
|
||||||
|
assert.ok(parsed.argv[4].includes('C:'), 'windows drive prefix should be preserved');
|
||||||
|
assert.ok(Object.prototype.hasOwnProperty.call(parsed.kwargs, 'creationflags'));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('maximize_window falls back to Linux zoom attribute when zoomed state is unsupported', () => {
|
||||||
|
const output = runPython(`
|
||||||
|
import importlib.util, json
|
||||||
|
spec = importlib.util.spec_from_file_location("ecc_dashboard_runtime", r"""${runtimeHelpersPath}""")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
class FakeWindow:
|
||||||
|
def __init__(self):
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def state(self, value):
|
||||||
|
self.calls.append(['state', value])
|
||||||
|
raise RuntimeError('bad argument "zoomed"')
|
||||||
|
|
||||||
|
def attributes(self, name, value):
|
||||||
|
self.calls.append(['attributes', name, value])
|
||||||
|
|
||||||
|
original = module.platform.system
|
||||||
|
module.platform.system = lambda: 'Linux'
|
||||||
|
try:
|
||||||
|
window = FakeWindow()
|
||||||
|
module.maximize_window(window)
|
||||||
|
finally:
|
||||||
|
module.platform.system = original
|
||||||
|
|
||||||
|
print(json.dumps(window.calls))
|
||||||
|
`);
|
||||||
|
const parsed = JSON.parse(output);
|
||||||
|
assert.deepStrictEqual(parsed, [
|
||||||
|
['state', 'zoomed'],
|
||||||
|
['attributes', '-zoomed', true],
|
||||||
|
]);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests();
|
||||||
Reference in New Issue
Block a user