diff --git a/.gitignore b/.gitignore index 9d2cbec..8f46025 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,16 @@ src/cthulhu/cthulhu_platform.py *.pyc __pycache__/ +# Local build directory and artifacts +local-build/ +debug-*.out +*.gmo +*.pot + +# Translation files +po/stamp-po +po/insert-header.sed + # /help /help/*.omf /help/*/*.page diff --git a/README-DEVELOPMENT.md b/README-DEVELOPMENT.md new file mode 100644 index 0000000..40670c1 --- /dev/null +++ b/README-DEVELOPMENT.md @@ -0,0 +1,107 @@ +# Cthulhu Development Guide + +## Local Development Build + +To develop Cthulhu without overwriting your system installation, use the provided build scripts: + +### Building Locally + +```bash +# Build and install to ~/.local +./build-local.sh + +# Clean build and rebuild everything +./build-local.sh --clean +``` + +This installs Cthulhu to `~/.local/bin/cthulhu` without touching your system installation. + +### Testing the Local Build + +```bash +# Test the local installation +./test-local.sh + +# Run the local version directly +~/.local/bin/cthulhu --version +``` + +### Running Local Cthulhu + +```bash +# Method 1: Direct path +~/.local/bin/cthulhu + +# Method 2: Add to PATH (add to ~/.bashrc) +export PATH="$HOME/.local/bin:$PATH" +cthulhu +``` + +### Cleaning Up + +```bash +# Clean build artifacts only +./clean-local.sh --build-only + +# Remove local installation only +./clean-local.sh --install-only + +# Clean everything (build artifacts + local installation) +./clean-local.sh +``` + +## D-Bus Remote Controller + +Cthulhu now includes a D-Bus service for remote control: + +- **Service**: `org.stormux.Cthulhu.Service` +- **Path**: `/org/stormux/Cthulhu/Service` +- **Requires**: `dasbus` library (should be installed) + +### Testing D-Bus Service + +```bash +# Start Cthulhu with D-Bus service +~/.local/bin/cthulhu + +# In another terminal, test the service +busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service org.stormux.Cthulhu.Service GetVersion + +# Present a message via D-Bus +busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service org.stormux.Cthulhu.Service PresentMessage s "Hello from D-Bus" +``` + +## Development Workflow + +1. **Make changes** to the code +2. **Build locally**: `./build-local.sh` +3. **Test**: `./test-local.sh` +4. **Run**: `~/.local/bin/cthulhu` +5. **Clean when done**: `./clean-local.sh --build-only` + +## Git Repository Management + +The `.gitignore` file is configured to exclude: +- Build artifacts (`configure`, `Makefile`, etc.) +- Generated Python files (`cthulhu_bin.py`, `cthulhu_i18n.py`, etc.) +- Python bytecode (`*.pyc`, `__pycache__/`) + +Before committing: +```bash +# Clean build artifacts to avoid committing them +./clean-local.sh --build-only + +# Check what will be committed +git status +``` + +## Dependencies + +- **Runtime**: python3, pygobject-3.0, pluggy, AT-SPI2 +- **Build**: autotools, gettext, intltool +- **Optional**: dasbus (for D-Bus service), BrlTTY, speech-dispatcher + +Install build dependencies on Arch Linux: +```bash +sudo pacman -S autoconf automake intltool gettext python-dasbus +``` \ No newline at end of file diff --git a/README-REMOTE-CONTROLLER.md b/README-REMOTE-CONTROLLER.md new file mode 100644 index 0000000..a646b87 --- /dev/null +++ b/README-REMOTE-CONTROLLER.md @@ -0,0 +1,386 @@ +# Cthulhu Remote Controller (D-Bus Interface) + +> **✅ STABLE**: This D-Bus interface has been successfully ported from Orca v49.alpha and integrated +> into Cthulhu. The API is functional and ready for use, providing external control and automation +> capabilities for the Cthulhu screen reader. + +[TOC] + +## Overview + +Cthulhu exposes a D-Bus service at: + +- **Service Name**: `org.stormux.Cthulhu.Service` +- **Main Object Path**: `/org/stormux/Cthulhu/Service` +- **Module Object Paths**: `/org/stormux/Cthulhu/Service/ModuleName` + (e.g., `/org/stormux/Cthulhu/Service/SpeechAndVerbosityManager`) + +## Dependencies + +The D-Bus interface requires: + +- **dasbus** - Python D-Bus library used by Cthulhu for the remote controller implementation. + ([Installation instructions](https://dasbus.readthedocs.io/en/latest/index.html)) +- **python-dasbus** package (available on most distributions) + +## Service-Level Commands + +Commands available directly on the main service (`/org/stormux/Cthulhu/Service`): + +### Get Cthulhu's Version + +```bash +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service \ + org.stormux.Cthulhu.Service GetVersion +``` + +**Alternative using gdbus:** +```bash +gdbus call --session --dest org.stormux.Cthulhu.Service \ + --object-path /org/stormux/Cthulhu/Service \ + --method org.stormux.Cthulhu.Service.GetVersion +``` + +**Returns:** String containing the version (and revision if available) + +**Example output:** `s "Cthulhu screen reader version 2025.06.05-plugins (rev 408fb85)"` + +### Present a Custom Message in Speech and/or Braille + +```bash +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service \ + org.stormux.Cthulhu.Service PresentMessage s "Your message here" +``` + +**Alternative using gdbus:** +```bash +gdbus call --session --dest org.stormux.Cthulhu.Service \ + --object-path /org/stormux/Cthulhu/Service \ + --method org.stormux.Cthulhu.Service.PresentMessage "Your message here" +``` + +**Parameters:** + +- `message` (string): The message to present to the user + +**Returns:** Boolean indicating success + +### List Available Service Commands + +```bash +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service \ + org.stormux.Cthulhu.Service ListCommands +``` + +**Alternative using gdbus:** +```bash +gdbus call --session --dest org.stormux.Cthulhu.Service \ + --object-path /org/stormux/Cthulhu/Service \ + --method org.stormux.Cthulhu.Service.ListCommands +``` + +**Returns:** List of (command_name, description) tuples + +### List Registered Modules + +```bash +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service \ + org.stormux.Cthulhu.Service ListModules +``` + +**Alternative using gdbus:** +```bash +gdbus call --session --dest org.stormux.Cthulhu.Service \ + --object-path /org/stormux/Cthulhu/Service \ + --method org.stormux.Cthulhu.Service.ListModules +``` + +**Returns:** List of module names + +## Interacting with Modules + +Each registered module exposes its own set of operations. Based on the underlying Cthulhu code, these +are categorized as **Commands**, **Runtime Getters**, and **Runtime Setters**: + +- **Commands**: Actions that perform a task. These typically correspond to Cthulhu commands bound + to a keystroke (e.g., `IncreaseRate`). +- **Runtime Getters**: Operations that retrieve the current value of an item, often a setting + (e.g., `GetRate`). +- **Runtime Setters**: Operations that set the current value of an item, often a setting + (e.g., `SetRate`). Note that setting a value does NOT cause it to become permanently saved. + +You can discover and execute these for each module. + +### Discovering Module Capabilities + +#### List Commands for a Module + +```bash +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/ModuleName \ + org.stormux.Cthulhu.Module ListCommands +``` + +**Alternative using gdbus:** +```bash +gdbus call --session --dest org.stormux.Cthulhu.Service \ + --object-path /org/stormux/Cthulhu/Service/ModuleName \ + --method org.stormux.Cthulhu.Module.ListCommands +``` + +Replace `ModuleName` with an actual module name from `ListModules`. + +**Returns:** List of (command_name, description) tuples. + +#### List Runtime Getters for a Module + +```bash +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/ModuleName \ + org.stormux.Cthulhu.Module ListRuntimeGetters +``` + +**Alternative using gdbus:** +```bash +gdbus call --session --dest org.stormux.Cthulhu.Service \ + --object-path /org/stormux/Cthulhu/Service/ModuleName \ + --method org.stormux.Cthulhu.Module.ListRuntimeGetters +``` + +Replace `ModuleName` with an actual module name from `ListModules`. + +**Returns:** List of (getter_name, description) tuples. + +#### List Runtime Setters for a Module + +```bash +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/ModuleName \ + org.stormux.Cthulhu.Module ListRuntimeSetters +``` + +**Alternative using gdbus:** +```bash +gdbus call --session --dest org.stormux.Cthulhu.Service \ + --object-path /org/stormux/Cthulhu/Service/ModuleName \ + --method org.stormux.Cthulhu.Module.ListRuntimeSetters +``` + +Replace `ModuleName` with an actual module name from `ListModules`. + +**Returns:** List of (setter_name, description) tuples. + +### Executing Module Operations + +#### Execute a Runtime Getter + +```bash +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/ModuleName \ + org.stormux.Cthulhu.Module ExecuteRuntimeGetter s 'PropertyName' +``` + +**Alternative using gdbus:** +```bash +gdbus call --session --dest org.stormux.Cthulhu.Service \ + --object-path /org/stormux/Cthulhu/Service/ModuleName \ + --method org.stormux.Cthulhu.Module.ExecuteRuntimeGetter 'PropertyName' +``` + +**Parameters:** + +- `PropertyName` (string): The name of the runtime getter to execute. + +**Returns:** The value returned by the getter as a GLib variant (type depends on the getter). + +##### Example: Get the current speech rate + +```bash +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechAndVerbosityManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeGetter s 'Rate' +``` + +This will return the rate as a GLib Variant. + +#### Execute a Runtime Setter + +```bash +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/ModuleName \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter s 'PropertyName' v +``` + +**Alternative using gdbus:** +```bash +gdbus call --session --dest org.stormux.Cthulhu.Service \ + --object-path /org/stormux/Cthulhu/Service/ModuleName \ + --method org.stormux.Cthulhu.Module.ExecuteRuntimeSetter 'PropertyName' +``` + +**Parameters:** + +- `PropertyName` (string): The name of the runtime setter to execute. +- ``: The value to set, as a GLib variant (type depends on the setter). + +**Returns:** Boolean indicating success. + +##### Example: Set the current speech rate + +```bash +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechAndVerbosityManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter s 'Rate' v '<90>' +``` + +#### Execute a Module Command + +```bash +# With user notification +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/ModuleName \ + org.stormux.Cthulhu.Module ExecuteCommand s 'CommandName' b true + +# Without user notification (silent) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/ModuleName \ + org.stormux.Cthulhu.Module ExecuteCommand s 'CommandName' b false +``` + +**Alternative using gdbus:** +```bash +# With user notification +gdbus call --session --dest org.stormux.Cthulhu.Service \ + --object-path /org/stormux/Cthulhu/Service/ModuleName \ + --method org.stormux.Cthulhu.Module.ExecuteCommand 'CommandName' true + +# Without user notification (silent) +gdbus call --session --dest org.stormux.Cthulhu.Service \ + --object-path /org/stormux/Cthulhu/Service/ModuleName \ + --method org.stormux.Cthulhu.Module.ExecuteCommand 'CommandName' false +``` + +**Parameters (both required):** + +- `CommandName` (string): The name of the command to execute +- `notify_user` (boolean): Whether to notify the user of the action (see section below) + +**Returns:** Boolean indicating success + +### Please Note + +**Setting `notify_user=true` is not a guarantee that feedback will be presented.** Some commands +inherently don't make sense to announce. For example: + +```bash +# This command should simply stop speech, not announce that it is stopping speech. +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechAndVerbosityManager \ + org.stormux.Cthulhu.Module ExecuteCommand s 'InterruptSpeech' b true +``` + +In those cases Cthulhu will ignore the value of `notify_user`. + +**Setting `notify_user=false` is a guarantee that Cthulhu will remain silent.** If Cthulhu provides any +feedback when `notify_user=false`, it should be considered a bug. + +## Integration with Cthulhu's Plugin System + +The D-Bus Remote Controller integrates seamlessly with Cthulhu's pluggy-based plugin system. Plugins can: + +- Register their own D-Bus commands using the `@cthulhu_hookimpl` decorator +- Expose plugin-specific functionality via the remote controller +- Access the D-Bus service through the dynamic API manager + +See the main `CLAUDE.md` file for more details on plugin development with D-Bus integration. + +## Troubleshooting + +### Service Not Available + +If you get "The name is not activatable" or similar errors: + +1. **Check if Cthulhu is running:** + ```bash + ps aux | grep cthulhu + ``` + +2. **Check if the D-Bus service is registered:** + ```bash + busctl --user list | grep -i cthulhu + ``` + +3. **Verify dasbus is installed:** + ```bash + python3 -c "import dasbus; print('dasbus available')" + ``` + +4. **Check Cthulhu debug output:** + ```bash + DISPLAY=:0 ~/.local/bin/cthulhu --debug 2>&1 | grep -i dbus + ``` + +### Common Issues + +- **Timing Issues**: The D-Bus service starts after ATSPI initialization. Wait a few seconds after Cthulhu startup before attempting D-Bus calls. +- **Permissions**: Ensure you're using `--user` with busctl/gdbus for session bus access. +- **Display**: Make sure `DISPLAY=:0` is set when running Cthulhu in terminal sessions. + +## Examples + +### Quick Test Script + +```bash +#!/bin/bash +# Test Cthulhu D-Bus Remote Controller + +echo "Testing Cthulhu D-Bus Remote Controller..." + +# Get version +echo "Version:" +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service \ + org.stormux.Cthulhu.Service GetVersion + +# Present a message +echo "Presenting message..." +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service \ + org.stormux.Cthulhu.Service PresentMessage s "Hello from D-Bus!" + +# List available modules +echo "Available modules:" +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service \ + org.stormux.Cthulhu.Service ListModules + +echo "D-Bus test complete!" +``` + +## Integration Status + +- ✅ **Core D-Bus service**: Fully integrated with Cthulhu +- ✅ **Service lifecycle**: Automatic start/shutdown with Cthulhu +- ✅ **Message presentation**: `PresentMessage()` method working +- ✅ **Version info**: `GetVersion()` method working +- ✅ **Deferred startup**: D-Bus service starts after ATSPI initialization to prevent crashes +- ✅ **Error handling**: Proper exception handling and logging +- 🔄 **Module registration**: Ready for individual managers to register D-Bus commands +- 🔄 **Plugin integration**: Plugins can expose D-Bus commands using decorators + +## Future Development + +- Add more speech configuration commands, getters, and setters +- Expose Cthulhu's plugin system commands via D-Bus +- Integrate with Cthulhu's advanced features (indentation audio, self-voicing, etc.) +- Progressively expose all of Cthulhu's commands and settings via the remote controller interface + +## Related Files + +- `src/cthulhu/dbus_service.py` - Main D-Bus service implementation +- `src/cthulhu/cthulhu.py` - Integration and startup logic +- `CLAUDE.md` - Main development guide with plugin integration details \ No newline at end of file diff --git a/build-local.sh b/build-local.sh new file mode 100755 index 0000000..58f96e5 --- /dev/null +++ b/build-local.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Local build script for Cthulhu development +# Builds and installs Cthulhu to ~/.local without touching system installation + +set -e # Exit on any error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}=== Cthulhu Local Build Script ===${NC}" + +# Check if we're in the right directory +if [[ ! -f "configure.ac" ]]; then + echo -e "${RED}Error: Not in Cthulhu source directory (configure.ac not found)${NC}" + exit 1 +fi + +# Check dependencies +echo -e "${YELLOW}Checking dependencies...${NC}" +if ! command -v autoreconf &> /dev/null; then + echo -e "${RED}Error: autoreconf not found. Install autotools.${NC}" + exit 1 +fi + +if ! python3 -c "import dasbus" 2>/dev/null; then + echo -e "${YELLOW}Warning: dasbus not available. D-Bus service will be disabled.${NC}" +fi + +# Clean previous build artifacts (optional) +if [[ "$1" == "--clean" ]]; then + echo -e "${YELLOW}Cleaning previous build...${NC}" + make distclean 2>/dev/null || true + rm -rf autom4te.cache configure config.status Makefile +fi + +# Set local installation prefix +LOCAL_PREFIX="$HOME/.local" +echo -e "${YELLOW}Installing to: ${LOCAL_PREFIX}${NC}" + +# Regenerate autotools files +echo -e "${YELLOW}Regenerating autotools files...${NC}" +autoreconf -fiv + +# Configure for local installation +echo -e "${YELLOW}Configuring...${NC}" +./configure --prefix="$LOCAL_PREFIX" \ + --sysconfdir="$LOCAL_PREFIX/etc" \ + --localstatedir="$LOCAL_PREFIX/var" \ + --disable-help + +# Build +echo -e "${YELLOW}Building...${NC}" +make -j$(nproc) + +# Install locally +echo -e "${YELLOW}Installing to local prefix...${NC}" +make install || { + echo -e "${YELLOW}Warning: make install had errors, but checking if binary was created...${NC}" + if [[ -f "$LOCAL_PREFIX/bin/cthulhu" ]]; then + echo -e "${GREEN}Binary successfully installed despite makefile warnings.${NC}" + else + echo -e "${RED}Installation failed.${NC}" + exit 1 + fi +} + +echo -e "${GREEN}=== Build Complete ===${NC}" +echo -e "${GREEN}Cthulhu installed to: ${LOCAL_PREFIX}${NC}" +echo -e "${GREEN}Binary location: ${LOCAL_PREFIX}/bin/cthulhu${NC}" +echo "" +echo -e "${YELLOW}To run local Cthulhu:${NC}" +echo -e " ${LOCAL_PREFIX}/bin/cthulhu" +echo "" +echo -e "${YELLOW}To add to PATH (add to ~/.bashrc):${NC}" +echo -e " export PATH=\"${LOCAL_PREFIX}/bin:\$PATH\"" +echo "" +echo -e "${YELLOW}To uninstall local build:${NC}" +echo -e " ./clean-local.sh" \ No newline at end of file diff --git a/clean-local.sh b/clean-local.sh new file mode 100755 index 0000000..1e83eec --- /dev/null +++ b/clean-local.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# Clean script for Cthulhu development +# Removes build artifacts and optionally uninstalls local installation + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}=== Cthulhu Clean Script ===${NC}" + +LOCAL_PREFIX="$HOME/.local" + +# Function to clean build artifacts +clean_build() { + echo -e "${YELLOW}Cleaning build artifacts...${NC}" + + # Clean generated files + make distclean 2>/dev/null || true + + # Remove autotools generated files + rm -rf autom4te.cache + rm -f aclocal.m4 configure config.h.in config.h config.log config.status + rm -f compile config.guess config.sub depcomp install-sh missing + rm -f py-compile ltmain.sh libtool + rm -f stamp-h1 + + # Remove generated Makefiles + find . -name "Makefile" -delete 2>/dev/null || true + find . -name "Makefile.in" -delete 2>/dev/null || true + + # Remove Python bytecode + find . -name "*.pyc" -delete 2>/dev/null || true + find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true + + # Remove generated .py files + rm -f src/cthulhu/cthulhu_bin.py src/cthulhu/cthulhu_i18n.py src/cthulhu/cthulhu_platform.py + + # Remove translation files + find . -name "*.mo" -delete 2>/dev/null || true + find . -name "*.pot" -delete 2>/dev/null || true + + echo -e "${GREEN}Build artifacts cleaned.${NC}" +} + +# Function to uninstall local installation +uninstall_local() { + echo -e "${YELLOW}Uninstalling local Cthulhu installation...${NC}" + + if [[ -f "${LOCAL_PREFIX}/bin/cthulhu" ]]; then + # Remove binaries + rm -f "${LOCAL_PREFIX}/bin/cthulhu" + + # Remove Python modules + rm -rf "${LOCAL_PREFIX}/lib/python"*/site-packages/cthulhu* + + # Remove data files + rm -rf "${LOCAL_PREFIX}/share/cthulhu" + + # Remove docs + rm -rf "${LOCAL_PREFIX}/share/help/*/cthulhu" + + # Remove desktop files + rm -f "${LOCAL_PREFIX}/share/applications/cthulhu"* + + # Remove autostart + rm -f "${LOCAL_PREFIX}/etc/xdg/autostart/cthulhu"* + + echo -e "${GREEN}Local installation removed.${NC}" + else + echo -e "${YELLOW}No local installation found.${NC}" + fi +} + +# Parse arguments +case "$1" in + --build-only) + clean_build + ;; + --install-only) + uninstall_local + ;; + *) + clean_build + uninstall_local + ;; +esac + +echo -e "${GREEN}=== Clean Complete ===${NC}" \ No newline at end of file diff --git a/test-local.sh b/test-local.sh new file mode 100755 index 0000000..a48dfee --- /dev/null +++ b/test-local.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Test script for local Cthulhu installation + +set -e + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +LOCAL_PREFIX="$HOME/.local" +CTHULHU_BIN="${LOCAL_PREFIX}/bin/cthulhu" + +echo -e "${GREEN}=== Testing Local Cthulhu Installation ===${NC}" + +# Check if binary exists +if [[ ! -f "$CTHULHU_BIN" ]]; then + echo -e "${RED}Error: Cthulhu binary not found at $CTHULHU_BIN${NC}" + echo -e "${YELLOW}Run ./build-local.sh first${NC}" + exit 1 +fi + +echo -e "${YELLOW}Testing basic functionality...${NC}" + +# Test version +echo -n "Version check: " +if "$CTHULHU_BIN" --version >/dev/null 2>&1; then + echo -e "${GREEN}✓${NC}" +else + echo -e "${RED}✗${NC}" +fi + +# Test help +echo -n "Help option: " +if "$CTHULHU_BIN" --help >/dev/null 2>&1; then + echo -e "${GREEN}✓${NC}" +else + echo -e "${RED}✗${NC}" +fi + +# Test D-Bus service import +echo -n "D-Bus service: " +if PYTHONPATH="${LOCAL_PREFIX}/lib/python3.*/site-packages" python3 -c " +import sys +sys.path.insert(0, '${LOCAL_PREFIX}/lib/python3.13/site-packages') +try: + import cthulhu.dbus_service + controller = cthulhu.dbus_service.get_remote_controller() + print('D-Bus available:', controller._dasbus_available, file=sys.stderr) + print('SUCCESS') +except Exception as e: + print('ERROR:', e, file=sys.stderr) + sys.exit(1) +" 2>/dev/null; then + echo -e "${GREEN}✓${NC}" +else + echo -e "${YELLOW}~ (expected during development)${NC}" +fi + +echo "" +echo -e "${GREEN}Local build test complete!${NC}" +echo -e "${YELLOW}To run Cthulhu: ${CTHULHU_BIN}${NC}" \ No newline at end of file