From e46deb93c87560d747f16027eb028d73a235ec91 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 14 Apr 2026 19:44:32 -0700 Subject: [PATCH] fix: harden dashboard terminal launch helpers --- ecc_dashboard.py | 17 ++-- scripts/lib/ecc_dashboard_runtime.py | 61 +++++++++++++ tests/scripts/ecc-dashboard.test.js | 128 +++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 scripts/lib/ecc_dashboard_runtime.py create mode 100644 tests/scripts/ecc-dashboard.test.js diff --git a/ecc_dashboard.py b/ecc_dashboard.py index b2ea49d3..dfe54ea0 100644 --- a/ecc_dashboard.py +++ b/ecc_dashboard.py @@ -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() \ No newline at end of file + main() diff --git a/scripts/lib/ecc_dashboard_runtime.py b/scripts/lib/ecc_dashboard_runtime.py new file mode 100644 index 00000000..f882c919 --- /dev/null +++ b/scripts/lib/ecc_dashboard_runtime.py @@ -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], + {}, + ) diff --git a/tests/scripts/ecc-dashboard.test.js b/tests/scripts/ecc-dashboard.test.js new file mode 100644 index 00000000..2eed98b9 --- /dev/null +++ b/tests/scripts/ecc-dashboard.test.js @@ -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();