├── .clangd ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── release.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── include └── nutshell │ ├── ai.h │ ├── config.h │ ├── core.h │ ├── pkg.h │ ├── theme.h │ └── utils.h ├── nutshell.rb ├── packages ├── gitify │ ├── README.md │ ├── gitify.sh │ └── manifest.json └── sample │ └── package.nut ├── scripts ├── build_release.sh ├── debug_ai.sh ├── debug_config.sh ├── debug_nutshell.sh ├── generate_checksum.sh ├── install_theme.sh ├── run_ai_tests.sh ├── run_pkg_test.sh ├── run_theme_test.sh └── uninstall.sh ├── src ├── ai │ ├── openai.c │ └── shell.c ├── core │ ├── executor.c │ ├── main.c │ ├── parser.c │ └── shell.c ├── pkg │ ├── integrity.c │ ├── nutpkg.c │ ├── packager.c │ └── registry.c └── utils │ ├── autocomplete.c │ ├── config.c │ ├── helpers.c │ ├── security.c │ └── theme.c ├── tests ├── test_ai_integration.c ├── test_ai_shell_integration.c ├── test_config.c ├── test_directory_config.c ├── test_openai_commands.c ├── test_parser.c ├── test_pkg_install.c ├── test_theme.c └── test_theme_json.sh └── themes ├── cyberpunk.json ├── default.json ├── developer.json └── minimal.json /.clangd: -------------------------------------------------------------------------------- 1 | CompileFlags: 2 | Add: ["--std=c2x"] 3 | Compiler: gcc 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Nutshell Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Release version (e.g., 1.2.3)' 8 | required: true 9 | push: 10 | tags: 11 | - 'v*' 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | build-and-release: 18 | name: Build and Create Release 19 | runs-on: ubuntu-latest 20 | env: 21 | VERSION: ${{ github.event.inputs.version }} 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Install dependencies 27 | run: | 28 | sudo apt-get update 29 | sudo apt-get install -y build-essential clang libreadline-dev libcurl4-openssl-dev libjansson-dev libssl-dev 30 | 31 | - name: Set version 32 | id: version 33 | run: | 34 | if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then 35 | echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV 36 | else 37 | # Extract version from tag (remove leading 'v') 38 | echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV 39 | fi 40 | echo "Version: ${{ env.VERSION }}" 41 | 42 | - name: Build release 43 | run: | 44 | ./scripts/build_release.sh ${{ env.VERSION }} 45 | 46 | - name: Run tests 47 | run: make test 48 | 49 | - name: Create Release 50 | id: create_release 51 | uses: softprops/action-gh-release@v2 52 | with: 53 | name: Nutshell ${{ env.VERSION }} 54 | tag_name: v${{ env.VERSION }} 55 | draft: false 56 | prerelease: false 57 | files: | 58 | build/nutshell-${{ env.VERSION }}.tar.gz 59 | build/nutshell-${{ env.VERSION }}.sha256 60 | body: | 61 | # Nutshell ${{ env.VERSION }} 62 | 63 | ## Installation 64 | 65 | ```bash 66 | # Download and extract 67 | tar -xzf nutshell-${{ env.VERSION }}.tar.gz 68 | cd nutshell-${{ env.VERSION }} 69 | 70 | # Install for current user 71 | ./install.sh --user 72 | 73 | # Or system-wide (requires sudo) 74 | sudo ./install.sh 75 | ``` 76 | 77 | ## What's New 78 | - See the [CHANGELOG.md](CHANGELOG.md) for details 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Nutshell Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build-and-test: 11 | name: Build and Run Tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Install dependencies 18 | run: | 19 | sudo apt-get update 20 | sudo apt-get install -y build-essential clang libreadline-dev libcurl4-openssl-dev libjansson-dev libssl-dev jq 21 | 22 | - name: Create directories 23 | run: | 24 | mkdir -p ~/.nutshell/themes 25 | mkdir -p ~/.nutshell/packages 26 | 27 | - name: Build project 28 | run: make 29 | 30 | - name: Copy theme files for tests 31 | run: | 32 | cp themes/*.json ~/.nutshell/themes/ 33 | ls -la ~/.nutshell/themes/ 34 | 35 | - name: Run tests 36 | run: | 37 | make test 38 | 39 | - name: Run theme JSON validation 40 | run: | 41 | chmod +x tests/test_theme_json.sh 42 | ./tests/test_theme_json.sh 43 | 44 | - name: Report status 45 | run: echo "All tests completed successfully!" 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | build/ 3 | *.test 4 | *.dSYM/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to Nutshell will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.0.4] - 2025-03-11 9 | 10 | ### Added 11 | 12 | - Directory-level configuration with `.nutshell.json` files 13 | - Configuration hierarchy: directory configs override user configs which override system configs 14 | - Automatic config reloading when changing directories with `cd` 15 | - Project-specific aliases and settings through directory configs 16 | - Support for different themes per project 17 | 18 | ### Fixed 19 | 20 | - Memory leak in directory path traversal 21 | - Config loading order to properly respect precedence rules 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Chandra Irugalbandara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC = clang 2 | CFLAGS = -std=c17 -Wall -Wextra -I./include -I/usr/local/include -I/opt/homebrew/include 3 | 4 | # Base libraries that are required - add OpenSSL 5 | LDFLAGS = -lreadline -lcurl -ldl -lssl -lcrypto -L/usr/local/lib 6 | 7 | # Check if pkg-config exists 8 | PKG_CONFIG_EXISTS := $(shell which pkg-config >/dev/null 2>&1 && echo "yes" || echo "no") 9 | 10 | # Check if jansson is available through brew 11 | ifeq ($(PKG_CONFIG_EXISTS),yes) 12 | JANSSON_AVAILABLE := $(shell pkg-config --exists jansson && echo "yes" || echo "no") 13 | ifeq ($(JANSSON_AVAILABLE),yes) 14 | JANSSON_CFLAGS := $(shell pkg-config --cflags jansson) 15 | JANSSON_LIBS := $(shell pkg-config --libs jansson) 16 | endif 17 | else 18 | JANSSON_AVAILABLE := $(shell brew list jansson >/dev/null 2>&1 && echo "yes" || echo "no") 19 | ifeq ($(JANSSON_AVAILABLE),yes) 20 | JANSSON_CFLAGS := -I$(shell brew --prefix jansson 2>/dev/null || echo "/usr/local")/include 21 | JANSSON_LIBS := -L$(shell brew --prefix jansson 2>/dev/null || echo "/usr/local")/lib -ljansson 22 | endif 23 | endif 24 | 25 | ifeq ($(JANSSON_AVAILABLE),yes) 26 | CFLAGS += -DJANSSON_AVAILABLE=1 $(JANSSON_CFLAGS) 27 | LDFLAGS += $(JANSSON_LIBS) 28 | else 29 | CFLAGS += -DJANSSON_AVAILABLE=0 30 | $(warning Jansson library not found. Package management functionality will be limited.) 31 | endif 32 | 33 | SRC = $(wildcard src/core/*.c) \ 34 | $(wildcard src/ai/*.c) \ 35 | $(wildcard src/pkg/*.c) \ 36 | $(wildcard src/utils/*.c) 37 | OBJ = $(SRC:.c=.o) 38 | 39 | TEST_SRC = $(wildcard tests/*.c) 40 | TEST_OBJ = $(TEST_SRC:.c=.o) 41 | TEST_BINS = $(TEST_SRC:.c=.test) 42 | 43 | .PHONY: all clean install install-user test test-pkg test-theme test-ai test-config test-dirconfig release uninstall uninstall-user 44 | 45 | all: nutshell 46 | 47 | nutshell: $(OBJ) 48 | $(CC) -o $@ $^ $(LDFLAGS) 49 | 50 | %.o: %.c 51 | $(CC) $(CFLAGS) -c -o $@ $< 52 | 53 | # Make sure install depends on building nutshell first 54 | install: nutshell 55 | @echo "Installing nutshell..." 56 | @if [ -w $(DESTDIR)/usr/local/bin ] && mkdir -p $(DESTDIR)/usr/local/nutshell/packages 2>/dev/null; then \ 57 | mkdir -p $(DESTDIR)/usr/local/bin && \ 58 | cp nutshell $(DESTDIR)/usr/local/bin && \ 59 | echo "Installation completed successfully in system directories."; \ 60 | else \ 61 | echo "Error: Permission denied when installing to system directories."; \ 62 | echo "You can either:"; \ 63 | echo " 1. Use 'sudo make install' to install with admin privileges"; \ 64 | echo " 2. Use 'make install-user' to install in your home directory"; \ 65 | exit 1; \ 66 | fi 67 | 68 | # User-level installation (doesn't require sudo) 69 | install-user: nutshell 70 | @echo "Installing nutshell in user's home directory..." 71 | @mkdir -p $(HOME)/bin 72 | @cp nutshell $(HOME)/bin/ 73 | @mkdir -p $(HOME)/.nutshell/packages 74 | @echo "Installation completed successfully in $(HOME)/bin." 75 | @echo "Make sure $(HOME)/bin is in your PATH variable." 76 | @echo "You can add it by running: echo 'export PATH=\$$PATH:\$$HOME/bin' >> ~/.bashrc" 77 | 78 | # Uninstall system-wide installation (requires sudo) 79 | uninstall: 80 | @echo "Uninstalling nutshell from system directories..." 81 | @rm -f $(DESTDIR)/usr/local/bin/nutshell 82 | @rm -rf $(DESTDIR)/usr/local/nutshell 83 | @echo "Nutshell has been uninstalled from system directories." 84 | 85 | # Uninstall user-level installation 86 | uninstall-user: 87 | @echo "Uninstalling nutshell from user directory..." 88 | @rm -f $(HOME)/bin/nutshell 89 | @rm -rf $(HOME)/.nutshell 90 | @echo "Nutshell has been uninstalled from user directory." 91 | @echo "You may want to remove ~/bin from your PATH if you don't use it for other programs." 92 | 93 | # Standard test target 94 | test: $(TEST_BINS) 95 | @for test in $(TEST_BINS); do \ 96 | echo "Running $$test..."; \ 97 | ./$$test; \ 98 | done 99 | 100 | # Add a new target for package tests 101 | test-pkg: tests/test_pkg_install.test 102 | @echo "Running package installation tests..." 103 | @chmod +x scripts/run_pkg_test.sh 104 | @./scripts/run_pkg_test.sh 105 | 106 | # Add a new target for theme tests 107 | test-theme: tests/test_theme.test 108 | @echo "Running theme tests..." 109 | @./tests/test_theme.test 110 | 111 | # Add a new target for AI tests 112 | test-ai: tests/test_ai_integration.test tests/test_openai_commands.test tests/test_ai_shell_integration.test 113 | @echo "Running AI integration tests..." 114 | @./tests/test_ai_integration.test 115 | @./tests/test_openai_commands.test 116 | @./tests/test_ai_shell_integration.test 117 | @echo "All AI tests completed!" 118 | 119 | # Add a new target for config tests 120 | test-config: tests/test_config.test 121 | @echo "Running configuration system tests..." 122 | @./tests/test_config.test 123 | 124 | # Add a target for directory config tests 125 | test-dirconfig: tests/test_directory_config.test 126 | @echo "Running directory config tests..." 127 | @./tests/test_directory_config.test 128 | 129 | # Update the test build rule to exclude main.o 130 | tests/%.test: tests/%.o $(filter-out src/core/main.o, $(OBJ)) 131 | $(CC) -o $@ $^ $(LDFLAGS) 132 | 133 | # Add a release target that calls the build script 134 | release: 135 | @echo "Building release package..." 136 | @chmod +x scripts/build_release.sh 137 | @./scripts/build_release.sh $(VERSION) 138 | @echo "Release build complete." 139 | 140 | clean: 141 | rm -f $(OBJ) $(TEST_OBJ) $(TEST_BINS) nutshell -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nutshell 🥜 2 | 3 | Nutshell is an enhanced Unix shell that provides a simplified command language, package management, and AI-powered assistance. 4 | 5 | ## Features 6 | 7 | - Friendly command aliases (e.g., `peekaboo` for `ls`, `hop` for `cd`) 8 | - Built-in package management system 9 | - Dynamic package installation without rebuilding 10 | - Debug logging for development 11 | - Interactive Git commit helper (gitify package) 12 | - Shell command history 13 | - Redirection and background process support 14 | - AI-powered command assistance (NEW) 15 | 16 | ## Installation 17 | 18 | ### Prerequisites 19 | 20 | - C compiler (clang or gcc) 21 | - readline library 22 | - OpenSSL 23 | - libcurl 24 | - Jansson (optional, for package management) 25 | 26 | ### Building from source 27 | 28 | ```bash 29 | git clone https://github.com/chandralegend/nutshell.git 30 | cd nutshell 31 | make 32 | ``` 33 | 34 | ### Installing 35 | 36 | #### Via Homebrew (macOS & Linux) 37 | 38 | ```bash 39 | # If using the formula directly 40 | brew install --build-from-source ./nutshell.rb 41 | 42 | # If using the tap (Note: This tap is not yet available) 43 | brew tap chandralegend/nutshell 44 | brew install nutshell 45 | ``` 46 | 47 | #### System-wide installation (requires sudo) 48 | 49 | ```bash 50 | sudo make install 51 | ``` 52 | 53 | #### User-level installation 54 | 55 | ```bash 56 | make install-user 57 | ``` 58 | 59 | Then add `$HOME/bin` to your PATH if it's not already there: 60 | 61 | ```bash 62 | echo 'export PATH=$PATH:$HOME/bin' >> ~/.bashrc # or ~/.zshrc or ~/.bash_profile 63 | ``` 64 | 65 | ## Usage 66 | 67 | ### Basic commands 68 | 69 | Nutshell command | Unix equivalent | Description 70 | ---------------- | --------------- | ----------- 71 | `peekaboo` | `ls` | List directory contents 72 | `hop` | `cd` | Change directory 73 | `roast` | `exit` | Exit the shell 74 | 75 | ### Running commands 76 | 77 | Commands work just like in a standard Unix shell: 78 | 79 | ```bash 80 | 🥜 ~/projects ➜ peekaboo -la 81 | 🥜 ~/projects ➜ hop nutshell 82 | 🥜 ~/projects/nutshell ➜ command arg1 arg2 83 | ``` 84 | 85 | ## AI Command Assistance 86 | 87 | Nutshell includes AI features to help with shell commands: 88 | 89 | ### Setup 90 | 91 | 1. Get an OpenAI API key from [OpenAI Platform](https://platform.openai.com/) 92 | 2. Set your API key: 93 | 94 | ```bash 95 | 🥜 ~ ➜ set-api-key YOUR_API_KEY 96 | ``` 97 | 98 | Alternatively, set it as an environment variable: 99 | 100 | ```bash 101 | export OPENAI_API_KEY=your_api_key 102 | ``` 103 | 104 | ### AI Commands 105 | 106 | #### Ask for a command 107 | 108 | Convert natural language to shell commands: 109 | 110 | ```bash 111 | 🥜 ~ ➜ ask find all PDF files modified in the last week 112 | ``` 113 | 114 | The shell will return the proper command and ask if you want to execute it. 115 | 116 | #### Explain commands 117 | 118 | Get explanations for complex commands: 119 | 120 | ```bash 121 | 🥜 ~ ➜ explain find . -name "*.txt" -mtime -7 -exec grep -l "important" {} \; 122 | ``` 123 | 124 | #### Fix commands 125 | 126 | Automatically fix common command errors: 127 | 128 | ```bash 129 | 🥜 ~ ➜ torch apple.txt 130 | 🥜 ~ ➜ fix # Will suggest to use touch apple.txt instead. 131 | ``` 132 | 133 | The shell will suggest corrections for common mistakes and ask if you want to apply them. 134 | 135 | ### Debug Options 136 | 137 | Enable AI debugging with environment variables: 138 | 139 | ```bash 140 | # Run with AI debugging enabled 141 | NUT_DEBUG_AI=1 ./nutshell 142 | 143 | # Run with verbose API response logging 144 | NUT_DEBUG_AI=1 NUT_DEBUG_AI_VERBOSE=1 ./nutshell 145 | 146 | # Use the debug script 147 | ./scripts/debug_ai.sh 148 | ``` 149 | 150 | ## Package Management 151 | 152 | Nutshell has a built-in package system for extending functionality. 153 | 154 | ### Installing packages 155 | 156 | ``` 157 | 🥜 ~/projects ➜ install-pkg gitify 158 | ``` 159 | 160 | You can also install from a local directory: 161 | 162 | ``` 163 | 🥜 ~/projects ➜ install-pkg /path/to/package 164 | ``` 165 | 166 | ### Using the gitify package 167 | 168 | The gitify package provides an interactive Git commit helper: 169 | 170 | ``` 171 | 🥜 ~/projects/my-git-repo ➜ gitify 172 | ``` 173 | 174 | Follow the prompts to stage files and create semantic commit messages. 175 | 176 | ## Customizing Themes 177 | 178 | Nutshell comes with a customizable theme system: 179 | 180 | ### Listing available themes 181 | 182 | ```bash 183 | 🥜 ~ ➜ theme 184 | ``` 185 | 186 | This will show all available themes, with the current theme marked with an asterisk (*). 187 | 188 | ### Switching themes 189 | 190 | ```bash 191 | 🥜 ~ ➜ theme minimal 192 | ``` 193 | 194 | This will switch to the "minimal" theme. 195 | 196 | ### Creating your own theme 197 | 198 | 1. Create a new JSON file in `~/.nutshell/themes/` named `mytheme.json` 199 | 2. Use the following template: 200 | 201 | ```json 202 | { 203 | "name": "mytheme", 204 | "description": "My custom theme", 205 | "colors": { 206 | "reset": "\u001b[0m", 207 | "primary": "\u001b[1;35m", // Purple 208 | "secondary": "\u001b[1;33m", // Yellow 209 | "error": "\u001b[1;31m", 210 | "warning": "\u001b[0;33m", 211 | "info": "\u001b[0;34m", 212 | "success": "\u001b[0;32m" 213 | }, 214 | "prompt": { 215 | "left": { 216 | "format": "{primary}{icon} {directory}{reset} ", 217 | "icon": "🌟" 218 | }, 219 | "right": { 220 | "format": "{git_branch}" 221 | }, 222 | "multiline": false, 223 | "prompt_symbol": "→ ", 224 | "prompt_symbol_color": "primary" 225 | }, 226 | "segments": { 227 | "git_branch": { 228 | "enabled": true, 229 | "format": "{secondary}git:({branch}){reset} ", 230 | "command": "git branch --show-current 2>/dev/null" 231 | }, 232 | "directory": { 233 | "format": "{directory}", 234 | "command": "pwd | sed \"s|$HOME|~|\"" 235 | } 236 | } 237 | } 238 | ``` 239 | 240 | 3. Switch to your theme with `theme mytheme` 241 | 242 | ## Directory-level Configuration 243 | 244 | Nutshell now supports project-specific configurations through directory-level config files: 245 | 246 | ### How it works 247 | 248 | - Nutshell searches for a `.nutshell.json` configuration file in the current directory 249 | - If not found, it looks in parent directories until reaching your home directory 250 | - Directory configs take precedence over user configs which take precedence over system configs 251 | - Configurations are automatically reloaded when you change directories using `cd` 252 | 253 | ### Creating a directory config 254 | 255 | Create a `.nutshell.json` file in your project directory: 256 | 257 | ```json 258 | { 259 | "theme": "minimal", 260 | "aliases": { 261 | "build": "make all", 262 | "test": "make test", 263 | "deploy": "scripts/deploy.sh" 264 | }, 265 | "packages": ["gitify"] 266 | } 267 | ``` 268 | 269 | ### Benefits 270 | 271 | - Project-specific aliases and settings 272 | - Different themes for different projects 273 | - Shared configurations for team projects (add `.nutshell.json` to version control) 274 | - Hierarchical configuration (team settings in parent dir, personal tweaks in subdirs) 275 | 276 | ## Debugging 277 | 278 | For development or troubleshooting, run the debug script: 279 | 280 | ```bash 281 | ./scripts/debug-nutshell.sh 282 | ``` 283 | 284 | This enables detailed logging of command parsing and execution. 285 | 286 | ### Debug Flags 287 | 288 | Nutshell supports the following debug environment variables: 289 | 290 | - `NUT_DEBUG=1` - Enable general debug output 291 | - `NUT_DEBUG_THEME=1` - Enable theme system debugging 292 | - `NUT_DEBUG_PKG=1` - Enable package system debugging 293 | - `NUT_DEBUG_PARSER=1` - Enable command parser debugging 294 | - `NUT_DEBUG_EXEC=1` - Enable command execution debugging 295 | - `NUT_DEBUG_REGISTRY=1` - Enable command registry debugging 296 | - `NUT_DEBUG_AI=1` - Enable AI integration debugging 297 | - `NUT_DEBUG_AI_SHELL=1` - Enable AI shell integration debugging 298 | - `NUT_DEBUG_AI_VERBOSE=1` - Enable verbose API response logging 299 | 300 | Example: 301 | 302 | ```bash 303 | # Run with theme debugging enabled 304 | NUT_DEBUG_THEME=1 ./nutshell 305 | 306 | # Run with all debugging enabled 307 | NUT_DEBUG=1 NUT_DEBUG_THEME=1 NUT_DEBUG_PARSER=1 NUT_DEBUG_EXEC=1 NUT_DEBUG_REGISTRY=1 ./nutshell 308 | ``` 309 | 310 | ## Creating Packages 311 | 312 | Packages are directories containing: 313 | 314 | - `manifest.json`: Package metadata 315 | - `[package-name].sh`: Main executable script 316 | - Additional files as needed 317 | 318 | Example manifest.json: 319 | 320 | ```json 321 | { 322 | "name": "mypackage", 323 | "version": "1.0.0", 324 | "description": "My awesome package", 325 | "author": "Your Name", 326 | "dependencies": [], 327 | "sha256": "checksum" 328 | } 329 | ``` 330 | 331 | Generate a checksum for your package with: 332 | 333 | ```bash 334 | ./scripts/generate_checksum.sh mypackage 335 | ``` 336 | 337 | ## Testing 338 | 339 | ```bash 340 | make test # Run all tests 341 | make test-pkg # Test package installation 342 | make test-ai # Test AI integration 343 | ``` 344 | 345 | ## Contributing 346 | 347 | Contributions are welcome! Please read the [contributing guidelines](CONTRIBUTING.md) before submitting a pull request. 348 | 349 | ```bash 350 | nutshell/ 351 | ├── src/ 352 | │ ├── core/ 353 | │ │ ├── shell.c # Main shell loop 354 | │ │ ├── parser.c # Command parsing 355 | │ │ └── executor.c # Command execution 356 | │ ├── ai/ 357 | │ │ ├── openai.c # OpenAI integration 358 | │ │ └── local_ml.c # On-device ML 359 | │ ├── pkg/ 360 | │ │ ├── nutpkg.c # Package manager core 361 | │ │ └── registry.c # Package registry handling 362 | │ ├── utils/ 363 | │ │ ├── security.c # Security features 364 | │ │ ├── autocomplete.c # Tab completion 365 | │ │ └── helpers.c # Utility functions 366 | │ └── plugins/ # Loadable plugins 367 | ├── include/ 368 | │ ├── nutshell/ 369 | │ │ ├── core.h 370 | │ │ ├── ai.h 371 | │ │ ├── pkg.h 372 | │ │ └── utils.h 373 | ├── lib/ # 3rd party libs 374 | ├── scripts/ # Build/install scripts 375 | ├── packages/ # Local package cache 376 | ├── tests/ # Test suite 377 | ├── Makefile 378 | └── README.md 379 | ``` 380 | 381 | ## License 382 | 383 | [MIT License](LICENSE) 384 | -------------------------------------------------------------------------------- /include/nutshell/ai.h: -------------------------------------------------------------------------------- 1 | #ifndef NUTSHELL_AI_H 2 | #define NUTSHELL_AI_H 3 | 4 | #include 5 | #include // Add this to get ParsedCommand definition 6 | 7 | // Initialize AI integration 8 | bool init_ai_integration(); 9 | 10 | // Convert natural language to shell command 11 | char *nl_to_command(const char *natural_language_query); 12 | 13 | // Get command explanation 14 | char *explain_command_ai(const char *command); 15 | 16 | // Get command suggestions based on context 17 | char *suggest_commands(const char *context); 18 | 19 | // Get fix suggestion for an error 20 | char *suggest_fix(const char *command, const char *error, int exit_status); 21 | 22 | // Handle AI commands in the shell 23 | bool handle_ai_command(ParsedCommand *cmd); 24 | 25 | // Initialize AI shell integration 26 | void init_ai_shell(); 27 | 28 | // Cleanup AI resources 29 | void cleanup_ai_integration(); 30 | 31 | // Configuration 32 | void set_api_key(const char *key); 33 | bool has_api_key(); 34 | 35 | // Testing support - function pointer types 36 | typedef char* (*NlToCommandFunc)(const char*); 37 | typedef char* (*ExplainCommandFunc)(const char*); 38 | 39 | // Set mock implementations for testing 40 | void set_ai_mock_functions(NlToCommandFunc nl_func, ExplainCommandFunc explain_func); 41 | 42 | // Function to reset API key state for testing 43 | void reset_api_key_for_testing(); 44 | 45 | #endif // NUTSHELL_AI_H 46 | -------------------------------------------------------------------------------- /include/nutshell/config.h: -------------------------------------------------------------------------------- 1 | #ifndef NUTSHELL_CONFIG_H 2 | #define NUTSHELL_CONFIG_H 3 | 4 | #include 5 | 6 | // Configuration structure to store settings 7 | typedef struct { 8 | char *theme; // Current theme name 9 | char **enabled_packages; // Array of enabled package names 10 | int package_count; // Number of enabled packages 11 | char **aliases; // Array of custom aliases 12 | char **alias_commands; // Array of commands for each alias 13 | int alias_count; // Number of aliases 14 | char **scripts; // Array of custom script paths 15 | int script_count; // Number of custom scripts 16 | } Config; 17 | 18 | // Global configuration 19 | extern Config *global_config; 20 | 21 | // Configuration functions 22 | void init_config_system(); 23 | void cleanup_config_system(); 24 | 25 | // Load configuration from files (checks dir, user, system in that order) 26 | bool load_config_files(); // Renamed from load_config to avoid conflict 27 | 28 | // Save current configuration to user config file 29 | bool save_config(); 30 | 31 | // Update specific configuration settings 32 | bool set_config_theme(const char *theme_name); 33 | bool add_config_package(const char *package_name); 34 | bool remove_config_package(const char *package_name); 35 | bool add_config_alias(const char *alias_name, const char *command); 36 | bool remove_config_alias(const char *alias_name); 37 | bool add_config_script(const char *script_path); 38 | bool remove_config_script(const char *script_path); 39 | 40 | // New functions for directory-level configuration 41 | bool reload_directory_config(); 42 | void cleanup_config_values(); 43 | 44 | // Get configuration settings 45 | const char *get_config_theme(); 46 | bool is_package_enabled(const char *package_name); 47 | const char *get_alias_command(const char *alias_name); 48 | 49 | #endif // NUTSHELL_CONFIG_H 50 | -------------------------------------------------------------------------------- /include/nutshell/core.h: -------------------------------------------------------------------------------- 1 | #ifndef NUTSHELL_CORE_H 2 | #define NUTSHELL_CORE_H 3 | 4 | // Add these feature test macros 5 | #define _POSIX_C_SOURCE 200809L 6 | #define _GNU_SOURCE 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #define MAX_ARGS 64 14 | #define MAX_CMD_LEN 1024 15 | #define PROMPT_MAX 256 16 | 17 | typedef struct CommandMapping { 18 | char *unix_cmd; 19 | char *nut_cmd; 20 | bool is_builtin; 21 | } CommandMapping; 22 | 23 | typedef struct CommandRegistry { 24 | CommandMapping *commands; 25 | size_t count; 26 | } CommandRegistry; 27 | 28 | typedef struct ParsedCommand { 29 | char **args; 30 | char *input_file; 31 | char *output_file; 32 | bool background; 33 | } ParsedCommand; 34 | 35 | // Command history tracking 36 | typedef struct CommandHistory { 37 | char *last_command; 38 | char *last_output; 39 | int exit_status; 40 | bool has_error; 41 | } CommandHistory; 42 | 43 | // Global command history for error fixing 44 | extern CommandHistory cmd_history; 45 | 46 | // Registry functions 47 | void init_registry(); 48 | void register_command(const char *unix_cmd, const char *nut_cmd, bool is_builtin); 49 | const CommandMapping *find_command(const char *input_cmd); 50 | void free_registry(); 51 | 52 | // Parser functions 53 | ParsedCommand *parse_command(char *input); 54 | void free_parsed_command(ParsedCommand *cmd); 55 | 56 | // Executor functions 57 | void execute_command(ParsedCommand *cmd); 58 | 59 | // Shell core 60 | void shell_loop(); 61 | char *get_prompt(); 62 | void handle_sigint(int sig); 63 | 64 | // Add to the function declarations section 65 | void load_packages_from_dir(const char *dir_path); 66 | 67 | #endif // NUTSHELL_CORE_H -------------------------------------------------------------------------------- /include/nutshell/pkg.h: -------------------------------------------------------------------------------- 1 | #ifndef NUTSHELL_PKG_H 2 | #define NUTSHELL_PKG_H 3 | 4 | #include 5 | 6 | #define NUTPKG_REGISTRY "https://registry.nutshell.sh/v1" 7 | #define MAX_DEPENDENCIES 32 8 | 9 | typedef struct { 10 | char* name; 11 | char* version; 12 | char* description; 13 | char* dependencies[MAX_DEPENDENCIES]; 14 | char* author; 15 | char* checksum; 16 | } PackageManifest; 17 | 18 | typedef enum { 19 | PKG_INSTALL_SUCCESS, 20 | PKG_INSTALL_FAILED, 21 | PKG_DEPENDENCY_FAILED, 22 | PKG_INTEGRITY_FAILED 23 | } PkgInstallResult; 24 | 25 | // Package management functions 26 | PkgInstallResult nutpkg_install(const char* pkg_name); 27 | bool nutpkg_uninstall(const char* pkg_name); 28 | PackageManifest* nutpkg_search(const char* query); 29 | void nutpkg_cleanup(PackageManifest* manifest); 30 | 31 | // Dependency resolution 32 | bool resolve_dependencies(const char* pkg_name); 33 | 34 | // Integrity verification 35 | bool verify_package_integrity(const PackageManifest* manifest); 36 | 37 | // Dynamic package installation functions 38 | bool install_package_from_path(const char *path); 39 | bool install_package_from_name(const char *name); 40 | bool register_package_commands(const char *pkg_dir, const char *pkg_name); 41 | 42 | #endif // NUTSHELL_PKG_H -------------------------------------------------------------------------------- /include/nutshell/theme.h: -------------------------------------------------------------------------------- 1 | #ifndef NUTSHELL_THEME_H 2 | #define NUTSHELL_THEME_H 3 | 4 | #include 5 | 6 | // Command with its output 7 | typedef struct { 8 | char *name; // Name of the command (key) 9 | char *command; // Command to execute 10 | char *output; // Cached output 11 | } ThemeCommand; 12 | 13 | // Theme segment structure 14 | typedef struct { 15 | bool enabled; 16 | char *key; // Add this field to store the segment name from JSON 17 | char *format; 18 | int command_count; 19 | ThemeCommand **commands; // Array of commands for this segment 20 | } ThemeSegment; 21 | 22 | // Theme color mapping 23 | typedef struct { 24 | char *reset; 25 | char *primary; 26 | char *secondary; 27 | char *error; 28 | char *warning; 29 | char *info; 30 | char *success; 31 | } ThemeColors; 32 | 33 | // Prompt configuration 34 | typedef struct { 35 | char *format; 36 | char *icon; 37 | } PromptConfig; 38 | 39 | // Overall theme structure 40 | typedef struct { 41 | char *name; 42 | char *description; 43 | ThemeColors *colors; 44 | PromptConfig *left_prompt; 45 | PromptConfig *right_prompt; 46 | bool multiline; 47 | char *prompt_symbol; 48 | char *prompt_symbol_color; 49 | ThemeSegment **segments; 50 | int segment_count; 51 | } Theme; 52 | 53 | // Theme management functions 54 | void init_theme_system(); 55 | void cleanup_theme_system(); 56 | Theme *load_theme(const char *theme_name); 57 | void free_theme(Theme *theme); 58 | char *get_theme_prompt(Theme *theme); 59 | char *expand_theme_format(Theme *theme, const char *format); 60 | char *get_segment_output(Theme *theme, const char *segment_name); 61 | void execute_segment_commands(ThemeSegment *segment); // Added this function declaration 62 | 63 | // Builtin theme command 64 | int theme_command(int argc, char **argv); 65 | 66 | // Current theme 67 | extern Theme *current_theme; 68 | 69 | #endif // NUTSHELL_THEME_H 70 | -------------------------------------------------------------------------------- /include/nutshell/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef NUTSHELL_UTILS_H 2 | #define NUTSHELL_UTILS_H 3 | 4 | #include 5 | #include "core.h" 6 | 7 | // Error handling 8 | void print_error(const char *msg); 9 | void print_success(const char *msg); 10 | 11 | // File utilities 12 | bool file_exists(const char *path); 13 | char *expand_path(const char *path); 14 | 15 | // String utilities 16 | char **split_string(const char *input, const char *delim, int *count); 17 | char *trim_whitespace(char *str); 18 | char *str_replace(const char *str, const char *find, const char *replace); 19 | 20 | // Security utilities 21 | bool sanitize_command(const char *cmd); 22 | bool is_safe_path(const char *path); 23 | 24 | // Configuration utilities 25 | void load_config(const char *path); 26 | void reload_config(); 27 | 28 | // Helper macros 29 | #define DEBUG_LOG(fmt, ...) \ 30 | do { if (getenv("NUT_DEBUG")) fprintf(stderr, "DEBUG: " fmt "\n", ##__VA_ARGS__); } while(0) 31 | 32 | #define NUT_ASSERT(expr) \ 33 | do { \ 34 | if (!(expr)) { \ 35 | fprintf(stderr, "Assertion failed: %s (%s:%d)\n", #expr, __FILE__, __LINE__); \ 36 | abort(); \ 37 | } \ 38 | } while(0) 39 | 40 | #endif // NUTSHELL_UTILS_H -------------------------------------------------------------------------------- /nutshell.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Nutshell < Formula 4 | desc 'Enhanced Unix shell with simplified command language and AI assistance' 5 | homepage 'https://github.com/chandralegend/nutshell' 6 | url 'https://github.com/chandralegend/nutshell/archive/refs/tags/v0.0.4.tar.gz' 7 | sha256 'd3cd4b9b64fb6d657195beb7ea9d47a193ace561d8d54b64e9890304e41c6829' 8 | license 'MIT' 9 | head 'https://github.com/chandralegend/nutshell.git', branch: 'main' 10 | 11 | depends_on 'pkg-config' => :build 12 | depends_on 'jansson' 13 | depends_on 'readline' 14 | depends_on 'openssl@3' 15 | depends_on 'curl' 16 | 17 | def install 18 | # Pass correct environment variables to find libraries 19 | ENV.append 'CFLAGS', "-I#{Formula['jansson'].opt_include}" 20 | ENV.append 'LDFLAGS', "-L#{Formula['jansson'].opt_lib} -ljansson" 21 | ENV.append 'CFLAGS', "-I#{Formula['openssl@3'].opt_include}" 22 | ENV.append 'LDFLAGS', "-L#{Formula['openssl@3'].opt_lib}" 23 | 24 | system 'make' 25 | bin.install 'nutshell' 26 | 27 | # Install documentation 28 | doc.install 'README.md', 'CHANGELOG.md' 29 | 30 | # Create themes directory and install themes directly in the Cellar 31 | # The themes directory will be in the Formula's prefix, not in /usr/local/share 32 | themes_dir = prefix / 'themes' 33 | themes_dir.mkpath 34 | Dir['themes/*.json'].each do |theme_file| 35 | themes_dir.install theme_file 36 | end 37 | 38 | # Create a nutshell config directory in the Formula's prefix for packages 39 | (prefix / 'packages').mkpath 40 | end 41 | 42 | def post_install 43 | # Create ~/.nutshell directory structure for the user if it doesn't exist 44 | user_config_dir = "#{Dir.home}/.nutshell" 45 | user_themes_dir = "#{user_config_dir}/themes" 46 | user_packages_dir = "#{user_config_dir}/packages" 47 | 48 | system 'mkdir', '-p', user_themes_dir 49 | system 'mkdir', '-p', user_packages_dir 50 | 51 | # Copy themes to user directory if it doesn't already have them 52 | if Dir.exist?(user_themes_dir) && Dir.empty?(user_themes_dir) 53 | Dir["#{prefix}/themes/*.json"].each do |theme| 54 | system 'cp', theme, user_themes_dir 55 | end 56 | end 57 | 58 | # Print instructions for the user 59 | ohai 'Nutshell has been installed!' 60 | opoo 'Make sure to set an API key for AI features with: set-api-key YOUR_API_KEY' 61 | end 62 | 63 | test do 64 | # Test that nutshell runs without errors (--help should return 0) 65 | assert_match 'Nutshell', shell_output("#{bin}/nutshell --help", 0) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /packages/gitify/README.md: -------------------------------------------------------------------------------- 1 | # Gitify - Interactive Git Commit Helper 2 | 3 | ## Overview 4 | Gitify is a simple interactive tool that makes Git commits easier by guiding you through selecting files and creating semantic commit messages. 5 | 6 | ## Features 7 | - Check if you're in a Git repository 8 | - Show current Git status 9 | - Interactive file selection for staging 10 | - Semantic commit message creation with types and scopes 11 | - Confirmation before committing 12 | 13 | ## Usage 14 | ``` 15 | gitify 16 | ``` 17 | 18 | Simply run the command and follow the interactive prompts. 19 | 20 | ## Commit Types 21 | - **feat**: A new feature 22 | - **fix**: A bug fix 23 | - **docs**: Documentation only changes 24 | - **style**: Changes that do not affect the meaning of the code 25 | - **refactor**: A code change that neither fixes a bug nor adds a feature 26 | - **perf**: A code change that improves performance 27 | - **test**: Adding missing tests 28 | - **build**: Changes that affect the build system 29 | - **ci**: Changes to CI configuration 30 | - **chore**: Other changes that don't modify src or test files 31 | 32 | ## Installation 33 | This package is installed automatically when you install Nutshell. 34 | ``` 35 | ``` 36 | -------------------------------------------------------------------------------- /packages/gitify/gitify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if we're in a git repository 4 | if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then 5 | echo "🥜 Error: Not in a git repository" 6 | exit 1 7 | fi 8 | 9 | # Show git status 10 | echo "🥜 Current git status:" 11 | git status -s 12 | 13 | # Check if there are any changes 14 | if [ -z "$(git status --porcelain)" ]; then 15 | echo "🥜 No changes to commit" 16 | exit 0 17 | fi 18 | 19 | # Ask for files to stage 20 | echo "🥜 Select files to stage (space-separated list, or 'all' for all changes):" 21 | read -r files_to_stage 22 | 23 | if [ "$files_to_stage" = "all" ]; then 24 | git add . 25 | echo "🥜 All files staged" 26 | else 27 | for file in $files_to_stage; do 28 | if git status -s "$file" > /dev/null 2>&1; then 29 | git add "$file" 30 | echo "🥜 Staged: $file" 31 | else 32 | echo "🥜 Warning: $file not found or not modified" 33 | fi 34 | done 35 | fi 36 | 37 | # Commit type selection 38 | echo "🥜 Select commit type:" 39 | commit_types=( 40 | "feat: A new feature" 41 | "fix: A bug fix" 42 | "docs: Documentation only changes" 43 | "style: Changes that do not affect the meaning of the code" 44 | "refactor: A code change that neither fixes a bug nor adds a feature" 45 | "perf: A code change that improves performance" 46 | "test: Adding missing tests or correcting existing tests" 47 | "build: Changes that affect the build system or external dependencies" 48 | "ci: Changes to CI configuration files and scripts" 49 | "chore: Other changes that don't modify src or test files" 50 | ) 51 | 52 | select commit_type in "${commit_types[@]}"; do 53 | if [ -n "$commit_type" ]; then 54 | selected_type="${commit_type%%:*}" 55 | break 56 | fi 57 | done 58 | 59 | # Ask for commit scope (optional) 60 | echo "🥜 Enter commit scope (optional, press enter to skip):" 61 | read -r commit_scope 62 | 63 | # Format scope if provided 64 | if [ -n "$commit_scope" ]; then 65 | commit_scope="($commit_scope)" 66 | fi 67 | 68 | # Ask for commit message 69 | echo "🥜 Enter commit message:" 70 | read -r commit_message 71 | 72 | # Build the full commit message 73 | full_commit_message="$selected_type$commit_scope: $commit_message" 74 | 75 | # Show the final commit message and confirm 76 | echo "🥜 Final commit message: $full_commit_message" 77 | echo "🥜 Proceed with commit? (y/n)" 78 | read -r confirm 79 | 80 | if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then 81 | git commit -m "$full_commit_message" 82 | echo "🥜 Changes committed successfully!" 83 | else 84 | echo "🥜 Commit aborted" 85 | fi 86 | -------------------------------------------------------------------------------- /packages/gitify/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitify", 3 | "version": "1.0.0", 4 | "description": "Interactive Git commit helper for Nutshell", 5 | "author": "Nutshell Team", 6 | "dependencies": [], 7 | "sha256": "to_be_generated_after_script_is_created" 8 | } 9 | -------------------------------------------------------------------------------- /packages/sample/package.nut: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nut-git", 3 | "version": "2.1.0", 4 | "description": "Git integration for Nutshell shell", 5 | "commands": { 6 | "acorn": "git commit -m", 7 | "treetoss": "git push", 8 | "squirrel": "git stash" 9 | }, 10 | "dependencies": [ 11 | "nut-corelib" 12 | ], 13 | "checksum": "a1b2c3...", 14 | "author": "Nutty Developers" 15 | } -------------------------------------------------------------------------------- /scripts/build_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # build_release.sh - Create a release bundle for Nutshell shell 4 | # Usage: ./scripts/build_release.sh [version] 5 | 6 | set -e # Exit on any error 7 | 8 | # Determine script directory and project root 9 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 10 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" 11 | cd "$PROJECT_ROOT" 12 | 13 | # Get version (either from argument or generate based on date) 14 | if [ -n "$1" ]; then 15 | VERSION="$1" 16 | else 17 | VERSION="$(date +%Y.%m.%d)" 18 | fi 19 | 20 | echo "Building Nutshell release version $VERSION..." 21 | 22 | # Create build directory 23 | BUILD_DIR="$PROJECT_ROOT/build" 24 | DIST_DIR="$BUILD_DIR/nutshell-$VERSION" 25 | rm -rf "$DIST_DIR" 26 | mkdir -p "$DIST_DIR" 27 | 28 | # Compile with optimizations 29 | echo "Compiling with optimizations..." 30 | make clean 31 | CFLAGS="-O2 -DNDEBUG" make 32 | 33 | # Create directory structure 34 | mkdir -p "$DIST_DIR/bin" 35 | mkdir -p "$DIST_DIR/packages" 36 | mkdir -p "$DIST_DIR/doc" 37 | 38 | # Copy binary and essential files 39 | cp "$PROJECT_ROOT/nutshell" "$DIST_DIR/bin/" 40 | cp "$PROJECT_ROOT/README.md" "$DIST_DIR/doc/" 41 | cp -r "$PROJECT_ROOT/packages/gitify" "$DIST_DIR/packages/" 42 | 43 | # Generate version file 44 | echo "$VERSION" > "$DIST_DIR/VERSION" 45 | 46 | # Create simple installer script 47 | cat > "$DIST_DIR/install.sh" << 'EOL' 48 | #!/bin/bash 49 | set -e 50 | 51 | # Get script directory 52 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 53 | 54 | # Default install locations 55 | PREFIX="/usr/local" 56 | USER_INSTALL=false 57 | 58 | # Parse arguments 59 | while [[ $# -gt 0 ]]; do 60 | key="$1" 61 | case $key in 62 | --prefix=*) 63 | PREFIX="${key#*=}" 64 | shift 65 | ;; 66 | --user) 67 | USER_INSTALL=true 68 | shift 69 | ;; 70 | *) 71 | echo "Unknown option: $key" 72 | echo "Usage: $0 [--prefix=PATH] [--user]" 73 | exit 1 74 | ;; 75 | esac 76 | done 77 | 78 | if [ "$USER_INSTALL" = true ]; then 79 | # User installation 80 | mkdir -p "$HOME/bin" 81 | mkdir -p "$HOME/.nutshell/packages" 82 | 83 | cp "$SCRIPT_DIR/bin/nutshell" "$HOME/bin/" 84 | cp -r "$SCRIPT_DIR/packages/"* "$HOME/.nutshell/packages/" 85 | 86 | echo "Nutshell installed to $HOME/bin/nutshell" 87 | echo "Make sure $HOME/bin is in your PATH" 88 | echo "You can add it with: echo 'export PATH=\$PATH:\$HOME/bin' >> ~/.bashrc" 89 | else 90 | # System installation 91 | if [ "$(id -u)" -ne 0 ]; then 92 | echo "System installation requires root privileges" 93 | echo "Please run with sudo or use --user for user installation" 94 | exit 1 95 | fi 96 | 97 | mkdir -p "$PREFIX/bin" 98 | mkdir -p "$PREFIX/share/nutshell/packages" 99 | 100 | cp "$SCRIPT_DIR/bin/nutshell" "$PREFIX/bin/" 101 | cp -r "$SCRIPT_DIR/packages/"* "$PREFIX/share/nutshell/packages/" 102 | 103 | echo "Nutshell installed to $PREFIX/bin/nutshell" 104 | fi 105 | 106 | echo "Installation complete!" 107 | EOL 108 | 109 | # Make installer executable 110 | chmod +x "$DIST_DIR/install.sh" 111 | 112 | # Create archive 113 | echo "Creating distribution archive..." 114 | cd "$BUILD_DIR" 115 | tar -czf "nutshell-$VERSION.tar.gz" "nutshell-$VERSION" 116 | 117 | # Generate checksum 118 | echo "Generating checksums..." 119 | cd "$BUILD_DIR" 120 | sha256sum "nutshell-$VERSION.tar.gz" > "nutshell-$VERSION.sha256" 121 | 122 | echo "Release bundle created at $BUILD_DIR/nutshell-$VERSION.tar.gz" 123 | echo "Checksums available at $BUILD_DIR/nutshell-$VERSION.sha256" 124 | -------------------------------------------------------------------------------- /scripts/debug_ai.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Debug script specifically for AI components 4 | # This script runs nutshell with all AI debugging enabled 5 | 6 | echo "Starting Nutshell with AI debugging enabled..." 7 | 8 | # Make sure the nutshell binary exists 9 | if [ ! -f "./nutshell" ]; then 10 | echo "Building Nutshell..." 11 | make clean && make 12 | fi 13 | 14 | # Set debugging environment variables 15 | export NUT_DEBUG=1 # General debug 16 | export NUT_DEBUG_AI=1 # AI module debug 17 | export NUT_DEBUG_AI_SHELL=1 # AI shell integration debug 18 | export NUT_DEBUG_AI_VERBOSE=0 # Set to 1 to see full API responses 19 | 20 | echo "Debug flags set:" 21 | echo "NUT_DEBUG=$NUT_DEBUG" 22 | echo "NUT_DEBUG_AI=$NUT_DEBUG_AI" 23 | echo "NUT_DEBUG_AI_SHELL=$NUT_DEBUG_AI_SHELL" 24 | echo "NUT_DEBUG_AI_VERBOSE=$NUT_DEBUG_AI_VERBOSE" 25 | 26 | echo "Starting Nutshell with AI debugging..." 27 | echo "--------------------------------------" 28 | ./nutshell 29 | 30 | # If you have a test OpenAI API key, you can uncomment this line: 31 | # OPENAI_API_KEY=your_test_key_here ./nutshell 32 | -------------------------------------------------------------------------------- /scripts/debug_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set debug environment variables 4 | export NUT_DEBUG=1 5 | export NUT_DEBUG_CONFIG=1 6 | 7 | # Run the configuration test with debugging 8 | make test-config 9 | 10 | # Or run the shell with debugging 11 | # make && ./nutshell 12 | -------------------------------------------------------------------------------- /scripts/debug_nutshell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run nutshell with environment variables for debugging 4 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 5 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" 6 | 7 | # Build nutshell if needed 8 | echo "Building nutshell..." 9 | cd "$PROJECT_ROOT" 10 | make clean && make 11 | 12 | echo "Starting nutshell in debug mode..." 13 | export NUT_DEBUG=1 14 | 15 | # Get the proper Jansson library path 16 | JANSSON_LIB_PATH=$(pkg-config --libs jansson 2>/dev/null || echo "-L/usr/local/opt/jansson/lib -ljansson") 17 | JANSSON_INCLUDE_PATH=$(pkg-config --cflags jansson 2>/dev/null || echo "-I/usr/local/opt/jansson/include") 18 | 19 | # Enable core dumps 20 | ulimit -c unlimited 21 | 22 | # First try a basic build without sanitizer 23 | echo "Building debug version without sanitizer..." 24 | clang -g -I./include -I/usr/local/include -I/opt/homebrew/include $JANSSON_INCLUDE_PATH \ 25 | -o nutshell-debug $PROJECT_ROOT/src/core/*.c $PROJECT_ROOT/src/pkg/*.c $PROJECT_ROOT/src/utils/*.c $PROJECT_ROOT/src/ai/*.c \ 26 | -lreadline -lcurl -ldl -lssl -lcrypto $JANSSON_LIB_PATH 27 | 28 | if [ -f "./nutshell-debug" ]; then 29 | echo "Running debug build..." 30 | ./nutshell-debug 31 | EXIT_CODE=$? 32 | if [ $EXIT_CODE -ne 0 ]; then 33 | echo "Debug build crashed with exit code $EXIT_CODE" 34 | 35 | # If LLDB is available, try to run with it for more details 36 | if command -v lldb >/dev/null 2>&1; then 37 | echo "Running with LLDB for more details..." 38 | lldb -- ./nutshell-debug 39 | elif command -v gdb >/dev/null 2>&1; then 40 | echo "Running with GDB for more details..." 41 | gdb --args ./nutshell-debug 42 | else 43 | echo "Consider installing LLDB or GDB for better debugging" 44 | echo "Falling back to regular binary" 45 | ./nutshell 46 | fi 47 | fi 48 | else 49 | echo "Failed to build debug version, falling back to regular binary" 50 | ./nutshell 51 | fi 52 | 53 | echo "Debug session ended" 54 | -------------------------------------------------------------------------------- /scripts/generate_checksum.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Generate SHA256 checksum for a package 4 | # Usage: ./generate_checksum.sh package_name 5 | 6 | if [ -z "$1" ]; then 7 | echo "Usage: $0 package_name" 8 | exit 1 9 | fi 10 | 11 | PACKAGE_NAME="$1" 12 | PACKAGES_DIR="/Users/chandralegend/Desktop/nutshell/packages" 13 | PACKAGE_DIR="$PACKAGES_DIR/$PACKAGE_NAME" 14 | 15 | if [ ! -d "$PACKAGE_DIR" ]; then 16 | echo "Package $PACKAGE_NAME not found in $PACKAGES_DIR" 17 | exit 1 18 | fi 19 | 20 | # Create tarball of the package 21 | TEMP_DIR=$(mktemp -d) 22 | TAR_FILE="$TEMP_DIR/$PACKAGE_NAME.tar.gz" 23 | 24 | # Exclude manifest.json since it contains the checksum we'll update 25 | tar -czf "$TAR_FILE" -C "$PACKAGE_DIR" --exclude="manifest.json" . 26 | 27 | # Generate SHA256 checksum 28 | CHECKSUM=$(openssl dgst -sha256 "$TAR_FILE" | awk '{print $2}') 29 | 30 | # Update manifest.json with the new checksum 31 | MANIFEST_FILE="$PACKAGE_DIR/manifest.json" 32 | TMP_MANIFEST=$(mktemp) 33 | 34 | # Replace the checksum in the manifest file 35 | sed "s/\"sha256\":.*/\"sha256\": \"$CHECKSUM\",/" "$MANIFEST_FILE" > "$TMP_MANIFEST" 36 | mv "$TMP_MANIFEST" "$MANIFEST_FILE" 37 | 38 | echo "Updated checksum for $PACKAGE_NAME: $CHECKSUM" 39 | 40 | # Cleanup 41 | rm -rf "$TEMP_DIR" 42 | -------------------------------------------------------------------------------- /scripts/install_theme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install a theme from a JSON file or URL 4 | # Usage: ./install_theme.sh 5 | 6 | set -e 7 | 8 | # Ensure themes directory exists 9 | THEME_DIR="$HOME/.nutshell/themes" 10 | mkdir -p "$THEME_DIR" 11 | 12 | if [ $# -lt 1 ]; then 13 | echo "Usage: $0 " 14 | exit 1 15 | fi 16 | 17 | SOURCE="$1" 18 | 19 | # Check if it's a URL (starts with http:// or https://) 20 | if [[ "$SOURCE" == http://* || "$SOURCE" == https://* ]]; then 21 | # Download the theme 22 | echo "Downloading theme from $SOURCE" 23 | TEMP_FILE=$(mktemp) 24 | if command -v curl &> /dev/null; then 25 | curl -s "$SOURCE" -o "$TEMP_FILE" 26 | elif command -v wget &> /dev/null; then 27 | wget -q -O "$TEMP_FILE" "$SOURCE" 28 | else 29 | echo "Error: Neither curl nor wget is installed" 30 | exit 1 31 | fi 32 | 33 | # Extract the filename from URL 34 | FILENAME=$(basename "$SOURCE") 35 | if [[ ! "$FILENAME" == *.json ]]; then 36 | FILENAME="downloaded_theme.json" 37 | fi 38 | 39 | # Check if it's valid JSON 40 | if ! jq . "$TEMP_FILE" > /dev/null 2>&1; then 41 | echo "Error: Not a valid JSON file" 42 | rm "$TEMP_FILE" 43 | exit 1 44 | fi 45 | 46 | # Get theme name from JSON if possible 47 | if command -v jq &> /dev/null; then 48 | THEME_NAME=$(jq -r '.name' "$TEMP_FILE" 2>/dev/null) 49 | if [ "$THEME_NAME" != "null" ] && [ -n "$THEME_NAME" ]; then 50 | FILENAME="${THEME_NAME}.json" 51 | fi 52 | fi 53 | 54 | # Copy to themes directory 55 | cp "$TEMP_FILE" "$THEME_DIR/$FILENAME" 56 | rm "$TEMP_FILE" 57 | 58 | echo "Theme installed to $THEME_DIR/$FILENAME" 59 | echo "You can now apply it with: theme ${FILENAME%.json}" 60 | 61 | else 62 | # Assume it's a local file 63 | if [ ! -f "$SOURCE" ]; then 64 | echo "Error: File not found: $SOURCE" 65 | exit 1 66 | fi 67 | 68 | # Check if it's valid JSON 69 | if ! jq . "$SOURCE" > /dev/null 2>&1; then 70 | echo "Error: Not a valid JSON file" 71 | exit 1 72 | fi 73 | 74 | # Get the filename 75 | FILENAME=$(basename "$SOURCE") 76 | 77 | # Get theme name from JSON if possible 78 | if command -v jq &> /dev/null; then 79 | THEME_NAME=$(jq -r '.name' "$SOURCE" 2>/dev/null) 80 | if [ "$THEME_NAME" != "null" ] && [ -n "$THEME_NAME" ]; then 81 | FILENAME="${THEME_NAME}.json" 82 | fi 83 | fi 84 | 85 | # Copy to themes directory 86 | cp "$SOURCE" "$THEME_DIR/$FILENAME" 87 | 88 | echo "Theme installed to $THEME_DIR/$FILENAME" 89 | echo "You can now apply it with: theme ${FILENAME%.json}" 90 | fi 91 | -------------------------------------------------------------------------------- /scripts/run_ai_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run AI integration tests 4 | echo "Setting up AI test environment..." 5 | 6 | # Create necessary directories 7 | mkdir -p "$HOME/.nutshell" 8 | 9 | # Remove any existing API key file to start with clean state 10 | rm -f "$HOME/.nutshell/openai_key" 11 | 12 | # Build all AI-related tests 13 | cd "$(dirname "$0")/.." 14 | echo "Building AI tests..." 15 | make tests/test_ai_integration.test 16 | make tests/test_openai_commands.test 17 | make tests/test_ai_shell_integration.test 18 | 19 | # Run the tests with debugging enabled 20 | echo "Running AI tests with debugging enabled..." 21 | export NUT_DEBUG=1 22 | export NUT_DEBUG_AI=1 23 | export NUT_DEBUG_AI_SHELL=1 24 | export NUT_DEBUG_AI_VERBOSE=1 # Enable verbose API response logging 25 | export NUTSHELL_TESTING=1 # Set testing mode to use mocks instead of real API 26 | 27 | # Run each test separately to isolate any failures 28 | echo "Running AI integration test..." 29 | ./tests/test_ai_integration.test 30 | 31 | echo "Running OpenAI commands test..." 32 | ./tests/test_openai_commands.test 33 | 34 | echo "Running AI shell integration test..." 35 | ./tests/test_ai_shell_integration.test 36 | 37 | echo "All AI tests completed!" 38 | -------------------------------------------------------------------------------- /scripts/run_pkg_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script runs the package installation test 4 | 5 | # Ensure packages directory structure is ready 6 | mkdir -p "$HOME/.nutshell/packages" 7 | 8 | # Build the test 9 | cd "$(dirname "$0")/.." 10 | make tests/test_pkg_install.test 11 | 12 | # Run the test 13 | ./tests/test_pkg_install.test 14 | 15 | echo "Test completed!" 16 | -------------------------------------------------------------------------------- /scripts/run_theme_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script runs the theme system tests 4 | echo "Setting up theme test environment..." 5 | 6 | # Ensure themes directory structure is ready 7 | mkdir -p "$HOME/.nutshell/themes" 8 | echo "Created directory: $HOME/.nutshell/themes" 9 | 10 | # Show current directory for debugging 11 | echo "Current directory: $(pwd)" 12 | echo "Contents of themes directory:" 13 | ls -la ./themes/ 14 | 15 | # Copy test themes to user directory 16 | echo "Copying theme files to user directory..." 17 | cp -fv "$PWD/themes/"*.json "$HOME/.nutshell/themes/" 18 | 19 | # Check that themes were copied 20 | echo "Contents of user themes directory:" 21 | ls -la "$HOME/.nutshell/themes/" 22 | 23 | # Build the test 24 | cd "$(dirname "$0")/.." 25 | echo "Building theme test..." 26 | make tests/test_theme.test 27 | 28 | # Run the test with some debug environment variables 29 | echo "Running theme test with debugging..." 30 | # Set both general debug and theme-specific debug 31 | NUT_DEBUG=1 NUT_DEBUG_THEME=1 ASAN_OPTIONS=detect_leaks=0 ./tests/test_theme.test 32 | 33 | echo "Theme tests completed!" 34 | -------------------------------------------------------------------------------- /scripts/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # uninstall.sh - Remove Nutshell installation 4 | # Usage: ./scripts/uninstall.sh [--user] 5 | 6 | USER_UNINSTALL=false 7 | 8 | # Parse arguments 9 | while [[ $# -gt 0 ]]; do 10 | key="$1" 11 | case $key in 12 | --user) 13 | USER_UNINSTALL=true 14 | shift 15 | ;; 16 | *) 17 | echo "Unknown option: $key" 18 | echo "Usage: $0 [--user]" 19 | exit 1 20 | ;; 21 | esac 22 | done 23 | 24 | if [ "$USER_UNINSTALL" = true ]; then 25 | # User installation removal 26 | echo "Removing user installation..." 27 | if [ -f "$HOME/bin/nutshell" ]; then 28 | rm -f "$HOME/bin/nutshell" 29 | echo "Removed binary from $HOME/bin/nutshell" 30 | fi 31 | 32 | if [ -d "$HOME/.nutshell" ]; then 33 | rm -rf "$HOME/.nutshell" 34 | echo "Removed configuration directory $HOME/.nutshell" 35 | fi 36 | else 37 | # System installation removal 38 | if [ "$(id -u)" -ne 0 ]; then 39 | echo "System uninstallation requires root privileges" 40 | echo "Please run with sudo or use --user for user uninstallation" 41 | exit 1 42 | fi 43 | 44 | echo "Removing system installation..." 45 | if [ -f "/usr/local/bin/nutshell" ]; then 46 | rm -f "/usr/local/bin/nutshell" 47 | echo "Removed binary from /usr/local/bin/nutshell" 48 | fi 49 | 50 | if [ -d "/usr/local/share/nutshell" ]; then 51 | rm -rf "/usr/local/share/nutshell" 52 | echo "Removed package directory /usr/local/share/nutshell" 53 | fi 54 | fi 55 | 56 | echo "Nutshell has been uninstalled successfully." 57 | -------------------------------------------------------------------------------- /src/ai/shell.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // Define debug macro for AI shell integration 11 | #define AI_SHELL_DEBUG(fmt, ...) \ 12 | do { if (getenv("NUT_DEBUG_AI_SHELL")) fprintf(stderr, "AI_SHELL: " fmt "\n", ##__VA_ARGS__); } while(0) 13 | 14 | // External declarations for AI commands 15 | extern int set_api_key_command(int argc, char **argv); 16 | extern int ask_ai_command(int argc, char **argv); 17 | extern int explain_command(int argc, char **argv); 18 | extern int fix_command(int argc, char **argv); // Add the new command 19 | 20 | // Register AI commands with the shell 21 | void register_ai_commands() { 22 | AI_SHELL_DEBUG("Registering AI commands"); 23 | 24 | // Register the AI commands 25 | register_command("set-api-key", "set-api-key", true); 26 | register_command("ask", "ask", true); 27 | register_command("explain", "explain", true); 28 | register_command("fix", "fix", true); // Register the new command 29 | 30 | AI_SHELL_DEBUG("AI commands registered successfully"); 31 | } 32 | 33 | // Initialize AI integration for the shell 34 | void init_ai_shell() { 35 | AI_SHELL_DEBUG("Initializing AI shell integration"); 36 | 37 | // Register commands 38 | register_ai_commands(); 39 | 40 | // Initialize AI systems 41 | bool success = init_ai_integration(); 42 | 43 | if (success) { 44 | AI_SHELL_DEBUG("AI shell integration initialized successfully"); 45 | } else { 46 | AI_SHELL_DEBUG("AI shell integration initialization failed - continuing without AI features"); 47 | } 48 | } 49 | 50 | // Update the shell loop function to handle AI commands 51 | bool handle_ai_command(ParsedCommand *cmd) { 52 | if (!cmd || !cmd->args[0]) return false; 53 | 54 | AI_SHELL_DEBUG("Checking if '%s' is an AI command", cmd->args[0]); 55 | 56 | if (strcmp(cmd->args[0], "set-api-key") == 0) { 57 | AI_SHELL_DEBUG("Handling set-api-key command"); 58 | // Count arguments 59 | int argc = 0; 60 | while (cmd->args[argc]) argc++; 61 | bool result = set_api_key_command(argc, cmd->args) == 0; 62 | AI_SHELL_DEBUG("set-api-key command %s", result ? "succeeded" : "failed"); 63 | return result; 64 | } 65 | else if (strcmp(cmd->args[0], "ask") == 0) { 66 | AI_SHELL_DEBUG("Handling ask command"); 67 | // Count arguments 68 | int argc = 0; 69 | while (cmd->args[argc]) argc++; 70 | bool result = ask_ai_command(argc, cmd->args) == 0; 71 | AI_SHELL_DEBUG("ask command %s", result ? "succeeded" : "failed"); 72 | return result; 73 | } 74 | else if (strcmp(cmd->args[0], "explain") == 0) { 75 | AI_SHELL_DEBUG("Handling explain command"); 76 | // Count arguments 77 | int argc = 0; 78 | while (cmd->args[argc]) argc++; 79 | bool result = explain_command(argc, cmd->args) == 0; 80 | AI_SHELL_DEBUG("explain command %s", result ? "succeeded" : "failed"); 81 | return result; 82 | } 83 | else if (strcmp(cmd->args[0], "fix") == 0) { // Add handling for the new command 84 | AI_SHELL_DEBUG("Handling fix command"); 85 | // Count arguments 86 | int argc = 0; 87 | while (cmd->args[argc]) argc++; 88 | bool result = fix_command(argc, cmd->args) == 0; 89 | AI_SHELL_DEBUG("fix command %s", result ? "succeeded" : "failed"); 90 | return result; 91 | } 92 | 93 | AI_SHELL_DEBUG("'%s' is not an AI command", cmd->args[0]); 94 | return false; 95 | } 96 | -------------------------------------------------------------------------------- /src/core/executor.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include // Add this include for reload_directory_config 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | // Replace the debug flag with a macro that uses environment variable 16 | #define EXEC_DEBUG(fmt, ...) \ 17 | do { if (getenv("NUT_DEBUG_EXEC")) fprintf(stderr, "EXEC: " fmt "\n", ##__VA_ARGS__); } while(0) 18 | 19 | // Forward declarations 20 | extern int install_pkg_command(int argc, char **argv); 21 | 22 | // External declarations for AI commands 23 | extern int set_api_key_command(int argc, char **argv); 24 | extern int ask_ai_command(int argc, char **argv); 25 | extern int explain_command(int argc, char **argv); 26 | extern bool handle_ai_command(ParsedCommand *cmd); // Add declaration for handle_ai_command 27 | 28 | static void handle_redirection(ParsedCommand *cmd); 29 | 30 | // Add this function at the top of the file with other helper functions 31 | bool is_terminal_control_command(const char *cmd) { 32 | if (!cmd) return false; 33 | 34 | // List of commands that need direct terminal access 35 | static const char *terminal_cmds[] = { 36 | "clear", "reset", "tput", "stty", "tset", NULL 37 | }; 38 | 39 | for (int i = 0; terminal_cmds[i]; i++) { 40 | if (strcmp(cmd, terminal_cmds[i]) == 0) { 41 | return true; 42 | } 43 | } 44 | 45 | return false; 46 | } 47 | 48 | // Debug helper function 49 | static void debug_print_command(ParsedCommand *cmd) { 50 | if (!getenv("NUT_DEBUG_EXEC")) return; 51 | 52 | EXEC_DEBUG("Executing command: '%s'", cmd->args[0]); 53 | for (int i = 0; cmd->args[i]; i++) { 54 | EXEC_DEBUG(" Arg %d: '%s'", i, cmd->args[i]); 55 | } 56 | 57 | if (cmd->input_file) { 58 | EXEC_DEBUG(" Input from: %s", cmd->input_file); 59 | } 60 | if (cmd->output_file) { 61 | EXEC_DEBUG(" Output to: %s", cmd->output_file); 62 | } 63 | if (cmd->background) { 64 | EXEC_DEBUG(" Running in background"); 65 | } 66 | } 67 | 68 | void execute_command(ParsedCommand *cmd) { 69 | if (!cmd || !cmd->args[0]) return; 70 | 71 | debug_print_command(cmd); 72 | 73 | // Handle builtin commands without forking 74 | if (strcmp(cmd->args[0], "cd") == 0) { 75 | if (cmd->args[1]) { 76 | if (chdir(cmd->args[1]) == 0) { 77 | // Successfully changed directory, reload directory-specific config 78 | reload_directory_config(); 79 | } 80 | } 81 | return; 82 | } 83 | 84 | if (strcmp(cmd->args[0], "exit") == 0) { 85 | exit(EXIT_SUCCESS); 86 | } 87 | 88 | if (strcmp(cmd->args[0], "install-pkg") == 0) { 89 | int argc = 0; 90 | while (cmd->args[argc]) argc++; 91 | install_pkg_command(argc, cmd->args); 92 | return; 93 | } 94 | 95 | // Handle AI commands 96 | if (strcmp(cmd->args[0], "set-api-key") == 0 || 97 | strcmp(cmd->args[0], "ask") == 0 || 98 | strcmp(cmd->args[0], "explain") == 0 || 99 | strcmp(cmd->args[0], "fix") == 0) { 100 | handle_ai_command(cmd); 101 | return; 102 | } 103 | 104 | // Special handling for terminal control commands 105 | if (is_terminal_control_command(cmd->args[0])) { 106 | EXEC_DEBUG("Directly executing terminal command: %s", cmd->args[0]); 107 | 108 | // Create the full command with arguments 109 | char full_cmd[1024] = {0}; 110 | for (int i = 0; cmd->args[i]; i++) { 111 | if (i > 0) strcat(full_cmd, " "); 112 | strcat(full_cmd, cmd->args[i]); 113 | } 114 | 115 | // Execute the command directly 116 | system(full_cmd); 117 | return; 118 | } 119 | 120 | // Look up the command in our registry 121 | const CommandMapping *mapping = find_command(cmd->args[0]); 122 | if (getenv("NUT_DEBUG_EXEC") && mapping) { 123 | EXEC_DEBUG("Command '%s' found in registry as '%s' (builtin: %s)", 124 | cmd->args[0], mapping->unix_cmd, 125 | mapping->is_builtin ? "yes" : "no"); 126 | } 127 | 128 | // Create a clean array for arguments 129 | char *clean_args[MAX_ARGS]; 130 | int i = 0; 131 | 132 | if (mapping) { 133 | EXEC_DEBUG("Using mapped command %s (builtin: %s)", 134 | mapping->unix_cmd, mapping->is_builtin ? "yes" : "no"); 135 | 136 | if (mapping->is_builtin) { 137 | // For system commands (builtins), replace the command name but keep arg structure 138 | clean_args[0] = strdup(mapping->unix_cmd); 139 | for (i = 1; cmd->args[i] && i < MAX_ARGS - 1; i++) { 140 | clean_args[i] = strdup(cmd->args[i]); 141 | EXEC_DEBUG(" Arg %d: '%s'", i, clean_args[i]); // Debug: print each arg 142 | } 143 | } else { 144 | // For custom scripts, use the script path as command and preserve original args 145 | clean_args[0] = strdup(mapping->unix_cmd); 146 | for (i = 1; cmd->args[i] && i < MAX_ARGS - 1; i++) { 147 | clean_args[i] = strdup(cmd->args[i]); 148 | EXEC_DEBUG(" Arg %d: '%s'", i, clean_args[i]); // Debug: print each arg 149 | } 150 | } 151 | } else { 152 | // Regular system command - keep all args unchanged 153 | for (i = 0; cmd->args[i] && i < MAX_ARGS - 1; i++) { 154 | clean_args[i] = strdup(cmd->args[i]); 155 | EXEC_DEBUG(" Arg %d: '%s'", i, clean_args[i]); // Debug: print each arg 156 | } 157 | } 158 | clean_args[i] = NULL; // Ensure NULL termination 159 | 160 | if (getenv("NUT_DEBUG_EXEC")) { 161 | EXEC_DEBUG("Final command array:"); 162 | for (int j = 0; clean_args[j]; j++) { 163 | EXEC_DEBUG(" clean_args[%d] = '%s'", j, clean_args[j]); 164 | } 165 | } 166 | 167 | // Fork and execute 168 | pid_t pid = fork(); 169 | 170 | if (pid < 0) { 171 | perror("fork"); 172 | goto cleanup; 173 | } 174 | 175 | if (pid == 0) { // Child process 176 | // Set up any redirections 177 | handle_redirection(cmd); 178 | 179 | if (mapping && !mapping->is_builtin) { 180 | // For custom scripts, use direct execution with path 181 | EXEC_DEBUG("Executing script with execv: %s", clean_args[0]); 182 | execv(clean_args[0], clean_args); 183 | } else { 184 | // For system commands and built-ins, use PATH lookup 185 | EXEC_DEBUG("Executing command with execvp: %s", clean_args[0]); 186 | execvp(clean_args[0], clean_args); 187 | } 188 | 189 | // If we get here, execution failed 190 | fprintf(stderr, "ERROR: Failed to execute '%s': %s\n", 191 | clean_args[0], strerror(errno)); 192 | 193 | // Free memory before exit 194 | for (int j = 0; clean_args[j]; j++) { 195 | free(clean_args[j]); 196 | } 197 | 198 | exit(EXIT_FAILURE); 199 | } else { 200 | // Parent process 201 | if (!cmd->background) { 202 | int status; 203 | waitpid(pid, &status, 0); 204 | if (getenv("NUT_DEBUG_EXEC")) { 205 | if (WIFEXITED(status)) { 206 | EXEC_DEBUG("Child exited with status %d", WEXITSTATUS(status)); 207 | } else if (WIFSIGNALED(status)) { 208 | EXEC_DEBUG("Child killed by signal %d", WTERMSIG(status)); 209 | } 210 | } 211 | } 212 | } 213 | 214 | cleanup: 215 | // Free the argument array in the parent 216 | for (int j = 0; clean_args[j]; j++) { 217 | free(clean_args[j]); 218 | } 219 | } 220 | 221 | static void handle_redirection(ParsedCommand *cmd) { 222 | if (cmd->input_file) { 223 | int fd = open(cmd->input_file, O_RDONLY); 224 | if (fd == -1) { 225 | perror("open"); 226 | exit(EXIT_FAILURE); 227 | } 228 | dup2(fd, STDIN_FILENO); 229 | close(fd); 230 | } 231 | 232 | if (cmd->output_file) { 233 | int fd = open(cmd->output_file, O_WRONLY | O_CREAT | O_TRUNC, 0644); 234 | if (fd == -1) { 235 | perror("open"); 236 | exit(EXIT_FAILURE); 237 | } 238 | dup2(fd, STDOUT_FILENO); 239 | close(fd); 240 | } 241 | } -------------------------------------------------------------------------------- /src/core/main.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // Define version information 13 | #define NUTSHELL_VERSION "0.0.4" 14 | #define NUTSHELL_RELEASE_DATE "March 2025" 15 | 16 | // External declaration for AI shell initialization 17 | extern void init_ai_shell(); 18 | 19 | // Display version information 20 | void print_version() { 21 | printf("Nutshell Shell v%s (%s)\n", NUTSHELL_VERSION, NUTSHELL_RELEASE_DATE); 22 | } 23 | 24 | // Display help information 25 | void print_usage() { 26 | printf("Usage: nutshell [OPTIONS]\n\n"); 27 | printf("An enhanced Unix shell with simplified command language, package management, and AI assistance.\n\n"); 28 | printf("Options:\n"); 29 | printf(" --help Display this help message and exit\n"); 30 | printf(" --version Display version information and exit\n"); 31 | printf(" --test Run in test mode (for internal testing)\n\n"); 32 | printf("Environment variables:\n"); 33 | printf(" NUT_DEBUG=1 Enable general debug output\n"); 34 | printf(" OPENAI_API_KEY= Set API key for AI features\n"); 35 | printf(" NUT_DEBUG_THEME=1 Enable theme system debugging\n"); 36 | printf(" NUT_DEBUG_CONFIG=1 Enable config system debugging\n\n"); 37 | printf("Documentation: https://github.com/chandralegend/nutshell\n"); 38 | } 39 | 40 | int main(int argc, char *argv[]) { 41 | // Check for command line arguments 42 | if (argc > 1) { 43 | if (strcmp(argv[1], "--version") == 0) { 44 | print_version(); 45 | return 0; 46 | } 47 | else if (strcmp(argv[1], "--help") == 0) { 48 | print_usage(); 49 | return 0; 50 | } 51 | else if (strcmp(argv[1], "--test") == 0) { 52 | printf("Running in test mode\n"); 53 | // Continue with initialization but don't start shell loop 54 | } 55 | else { 56 | printf("Unknown option: %s\n", argv[1]); 57 | print_usage(); 58 | return 1; 59 | } 60 | } 61 | 62 | // Initialize the command registry 63 | init_registry(); 64 | 65 | // Initialize AI integration 66 | init_ai_shell(); 67 | 68 | // Only start the shell loop in normal mode (not test mode) 69 | if (argc <= 1 || (argc > 1 && strcmp(argv[1], "--test") != 0)) { 70 | shell_loop(); 71 | } 72 | 73 | // Free resources before exit 74 | free_registry(); 75 | cleanup_ai_integration(); 76 | 77 | return 0; 78 | } 79 | -------------------------------------------------------------------------------- /src/core/parser.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // Replace the debug flag with a macro that uses environment variable 10 | #define PARSER_DEBUG(fmt, ...) \ 11 | do { if (getenv("NUT_DEBUG_PARSER")) fprintf(stderr, "PARSER: " fmt "\n", ##__VA_ARGS__); } while(0) 12 | 13 | // Function prototype 14 | char* trim_whitespace(char* str); 15 | 16 | ParsedCommand *parse_command(char *input) { 17 | if (!input) return NULL; 18 | 19 | PARSER_DEBUG("Parsing command: '%s'", input); 20 | 21 | // Make a copy of the input to avoid modifying the original 22 | char *input_copy = strdup(input); 23 | if (!input_copy) return NULL; 24 | 25 | char *original_input_copy = input_copy; 26 | input_copy = trim_whitespace(input_copy); 27 | if (strlen(input_copy) == 0) { 28 | PARSER_DEBUG("Empty command after trimming"); 29 | free(original_input_copy); 30 | original_input_copy = NULL; 31 | return NULL; 32 | } 33 | 34 | ParsedCommand *cmd = calloc(1, sizeof(ParsedCommand)); 35 | if (!cmd) { 36 | PARSER_DEBUG("Failed to allocate ParsedCommand"); 37 | free(original_input_copy); 38 | original_input_copy = NULL; 39 | return NULL; 40 | } 41 | 42 | // Use calloc to ensure all entries are initialized to NULL 43 | cmd->args = calloc(MAX_ARGS, sizeof(char *)); 44 | if (!cmd->args) { 45 | PARSER_DEBUG("Failed to allocate args array"); 46 | free(original_input_copy); 47 | original_input_copy = NULL; 48 | free(cmd); 49 | return NULL; 50 | } 51 | int arg_count = 0; 52 | char *token, *saveptr = NULL; 53 | // Tokenize and process input 54 | token = strtok_r(input_copy, " \t", &saveptr); 55 | while (token != NULL && arg_count < MAX_ARGS - 1) { 56 | // Check if token is a special character 57 | if (strcmp(token, "<") == 0) { 58 | token = strtok_r(NULL, " \t", &saveptr); 59 | if (token) { 60 | cmd->input_file = strdup(token); 61 | PARSER_DEBUG("Input file: %s", cmd->input_file); 62 | } else { 63 | PARSER_DEBUG("Missing input file after <"); 64 | } 65 | } else if (strcmp(token, ">") == 0) { 66 | token = strtok_r(NULL, " \t", &saveptr); 67 | if (token) { 68 | cmd->output_file = strdup(token); 69 | PARSER_DEBUG("Output file: %s", cmd->output_file); 70 | } else { 71 | PARSER_DEBUG("Missing output file after >"); 72 | } 73 | } else if (strcmp(token, "&") == 0) { 74 | cmd->background = true; 75 | PARSER_DEBUG("Background process"); 76 | } else { 77 | // Regular argument 78 | cmd->args[arg_count] = strdup(token); 79 | PARSER_DEBUG("Arg[%d] = '%s'", arg_count, cmd->args[arg_count]); 80 | arg_count++; 81 | } 82 | 83 | // Get next token 84 | token = strtok_r(NULL, " \t", &saveptr); 85 | } 86 | // Ensure NULL termination 87 | cmd->args[arg_count] = NULL; 88 | 89 | PARSER_DEBUG("Command parsed with %d arguments", arg_count); 90 | 91 | if(original_input_copy){ 92 | free(original_input_copy); 93 | original_input_copy = NULL; 94 | } 95 | 96 | 97 | return cmd; 98 | } 99 | 100 | void free_parsed_command(ParsedCommand *cmd) { 101 | if (!cmd) return; 102 | 103 | if (cmd->args) { 104 | for (int i = 0; cmd->args[i]; i++) 105 | free(cmd->args[i]); 106 | free(cmd->args); 107 | } 108 | 109 | free(cmd->input_file); 110 | free(cmd->output_file); 111 | free(cmd); 112 | } 113 | -------------------------------------------------------------------------------- /src/core/shell.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #define RL_READLINE_VERSION 0x0603 3 | #include 4 | #include 5 | #include 6 | #include // Add this include to access AI functions 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | // Function prototypes 16 | char *expand_path(const char *path); 17 | char *get_current_dir(void); 18 | extern bool is_terminal_control_command(const char *cmd); 19 | 20 | volatile sig_atomic_t sigint_received = 0; 21 | 22 | void handle_sigint(int sig) { 23 | // Unused parameter 24 | (void)sig; 25 | 26 | // Simpler approach without using rl_replace_line 27 | sigint_received = 1; 28 | printf("\n"); 29 | rl_on_new_line(); 30 | rl_redisplay(); 31 | } 32 | 33 | extern int install_pkg_command(int argc, char **argv); 34 | extern int theme_command(int argc, char **argv); 35 | 36 | // Initialize command history 37 | CommandHistory cmd_history = {NULL, NULL, 0, false}; 38 | 39 | // Function to store command output 40 | void capture_command_output(const char *command, int exit_status, const char *output) { 41 | // Free previous entries 42 | free(cmd_history.last_command); 43 | free(cmd_history.last_output); 44 | 45 | // Store new entries 46 | cmd_history.last_command = strdup(command); 47 | cmd_history.last_output = output ? strdup(output) : NULL; 48 | cmd_history.exit_status = exit_status; 49 | cmd_history.has_error = (exit_status != 0); 50 | 51 | if (getenv("NUT_DEBUG")) { 52 | DEBUG_LOG("Stored command: %s", cmd_history.last_command); 53 | DEBUG_LOG("Exit status: %d", cmd_history.exit_status); 54 | DEBUG_LOG("Output: %.40s%s", cmd_history.last_output ? cmd_history.last_output : "(none)", 55 | cmd_history.last_output && strlen(cmd_history.last_output) > 40 ? "..." : ""); 56 | } 57 | } 58 | 59 | void shell_loop() { 60 | char *input; 61 | struct sigaction sa; 62 | 63 | // Initialize the configuration system first 64 | if (getenv("NUT_DEBUG")) { 65 | DEBUG_LOG("Initializing configuration system"); 66 | } 67 | init_config_system(); 68 | 69 | // Initialize the theme system 70 | if (getenv("NUT_DEBUG")) { 71 | DEBUG_LOG("Initializing theme system"); 72 | } 73 | init_theme_system(); 74 | 75 | // Load saved theme from config if available 76 | const char *saved_theme = get_config_theme(); 77 | if (saved_theme && current_theme && strcmp(current_theme->name, saved_theme) != 0) { 78 | if (getenv("NUT_DEBUG")) { 79 | DEBUG_LOG("Loading saved theme from config: %s", saved_theme); 80 | } 81 | Theme *theme = load_theme(saved_theme); 82 | if (theme) { 83 | if (current_theme) { 84 | free_theme(current_theme); 85 | } 86 | current_theme = theme; 87 | if (getenv("NUT_DEBUG")) { 88 | DEBUG_LOG("Successfully loaded saved theme: %s", theme->name); 89 | } 90 | } else if (getenv("NUT_DEBUG")) { 91 | DEBUG_LOG("Failed to load saved theme: %s", saved_theme); 92 | } 93 | } 94 | 95 | // Initialize the AI shell integration 96 | init_ai_shell(); 97 | 98 | sa.sa_handler = handle_sigint; 99 | sigemptyset(&sa.sa_mask); 100 | sa.sa_flags = SA_RESTART; 101 | 102 | if (sigaction(SIGINT, &sa, NULL) == -1) { 103 | perror("sigaction"); 104 | exit(EXIT_FAILURE); 105 | } 106 | 107 | printf("Nutshell initialized. Type commands or 'exit' to quit.\n"); 108 | 109 | while (1) { 110 | sigint_received = 0; 111 | char *prompt = get_prompt(); 112 | input = readline(prompt); 113 | free(prompt); 114 | 115 | if (!input) break; // EOF 116 | 117 | if (strlen(input) > 0) { 118 | add_history(input); 119 | ParsedCommand *cmd = parse_command(input); 120 | if (cmd) { 121 | // Save the original command string for history regardless of how we process it 122 | char full_cmd[1024] = {0}; 123 | for (int i = 0; cmd->args[i]; i++) { 124 | if (i > 0) strcat(full_cmd, " "); 125 | strcat(full_cmd, cmd->args[i]); 126 | } 127 | 128 | // Special handling for theme command 129 | if (cmd->args[0] && strcmp(cmd->args[0], "theme") == 0) { 130 | // Count arguments 131 | int argc = 0; 132 | while (cmd->args[argc]) argc++; 133 | 134 | // Capture theme command output 135 | char output_file[] = "/tmp/nutshell_output_XXXXXX"; 136 | int output_fd = mkstemp(output_file); 137 | if (output_fd != -1) { 138 | int stdout_bak = dup(STDOUT_FILENO); 139 | int stderr_bak = dup(STDERR_FILENO); 140 | 141 | // Redirect stdout and stderr to temp file 142 | dup2(output_fd, STDOUT_FILENO); 143 | dup2(output_fd, STDERR_FILENO); 144 | 145 | int status = theme_command(argc, cmd->args); 146 | 147 | // Restore stdout and stderr 148 | fflush(stdout); 149 | fflush(stderr); 150 | dup2(stdout_bak, STDOUT_FILENO); 151 | dup2(stderr_bak, STDERR_FILENO); 152 | close(stdout_bak); 153 | close(stderr_bak); 154 | 155 | // Read the captured output 156 | lseek(output_fd, 0, SEEK_SET); 157 | char output_buf[4096] = {0}; 158 | read(output_fd, output_buf, sizeof(output_buf) - 1); 159 | close(output_fd); 160 | unlink(output_file); 161 | 162 | // Display the output 163 | printf("%s", output_buf); 164 | 165 | // Store command history 166 | capture_command_output(full_cmd, status, output_buf); 167 | } else { 168 | int status = theme_command(argc, cmd->args); 169 | capture_command_output(full_cmd, status, NULL); 170 | } 171 | } else if (handle_ai_command(cmd)) { 172 | // For AI commands, just store the command without output capture 173 | capture_command_output(full_cmd, 0, NULL); 174 | 175 | // We can still capture stderr if needed in the future 176 | // by uncommenting and implementing this code: 177 | /* 178 | char output_file[] = "/tmp/nutshell_output_XXXXXX"; 179 | int output_fd = mkstemp(output_file); 180 | if (output_fd != -1) { 181 | // Implementation would go here 182 | close(output_fd); 183 | unlink(output_file); 184 | } 185 | */ 186 | } else if (cmd->args[0] && is_terminal_control_command(cmd->args[0])) { 187 | // For terminal control commands, execute directly but capture output where possible 188 | FILE *fp = NULL; 189 | 190 | // Create the full command with arguments 191 | char full_terminal_cmd[1024] = {0}; 192 | for (int i = 0; cmd->args[i]; i++) { 193 | if (i > 0) strcat(full_terminal_cmd, " "); 194 | strcat(full_terminal_cmd, cmd->args[i]); 195 | } 196 | 197 | // Try to capture any error output 198 | fp = popen(full_terminal_cmd, "r"); 199 | if (fp) { 200 | // Execute the command directly for terminal interaction 201 | system(full_terminal_cmd); 202 | 203 | // Read any output that might have been captured 204 | char output_buf[4096] = {0}; 205 | if (fread(output_buf, 1, sizeof(output_buf) - 1, fp) > 0) { 206 | capture_command_output(full_cmd, 0, output_buf); 207 | } else { 208 | capture_command_output(full_cmd, 0, NULL); 209 | } 210 | int status = pclose(fp); 211 | if (status != 0) { 212 | capture_command_output(full_cmd, WEXITSTATUS(status), NULL); 213 | } 214 | } else { 215 | // Fall back to direct execution 216 | execute_command(cmd); 217 | capture_command_output(full_cmd, 0, NULL); 218 | } 219 | } else { 220 | // For regular commands, track execution and capture output 221 | 222 | // Capture stdout and stderr for error tracking 223 | char output_file[] = "/tmp/nutshell_output_XXXXXX"; 224 | int output_fd = mkstemp(output_file); 225 | if (output_fd != -1) { 226 | int stdout_bak = dup(STDOUT_FILENO); 227 | int stderr_bak = dup(STDERR_FILENO); 228 | 229 | // Redirect stdout and stderr to temp file 230 | dup2(output_fd, STDOUT_FILENO); 231 | dup2(output_fd, STDERR_FILENO); 232 | 233 | // Execute the command 234 | execute_command(cmd); 235 | int exit_status = WEXITSTATUS(0); // Get last command status 236 | 237 | // Restore stdout and stderr 238 | fflush(stdout); 239 | fflush(stderr); 240 | dup2(stdout_bak, STDOUT_FILENO); 241 | dup2(stderr_bak, STDERR_FILENO); 242 | close(stdout_bak); 243 | close(stderr_bak); 244 | 245 | // Read the captured output 246 | lseek(output_fd, 0, SEEK_SET); 247 | char output_buf[4096] = {0}; 248 | read(output_fd, output_buf, sizeof(output_buf) - 1); 249 | close(output_fd); 250 | unlink(output_file); 251 | 252 | // IMPORTANT: Display the output to the user 253 | printf("%s", output_buf); 254 | 255 | // Store command history 256 | capture_command_output(full_cmd, exit_status, output_buf); 257 | } else { 258 | // Couldn't create temp file, execute normally 259 | execute_command(cmd); 260 | 261 | // Still store command but without output 262 | capture_command_output(full_cmd, 0, NULL); 263 | } 264 | } 265 | free_parsed_command(cmd); 266 | } 267 | } 268 | 269 | free(input); 270 | } 271 | 272 | // Clean up command history 273 | free(cmd_history.last_command); 274 | free(cmd_history.last_output); 275 | 276 | // Clean up theme system 277 | cleanup_theme_system(); 278 | 279 | // Clean up configuration system 280 | cleanup_config_system(); 281 | } 282 | 283 | char *get_prompt() { 284 | // Use the theme system if available 285 | if (current_theme) { 286 | return get_theme_prompt(current_theme); 287 | } 288 | 289 | // Fall back to default prompt 290 | static char prompt[256]; 291 | snprintf(prompt, sizeof(prompt), 292 | "\001\033[1;32m\002🥜 %s \001\033[0m\002➜ ", 293 | get_current_dir()); 294 | return strdup(prompt); 295 | } -------------------------------------------------------------------------------- /src/pkg/integrity.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include // Use EVP interface instead of direct SHA256 6 | #include 7 | #include 8 | 9 | // Define PKG_CACHE_DIR if not already defined 10 | #ifndef PKG_CACHE_DIR 11 | #define PKG_CACHE_DIR "/usr/local/nutshell/packages" 12 | #endif 13 | 14 | bool verify_package_integrity(const PackageManifest* manifest) { 15 | if(!manifest || !manifest->checksum) return false; 16 | 17 | char path[256]; 18 | snprintf(path, sizeof(path), "%s/%s", PKG_CACHE_DIR, manifest->name); 19 | 20 | FILE *file = fopen(path, "rb"); 21 | if(!file) return false; 22 | 23 | unsigned char hash[EVP_MAX_MD_SIZE]; 24 | unsigned int hash_len; 25 | 26 | // Use the EVP interface (modern approach) 27 | EVP_MD_CTX *mdctx = EVP_MD_CTX_new(); 28 | if (!mdctx) { 29 | fclose(file); 30 | return false; 31 | } 32 | 33 | EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL); 34 | 35 | unsigned char buffer[4096]; 36 | size_t bytesRead; 37 | while((bytesRead = fread(buffer, 1, sizeof(buffer), file))) { 38 | EVP_DigestUpdate(mdctx, buffer, bytesRead); 39 | } 40 | 41 | EVP_DigestFinal_ex(mdctx, hash, &hash_len); 42 | EVP_MD_CTX_free(mdctx); 43 | fclose(file); 44 | 45 | char hash_str[65]; 46 | for(int i = 0; i < 32; i++) { 47 | // Replace sprintf with snprintf to avoid deprecation warning 48 | snprintf(hash_str + (i * 2), 3, "%02x", hash[i]); 49 | } 50 | hash_str[64] = '\0'; 51 | 52 | return strcmp(hash_str, manifest->checksum) == 0; 53 | } -------------------------------------------------------------------------------- /src/pkg/nutpkg.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | // Try to find jansson.h in various locations 7 | #if __has_include() 8 | #include 9 | #define JANSSON_AVAILABLE 1 10 | #elif __has_include("jansson.h") 11 | #include "jansson.h" 12 | #define JANSSON_AVAILABLE 1 13 | #else 14 | #warning "jansson.h not found - some functionality will be limited" 15 | #define JANSSON_AVAILABLE 0 16 | // Minimal stubs for jansson types 17 | typedef void* json_t; 18 | typedef struct { char text[256]; } json_error_t; 19 | #endif 20 | 21 | #include 22 | #include 23 | #include // Make sure string.h is included 24 | #include 25 | #include 26 | 27 | // Fix function declaration to match implementation 28 | bool load_manifest(const char* pkg_name, PackageManifest* manifest); 29 | char* download_to_string(const char *url); 30 | 31 | // Add missing struct and callback at the top of the file 32 | struct MemoryStruct { 33 | char *memory; 34 | size_t size; 35 | }; 36 | 37 | static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) { 38 | size_t real_size = size * nmemb; 39 | struct MemoryStruct *mem = (struct MemoryStruct *)userp; 40 | 41 | char *ptr = realloc(mem->memory, mem->size + real_size + 1); 42 | if (!ptr) { 43 | fprintf(stderr, "Not enough memory (realloc returned NULL)\n"); 44 | return 0; 45 | } 46 | 47 | mem->memory = ptr; 48 | memcpy(&(mem->memory[mem->size]), contents, real_size); 49 | mem->size += real_size; 50 | mem->memory[mem->size] = 0; 51 | 52 | return real_size; 53 | } 54 | 55 | static const char* PKG_CACHE_DIR = "/var/cache/nutshell/packages"; 56 | 57 | // Update package registry URL to include our new example package 58 | #ifndef NUTPKG_REGISTRY 59 | #define NUTPKG_REGISTRY "https://raw.githubusercontent.com/chandralegend/nutshell/main/packages" 60 | #endif 61 | 62 | static size_t write_data(void *ptr, size_t size, size_t nmemb, FILE *stream) { 63 | return fwrite(ptr, size, nmemb, stream); 64 | } 65 | 66 | PkgInstallResult nutpkg_install(const char* pkg_name) { 67 | char url[256]; 68 | snprintf(url, sizeof(url), "%s/packages/%s", NUTPKG_REGISTRY, pkg_name); 69 | 70 | // Create cache directory if it doesn't exist 71 | mkdir(PKG_CACHE_DIR, 0755); 72 | 73 | char tmpfile[256]; 74 | snprintf(tmpfile, sizeof(tmpfile), "%s/%s.tmp", PKG_CACHE_DIR, pkg_name); 75 | 76 | CURL *curl = curl_easy_init(); 77 | if(!curl) return PKG_INSTALL_FAILED; 78 | 79 | FILE *fp = fopen(tmpfile, "wb"); 80 | if(!fp) return PKG_INSTALL_FAILED; 81 | 82 | curl_easy_setopt(curl, CURLOPT_URL, url); 83 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); 84 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); 85 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 86 | 87 | CURLcode res = curl_easy_perform(curl); 88 | fclose(fp); 89 | curl_easy_cleanup(curl); 90 | 91 | if(res != CURLE_OK) { 92 | remove(tmpfile); 93 | return PKG_INSTALL_FAILED; 94 | } 95 | 96 | // Verify package integrity 97 | PackageManifest manifest; 98 | if(!load_manifest(pkg_name, &manifest)) { 99 | remove(tmpfile); 100 | return PKG_INTEGRITY_FAILED; 101 | } 102 | 103 | if(!verify_package_integrity(&manifest)) { 104 | remove(tmpfile); 105 | return PKG_INTEGRITY_FAILED; 106 | } 107 | 108 | // Install dependencies 109 | for(int i = 0; manifest.dependencies[i]; i++) { 110 | if(nutpkg_install(manifest.dependencies[i]) != PKG_INSTALL_SUCCESS) { 111 | remove(tmpfile); 112 | return PKG_DEPENDENCY_FAILED; 113 | } 114 | } 115 | 116 | // Final installation 117 | char target[256]; 118 | snprintf(target, sizeof(target), "/usr/local/nutshell/packages/%s", pkg_name); 119 | rename(tmpfile, target); 120 | 121 | return PKG_INSTALL_SUCCESS; 122 | } 123 | 124 | // Fix load_manifest function to handle missing jansson library 125 | bool load_manifest(const char* pkg_name, PackageManifest* manifest) { 126 | if (!pkg_name || !manifest) return false; 127 | 128 | #if !JANSSON_AVAILABLE 129 | fprintf(stderr, "ERROR: Jansson library not available. Cannot load package manifest.\n"); 130 | fprintf(stderr, "Please install Jansson with: brew install jansson\n"); 131 | return false; 132 | #else 133 | char manifest_url[256]; 134 | snprintf(manifest_url, sizeof(manifest_url), 135 | "%s/packages/%s/manifest", NUTPKG_REGISTRY, pkg_name); 136 | 137 | char* json_str = download_to_string(manifest_url); 138 | if(!json_str) return false; 139 | 140 | json_error_t error; 141 | json_t *root = json_loads(json_str, 0, &error); 142 | free(json_str); 143 | 144 | if(!root) return false; 145 | 146 | // Parse JSON manifest 147 | manifest->name = strdup(json_string_value(json_object_get(root, "name"))); 148 | manifest->version = strdup(json_string_value(json_object_get(root, "version"))); 149 | manifest->description = strdup(json_string_value(json_object_get(root, "description"))); 150 | manifest->checksum = strdup(json_string_value(json_object_get(root, "sha256"))); 151 | 152 | json_t *deps = json_object_get(root, "dependencies"); 153 | if(deps) { 154 | size_t index; 155 | json_t *value; 156 | json_array_foreach(deps, index, value) { 157 | if(index < MAX_DEPENDENCIES) { 158 | manifest->dependencies[index] = strdup(json_string_value(value)); 159 | } 160 | } 161 | } 162 | 163 | json_decref(root); 164 | return true; 165 | #endif 166 | } 167 | 168 | char* download_to_string(const char *url) { 169 | CURL *curl = curl_easy_init(); 170 | if(!curl) return NULL; 171 | 172 | struct MemoryStruct chunk; 173 | chunk.memory = malloc(1); 174 | chunk.size = 0; 175 | 176 | curl_easy_setopt(curl, CURLOPT_URL, url); 177 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); 178 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk); 179 | curl_easy_setopt(curl, CURLOPT_USERAGENT, "nutshell-pkg/1.0"); 180 | 181 | CURLcode res = curl_easy_perform(curl); 182 | curl_easy_cleanup(curl); 183 | 184 | if(res != CURLE_OK) { 185 | free(chunk.memory); 186 | return NULL; 187 | } 188 | 189 | return chunk.memory; 190 | } 191 | 192 | // Add function to list available packages 193 | void nutpkg_list_available() { 194 | printf("Available packages:\n"); 195 | printf("- gitify: Interactive Git commit helper\n"); 196 | // Add more packages here as they become available 197 | } -------------------------------------------------------------------------------- /src/pkg/packager.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | static const char* USER_PKG_DIR = "/.nutshell/packages"; 14 | 15 | // Built-in command to install packages 16 | int install_pkg_command(int argc, char **argv) { 17 | if (argc < 2) { 18 | printf("Usage: install-pkg \n"); 19 | return 1; 20 | } 21 | 22 | const char *target = argv[1]; 23 | struct stat st; 24 | 25 | // Check if the argument is a directory path or a package name 26 | if (stat(target, &st) == 0 && S_ISDIR(st.st_mode)) { 27 | // It's a directory, try to install from path 28 | if (install_package_from_path(target)) { 29 | printf("Package installed successfully from %s\n", target); 30 | return 0; 31 | } else { 32 | printf("Failed to install package from %s\n", target); 33 | return 1; 34 | } 35 | } else { 36 | // Assume it's a package name to download 37 | if (install_package_from_name(target)) { 38 | printf("Package %s installed successfully\n", target); 39 | return 0; 40 | } else { 41 | printf("Failed to install package %s\n", target); 42 | return 1; 43 | } 44 | } 45 | } 46 | 47 | // Install a package from a local directory 48 | bool install_package_from_path(const char *path) { 49 | char *home = getenv("HOME"); 50 | if (!home) { 51 | print_error("HOME environment variable not set"); 52 | return false; 53 | } 54 | 55 | char pkg_name[128]; 56 | char *last_slash = strrchr(path, '/'); 57 | if (last_slash) { 58 | strcpy(pkg_name, last_slash + 1); 59 | } else { 60 | strcpy(pkg_name, path); 61 | } 62 | 63 | // Create user package directory if it doesn't exist 64 | char dest_dir[512]; 65 | snprintf(dest_dir, sizeof(dest_dir), "%s%s", home, USER_PKG_DIR); 66 | mkdir(dest_dir, 0755); 67 | 68 | // Create destination package directory 69 | char pkg_dir[512]; 70 | snprintf(pkg_dir, sizeof(pkg_dir), "%s/%s", dest_dir, pkg_name); 71 | mkdir(pkg_dir, 0755); 72 | 73 | // Copy package files 74 | char cmd[1024]; 75 | snprintf(cmd, sizeof(cmd), "cp -r %s/* %s/", path, pkg_dir); 76 | if (system(cmd) != 0) { 77 | print_error("Failed to copy package files"); 78 | return false; 79 | } 80 | 81 | // Make script executable 82 | snprintf(cmd, sizeof(cmd), "chmod +x %s/%s.sh", pkg_dir, pkg_name); 83 | if (system(cmd) != 0) { 84 | print_error("Failed to make script executable"); 85 | return false; 86 | } 87 | 88 | // Register the new command - fix the function call 89 | if (!register_package_commands(dest_dir, pkg_name)) { 90 | print_error("Failed to register package command"); 91 | return false; 92 | } 93 | 94 | return true; 95 | } 96 | 97 | // Install a package by name from the package registry 98 | bool install_package_from_name(const char *name) { 99 | // For now, just use nutpkg_install 100 | PkgInstallResult result = nutpkg_install(name); 101 | return (result == PKG_INSTALL_SUCCESS); 102 | } 103 | -------------------------------------------------------------------------------- /src/pkg/registry.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // Replace the debug flag with a macro that uses environment variable 14 | #define REGISTRY_DEBUG(fmt, ...) \ 15 | do { if (getenv("NUT_DEBUG_REGISTRY")) fprintf(stderr, "REGISTRY: " fmt "\n", ##__VA_ARGS__); } while(0) 16 | 17 | static CommandRegistry *registry = NULL; 18 | static const char* PACKAGES_DIR = "/.nutshell/packages"; 19 | 20 | // Function prototype for register_package_commands 21 | bool register_package_commands(const char *pkg_dir, const char *pkg_name); 22 | 23 | void init_registry() { 24 | registry = malloc(sizeof(CommandRegistry)); 25 | registry->commands = NULL; 26 | registry->count = 0; 27 | 28 | // Default commands 29 | register_command("exit", "roast", true); 30 | register_command("ls", "peekaboo", true); // Change to true - ls is a builtin command 31 | register_command("cd", "hop", true); 32 | 33 | // Register the package installer command 34 | register_command("install-pkg", "install-pkg", true); 35 | 36 | // Register theme command 37 | register_command("theme", "theme", true); 38 | 39 | REGISTRY_DEBUG("Initialized registry with default commands"); 40 | 41 | // Load installed packages 42 | char home_path[256]; 43 | char *home = getenv("HOME"); 44 | if (home) { 45 | snprintf(home_path, sizeof(home_path), "%s%s", home, PACKAGES_DIR); 46 | REGISTRY_DEBUG("Loading packages from user dir: %s", home_path); 47 | load_packages_from_dir(home_path); 48 | } else { 49 | REGISTRY_DEBUG("HOME environment variable not set"); 50 | } 51 | 52 | // Also check system-wide packages if accessible 53 | REGISTRY_DEBUG("Loading packages from system dir: /usr/local/nutshell/packages"); 54 | load_packages_from_dir("/usr/local/nutshell/packages"); 55 | } 56 | 57 | // New function to scan and load packages from directories 58 | void load_packages_from_dir(const char *dir_path) { 59 | DIR *dir = opendir(dir_path); 60 | if (!dir) return; 61 | 62 | struct dirent *entry; 63 | while ((entry = readdir(dir)) != NULL) { 64 | // Use stat instead of d_type which is more portable 65 | char full_path[512]; 66 | struct stat st; 67 | 68 | // Skip . and .. entries 69 | if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) 70 | continue; 71 | 72 | // Build the full path 73 | snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); 74 | 75 | // Check if it's a directory using stat 76 | if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) { 77 | register_package_commands(dir_path, entry->d_name); 78 | } 79 | } 80 | 81 | closedir(dir); 82 | } 83 | 84 | // Register commands from a specific package 85 | bool register_package_commands(const char *pkg_dir, const char *pkg_name) { 86 | char script_path[512]; 87 | snprintf(script_path, sizeof(script_path), "%s/%s/%s.sh", pkg_dir, pkg_name, pkg_name); 88 | 89 | // Check if the script exists 90 | struct stat st; 91 | if (stat(script_path, &st) == 0) { 92 | // Register command with the package name 93 | register_command(script_path, pkg_name, false); 94 | return true; 95 | } 96 | 97 | return false; 98 | } 99 | 100 | void register_command(const char *unix_cmd, const char *nut_cmd, bool is_builtin) { 101 | registry->count++; 102 | registry->commands = realloc(registry->commands, 103 | registry->count * sizeof(CommandMapping)); 104 | 105 | CommandMapping *cmd = ®istry->commands[registry->count - 1]; 106 | cmd->unix_cmd = strdup(unix_cmd); 107 | cmd->nut_cmd = strdup(nut_cmd); 108 | cmd->is_builtin = is_builtin; 109 | 110 | REGISTRY_DEBUG("Registered command: %s -> %s (builtin: %s)", 111 | nut_cmd, unix_cmd, is_builtin ? "yes" : "no"); 112 | } 113 | 114 | const CommandMapping *find_command(const char *input_cmd) { 115 | REGISTRY_DEBUG("Looking for command: %s", input_cmd); 116 | 117 | for (size_t i = 0; i < registry->count; i++) { 118 | if (strcmp(registry->commands[i].nut_cmd, input_cmd) == 0 || 119 | strcmp(registry->commands[i].unix_cmd, input_cmd) == 0) { 120 | 121 | REGISTRY_DEBUG("Found command: %s -> %s (builtin: %s)", 122 | input_cmd, registry->commands[i].unix_cmd, 123 | registry->commands[i].is_builtin ? "yes" : "no"); 124 | return ®istry->commands[i]; 125 | } 126 | } 127 | 128 | REGISTRY_DEBUG("Command not found: %s", input_cmd); 129 | return NULL; 130 | } 131 | 132 | void free_registry() { 133 | for (size_t i = 0; i < registry->count; i++) { 134 | free(registry->commands[i].unix_cmd); 135 | free(registry->commands[i].nut_cmd); 136 | } 137 | free(registry->commands); 138 | free(registry); 139 | } 140 | 141 | void print_command_registry() { 142 | // Use size_t instead of int for loop counter 143 | for (size_t i = 0; i < registry->count; i++) { 144 | printf("Unix Command: %s, Nut Command: %s, Builtin: %s\n", 145 | registry->commands[i].unix_cmd, 146 | registry->commands[i].nut_cmd, 147 | registry->commands[i].is_builtin ? "true" : "false"); 148 | } 149 | } -------------------------------------------------------------------------------- /src/utils/autocomplete.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dowhiledev/nutshell/46f81f6ef6b11044ad4437e25258973c07b6aa75/src/utils/autocomplete.c -------------------------------------------------------------------------------- /src/utils/config.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include // For PATH_MAX constant 14 | #include // For dirname function 15 | #include // Add this include for errno 16 | 17 | // Configuration debug macro 18 | #define CONFIG_DEBUG(fmt, ...) \ 19 | do { if (getenv("NUT_DEBUG_CONFIG")) fprintf(stderr, "CONFIG: " fmt "\n", ##__VA_ARGS__); } while(0) 20 | 21 | // Global configuration instance 22 | Config *global_config = NULL; 23 | 24 | // Configuration file names 25 | static const char *DIR_CONFIG_FILE = ".nutshell.json"; 26 | static const char *USER_CONFIG_DIR = "/.nutshell"; 27 | static const char *USER_CONFIG_FILE = "/.nutshell/config.json"; 28 | static const char *SYSTEM_CONFIG_FILE = "/usr/local/nutshell/config.json"; 29 | 30 | // Initialize configuration system 31 | void init_config_system() { 32 | CONFIG_DEBUG("Initializing configuration system"); 33 | 34 | // Create empty configuration structure 35 | global_config = calloc(1, sizeof(Config)); 36 | if (!global_config) { 37 | fprintf(stderr, "Error: Failed to initialize configuration system\n"); 38 | return; 39 | } 40 | 41 | // Ensure user config directory exists 42 | char *home = getenv("HOME"); 43 | if (home) { 44 | char config_dir[512]; 45 | snprintf(config_dir, sizeof(config_dir), "%s%s", home, USER_CONFIG_DIR); 46 | 47 | struct stat st = {0}; 48 | if (stat(config_dir, &st) == -1) { 49 | // Create directory if it doesn't exist 50 | CONFIG_DEBUG("Creating user config directory %s", config_dir); 51 | mkdir(config_dir, 0700); 52 | } 53 | } 54 | 55 | // Load configuration from files 56 | load_config_files(); // Updated function name 57 | } 58 | 59 | // Clean up configuration resources 60 | void cleanup_config_system() { 61 | if (!global_config) return; 62 | 63 | free(global_config->theme); 64 | 65 | for (int i = 0; i < global_config->package_count; i++) { 66 | free(global_config->enabled_packages[i]); 67 | } 68 | free(global_config->enabled_packages); 69 | 70 | for (int i = 0; i < global_config->alias_count; i++) { 71 | free(global_config->aliases[i]); 72 | free(global_config->alias_commands[i]); 73 | } 74 | free(global_config->aliases); 75 | free(global_config->alias_commands); 76 | 77 | for (int i = 0; i < global_config->script_count; i++) { 78 | free(global_config->scripts[i]); 79 | } 80 | free(global_config->scripts); 81 | 82 | free(global_config); 83 | global_config = NULL; 84 | } 85 | 86 | // Load JSON file into configuration 87 | static bool load_config_from_file(const char *path) { 88 | CONFIG_DEBUG("Attempting to load config from: %s", path); 89 | 90 | // Check if file exists 91 | struct stat st; 92 | if (stat(path, &st) != 0) { 93 | CONFIG_DEBUG("Config file not found: %s", path); 94 | return false; 95 | } 96 | 97 | // Open file 98 | FILE *file = fopen(path, "r"); 99 | if (!file) { 100 | CONFIG_DEBUG("Failed to open config file: %s", path); 101 | return false; 102 | } 103 | 104 | // Read file contents 105 | fseek(file, 0, SEEK_END); 106 | long length = ftell(file); 107 | fseek(file, 0, SEEK_SET); 108 | 109 | char *data = malloc(length + 1); 110 | if (!data) { 111 | CONFIG_DEBUG("Failed to allocate memory for config data"); 112 | fclose(file); 113 | return false; 114 | } 115 | 116 | fread(data, 1, length, file); 117 | data[length] = '\0'; 118 | fclose(file); 119 | 120 | // Parse JSON 121 | json_error_t error; 122 | json_t *root = json_loads(data, 0, &error); 123 | free(data); 124 | 125 | if (!root) { 126 | CONFIG_DEBUG("JSON parse error: %s (line: %d, col: %d)", 127 | error.text, error.line, error.column); 128 | return false; 129 | } 130 | 131 | // Extract theme setting 132 | json_t *theme_json = json_object_get(root, "theme"); 133 | if (json_is_string(theme_json)) { 134 | free(global_config->theme); 135 | global_config->theme = strdup(json_string_value(theme_json)); 136 | CONFIG_DEBUG("Loaded theme: %s", global_config->theme); 137 | } 138 | 139 | // Extract packages 140 | json_t *packages_json = json_object_get(root, "packages"); 141 | if (json_is_array(packages_json)) { 142 | // Free existing packages 143 | for (int i = 0; i < global_config->package_count; i++) { 144 | free(global_config->enabled_packages[i]); 145 | } 146 | free(global_config->enabled_packages); 147 | 148 | // Allocate new packages array 149 | size_t package_count = json_array_size(packages_json); 150 | global_config->enabled_packages = calloc(package_count, sizeof(char*)); 151 | global_config->package_count = package_count; 152 | 153 | // Load each package 154 | for (size_t i = 0; i < package_count; i++) { 155 | json_t *pkg = json_array_get(packages_json, i); 156 | if (json_is_string(pkg)) { 157 | global_config->enabled_packages[i] = strdup(json_string_value(pkg)); 158 | CONFIG_DEBUG("Loaded package: %s", global_config->enabled_packages[i]); 159 | } 160 | } 161 | } 162 | 163 | // Extract aliases 164 | json_t *aliases_json = json_object_get(root, "aliases"); 165 | if (json_is_object(aliases_json)) { 166 | // Free existing aliases 167 | for (int i = 0; i < global_config->alias_count; i++) { 168 | free(global_config->aliases[i]); 169 | free(global_config->alias_commands[i]); 170 | } 171 | free(global_config->aliases); 172 | free(global_config->alias_commands); 173 | 174 | // Allocate new aliases array 175 | size_t alias_count = json_object_size(aliases_json); 176 | global_config->aliases = calloc(alias_count, sizeof(char*)); 177 | global_config->alias_commands = calloc(alias_count, sizeof(char*)); 178 | global_config->alias_count = alias_count; 179 | 180 | // Load each alias 181 | const char *key; 182 | json_t *value; 183 | int i = 0; 184 | 185 | json_object_foreach(aliases_json, key, value) { 186 | if (json_is_string(value)) { 187 | global_config->aliases[i] = strdup(key); 188 | global_config->alias_commands[i] = strdup(json_string_value(value)); 189 | CONFIG_DEBUG("Loaded alias: %s -> %s", 190 | global_config->aliases[i], global_config->alias_commands[i]); 191 | i++; 192 | } 193 | } 194 | } 195 | 196 | // Extract scripts 197 | json_t *scripts_json = json_object_get(root, "scripts"); 198 | if (json_is_array(scripts_json)) { 199 | // Free existing scripts 200 | for (int i = 0; i < global_config->script_count; i++) { 201 | free(global_config->scripts[i]); 202 | } 203 | free(global_config->scripts); 204 | 205 | // Allocate new scripts array 206 | size_t script_count = json_array_size(scripts_json); 207 | global_config->scripts = calloc(script_count, sizeof(char*)); 208 | global_config->script_count = script_count; 209 | 210 | // Load each script 211 | for (size_t i = 0; i < script_count; i++) { 212 | json_t *script = json_array_get(scripts_json, i); 213 | if (json_is_string(script)) { 214 | global_config->scripts[i] = strdup(json_string_value(script)); 215 | CONFIG_DEBUG("Loaded script: %s", global_config->scripts[i]); 216 | } 217 | } 218 | } 219 | 220 | json_decref(root); 221 | CONFIG_DEBUG("Successfully loaded config from %s", path); 222 | return true; 223 | } 224 | 225 | // Check up the directory tree for config files 226 | static bool find_directory_config(char *result_path, size_t max_size) { 227 | char current_dir[PATH_MAX] = {0}; 228 | char config_path[PATH_MAX] = {0}; 229 | 230 | // Get the absolute path of the current directory 231 | if (!getcwd(current_dir, sizeof(current_dir))) { 232 | CONFIG_DEBUG("Failed to get current directory"); 233 | return false; 234 | } 235 | 236 | CONFIG_DEBUG("Searching for directory config starting from: %s", current_dir); 237 | CONFIG_DEBUG("Looking for file named: %s", DIR_CONFIG_FILE); 238 | 239 | // Start from the current directory and go up until root 240 | char *dir_path = strdup(current_dir); 241 | if (!dir_path) { 242 | CONFIG_DEBUG("Failed to allocate memory for dir_path"); 243 | return false; 244 | } 245 | 246 | while (dir_path && strlen(dir_path) > 0) { 247 | // Create the config file path 248 | snprintf(config_path, sizeof(config_path), "%s/%s", dir_path, DIR_CONFIG_FILE); 249 | CONFIG_DEBUG("Checking for config at: %s", config_path); 250 | 251 | // Check if config file exists 252 | if (access(config_path, R_OK) == 0) { 253 | CONFIG_DEBUG("Found directory config at: %s", config_path); 254 | 255 | // Check if we can actually open and read the file 256 | FILE *check = fopen(config_path, "r"); 257 | if (check) { 258 | char buffer[256]; 259 | size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, check); 260 | buffer[bytes_read] = '\0'; 261 | fclose(check); 262 | CONFIG_DEBUG("First %zu bytes of config: %.100s...", bytes_read, buffer); 263 | } else { 264 | CONFIG_DEBUG("WARNING: Found config but cannot open for reading: %s", strerror(errno)); 265 | } 266 | 267 | strncpy(result_path, config_path, max_size); 268 | free(dir_path); 269 | return true; 270 | } else { 271 | CONFIG_DEBUG("Config file not found at: %s (%s)", config_path, strerror(errno)); 272 | } 273 | 274 | // Go up one directory level - completely rewritten to avoid memory issues 275 | char *parent_dir = strdup(dir_path); 276 | if (!parent_dir) { 277 | CONFIG_DEBUG("Failed to allocate memory for parent_dir"); 278 | free(dir_path); 279 | return false; 280 | } 281 | 282 | // Use dirname() on the copy 283 | char *dirname_result = dirname(parent_dir); 284 | 285 | // If we've reached the root directory 286 | if (strcmp(dir_path, dirname_result) == 0 || 287 | strcmp(dirname_result, "/") == 0) { 288 | CONFIG_DEBUG("Reached root directory or can't go up further"); 289 | free(parent_dir); 290 | free(dir_path); 291 | return false; 292 | } 293 | 294 | // Replace current path with parent 295 | free(dir_path); 296 | dir_path = strdup(dirname_result); 297 | free(parent_dir); 298 | 299 | if (!dir_path) { 300 | CONFIG_DEBUG("Failed to allocate memory for new dir_path"); 301 | return false; 302 | } 303 | } 304 | 305 | CONFIG_DEBUG("No directory config found in path"); 306 | if (dir_path) { 307 | free(dir_path); 308 | } 309 | return false; 310 | } 311 | 312 | // Load configuration from files with improved directory hierarchy search 313 | bool load_config_files() { 314 | bool loaded_any = false; 315 | 316 | // Track which configuration sources were loaded 317 | bool dir_loaded = false; 318 | bool user_loaded = false; 319 | bool system_loaded = false; 320 | 321 | // Load in reverse precedence order: system (lowest) -> user -> directory (highest) 322 | 323 | // Check for system config first (lowest precedence) 324 | if (load_config_from_file(SYSTEM_CONFIG_FILE)) { 325 | CONFIG_DEBUG("Loaded system config from: %s", SYSTEM_CONFIG_FILE); 326 | system_loaded = true; 327 | loaded_any = true; 328 | } 329 | 330 | // Check for user config next (medium precedence) 331 | char user_config[512]; 332 | char *home = getenv("HOME"); 333 | if (home) { 334 | snprintf(user_config, sizeof(user_config), "%s%s", home, USER_CONFIG_FILE); 335 | if (load_config_from_file(user_config)) { 336 | CONFIG_DEBUG("Loaded user config from: %s", user_config); 337 | user_loaded = true; 338 | loaded_any = true; 339 | } 340 | } 341 | 342 | // Finally, try to find directory-specific config (highest precedence) 343 | char dir_config_path[PATH_MAX] = {0}; 344 | if (find_directory_config(dir_config_path, sizeof(dir_config_path))) { 345 | if (load_config_from_file(dir_config_path)) { 346 | CONFIG_DEBUG("Loaded directory-specific config from: %s", dir_config_path); 347 | dir_loaded = true; 348 | loaded_any = true; 349 | 350 | // Store the directory where we found the config 351 | if (global_config) { 352 | char *dir_name = strdup(dirname(dir_config_path)); 353 | CONFIG_DEBUG("Setting active config directory to: %s", dir_name); 354 | // Store this path for later reference 355 | free(dir_name); // Free the temporary string 356 | } 357 | } 358 | } 359 | 360 | CONFIG_DEBUG("Config loading summary: directory=%s, user=%s, system=%s", 361 | dir_loaded ? "yes" : "no", 362 | user_loaded ? "yes" : "no", 363 | system_loaded ? "yes" : "no"); 364 | 365 | return loaded_any; 366 | } 367 | 368 | // Force reload of configuration based on current directory 369 | bool reload_directory_config() { 370 | CONFIG_DEBUG("Reloading configuration for current directory"); 371 | 372 | // Temporarily store current theme to preserve it if not overridden 373 | char *current_theme = global_config && global_config->theme ? 374 | strdup(global_config->theme) : NULL; 375 | 376 | // Clear existing configuration but don't free the struct 377 | cleanup_config_values(); 378 | 379 | // Reload configuration from files 380 | bool result = load_config_files(); 381 | 382 | // If we had a theme before and no new theme was loaded, restore it 383 | if (current_theme && global_config && !global_config->theme) { 384 | global_config->theme = current_theme; 385 | } else { 386 | free(current_theme); 387 | } 388 | 389 | return result; 390 | } 391 | 392 | // Clean up only the values inside the configuration, not the struct itself 393 | void cleanup_config_values() { 394 | if (!global_config) return; 395 | 396 | free(global_config->theme); 397 | global_config->theme = NULL; 398 | 399 | for (int i = 0; i < global_config->package_count; i++) { 400 | free(global_config->enabled_packages[i]); 401 | } 402 | free(global_config->enabled_packages); 403 | global_config->enabled_packages = NULL; 404 | global_config->package_count = 0; 405 | 406 | for (int i = 0; i < global_config->alias_count; i++) { 407 | free(global_config->aliases[i]); 408 | free(global_config->alias_commands[i]); 409 | } 410 | free(global_config->aliases); 411 | free(global_config->alias_commands); 412 | global_config->aliases = NULL; 413 | global_config->alias_commands = NULL; 414 | global_config->alias_count = 0; 415 | 416 | for (int i = 0; i < global_config->script_count; i++) { 417 | free(global_config->scripts[i]); 418 | } 419 | free(global_config->scripts); 420 | global_config->scripts = NULL; 421 | global_config->script_count = 0; 422 | } 423 | 424 | // Save current configuration to user config file 425 | bool save_config() { 426 | if (!global_config) return false; 427 | 428 | CONFIG_DEBUG("Saving configuration"); 429 | 430 | char user_config[512]; 431 | char *home = getenv("HOME"); 432 | if (!home) { 433 | CONFIG_DEBUG("HOME environment variable not set, can't save config"); 434 | return false; 435 | } 436 | 437 | snprintf(user_config, sizeof(user_config), "%s%s", home, USER_CONFIG_FILE); 438 | 439 | // Create JSON structure 440 | json_t *root = json_object(); 441 | 442 | // Save theme 443 | if (global_config->theme) { 444 | json_object_set_new(root, "theme", json_string(global_config->theme)); 445 | } 446 | 447 | // Save packages 448 | if (global_config->package_count > 0) { 449 | json_t *packages = json_array(); 450 | for (int i = 0; i < global_config->package_count; i++) { 451 | json_array_append_new(packages, json_string(global_config->enabled_packages[i])); 452 | } 453 | json_object_set_new(root, "packages", packages); 454 | } 455 | 456 | // Save aliases 457 | if (global_config->alias_count > 0) { 458 | json_t *aliases = json_object(); 459 | for (int i = 0; i < global_config->alias_count; i++) { 460 | json_object_set_new(aliases, global_config->aliases[i], 461 | json_string(global_config->alias_commands[i])); 462 | } 463 | json_object_set_new(root, "aliases", aliases); 464 | } 465 | 466 | // Save scripts 467 | if (global_config->script_count > 0) { 468 | json_t *scripts = json_array(); 469 | for (int i = 0; i < global_config->script_count; i++) { 470 | json_array_append_new(scripts, json_string(global_config->scripts[i])); 471 | } 472 | json_object_set_new(root, "scripts", scripts); 473 | } 474 | 475 | // Write JSON to file 476 | char *json_str = json_dumps(root, JSON_INDENT(2)); 477 | json_decref(root); 478 | 479 | if (!json_str) { 480 | CONFIG_DEBUG("Failed to generate JSON"); 481 | return false; 482 | } 483 | 484 | FILE *file = fopen(user_config, "w"); 485 | if (!file) { 486 | CONFIG_DEBUG("Failed to open config file for writing: %s", user_config); 487 | free(json_str); 488 | return false; 489 | } 490 | 491 | fputs(json_str, file); 492 | fclose(file); 493 | free(json_str); 494 | 495 | CONFIG_DEBUG("Configuration saved to %s", user_config); 496 | return true; 497 | } 498 | 499 | // Update theme in configuration 500 | bool set_config_theme(const char *theme_name) { 501 | if (!global_config || !theme_name) return false; 502 | 503 | CONFIG_DEBUG("Setting config theme to %s", theme_name); 504 | 505 | free(global_config->theme); 506 | global_config->theme = strdup(theme_name); 507 | 508 | return save_config(); 509 | } 510 | 511 | // Add package to configuration 512 | bool add_config_package(const char *package_name) { 513 | if (!global_config || !package_name) return false; 514 | 515 | CONFIG_DEBUG("Adding package: %s", package_name); 516 | 517 | // Check if package is already in config 518 | for (int i = 0; i < global_config->package_count; i++) { 519 | if (strcmp(global_config->enabled_packages[i], package_name) == 0) { 520 | CONFIG_DEBUG("Package %s already exists in config", package_name); 521 | return true; // Already exists 522 | } 523 | } 524 | 525 | CONFIG_DEBUG("Package count before adding: %d", global_config->package_count); 526 | 527 | // Add new package 528 | char **new_packages = realloc(global_config->enabled_packages, 529 | (global_config->package_count + 1) * sizeof(char*)); 530 | 531 | if (!new_packages) { 532 | CONFIG_DEBUG("Failed to reallocate package array"); 533 | return false; 534 | } 535 | 536 | global_config->enabled_packages = new_packages; 537 | global_config->enabled_packages[global_config->package_count] = strdup(package_name); 538 | global_config->package_count++; 539 | 540 | CONFIG_DEBUG("Package count after adding: %d", global_config->package_count); 541 | CONFIG_DEBUG("Added package: %s", global_config->enabled_packages[global_config->package_count-1]); 542 | 543 | return save_config(); 544 | } 545 | 546 | // Remove package from configuration 547 | bool remove_config_package(const char *package_name) { 548 | if (!global_config || !package_name) return false; 549 | 550 | for (int i = 0; i < global_config->package_count; i++) { 551 | if (strcmp(global_config->enabled_packages[i], package_name) == 0) { 552 | // Found the package, remove it 553 | free(global_config->enabled_packages[i]); 554 | 555 | // Shift remaining packages 556 | for (int j = i; j < global_config->package_count - 1; j++) { 557 | global_config->enabled_packages[j] = global_config->enabled_packages[j+1]; 558 | } 559 | 560 | global_config->package_count--; 561 | return save_config(); 562 | } 563 | } 564 | 565 | return false; // Package not found 566 | } 567 | 568 | // Add alias to configuration 569 | bool add_config_alias(const char *alias_name, const char *command) { 570 | if (!global_config || !alias_name || !command) return false; 571 | 572 | // Check if alias already exists 573 | for (int i = 0; i < global_config->alias_count; i++) { 574 | if (strcmp(global_config->aliases[i], alias_name) == 0) { 575 | // Update existing alias 576 | free(global_config->alias_commands[i]); 577 | global_config->alias_commands[i] = strdup(command); 578 | return save_config(); 579 | } 580 | } 581 | 582 | // Add new alias 583 | global_config->aliases = realloc(global_config->aliases, 584 | (global_config->alias_count + 1) * sizeof(char*)); 585 | global_config->alias_commands = realloc(global_config->alias_commands, 586 | (global_config->alias_count + 1) * sizeof(char*)); 587 | 588 | global_config->aliases[global_config->alias_count] = strdup(alias_name); 589 | global_config->alias_commands[global_config->alias_count] = strdup(command); 590 | global_config->alias_count++; 591 | 592 | return save_config(); 593 | } 594 | 595 | // Remove alias from configuration 596 | bool remove_config_alias(const char *alias_name) { 597 | if (!global_config || !alias_name) return false; 598 | 599 | for (int i = 0; i < global_config->alias_count; i++) { 600 | if (strcmp(global_config->aliases[i], alias_name) == 0) { 601 | // Found the alias, remove it 602 | free(global_config->aliases[i]); 603 | free(global_config->alias_commands[i]); 604 | 605 | // Shift remaining aliases 606 | for (int j = i; j < global_config->alias_count - 1; j++) { 607 | global_config->aliases[j] = global_config->aliases[j+1]; 608 | global_config->alias_commands[j] = global_config->alias_commands[j+1]; 609 | } 610 | 611 | global_config->alias_count--; 612 | return save_config(); 613 | } 614 | } 615 | 616 | return false; // Alias not found 617 | } 618 | 619 | // Add script to configuration 620 | bool add_config_script(const char *script_path) { 621 | if (!global_config || !script_path) return false; 622 | 623 | // Check if script already exists 624 | for (int i = 0; i < global_config->script_count; i++) { 625 | if (strcmp(global_config->scripts[i], script_path) == 0) { 626 | return true; // Already exists 627 | } 628 | } 629 | 630 | // Add new script 631 | global_config->scripts = realloc(global_config->scripts, 632 | (global_config->script_count + 1) * sizeof(char*)); 633 | global_config->scripts[global_config->script_count] = strdup(script_path); 634 | global_config->script_count++; 635 | 636 | return save_config(); 637 | } 638 | 639 | // Remove script from configuration 640 | bool remove_config_script(const char *script_path) { 641 | if (!global_config || !script_path) return false; 642 | 643 | for (int i = 0; i < global_config->script_count; i++) { 644 | if (strcmp(global_config->scripts[i], script_path) == 0) { 645 | // Found the script, remove it 646 | free(global_config->scripts[i]); 647 | 648 | // Shift remaining scripts 649 | for (int j = i; j < global_config->script_count - 1; j++) { 650 | global_config->scripts[j] = global_config->scripts[j+1]; 651 | } 652 | 653 | global_config->script_count--; 654 | return save_config(); 655 | } 656 | } 657 | 658 | return false; // Script not found 659 | } 660 | 661 | // Get theme from configuration 662 | const char *get_config_theme() { 663 | return global_config ? global_config->theme : NULL; 664 | } 665 | 666 | // Check if package is enabled 667 | bool is_package_enabled(const char *package_name) { 668 | if (!global_config || !package_name) return false; 669 | 670 | for (int i = 0; i < global_config->package_count; i++) { 671 | if (strcmp(global_config->enabled_packages[i], package_name) == 0) { 672 | return true; 673 | } 674 | } 675 | 676 | return false; 677 | } 678 | 679 | // Get alias command 680 | const char *get_alias_command(const char *alias_name) { 681 | if (!global_config || !alias_name) return NULL; 682 | 683 | for (int i = 0; i < global_config->alias_count; i++) { 684 | if (strcmp(global_config->aliases[i], alias_name) == 0) { 685 | return global_config->alias_commands[i]; 686 | } 687 | } 688 | 689 | return NULL; 690 | } 691 | -------------------------------------------------------------------------------- /src/utils/helpers.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // String utilities 14 | char *trim_whitespace(char *str) { 15 | if (!str) return NULL; 16 | 17 | // Trim leading space 18 | while (isspace((unsigned char)*str)) str++; 19 | 20 | if (*str == 0) return str; // All spaces 21 | 22 | // Trim trailing space 23 | char *end = str + strlen(str) - 1; 24 | while (end > str && isspace((unsigned char)*end)) end--; 25 | 26 | // Write new null terminator 27 | *(end + 1) = '\0'; 28 | 29 | return str; 30 | } 31 | 32 | char **split_string(const char *input, const char *delim, int *count) { 33 | if (!input || !delim || !count) return NULL; 34 | 35 | *count = 0; 36 | char *copy = strdup(input); 37 | if (!copy) return NULL; 38 | 39 | // First pass: count tokens 40 | char *token, *saveptr; 41 | token = strtok_r(copy, delim, &saveptr); 42 | while (token) { 43 | (*count)++; 44 | token = strtok_r(NULL, delim, &saveptr); 45 | } 46 | 47 | // Allocate result array 48 | char **result = malloc((*count + 1) * sizeof(char *)); 49 | if (!result) { 50 | free(copy); 51 | return NULL; 52 | } 53 | 54 | // Second pass: store tokens 55 | free(copy); 56 | copy = strdup(input); 57 | if (!copy) { 58 | free(result); 59 | return NULL; 60 | } 61 | 62 | token = strtok_r(copy, delim, &saveptr); 63 | for (int i = 0; i < *count; i++) { 64 | result[i] = strdup(token); 65 | token = strtok_r(NULL, delim, &saveptr); 66 | } 67 | result[*count] = NULL; 68 | 69 | free(copy); 70 | return result; 71 | } 72 | 73 | // File utilities 74 | bool file_exists(const char *path) { 75 | struct stat buffer; 76 | return (stat(path, &buffer) == 0); 77 | } 78 | 79 | char *expand_path(const char *path) { 80 | if (!path) return NULL; 81 | 82 | static char result[1024]; 83 | 84 | if (path[0] == '~') { 85 | struct passwd *pw = getpwuid(getuid()); 86 | if (pw) { 87 | snprintf(result, sizeof(result), "%s%s", pw->pw_dir, path + 1); 88 | return result; 89 | } 90 | } 91 | 92 | // Handle relative paths if needed 93 | if (path[0] != '/') { 94 | char cwd[1024]; 95 | if (getcwd(cwd, sizeof(cwd))) { 96 | snprintf(result, sizeof(result), "%s/%s", cwd, path); 97 | return result; 98 | } 99 | } 100 | 101 | return strdup(path); 102 | } 103 | 104 | // Error handling 105 | void print_error(const char *msg) { 106 | fprintf(stderr, "\033[1;31mError: %s\033[0m\n", msg); 107 | } 108 | 109 | void print_success(const char *msg) { 110 | fprintf(stdout, "\033[1;32m%s\033[0m\n", msg); 111 | } 112 | 113 | // For shell.c 114 | char *get_current_dir() { 115 | static char cwd[1024]; 116 | if (!getcwd(cwd, sizeof(cwd))) { 117 | strcpy(cwd, "unknown"); 118 | } 119 | return cwd; 120 | } 121 | 122 | // Debug flag - set to 1 to enable debug output 123 | #define DEBUG_HELPERS 1 124 | 125 | bool sanitize_command(const char *cmd) { 126 | if (!cmd) return false; 127 | 128 | if (DEBUG_HELPERS) { 129 | fprintf(stderr, "DEBUG: Sanitizing command: '%s'\n", cmd); 130 | } 131 | 132 | // List of disallowed commands for security 133 | const char *blocked_commands[] = { 134 | "rm -rf /", "rm -rf /*", ":(){ :|:& };:", "dd", 135 | NULL 136 | }; 137 | 138 | // Check against blocked commands 139 | for (int i = 0; blocked_commands[i]; i++) { 140 | if (strcmp(cmd, blocked_commands[i]) == 0) { 141 | if (DEBUG_HELPERS) { 142 | fprintf(stderr, "DEBUG: Command '%s' blocked by security policy\n", cmd); 143 | } 144 | return false; 145 | } 146 | } 147 | 148 | return true; 149 | } 150 | -------------------------------------------------------------------------------- /src/utils/security.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include // Use EVP interface instead of direct SHA256 9 | #include 10 | #include 11 | #include 12 | 13 | // Define PKG_DIR if not already defined 14 | #ifndef PKG_DIR 15 | #define PKG_DIR "/usr/local/nutshell/packages" 16 | #endif 17 | 18 | void error(const char *msg) { 19 | fprintf(stderr, "Error: %s\n", msg); 20 | exit(EXIT_FAILURE); 21 | } 22 | 23 | bool verify_package_hash(const char *pkg_name) { 24 | char path[256]; 25 | snprintf(path, sizeof(path), "%s/%s", PKG_DIR, pkg_name); 26 | 27 | FILE *file = fopen(path, "rb"); 28 | if (!file) return false; 29 | 30 | unsigned char hash[EVP_MAX_MD_SIZE]; 31 | unsigned int hash_len; 32 | 33 | // Use the EVP interface (modern approach) 34 | EVP_MD_CTX *mdctx = EVP_MD_CTX_new(); 35 | if (!mdctx) { 36 | fclose(file); 37 | return false; 38 | } 39 | 40 | EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL); 41 | 42 | unsigned char buffer[4096]; 43 | size_t bytes_read; 44 | while((bytes_read = fread(buffer, 1, sizeof(buffer), file))) { 45 | EVP_DigestUpdate(mdctx, buffer, bytes_read); 46 | } 47 | 48 | EVP_DigestFinal_ex(mdctx, hash, &hash_len); 49 | EVP_MD_CTX_free(mdctx); 50 | fclose(file); 51 | 52 | // Read expected hash 53 | unsigned char expected_hash[EVP_MAX_MD_SIZE]; 54 | char hash_path[256]; 55 | snprintf(hash_path, sizeof(hash_path), "%s/%s.sha256", PKG_DIR, pkg_name); 56 | 57 | FILE *hash_file = fopen(hash_path, "rb"); 58 | if (!hash_file) { 59 | DEBUG_LOG("No hash file found for package: %s", pkg_name); 60 | return false; 61 | } 62 | 63 | if (fread(expected_hash, 1, hash_len, hash_file) != hash_len) { 64 | DEBUG_LOG("Invalid hash file for package: %s", pkg_name); 65 | fclose(hash_file); 66 | return false; 67 | } 68 | fclose(hash_file); 69 | 70 | // Compare hashes 71 | if(memcmp(expected_hash, hash, hash_len) != 0) { 72 | DEBUG_LOG("Hash verification failed for package: %s", pkg_name); 73 | return false; 74 | } 75 | 76 | return true; 77 | } -------------------------------------------------------------------------------- /tests/test_ai_integration.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // Mock API key for testing 14 | #define TEST_API_KEY "test_api_key_12345" 15 | 16 | // External declaration for the function we're using from openai.c 17 | extern int set_api_key_command(int argc, char **argv); 18 | 19 | // Test API key management 20 | void test_api_key_functions() { 21 | printf("Testing API key functions...\n"); 22 | 23 | // Initially no key should be set 24 | assert(has_api_key() == false); 25 | 26 | // Set a key and verify 27 | set_api_key(TEST_API_KEY); 28 | assert(has_api_key() == true); 29 | 30 | // Check that key is persisted in home directory 31 | char key_path[512]; 32 | char *home = getenv("HOME"); 33 | snprintf(key_path, sizeof(key_path), "%s/.nutshell/openai_key", home); 34 | 35 | FILE *key_file = fopen(key_path, "r"); 36 | assert(key_file != NULL); 37 | 38 | char buffer[256]; 39 | fgets(buffer, sizeof(buffer), key_file); 40 | buffer[strcspn(buffer, "\n")] = 0; // Remove newline 41 | 42 | assert(strcmp(buffer, TEST_API_KEY) == 0); 43 | fclose(key_file); 44 | 45 | printf("API key functions test passed\n"); 46 | } 47 | 48 | // Test init and cleanup 49 | void test_init_and_cleanup() { 50 | printf("Testing AI initialization and cleanup...\n"); 51 | 52 | // Set a key first to ensure init will succeed 53 | set_api_key(TEST_API_KEY); 54 | 55 | // Initialize AI 56 | bool init_result = init_ai_integration(); 57 | assert(init_result == true); 58 | 59 | // Cleanup should not crash 60 | cleanup_ai_integration(); 61 | 62 | // After cleanup, key should be reset 63 | assert(has_api_key() == false); 64 | 65 | printf("Initialization and cleanup test passed\n"); 66 | } 67 | 68 | // Test API key command (built-in command for setting API key) 69 | void test_api_key_command() { 70 | printf("Testing set-api-key command...\n"); 71 | 72 | // Clean up any existing key 73 | cleanup_ai_integration(); 74 | 75 | // Test with valid args 76 | char *args[] = {"set-api-key", "new_test_key_6789"}; 77 | int result = set_api_key_command(2, args); 78 | assert(result == 0); 79 | assert(has_api_key() == true); 80 | 81 | // Test with invalid args (too few) 82 | char *invalid_args[] = {"set-api-key"}; 83 | result = set_api_key_command(1, invalid_args); 84 | assert(result != 0); // Should return error code 85 | 86 | printf("set-api-key command test passed\n"); 87 | } 88 | 89 | int main() { 90 | printf("Running AI integration tests...\n"); 91 | 92 | // Set testing mode 93 | setenv("NUTSHELL_TESTING", "1", 1); 94 | 95 | // Make sure we start with no API key 96 | reset_api_key_for_testing(); 97 | 98 | // Initialize test environment 99 | // Create .nutshell directory if it doesn't exist 100 | char *home = getenv("HOME"); 101 | if (home) { 102 | char dir_path[512]; 103 | snprintf(dir_path, sizeof(dir_path), "%s/.nutshell", home); 104 | mkdir(dir_path, 0755); 105 | 106 | // Remove any existing API key file to ensure clean test state 107 | char key_path[512]; 108 | snprintf(key_path, sizeof(key_path), "%s/.nutshell/openai_key", home); 109 | unlink(key_path); 110 | } 111 | 112 | // Run tests 113 | test_api_key_functions(); 114 | test_init_and_cleanup(); 115 | test_api_key_command(); 116 | 117 | printf("All AI integration tests passed!\n"); 118 | return 0; 119 | } 120 | -------------------------------------------------------------------------------- /tests/test_ai_shell_integration.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // External declarations for functions we're testing 13 | extern void register_ai_commands(); 14 | extern bool handle_ai_command(ParsedCommand *cmd); 15 | 16 | // Test AI command registration 17 | void test_ai_command_registration() { 18 | printf("Testing AI command registration...\n"); 19 | 20 | // Initialize registry 21 | init_registry(); 22 | 23 | // Register AI commands 24 | register_ai_commands(); 25 | 26 | // Check if AI commands are registered correctly 27 | const CommandMapping *set_api_key_cmd = find_command("set-api-key"); 28 | const CommandMapping *ask_cmd = find_command("ask"); 29 | const CommandMapping *explain_cmd = find_command("explain"); 30 | 31 | assert(set_api_key_cmd != NULL); 32 | assert(ask_cmd != NULL); 33 | assert(explain_cmd != NULL); 34 | 35 | assert(set_api_key_cmd->is_builtin == true); 36 | assert(ask_cmd->is_builtin == true); 37 | assert(explain_cmd->is_builtin == true); 38 | 39 | // Cleanup 40 | free_registry(); 41 | 42 | printf("AI command registration test passed\n"); 43 | } 44 | 45 | // Test handling AI commands 46 | void test_ai_command_handling() { 47 | printf("Testing AI command handling...\n"); 48 | 49 | // Create a parsed command for the set-api-key command 50 | ParsedCommand *cmd = calloc(1, sizeof(ParsedCommand)); 51 | cmd->args = calloc(3, sizeof(char*)); 52 | cmd->args[0] = strdup("set-api-key"); 53 | cmd->args[1] = strdup("test_key"); 54 | cmd->args[2] = NULL; 55 | 56 | // Test handling set-api-key command 57 | bool handled = handle_ai_command(cmd); 58 | assert(handled == true); 59 | 60 | // Clean up 61 | free_parsed_command(cmd); 62 | 63 | // Check that other commands aren't handled 64 | cmd = calloc(1, sizeof(ParsedCommand)); 65 | cmd->args = calloc(2, sizeof(char*)); 66 | cmd->args[0] = strdup("not_an_ai_command"); 67 | cmd->args[1] = NULL; 68 | 69 | handled = handle_ai_command(cmd); 70 | assert(handled == false); 71 | 72 | free_parsed_command(cmd); 73 | 74 | printf("AI command handling test passed\n"); 75 | } 76 | 77 | // Test integration with executor 78 | void test_ai_integration_with_executor() { 79 | printf("Testing AI integration with executor...\n"); 80 | 81 | // Create a test AI command 82 | ParsedCommand *cmd = calloc(1, sizeof(ParsedCommand)); 83 | cmd->args = calloc(4, sizeof(char*)); 84 | cmd->args[0] = strdup("ask"); 85 | cmd->args[1] = strdup("list"); 86 | cmd->args[2] = strdup("directory"); 87 | cmd->args[3] = NULL; 88 | 89 | // Initialize registry and AI commands 90 | init_registry(); 91 | register_ai_commands(); 92 | 93 | // Verify command is found in registry 94 | const CommandMapping *ask_cmd = find_command("ask"); 95 | assert(ask_cmd != NULL); 96 | 97 | // Cleanup 98 | free_parsed_command(cmd); 99 | free_registry(); 100 | 101 | printf("AI integration with executor test passed\n"); 102 | } 103 | 104 | int main() { 105 | printf("Running AI shell integration tests...\n"); 106 | 107 | // Run tests 108 | test_ai_command_registration(); 109 | test_ai_command_handling(); 110 | test_ai_integration_with_executor(); 111 | 112 | printf("All AI shell integration tests passed!\n"); 113 | return 0; 114 | } 115 | -------------------------------------------------------------------------------- /tests/test_config.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // Helper function to create a temporary config file for testing 14 | static void create_test_config_file(const char *content) { 15 | char *home = getenv("HOME"); 16 | if (!home) { 17 | printf("ERROR: HOME environment variable not set\n"); 18 | return; 19 | } 20 | 21 | char test_config_dir[512]; 22 | snprintf(test_config_dir, sizeof(test_config_dir), "%s/.nutshell", home); 23 | 24 | struct stat st = {0}; 25 | if (stat(test_config_dir, &st) == -1) { 26 | mkdir(test_config_dir, 0700); 27 | } 28 | 29 | char test_config_path[512]; 30 | snprintf(test_config_path, sizeof(test_config_path), "%s/.nutshell/config.json", home); 31 | 32 | FILE *fp = fopen(test_config_path, "w"); 33 | if (fp) { 34 | fputs(content, fp); 35 | fclose(fp); 36 | printf("Created test config file: %s\n", test_config_path); 37 | } else { 38 | printf("ERROR: Failed to create test config file\n"); 39 | } 40 | } 41 | 42 | // Helper function to reset the config file to an empty state 43 | static void create_empty_config_file() { 44 | char *home = getenv("HOME"); 45 | if (!home) { 46 | printf("ERROR: HOME environment variable not set\n"); 47 | return; 48 | } 49 | 50 | char test_config_path[512]; 51 | snprintf(test_config_path, sizeof(test_config_path), "%s/.nutshell/config.json", home); 52 | 53 | FILE *fp = fopen(test_config_path, "w"); 54 | if (fp) { 55 | // Write an empty JSON object 56 | fputs("{\n}\n", fp); 57 | fclose(fp); 58 | printf("Created empty config file: %s\n", test_config_path); 59 | } else { 60 | printf("ERROR: Failed to create empty config file\n"); 61 | } 62 | } 63 | 64 | // Test basic initialization and cleanup 65 | static void test_init_cleanup() { 66 | printf("Testing config initialization and cleanup...\n"); 67 | 68 | // Initialize the config system 69 | init_config_system(); 70 | 71 | // Verify that global_config is not NULL 72 | assert(global_config != NULL); 73 | 74 | // Cleanup 75 | cleanup_config_system(); 76 | 77 | // Verify that global_config is now NULL 78 | assert(global_config == NULL); 79 | 80 | printf("Config initialization and cleanup test passed!\n"); 81 | } 82 | 83 | // Test loading configuration from a file 84 | static void test_load_config() { 85 | printf("Testing config loading...\n"); 86 | 87 | // Create a test config file 88 | const char *test_config = 89 | "{\n" 90 | " \"theme\": \"test_theme\",\n" 91 | " \"packages\": [\"test_pkg1\", \"test_pkg2\"],\n" 92 | " \"aliases\": {\n" 93 | " \"ll\": \"ls -la\",\n" 94 | " \"gs\": \"git status\"\n" 95 | " },\n" 96 | " \"scripts\": [\"script1.sh\", \"script2.sh\"]\n" 97 | "}\n"; 98 | 99 | create_test_config_file(test_config); 100 | 101 | // Initialize and load the config 102 | init_config_system(); 103 | 104 | // Verify loaded values 105 | assert(global_config != NULL); 106 | assert(global_config->theme != NULL); 107 | assert(strcmp(global_config->theme, "test_theme") == 0); 108 | 109 | assert(global_config->package_count == 2); 110 | assert(strcmp(global_config->enabled_packages[0], "test_pkg1") == 0); 111 | assert(strcmp(global_config->enabled_packages[1], "test_pkg2") == 0); 112 | 113 | assert(global_config->alias_count == 2); 114 | bool found_ll = false; 115 | bool found_gs = false; 116 | 117 | for (int i = 0; i < global_config->alias_count; i++) { 118 | if (strcmp(global_config->aliases[i], "ll") == 0) { 119 | found_ll = true; 120 | assert(strcmp(global_config->alias_commands[i], "ls -la") == 0); 121 | } 122 | if (strcmp(global_config->aliases[i], "gs") == 0) { 123 | found_gs = true; 124 | assert(strcmp(global_config->alias_commands[i], "git status") == 0); 125 | } 126 | } 127 | 128 | assert(found_ll); 129 | assert(found_gs); 130 | 131 | assert(global_config->script_count == 2); 132 | assert(strcmp(global_config->scripts[0], "script1.sh") == 0); 133 | assert(strcmp(global_config->scripts[1], "script2.sh") == 0); 134 | 135 | cleanup_config_system(); 136 | printf("Config loading test passed!\n"); 137 | } 138 | 139 | // Test saving configuration to a file 140 | static void test_save_config() { 141 | printf("Testing config saving...\n"); 142 | 143 | // Initialize with empty config 144 | init_config_system(); 145 | printf("DEBUG: Initialized empty config\n"); 146 | 147 | // Set values 148 | set_config_theme("saved_theme"); 149 | printf("DEBUG: Set theme to 'saved_theme'\n"); 150 | 151 | printf("DEBUG: Before adding packages, count = %d\n", global_config->package_count); 152 | add_config_package("saved_pkg1"); 153 | printf("DEBUG: After adding 'saved_pkg1', count = %d\n", global_config->package_count); 154 | add_config_package("saved_pkg2"); 155 | printf("DEBUG: After adding 'saved_pkg2', count = %d\n", global_config->package_count); 156 | 157 | add_config_alias("st", "git status"); 158 | add_config_alias("cl", "clear"); 159 | add_config_script("/path/to/script.sh"); 160 | 161 | // Print current values for debugging 162 | printf("DEBUG: Current config state:\n"); 163 | printf("DEBUG: theme = '%s'\n", global_config->theme ? global_config->theme : "NULL"); 164 | printf("DEBUG: package_count = %d\n", global_config->package_count); 165 | 166 | if (global_config->package_count > 0 && global_config->enabled_packages) { 167 | for (int i = 0; i < global_config->package_count; i++) { 168 | printf("DEBUG: package[%d] = '%s'\n", i, 169 | global_config->enabled_packages[i] ? global_config->enabled_packages[i] : "NULL"); 170 | } 171 | } 172 | 173 | printf("DEBUG: alias_count = %d\n", global_config->alias_count); 174 | printf("DEBUG: script_count = %d\n", global_config->script_count); 175 | 176 | // Don't verify exact counts yet, just verify theme was set correctly 177 | assert(global_config->theme != NULL); 178 | assert(strcmp(global_config->theme, "saved_theme") == 0); 179 | 180 | // Save should return true 181 | printf("DEBUG: Saving config\n"); 182 | bool save_result = save_config(); 183 | printf("DEBUG: save_config() returned %s\n", save_result ? "true" : "false"); 184 | assert(save_result == true); 185 | 186 | // Cleanup 187 | printf("DEBUG: Cleaning up config\n"); 188 | cleanup_config_system(); 189 | 190 | // Re-load and verify 191 | printf("DEBUG: Re-initializing config to verify saved values\n"); 192 | init_config_system(); 193 | assert(global_config != NULL); 194 | assert(global_config->theme != NULL); 195 | assert(strcmp(global_config->theme, "saved_theme") == 0); 196 | 197 | // Verify packages - the test needs to be updated to check if they exist rather than exact count 198 | printf("DEBUG: After reload: package_count = %d\n", global_config->package_count); 199 | 200 | bool found_pkg1 = false; 201 | bool found_pkg2 = false; 202 | 203 | // Print and check each package 204 | for (int i = 0; i < global_config->package_count; i++) { 205 | printf("DEBUG: package[%d] = '%s'\n", i, 206 | global_config->enabled_packages[i] ? global_config->enabled_packages[i] : "NULL"); 207 | 208 | if (global_config->enabled_packages[i]) { 209 | if (strcmp(global_config->enabled_packages[i], "saved_pkg1") == 0) found_pkg1 = true; 210 | if (strcmp(global_config->enabled_packages[i], "saved_pkg2") == 0) found_pkg2 = true; 211 | } 212 | } 213 | 214 | // Check that both packages were found, but don't rely on specific count 215 | printf("DEBUG: found_pkg1 = %s, found_pkg2 = %s\n", 216 | found_pkg1 ? "true" : "false", found_pkg2 ? "true" : "false"); 217 | assert(found_pkg1); 218 | assert(found_pkg2); 219 | 220 | // Verify aliases 221 | bool found_st = false; 222 | bool found_cl = false; 223 | for (int i = 0; i < global_config->alias_count; i++) { 224 | if (strcmp(global_config->aliases[i], "st") == 0) { 225 | found_st = true; 226 | assert(strcmp(global_config->alias_commands[i], "git status") == 0); 227 | } 228 | if (strcmp(global_config->aliases[i], "cl") == 0) { 229 | found_cl = true; 230 | assert(strcmp(global_config->alias_commands[i], "clear") == 0); 231 | } 232 | } 233 | assert(found_st); 234 | assert(found_cl); 235 | 236 | // Verify script - use variable to store expected script count 237 | printf("DEBUG: After reload: script_count = %d\n", global_config->script_count); 238 | 239 | // Check for our specific script rather than count 240 | bool found_script = false; 241 | for (int i = 0; i < global_config->script_count; i++) { 242 | printf("DEBUG: script[%d] = '%s'\n", i, global_config->scripts[i]); 243 | if (strcmp(global_config->scripts[i], "/path/to/script.sh") == 0) { 244 | found_script = true; 245 | break; 246 | } 247 | } 248 | 249 | // Assert that our script was found 250 | assert(found_script); 251 | 252 | cleanup_config_system(); 253 | printf("Config saving test passed!\n"); 254 | } 255 | 256 | // Test update functions 257 | static void test_update_functions() { 258 | printf("Testing config update functions...\n"); 259 | 260 | // Reset to an empty config file before starting this test 261 | printf("DEBUG: Resetting to empty config file\n"); 262 | create_empty_config_file(); 263 | 264 | // Initialize with empty config 265 | init_config_system(); 266 | 267 | // Print initial state for debugging 268 | printf("DEBUG: After initialization: script_count = %d\n", global_config->script_count); 269 | 270 | // Test theme setting and getting 271 | set_config_theme("new_theme"); 272 | assert(strcmp(get_config_theme(), "new_theme") == 0); 273 | 274 | // Test package functions 275 | assert(is_package_enabled("test_pkg") == false); 276 | add_config_package("test_pkg"); 277 | assert(is_package_enabled("test_pkg") == true); 278 | remove_config_package("test_pkg"); 279 | assert(is_package_enabled("test_pkg") == false); 280 | 281 | // Test alias functions 282 | assert(get_alias_command("ta") == NULL); 283 | add_config_alias("ta", "touch all"); 284 | assert(strcmp(get_alias_command("ta"), "touch all") == 0); 285 | add_config_alias("ta", "touch any"); // Update existing 286 | assert(strcmp(get_alias_command("ta"), "touch any") == 0); 287 | remove_config_alias("ta"); 288 | assert(get_alias_command("ta") == NULL); 289 | 290 | // Test script functions 291 | assert(global_config->script_count == 0); 292 | add_config_script("/test/script.sh"); 293 | assert(global_config->script_count == 1); 294 | add_config_script("/test/script.sh"); // Add again, should be ignored 295 | assert(global_config->script_count == 1); 296 | add_config_script("/test/script2.sh"); 297 | assert(global_config->script_count == 2); 298 | remove_config_script("/test/script.sh"); 299 | assert(global_config->script_count == 1); 300 | assert(strcmp(global_config->scripts[0], "/test/script2.sh") == 0); 301 | 302 | cleanup_config_system(); 303 | printf("Config update functions test passed!\n"); 304 | } 305 | 306 | int main() { 307 | printf("Running configuration system tests...\n"); 308 | 309 | // Set debugging if needed 310 | const char *debug_env = getenv("NUT_DEBUG"); 311 | if (debug_env && strcmp(debug_env, "1") == 0) { 312 | setenv("NUT_DEBUG_CONFIG", "1", 1); 313 | printf("Config debugging enabled\n"); 314 | } 315 | 316 | // Run tests 317 | test_init_cleanup(); 318 | test_load_config(); 319 | test_save_config(); 320 | 321 | // Reset config file to empty state before running update functions test 322 | create_empty_config_file(); 323 | test_update_functions(); 324 | 325 | printf("All configuration system tests passed!\n"); 326 | return 0; 327 | } 328 | -------------------------------------------------------------------------------- /tests/test_directory_config.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | // Helper function to create a test directory structure with configs 15 | static void create_test_directory_structure() { 16 | // Create a temporary test directory structure 17 | char *home = getenv("HOME"); 18 | if (!home) { 19 | printf("ERROR: HOME environment variable not set\n"); 20 | return; 21 | } 22 | 23 | char test_root[512]; 24 | snprintf(test_root, sizeof(test_root), "%s/.nutshell/test_dirs", home); 25 | 26 | // Create directories 27 | mkdir(test_root, 0755); 28 | char parent_dir[512], child_dir[512], grandchild_dir[512]; 29 | 30 | snprintf(parent_dir, sizeof(parent_dir), "%s/parent", test_root); 31 | mkdir(parent_dir, 0755); 32 | 33 | snprintf(child_dir, sizeof(child_dir), "%s/parent/child", test_root); 34 | mkdir(child_dir, 0755); 35 | 36 | snprintf(grandchild_dir, sizeof(grandchild_dir), "%s/parent/child/grandchild", test_root); 37 | mkdir(grandchild_dir, 0755); 38 | 39 | // Create config files - the filename must match exactly what find_directory_config looks for 40 | char parent_config[512], child_config[512]; 41 | snprintf(parent_config, sizeof(parent_config), "%s/.nutshell.json", parent_dir); 42 | snprintf(child_config, sizeof(child_config), "%s/.nutshell.json", child_dir); 43 | 44 | printf("DEBUG: Creating parent config at: %s\n", parent_config); 45 | 46 | // Parent directory config 47 | FILE *fp = fopen(parent_config, "w"); 48 | if (fp) { 49 | fprintf(fp, "{\n" 50 | " \"theme\": \"parent_theme\",\n" 51 | " \"packages\": [\"parent_pkg\"],\n" 52 | " \"aliases\": {\n" 53 | " \"parent_alias\": \"echo parent\"\n" 54 | " }\n" 55 | "}\n"); 56 | fclose(fp); 57 | printf("Created parent config: %s\n", parent_config); 58 | } else { 59 | printf("ERROR: Failed to create parent config at %s\n", parent_config); 60 | perror("Reason"); 61 | } 62 | 63 | printf("DEBUG: Creating child config at: %s\n", child_config); 64 | 65 | // Child directory config 66 | fp = fopen(child_config, "w"); 67 | if (fp) { 68 | fprintf(fp, "{\n" 69 | " \"theme\": \"child_theme\",\n" 70 | " \"packages\": [\"child_pkg\"],\n" 71 | " \"aliases\": {\n" 72 | " \"child_alias\": \"echo child\"\n" 73 | " }\n" 74 | "}\n"); 75 | fclose(fp); 76 | printf("Created child config: %s\n", child_config); 77 | } else { 78 | printf("ERROR: Failed to create child config at %s\n", child_config); 79 | perror("Reason"); 80 | } 81 | 82 | // Verify files were created properly 83 | struct stat st; 84 | if (stat(parent_config, &st) == 0) { 85 | printf("DEBUG: Parent config file exists and is %lld bytes\n", (long long)st.st_size); 86 | } else { 87 | printf("DEBUG: Parent config file does not exist!\n"); 88 | } 89 | 90 | if (stat(child_config, &st) == 0) { 91 | printf("DEBUG: Child config file exists and is %lld bytes\n", (long long)st.st_size); 92 | } else { 93 | printf("DEBUG: Child config file does not exist!\n"); 94 | } 95 | } 96 | 97 | // Helper function to back up and restore user's config 98 | static void backup_user_config(bool restore) { 99 | char *home = getenv("HOME"); 100 | if (!home) return; 101 | 102 | char config_file[512], backup_file[512]; 103 | snprintf(config_file, sizeof(config_file), "%s/.nutshell/config.json", home); 104 | snprintf(backup_file, sizeof(backup_file), "%s/.nutshell/config.json.bak", home); 105 | 106 | if (restore) { 107 | // Restore the backup 108 | printf("DEBUG: Restoring user config from backup\n"); 109 | struct stat st; 110 | if (stat(backup_file, &st) == 0) { 111 | // If backup exists, restore it 112 | rename(backup_file, config_file); 113 | } 114 | } else { 115 | // Create backup and remove config 116 | printf("DEBUG: Backing up user config\n"); 117 | struct stat st; 118 | if (stat(config_file, &st) == 0) { 119 | // If config exists, make backup 120 | rename(config_file, backup_file); 121 | } 122 | } 123 | } 124 | 125 | // Test loading directory-specific config 126 | static void test_directory_config_loading() { 127 | printf("Testing directory config loading...\n"); 128 | 129 | // Back up any existing user config 130 | backup_user_config(false); 131 | 132 | // Create the test directory structure 133 | create_test_directory_structure(); 134 | 135 | char *home = getenv("HOME"); 136 | char test_root[512], parent_dir[512], child_dir[512], grandchild_dir[512]; 137 | snprintf(test_root, sizeof(test_root), "%s/.nutshell/test_dirs", home); 138 | snprintf(parent_dir, sizeof(parent_dir), "%s/parent", test_root); 139 | snprintf(child_dir, sizeof(child_dir), "%s/parent/child", test_root); 140 | snprintf(grandchild_dir, sizeof(grandchild_dir), "%s/parent/child/grandchild", test_root); 141 | 142 | // Save current directory 143 | char cwd[512]; 144 | getcwd(cwd, sizeof(cwd)); 145 | 146 | // Test parent directory config 147 | printf("Testing in parent directory: %s\n", parent_dir); 148 | 149 | // Make sure the config file exists 150 | char parent_config[512]; 151 | snprintf(parent_config, sizeof(parent_config), "%s/.nutshell.json", parent_dir); 152 | printf("DEBUG: Checking parent config file: %s\n", parent_config); 153 | 154 | FILE *check = fopen(parent_config, "r"); 155 | if (check) { 156 | char buffer[512] = {0}; 157 | size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, check); 158 | fclose(check); 159 | printf("DEBUG: Parent config content (%zu bytes):\n%s\n", bytes_read, buffer); 160 | } else { 161 | printf("DEBUG: Cannot open parent config for reading!\n"); 162 | perror("Reason"); 163 | } 164 | 165 | // Execute the actual test 166 | if (chdir(parent_dir) != 0) { 167 | printf("DEBUG: Failed to chdir to %s\n", parent_dir); 168 | perror("chdir"); 169 | return; 170 | } 171 | 172 | printf("DEBUG: Current directory after chdir: "); 173 | system("pwd"); 174 | system("ls -la"); 175 | 176 | // Initialize with debug enabled 177 | setenv("NUT_DEBUG_CONFIG", "1", 1); 178 | init_config_system(); 179 | 180 | printf("DEBUG: After init_config_system()\n"); 181 | 182 | assert(global_config != NULL); 183 | printf("DEBUG: global_config is not NULL\n"); 184 | 185 | // Check if theme was loaded 186 | printf("DEBUG: global_config->theme = '%s'\n", 187 | global_config->theme ? global_config->theme : "NULL"); 188 | 189 | // Make sure the theme is not NULL 190 | assert(global_config->theme != NULL); 191 | 192 | // Check if it matches what we expect 193 | assert(strcmp(global_config->theme, "parent_theme") == 0); 194 | 195 | // Verify the alias from parent config 196 | const char *parent_alias = get_alias_command("parent_alias"); 197 | assert(parent_alias != NULL); 198 | assert(strcmp(parent_alias, "echo parent") == 0); 199 | cleanup_config_system(); 200 | 201 | // Test child directory config 202 | printf("Testing in child directory: %s\n", child_dir); 203 | chdir(child_dir); 204 | init_config_system(); 205 | assert(global_config != NULL); 206 | assert(global_config->theme != NULL); 207 | assert(strcmp(global_config->theme, "child_theme") == 0); 208 | 209 | // Verify the alias from child config 210 | const char *child_alias = get_alias_command("child_alias"); 211 | assert(child_alias != NULL); 212 | assert(strcmp(child_alias, "echo child") == 0); 213 | cleanup_config_system(); 214 | 215 | // Test grandchild directory - should inherit from child 216 | printf("Testing in grandchild directory (should inherit from child): %s\n", grandchild_dir); 217 | chdir(grandchild_dir); 218 | init_config_system(); 219 | assert(global_config != NULL); 220 | assert(global_config->theme != NULL); 221 | assert(strcmp(global_config->theme, "child_theme") == 0); 222 | cleanup_config_system(); 223 | 224 | // Test config reload on directory change 225 | // Start in child directory 226 | printf("Testing config reload on directory change\n"); 227 | chdir(child_dir); 228 | init_config_system(); 229 | assert(global_config != NULL); 230 | assert(strcmp(global_config->theme, "child_theme") == 0); 231 | 232 | // Change to parent directory and reload 233 | chdir(parent_dir); 234 | reload_directory_config(); 235 | assert(strcmp(global_config->theme, "parent_theme") == 0); 236 | 237 | // Change to grandchild directory and reload 238 | chdir(grandchild_dir); 239 | reload_directory_config(); 240 | assert(strcmp(global_config->theme, "child_theme") == 0); 241 | 242 | cleanup_config_system(); 243 | 244 | // Restore original directory and user config 245 | chdir(cwd); 246 | backup_user_config(true); 247 | 248 | printf("Directory config loading test passed!\n"); 249 | } 250 | 251 | int main() { 252 | printf("Running directory config tests...\n"); 253 | 254 | // Set debugging if needed 255 | const char *debug_env = getenv("NUT_DEBUG"); 256 | if (debug_env && strcmp(debug_env, "1") == 0) { 257 | setenv("NUT_DEBUG_CONFIG", "1", 1); 258 | printf("Config debugging enabled\n"); 259 | } 260 | 261 | // Run tests 262 | test_directory_config_loading(); 263 | 264 | printf("All directory config tests passed!\n"); 265 | return 0; 266 | } 267 | -------------------------------------------------------------------------------- /tests/test_openai_commands.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // External declarations for functions we're testing 14 | extern int ask_ai_command(int argc, char **argv); 15 | extern int set_api_key_command(int argc, char **argv); 16 | extern int explain_command(int argc, char **argv); 17 | extern int fix_command(int argc, char **argv); 18 | 19 | // Mock implementation functions 20 | char *mock_nl_to_command(const char *natural_language_query) { 21 | if (strstr(natural_language_query, "find all pdf files")) { 22 | return strdup("find . -name \"*.pdf\""); 23 | } 24 | else if (strstr(natural_language_query, "list directory")) { 25 | return strdup("peekaboo"); 26 | } 27 | return strdup("echo 'Command not understood'"); 28 | } 29 | 30 | char *mock_explain_command_ai(const char *command) { 31 | if (strstr(command, "find . -name")) { 32 | return strdup("This command searches for files with names ending in .pdf in the current directory and all subdirectories."); 33 | } 34 | return strdup("This is a mock explanation for testing purposes."); 35 | } 36 | 37 | char *mock_suggest_fix(const char *command, const char *error, int exit_status) { 38 | // Prevent unused parameter warnings 39 | (void)error; 40 | (void)exit_status; 41 | 42 | if (strstr(command, "gti")) { 43 | return strdup("Explanation: You typed 'gti' instead of 'git'.\n\nCorrected command:\ngit status"); 44 | } else if (strstr(command, "cat nonexistent")) { 45 | return strdup("Explanation: The file 'nonexistent' does not exist.\n\nCorrected command:\ntouch nonexistent && cat nonexistent"); 46 | } 47 | return strdup("This is a mock fix suggestion for testing purposes."); 48 | } 49 | 50 | // Test natural language to command conversion 51 | void test_nl_to_command() { 52 | printf("Testing natural language to command conversion...\n"); 53 | 54 | // Set up the mock implementation 55 | set_ai_mock_functions(mock_nl_to_command, NULL); 56 | 57 | // Test with various inputs 58 | char *result = nl_to_command("find all pdf files in this directory"); 59 | assert(result != NULL); 60 | assert(strcmp(result, "find . -name \"*.pdf\"") == 0); 61 | free(result); 62 | 63 | result = nl_to_command("list directory"); 64 | assert(result != NULL); 65 | assert(strcmp(result, "peekaboo") == 0); 66 | free(result); 67 | 68 | printf("nl_to_command test passed\n"); 69 | } 70 | 71 | // Test command explanation 72 | void test_explain_command() { 73 | printf("Testing command explanation...\n"); 74 | 75 | // Set up the mock implementation 76 | set_ai_mock_functions(NULL, mock_explain_command_ai); 77 | 78 | char *result = explain_command_ai("find . -name \"*.pdf\""); 79 | assert(result != NULL); 80 | assert(strstr(result, "searches for files") != NULL); 81 | free(result); 82 | 83 | printf("explain_command_ai test passed\n"); 84 | } 85 | 86 | // Test ask_ai_command using our mocks 87 | void test_ask_command() { 88 | printf("Testing ask AI command...\n"); 89 | 90 | // Set up the mock implementation 91 | set_ai_mock_functions(mock_nl_to_command, NULL); 92 | 93 | // Setup test arguments 94 | char *args[] = {"ask", "find", "all", "pdf", "files"}; 95 | 96 | // Redirect stdout to capture output 97 | FILE *original_stdout = stdout; 98 | char temp_file[] = "/tmp/nutshell_test_output_XXXXXX"; 99 | int fd = mkstemp(temp_file); 100 | assert(fd != -1); 101 | 102 | FILE *temp_fp = fdopen(fd, "w+"); 103 | assert(temp_fp != NULL); 104 | 105 | stdout = temp_fp; 106 | 107 | // Call the command 108 | int result = ask_ai_command(5, args); 109 | 110 | // Reset stdout 111 | fflush(stdout); 112 | stdout = original_stdout; 113 | 114 | // Read captured output 115 | rewind(temp_fp); 116 | char output[1024] = {0}; 117 | size_t bytes_read = fread(output, 1, sizeof(output) - 1, temp_fp); 118 | output[bytes_read] = '\0'; 119 | 120 | fclose(temp_fp); 121 | unlink(temp_file); 122 | 123 | // Verify 124 | assert(result == 0); 125 | assert(strstr(output, "find . -name \"*.pdf\"") != NULL); 126 | 127 | printf("ask_ai_command test passed\n"); 128 | } 129 | 130 | // Test fix_command using our mocks 131 | void test_fix_command() { 132 | printf("Testing fix command...\n"); 133 | 134 | // Direct assignment since we can't use set_ai_mock_functions for this 135 | extern char* (*suggest_fix_impl)(const char*, const char*, int); 136 | suggest_fix_impl = mock_suggest_fix; 137 | 138 | // Set up command history for testing 139 | extern CommandHistory cmd_history; 140 | cmd_history.last_command = strdup("gti status"); 141 | cmd_history.last_output = strdup("Command 'gti' not found"); 142 | cmd_history.has_error = true; 143 | cmd_history.exit_status = 127; 144 | 145 | // Setup test arguments 146 | char *args[] = {"fix"}; 147 | 148 | // Redirect stdout to capture output 149 | FILE *original_stdout = stdout; 150 | char temp_file[] = "/tmp/nutshell_test_output_XXXXXX"; 151 | int fd = mkstemp(temp_file); 152 | assert(fd != -1); 153 | 154 | FILE *temp_fp = fdopen(fd, "w+"); 155 | assert(temp_fp != NULL); 156 | 157 | stdout = temp_fp; 158 | 159 | // Call the command 160 | int result = fix_command(1, args); 161 | 162 | // Reset stdout 163 | fflush(stdout); 164 | stdout = original_stdout; 165 | 166 | // Read captured output 167 | rewind(temp_fp); 168 | char output[1024] = {0}; 169 | size_t bytes_read = fread(output, 1, sizeof(output) - 1, temp_fp); 170 | output[bytes_read] = '\0'; 171 | 172 | fclose(temp_fp); 173 | unlink(temp_file); 174 | 175 | // Verify 176 | assert(result == 0); 177 | assert(strstr(output, "Analyzing: gti status") != NULL); 178 | assert(strstr(output, "You typed 'gti' instead of 'git'") != NULL); 179 | 180 | // Clean up 181 | free(cmd_history.last_command); 182 | free(cmd_history.last_output); 183 | cmd_history.last_command = NULL; 184 | cmd_history.last_output = NULL; 185 | 186 | printf("fix_command test passed\n"); 187 | } 188 | 189 | int main() { 190 | printf("Running OpenAI commands tests...\n"); 191 | 192 | // Set testing mode 193 | setenv("NUTSHELL_TESTING", "1", 1); 194 | 195 | // Ensure we start with a clean state 196 | reset_api_key_for_testing(); 197 | 198 | // Set up API key - we'll use a fake one since we're using mock functions 199 | set_api_key("test_api_key"); 200 | 201 | // Run tests 202 | test_nl_to_command(); 203 | test_explain_command(); 204 | test_ask_command(); 205 | test_fix_command(); 206 | 207 | // Reset mocks to use real functions for cleanup 208 | set_ai_mock_functions(NULL, NULL); 209 | 210 | printf("All OpenAI commands tests passed!\n"); 211 | return 0; 212 | } 213 | -------------------------------------------------------------------------------- /tests/test_parser.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // Test fixture for the parser 9 | void test_basic_parsing() { 10 | printf("Testing basic command parsing...\n"); 11 | 12 | char test_cmd[] = "echo hello world"; 13 | ParsedCommand *cmd = parse_command(test_cmd); 14 | 15 | assert(cmd != NULL); 16 | assert(cmd->args != NULL); 17 | assert(cmd->args[0] != NULL); 18 | assert(strcmp(cmd->args[0], "echo") == 0); 19 | assert(cmd->args[1] != NULL); 20 | assert(strcmp(cmd->args[1], "hello") == 0); 21 | assert(cmd->args[2] != NULL); 22 | assert(strcmp(cmd->args[2], "world") == 0); 23 | assert(cmd->args[3] == NULL); 24 | assert(cmd->input_file == NULL); 25 | assert(cmd->output_file == NULL); 26 | assert(cmd->background == 0); 27 | 28 | free_parsed_command(cmd); 29 | printf("Basic parsing test passed!\n"); 30 | } 31 | 32 | void test_redirection() { 33 | printf("Testing redirection parsing...\n"); 34 | 35 | char test_cmd[] = "cat file.txt > output.txt"; 36 | ParsedCommand *cmd = parse_command(test_cmd); 37 | 38 | assert(cmd != NULL); 39 | assert(cmd->args != NULL); 40 | assert(cmd->args[0] != NULL); 41 | assert(strcmp(cmd->args[0], "cat") == 0); 42 | assert(cmd->args[1] != NULL); 43 | assert(strcmp(cmd->args[1], "file.txt") == 0); 44 | assert(cmd->args[2] == NULL); 45 | assert(cmd->output_file != NULL); 46 | assert(strcmp(cmd->output_file, "output.txt") == 0); 47 | 48 | free_parsed_command(cmd); 49 | printf("Redirection test passed!\n"); 50 | } 51 | 52 | void test_null_input() { 53 | printf("Testing NULL input handling...\n"); 54 | 55 | ParsedCommand *cmd = parse_command(NULL); 56 | assert(cmd == NULL); 57 | 58 | printf("NULL input test passed!\n"); 59 | } 60 | 61 | int main() { 62 | printf("Running parser tests...\n"); 63 | 64 | // Initialize anything needed for tests 65 | init_registry(); 66 | 67 | // Run the tests 68 | test_basic_parsing(); 69 | test_redirection(); 70 | test_null_input(); 71 | 72 | // Clean up 73 | free_registry(); 74 | 75 | printf("All parser tests passed!\n"); 76 | return 0; 77 | } 78 | -------------------------------------------------------------------------------- /tests/test_pkg_install.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // Mock implementation for testing 11 | extern int install_pkg_command(int argc, char **argv); 12 | 13 | void test_install_from_path() { 14 | printf("Testing package installation from path...\n"); 15 | 16 | // Check if our sample package exists 17 | char package_path[256]; 18 | snprintf(package_path, sizeof(package_path), 19 | "%s/Desktop/nutshell/packages/gitify", getenv("HOME")); 20 | 21 | // We'll use the install_pkg_command function directly 22 | char *args[] = {"install-pkg", package_path, NULL}; 23 | int result = install_pkg_command(2, args); 24 | 25 | assert(result == 0); // Should succeed 26 | 27 | // Verify the package is installed in the user's directory 28 | char installed_path[512]; 29 | snprintf(installed_path, sizeof(installed_path), 30 | "%s/.nutshell/packages/gitify/gitify.sh", getenv("HOME")); 31 | 32 | assert(access(installed_path, F_OK) == 0); // File should exist 33 | 34 | printf("Package installation test passed!\n"); 35 | } 36 | 37 | void test_command_registration() { 38 | printf("Testing command registration...\n"); 39 | 40 | // The command should be registered in the registry 41 | const CommandMapping *cmd = find_command("gitify"); 42 | assert(cmd != NULL); 43 | assert(strcmp(cmd->nut_cmd, "gitify") == 0); 44 | assert(cmd->is_builtin == false); 45 | 46 | printf("Command registration test passed!\n"); 47 | } 48 | 49 | void test_package_script() { 50 | printf("Testing package script execution...\n"); 51 | 52 | // We'll execute a modified version that just checks its own functionality 53 | // without actually modifying any git repo 54 | char installed_path[512]; 55 | snprintf(installed_path, sizeof(installed_path), 56 | "%s/.nutshell/packages/gitify", getenv("HOME")); 57 | 58 | // Create a small test script that just returns 0 for testing 59 | char test_command[1024]; 60 | snprintf(test_command, sizeof(test_command), 61 | "echo '#!/bin/bash\necho \"Testing gitify\"\nexit 0' > %s/test.sh && chmod +x %s/test.sh", 62 | installed_path, installed_path); 63 | 64 | int result = system(test_command); 65 | assert(result == 0); 66 | 67 | // Execute the test script 68 | char exec_command[512]; 69 | snprintf(exec_command, sizeof(exec_command), "%s/test.sh", installed_path); 70 | result = system(exec_command); 71 | assert(result == 0); 72 | 73 | printf("Package script execution test passed!\n"); 74 | } 75 | 76 | int main() { 77 | printf("Running package installation tests...\n"); 78 | 79 | // Initialize registry 80 | init_registry(); 81 | 82 | // Run the tests 83 | test_install_from_path(); 84 | test_command_registration(); 85 | test_package_script(); 86 | 87 | // Cleanup 88 | free_registry(); 89 | 90 | printf("All package installation tests passed!\n"); 91 | return 0; 92 | } 93 | -------------------------------------------------------------------------------- /tests/test_theme.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include // Add this include for configuration functions 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // Helper function to create a simple test theme 12 | Theme* create_test_theme() { 13 | Theme *theme = calloc(1, sizeof(Theme)); 14 | 15 | // Theme basics 16 | theme->name = strdup("test_theme"); 17 | theme->description = strdup("Test theme for unit tests"); 18 | 19 | // Colors 20 | theme->colors = calloc(1, sizeof(ThemeColors)); 21 | theme->colors->reset = strdup("\001\033[0m\002"); 22 | theme->colors->primary = strdup("\001\033[1;32m\002"); 23 | theme->colors->secondary = strdup("\001\033[1;34m\002"); 24 | theme->colors->error = strdup("\001\033[1;31m\002"); 25 | theme->colors->warning = strdup("\001\033[1;33m\002"); 26 | theme->colors->info = strdup("\001\033[1;36m\002"); 27 | theme->colors->success = strdup("\001\033[1;32m\002"); 28 | 29 | // Left prompt - CHANGE THIS LINE to reference git_info instead of git_branch 30 | theme->left_prompt = calloc(1, sizeof(PromptConfig)); 31 | theme->left_prompt->format = strdup("{primary}{icon} {directory}{reset} {git_info}"); 32 | theme->left_prompt->icon = strdup("T"); 33 | 34 | // Right prompt 35 | theme->right_prompt = calloc(1, sizeof(PromptConfig)); 36 | theme->right_prompt->format = strdup(""); 37 | 38 | // Other prompt settings 39 | theme->multiline = false; 40 | theme->prompt_symbol = strdup("$ "); 41 | theme->prompt_symbol_color = strdup("primary"); 42 | 43 | // Segments 44 | theme->segment_count = 2; 45 | theme->segments = calloc(theme->segment_count + 1, sizeof(ThemeSegment*)); 46 | 47 | // Directory segment 48 | theme->segments[0] = calloc(1, sizeof(ThemeSegment)); 49 | theme->segments[0]->enabled = true; 50 | theme->segments[0]->key = strdup("directory"); // Add this line to set the segment key 51 | theme->segments[0]->format = strdup("{directory}"); 52 | // New format with multiple commands 53 | theme->segments[0]->command_count = 1; 54 | theme->segments[0]->commands = calloc(2, sizeof(ThemeCommand*)); // +1 for NULL terminator 55 | theme->segments[0]->commands[0] = calloc(1, sizeof(ThemeCommand)); 56 | theme->segments[0]->commands[0]->name = strdup("directory"); 57 | theme->segments[0]->commands[0]->command = strdup("echo test_dir"); 58 | theme->segments[0]->commands[0]->output = NULL; 59 | theme->segments[0]->commands[1] = NULL; // NULL terminator 60 | 61 | // Git branch segment - KEEP THE KEY CONSISTENT 62 | theme->segments[1] = calloc(1, sizeof(ThemeSegment)); 63 | theme->segments[1]->enabled = true; 64 | // Use git_info as the segment key to match prompt format 65 | theme->segments[1]->key = strdup("git_info"); // Set key to match prompt format 66 | theme->segments[1]->format = strdup("{secondary}git:({branch}){dirty_flag}{reset}"); 67 | 68 | theme->segments[1]->command_count = 2; 69 | theme->segments[1]->commands = calloc(3, sizeof(ThemeCommand*)); // +1 for NULL terminator 70 | 71 | // Branch command 72 | theme->segments[1]->commands[0] = calloc(1, sizeof(ThemeCommand)); 73 | theme->segments[1]->commands[0]->name = strdup("branch"); 74 | theme->segments[1]->commands[0]->command = strdup("echo test_branch"); 75 | theme->segments[1]->commands[0]->output = NULL; 76 | 77 | // Dirty flag command - demonstrates multiple commands per segment 78 | theme->segments[1]->commands[1] = calloc(1, sizeof(ThemeCommand)); 79 | theme->segments[1]->commands[1]->name = strdup("dirty_flag"); 80 | theme->segments[1]->commands[1]->command = strdup("echo '*'"); 81 | theme->segments[1]->commands[1]->output = NULL; 82 | 83 | theme->segments[1]->commands[2] = NULL; // NULL terminator 84 | 85 | theme->segments[2] = NULL; // NULL terminator for segments array 86 | 87 | return theme; 88 | } 89 | 90 | // Test theme loading from JSON - change return type to int 91 | int test_load_theme() { 92 | printf("Testing theme loading...\n"); 93 | 94 | // First ensure themes are properly set up 95 | printf("DEBUG: Creating themes directory\n"); 96 | system("mkdir -p ~/.nutshell/themes"); 97 | printf("DEBUG: Copying theme files\n"); 98 | system("cp ./themes/*.json ~/.nutshell/themes/ 2>/dev/null || echo 'DEBUG: Failed to copy themes'"); 99 | system("ls -la ./themes/ 2>/dev/null || echo 'DEBUG: No themes in ./themes'"); 100 | system("ls -la ~/.nutshell/themes/ || echo 'DEBUG: No themes in ~/.nutshell/themes'"); 101 | 102 | printf("DEBUG: Attempting to load default theme\n"); 103 | // Test loading the default theme 104 | Theme *theme = load_theme("default"); 105 | if (!theme) { 106 | printf("WARNING: Could not load default theme. Check JSON syntax.\n"); 107 | printf("DEBUG: Creating test theme instead\n"); 108 | theme = create_test_theme(); 109 | if (!theme) { 110 | printf("ERROR: Failed to create test theme\n"); 111 | return 1; 112 | } 113 | printf("DEBUG: Test theme created\n"); 114 | } else { 115 | printf("DEBUG: Default theme loaded successfully\n"); 116 | assert(theme->name != NULL); 117 | printf("DEBUG: Theme name: %s\n", theme->name); 118 | assert(strcmp(theme->name, "default") == 0); 119 | assert(theme->colors != NULL); 120 | assert(theme->left_prompt != NULL); 121 | assert(theme->right_prompt != NULL); 122 | } 123 | 124 | // Cleanup 125 | printf("DEBUG: Freeing theme\n"); 126 | free_theme(theme); 127 | 128 | // Test loading minimal theme 129 | printf("DEBUG: Attempting to load minimal theme\n"); 130 | theme = load_theme("minimal"); 131 | if (!theme) { 132 | printf("WARNING: Could not load minimal theme. Check JSON syntax.\n"); 133 | } else { 134 | printf("DEBUG: Minimal theme loaded successfully\n"); 135 | assert(theme->name != NULL); 136 | printf("DEBUG: Theme name: %s\n", theme->name); 137 | assert(strcmp(theme->name, "minimal") == 0); 138 | // Cleanup 139 | free_theme(theme); 140 | } 141 | 142 | printf("Theme loading test passed!\n"); 143 | return 0; // Add return value 144 | } 145 | 146 | // Test theme format expansion - change return type to int 147 | int test_expand_format() { 148 | printf("Testing theme format expansion...\n"); 149 | 150 | Theme *theme = create_test_theme(); 151 | if (!theme) { 152 | printf("ERROR: Failed to create test theme\n"); 153 | return 1; // Return error code 154 | } 155 | 156 | // Test basic color expansion 157 | char *result = expand_theme_format(theme, "Hello {primary}World{reset}"); 158 | if (!result) { 159 | printf("ERROR: expand_theme_format returned NULL\n"); 160 | free_theme(theme); 161 | return 1; 162 | } 163 | assert(result != NULL); 164 | assert(strstr(result, "\033") != NULL); // Should contain color codes 165 | free(result); 166 | 167 | // Test icon expansion 168 | result = expand_theme_format(theme, "Hello {icon}"); 169 | if (!result) { 170 | printf("ERROR: expand_theme_format returned NULL\n"); 171 | free_theme(theme); 172 | return 1; 173 | } 174 | assert(result != NULL); 175 | assert(strstr(result, "T") != NULL); // Should contain our test icon 176 | free(result); 177 | 178 | // Test segment expansion 179 | result = expand_theme_format(theme, "{directory}"); 180 | if (!result) { 181 | printf("ERROR: expand_theme_format returned NULL\n"); 182 | free_theme(theme); 183 | return 1; 184 | } 185 | assert(result != NULL); 186 | assert(strstr(result, "test_dir") != NULL); // Should contain segment output 187 | free(result); 188 | 189 | // Cleanup 190 | free_theme(theme); 191 | 192 | printf("Theme format expansion test passed!\n"); 193 | return 0; // Return success 194 | } 195 | 196 | // Test the prompt generation - change return type to int 197 | int test_get_prompt() { 198 | printf("Testing prompt generation...\n"); 199 | 200 | // Create a test theme we know is valid 201 | Theme *theme = create_test_theme(); 202 | if (!theme) { 203 | printf("ERROR: Failed to create test theme\n"); 204 | return 1; 205 | } 206 | 207 | printf("DEBUG: Test theme created successfully\n"); 208 | 209 | char *prompt = get_theme_prompt(theme); 210 | if (!prompt) { 211 | printf("ERROR: get_theme_prompt returned NULL\n"); 212 | free_theme(theme); 213 | return 1; 214 | } 215 | 216 | printf("DEBUG: Got prompt: %.40s...\n", prompt); 217 | 218 | assert(prompt != NULL); 219 | assert(strstr(prompt, "T") != NULL); // Should contain our icon 220 | assert(strstr(prompt, "test_dir") != NULL); // Should contain dir segment 221 | assert(strstr(prompt, "\033") != NULL); // Should contain color codes 222 | 223 | free(prompt); 224 | free_theme(theme); 225 | 226 | printf("Prompt generation test passed!\n"); 227 | return 0; // Add return value 228 | } 229 | 230 | // Test the theme command - update to handle config integration 231 | int test_theme_command() { 232 | printf("Testing theme command...\n"); 233 | 234 | // Also initialize the configuration system 235 | init_config_system(); 236 | 237 | // Setup 238 | extern Theme *current_theme; 239 | current_theme = NULL; 240 | 241 | // Test listing themes 242 | char *args1[] = {"theme"}; 243 | printf("DEBUG: Testing 'theme' command (list themes)\n"); 244 | int result = theme_command(1, args1); 245 | assert(result == 0); 246 | 247 | // Test setting theme 248 | char *args2[] = {"theme", "default"}; 249 | printf("DEBUG: Testing 'theme default' command\n"); 250 | result = theme_command(2, args2); 251 | assert(result == 0); 252 | assert(current_theme != NULL); 253 | printf("DEBUG: Current theme set to: %s\n", current_theme->name); 254 | assert(strcmp(current_theme->name, "default") == 0); 255 | 256 | // Check if theme was saved to config 257 | const char *saved_theme = get_config_theme(); 258 | printf("DEBUG: Config saved theme: %s\n", saved_theme ? saved_theme : "NULL"); 259 | assert(saved_theme != NULL); 260 | assert(strcmp(saved_theme, "default") == 0); 261 | 262 | // Test invalid theme 263 | char *args3[] = {"theme", "nonexistent_theme"}; 264 | printf("DEBUG: Testing 'theme nonexistent_theme' command\n"); 265 | result = theme_command(2, args3); 266 | assert(result != 0); // Should fail 267 | 268 | // Cleanup 269 | cleanup_theme_system(); 270 | cleanup_config_system(); 271 | 272 | printf("Theme command test passed!\n"); 273 | return 0; 274 | } 275 | 276 | // Test segment command execution and output storage 277 | int test_segment_commands() { 278 | printf("Testing segment command execution...\n"); 279 | 280 | Theme *theme = create_test_theme(); 281 | if (!theme) { 282 | printf("ERROR: Failed to create test theme\n"); 283 | return 1; 284 | } 285 | 286 | // Verify our test theme structure 287 | printf("DEBUG: Theme segment count: %d\n", theme->segment_count); 288 | printf("DEBUG: Left prompt format: '%s'\n", theme->left_prompt->format); 289 | for (int i = 0; i < theme->segment_count; i++) { 290 | printf("DEBUG: Segment %d key: '%s', format: '%s'\n", i, 291 | theme->segments[i]->key, theme->segments[i]->format); 292 | } 293 | 294 | // Test executing commands for a segment 295 | ThemeSegment *git_segment = theme->segments[1]; 296 | printf("DEBUG: Git segment format: '%s'\n", git_segment->format); 297 | 298 | execute_segment_commands(git_segment); 299 | 300 | printf("DEBUG: git_segment->commands[0]->output = '%s'\n", 301 | git_segment->commands[0]->output ? git_segment->commands[0]->output : "NULL"); 302 | printf("DEBUG: git_segment->commands[1]->output = '%s'\n", 303 | git_segment->commands[1]->output ? git_segment->commands[1]->output : "NULL"); 304 | 305 | // Verify branch command output is stored 306 | assert(git_segment->commands[0]->output != NULL); 307 | assert(strcmp(git_segment->commands[0]->output, "test_branch") == 0); 308 | 309 | // Verify dirty flag command output is stored 310 | assert(git_segment->commands[1]->output != NULL); 311 | assert(strcmp(git_segment->commands[1]->output, "*") == 0); 312 | 313 | // Test prompt generation with multiple command outputs 314 | setenv("NUT_DEBUG_THEME", "1", 1); // Enable theme debugging for this test 315 | printf("DEBUG: Getting theme prompt...\n"); 316 | char *prompt = get_theme_prompt(theme); 317 | assert(prompt != NULL); 318 | 319 | // Add debugging output 320 | printf("DEBUG: Generated prompt: '%s'\n", prompt); 321 | 322 | // Check if both command outputs appear in the prompt 323 | bool has_branch = strstr(prompt, "test_branch") != NULL; 324 | bool has_flag = strstr(prompt, "*") != NULL; 325 | 326 | printf("DEBUG: Contains branch? %s\n", has_branch ? "YES" : "NO"); 327 | printf("DEBUG: Contains flag? %s\n", has_flag ? "YES" : "NO"); 328 | 329 | assert(has_branch); 330 | assert(has_flag); 331 | 332 | free(prompt); 333 | free_theme(theme); 334 | 335 | printf("Segment command execution test passed!\n"); 336 | return 0; 337 | } 338 | 339 | int main() { 340 | printf("Running theme tests...\n"); 341 | 342 | // Initialize error handling 343 | signal(SIGSEGV, SIG_DFL); 344 | 345 | // Enable theme debug if NUT_DEBUG is set 346 | if (getenv("NUT_DEBUG")) { 347 | setenv("NUT_DEBUG_THEME", "1", 1); 348 | setenv("NUT_DEBUG_CONFIG", "1", 1); 349 | printf("Theme and config debugging enabled\n"); 350 | } 351 | 352 | // Run the tests with proper return value checking 353 | int result = 0; 354 | 355 | // Run each test and track overall success 356 | result = test_load_theme(); 357 | 358 | // Only continue if previous test passed 359 | if (result == 0) { 360 | result = test_expand_format(); 361 | } 362 | 363 | // Only continue if previous tests passed 364 | if (result == 0) { 365 | result = test_get_prompt(); 366 | } 367 | 368 | // Add the new test for segment commands 369 | if (result == 0) { 370 | result = test_segment_commands(); 371 | } 372 | 373 | // Only continue if previous tests passed 374 | if (result == 0) { 375 | result = test_theme_command(); 376 | } 377 | 378 | if (result == 0) { 379 | printf("All theme tests passed!\n"); 380 | } else { 381 | printf("Some theme tests failed!\n"); 382 | } 383 | 384 | return result; 385 | } 386 | -------------------------------------------------------------------------------- /tests/test_theme_json.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Test script for validating theme JSON files 4 | # This test checks that all theme files are valid JSON 5 | # and conform to our expected structure 6 | 7 | echo "Testing theme JSON files..." 8 | 9 | # Check if jq is available (needed for JSON validation) 10 | if ! command -v jq &> /dev/null; then 11 | echo "Warning: jq command not found. Installing JSON validation will be limited." 12 | HAS_JQ=0 13 | else 14 | HAS_JQ=1 15 | fi 16 | 17 | # Directories to check 18 | THEME_DIRS=( 19 | "./themes" 20 | "$HOME/.nutshell/themes" 21 | ) 22 | 23 | # Schema validation function 24 | validate_theme_schema() { 25 | local file="$1" 26 | local schema_errors=0 27 | 28 | # Basic checks that can be done with or without jq 29 | if grep -q '"segments"' "$file"; then 30 | echo "✓ Contains segments section" 31 | else 32 | echo "✗ Missing segments section" 33 | schema_errors=$((schema_errors+1)) 34 | fi 35 | 36 | if grep -q '"commands"' "$file"; then 37 | echo "✓ Contains commands section (new format)" 38 | else 39 | echo "⚠ May be using old format (missing 'commands')" 40 | fi 41 | 42 | # More detailed validation with jq if available 43 | if [ "$HAS_JQ" -eq 1 ]; then 44 | # Check required top-level fields 45 | if ! jq -e '.name' "$file" > /dev/null 2>&1; then 46 | echo "✗ Missing required field: name" 47 | schema_errors=$((schema_errors+1)) 48 | fi 49 | 50 | if ! jq -e '.prompt' "$file" > /dev/null 2>&1; then 51 | echo "✗ Missing required field: prompt" 52 | schema_errors=$((schema_errors+1)) 53 | fi 54 | 55 | if ! jq -e '.colors' "$file" > /dev/null 2>&1; then 56 | echo "✗ Missing required field: colors" 57 | schema_errors=$((schema_errors+1)) 58 | fi 59 | 60 | # Check that segments have commands objects 61 | if jq -e '.segments | keys[]' "$file" > /dev/null 2>&1; then 62 | for segment in $(jq -r '.segments | keys[]' "$file"); do 63 | if ! jq -e ".segments[\"$segment\"].commands" "$file" > /dev/null 2>&1; then 64 | echo "⚠ Segment '$segment' is using old format (no commands object)" 65 | else 66 | echo "✓ Segment '$segment' has commands object (new format)" 67 | fi 68 | done 69 | fi 70 | fi 71 | 72 | return $schema_errors 73 | } 74 | 75 | # Test results tracking 76 | TOTAL=0 77 | PASSED=0 78 | FAILED=0 79 | 80 | for dir in "${THEME_DIRS[@]}"; do 81 | if [ -d "$dir" ]; then 82 | echo "Checking themes in $dir..." 83 | 84 | # Find all JSON files 85 | for theme_file in "$dir"/*.json; do 86 | if [ -f "$theme_file" ]; then 87 | TOTAL=$((TOTAL+1)) 88 | filename=$(basename "$theme_file") 89 | echo "----------------------------------------" 90 | echo "Testing theme: $filename" 91 | 92 | # 1. Check if it's valid JSON 93 | if [ "$HAS_JQ" -eq 1 ]; then 94 | if jq '.' "$theme_file" > /dev/null 2>&1; then 95 | echo "✓ Valid JSON format" 96 | 97 | # 2. Validate theme schema 98 | errors=$(validate_theme_schema "$theme_file") 99 | if [ "$?" -eq 0 ]; then 100 | echo "✓ Schema validation passed" 101 | PASSED=$((PASSED+1)) 102 | else 103 | echo "✗ Schema validation failed" 104 | FAILED=$((FAILED+1)) 105 | fi 106 | else 107 | echo "✗ Invalid JSON format" 108 | FAILED=$((FAILED+1)) 109 | fi 110 | else 111 | # Fallback to simple validation 112 | if grep -q "{" "$theme_file" && grep -q "}" "$theme_file"; then 113 | echo "⚠ Appears to be JSON but cannot fully validate (jq not available)" 114 | validate_theme_schema "$theme_file" 115 | PASSED=$((PASSED+1)) 116 | else 117 | echo "✗ Does not appear to be valid JSON" 118 | FAILED=$((FAILED+1)) 119 | fi 120 | fi 121 | fi 122 | done 123 | fi 124 | done 125 | 126 | echo "----------------------------------------" 127 | echo "Theme JSON tests summary:" 128 | echo "Total themes tested: $TOTAL" 129 | echo "Passed: $PASSED" 130 | echo "Failed: $FAILED" 131 | 132 | if [ "$FAILED" -eq 0 ]; then 133 | echo "All theme tests passed!" 134 | exit 0 135 | else 136 | echo "Some theme tests failed!" 137 | exit 1 138 | fi 139 | -------------------------------------------------------------------------------- /themes/cyberpunk.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cyberpunk", 3 | "description": "A multiline cyberpunk-themed prompt with neon colors and futuristic design", 4 | "colors": { 5 | "reset": "\u001b[0m", 6 | "primary": "\u001b[1;38;5;123m", 7 | "secondary": "\u001b[1;38;5;205m", 8 | "error": "\u001b[1;38;5;196m", 9 | "warning": "\u001b[1;38;5;220m", 10 | "info": "\u001b[1;38;5;51m", 11 | "success": "\u001b[1;38;5;118m", 12 | "black": "\u001b[38;5;16m", 13 | "gray": "\u001b[38;5;240m" 14 | }, 15 | "prompt": { 16 | "left": { 17 | "format": "{primary}┌─[{secondary}サイバー{primary}]─[{info}{directory}{primary}]─[{secondary}{time}{primary}]{git_branch}\n{primary}└─({username}@{hostname}){primary} {icon} {reset}", 18 | "icon": "Ψ" 19 | }, 20 | "right": { 21 | "format": "" 22 | }, 23 | "multiline": true, 24 | "prompt_symbol": "→", 25 | "prompt_symbol_color": "secondary" 26 | }, 27 | "segments": { 28 | "git_branch": { 29 | "enabled": true, 30 | "format": "{primary}─[{warning}git:({branch}){dirty_flag}{primary}]", 31 | "commands": { 32 | "branch": "git branch --show-current 2>/dev/null", 33 | "dirty_flag": "git status --porcelain 2>/dev/null | awk '{print \"*\"; exit}'" 34 | } 35 | }, 36 | "git_branch_formatted": { 37 | "enabled": true, 38 | "format": "{output}", 39 | "commands": { 40 | "output": "git branch --show-current 2>/dev/null | awk '{if ($0) print \"{primary}─[{warning}git:(\"{$0}\"){primary}]\";}'" 41 | } 42 | }, 43 | "directory": { 44 | "format": "{directory}", 45 | "commands": { 46 | "directory": "pwd | sed \"s|$HOME|~|\"" 47 | } 48 | }, 49 | "username": { 50 | "format": "{username}", 51 | "commands": { 52 | "username": "whoami" 53 | } 54 | }, 55 | "hostname": { 56 | "format": "{hostname}", 57 | "commands": { 58 | "hostname": "hostname -s" 59 | } 60 | }, 61 | "time": { 62 | "format": "{time}", 63 | "commands": { 64 | "time": "date +\"%H:%M:%S\"" 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /themes/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "default", 3 | "description": "Default Nutshell theme with nutshell icon", 4 | "colors": { 5 | "reset": "\u001b[0m", 6 | "primary": "\u001b[1;32m", 7 | "secondary": "\u001b[1;34m", 8 | "error": "\u001b[1;31m", 9 | "warning": "\u001b[1;33m", 10 | "info": "\u001b[1;36m", 11 | "success": "\u001b[1;32m" 12 | }, 13 | "prompt": { 14 | "left": { 15 | "format": "{primary}{icon} {directory} {git_branch}{reset}", 16 | "icon": "🥜" 17 | }, 18 | "right": { 19 | "format": "{git_branch}{python_env}" 20 | }, 21 | "multiline": false, 22 | "prompt_symbol": " ➜", 23 | "prompt_symbol_color": "primary" 24 | }, 25 | "segments": { 26 | "git_branch": { 27 | "enabled": true, 28 | "format": "{secondary}git:({branch}){reset} ", 29 | "commands": { 30 | "branch": "git branch --show-current 2>/dev/null" 31 | } 32 | }, 33 | "directory": { 34 | "format": "{directory}", 35 | "commands": { 36 | "directory": "pwd | sed \"s|$HOME|~|\"" 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /themes/developer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "developer", 3 | "description": "Developer theme with additional information", 4 | "colors": { 5 | "reset": "\u001b[0m", 6 | "primary": "\u001b[1;35m", 7 | "secondary": "\u001b[1;33m", 8 | "error": "\u001b[1;31m", 9 | "warning": "\u001b[0;33m", 10 | "info": "\u001b[0;36m", 11 | "success": "\u001b[0;32m" 12 | }, 13 | "prompt": { 14 | "left": { 15 | "format": "{primary}{icon} {directory}{reset}", 16 | "icon": "💻" 17 | }, 18 | "right": { 19 | "format": "{git_info}{python_env}" 20 | }, 21 | "multiline": false, 22 | "prompt_symbol": "→ ", 23 | "prompt_symbol_color": "primary" 24 | }, 25 | "segments": { 26 | "git_info": { 27 | "enabled": true, 28 | "format": "{secondary}git:({branch}){dirty_flag}{reset} ", 29 | "commands": { 30 | "branch": "git branch --show-current 2>/dev/null", 31 | "dirty_flag": "git status --porcelain 2>/dev/null | awk '{print \"*\"; exit}' || echo ''" 32 | } 33 | }, 34 | "python_env": { 35 | "enabled": true, 36 | "format": "{info}py:({env_name} {version}){reset} ", 37 | "commands": { 38 | "env_name": "echo $CONDA_DEFAULT_ENV || echo $VIRTUAL_ENV | sed 's|.*/||' 2>/dev/null", 39 | "version": "python --version 2>&1 | cut -d' ' -f2 | cut -d. -f1,2" 40 | } 41 | }, 42 | "directory": { 43 | "format": "{directory}{git_root}", 44 | "commands": { 45 | "directory": "pwd | sed \"s|$HOME|~|\"", 46 | "git_root": "git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null | awk '{print \" @\" $0}' || echo ''" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /themes/minimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimal", 3 | "description": "Minimalist theme with simpler formatting", 4 | "colors": { 5 | "reset": "\u001b[0m", 6 | "primary": "\u001b[1;36m", 7 | "secondary": "\u001b[0;37m", 8 | "error": "\u001b[0;31m", 9 | "warning": "\u001b[0;33m", 10 | "info": "\u001b[0;34m", 11 | "success": "\u001b[0;32m" 12 | }, 13 | "prompt": { 14 | "left": { 15 | "format": "{primary}{directory}{reset}" 16 | }, 17 | "right": { 18 | "format": "" 19 | }, 20 | "multiline": false, 21 | "prompt_symbol": "$ ", 22 | "prompt_symbol_color": "reset" 23 | }, 24 | "segments": { 25 | "directory": { 26 | "format": "{directory}", 27 | "commands": { 28 | "directory": "pwd | sed \"s|$HOME|~|\"" 29 | } 30 | } 31 | } 32 | } 33 | --------------------------------------------------------------------------------