mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-15 14:33:33 +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
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
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
|
||||
# ============================================================================
|
||||
@@ -18,6 +21,7 @@ def get_project_path() -> str:
|
||||
"""Get the ECC project path - assumes this script is run from the project dir"""
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def load_agents(project_path: str) -> List[Dict]:
|
||||
"""Load agents from 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.title("ECC Dashboard - Everything Claude Code")
|
||||
|
||||
self.state('zoomed')
|
||||
maximize_window(self)
|
||||
|
||||
try:
|
||||
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):
|
||||
"""Open terminal at project path"""
|
||||
import subprocess
|
||||
path = self.path_entry.get()
|
||||
if os.name == 'nt': # Windows
|
||||
subprocess.Popen(['cmd', '/c', 'start', 'cmd', '/k', f'cd /d "{path}"'])
|
||||
elif os.uname().sysname == 'Darwin': # macOS
|
||||
subprocess.Popen(['open', '-a', 'Terminal', path])
|
||||
else: # Linux
|
||||
subprocess.Popen(['x-terminal-emulator', '-e', f'cd {path}'])
|
||||
argv, kwargs = build_terminal_launch(path)
|
||||
subprocess.Popen(argv, **kwargs)
|
||||
|
||||
def open_readme(self):
|
||||
"""Open README in default browser/reader"""
|
||||
@@ -911,4 +910,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
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