diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4d848a4
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,61 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [ main ]
+ push:
+ branches: [ main ]
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Test on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Zig
+ uses: goto-bus-stop/setup-zig@v2
+ with:
+ version: 0.15.2
+
+ - name: Run tests
+ run: zig build test
+
+ - name: Build project
+ run: zig build
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: repolicense-${{ matrix.os }}
+ path: zig-out/bin/repolicense*
+
+ fmt-check:
+ name: Format Check
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Zig
+ uses: goto-bus-stop/setup-zig@v2
+ with:
+ version: 0.15.2
+
+ - name: Check formatting
+ run: zig fmt --check src/
diff --git a/CI_CD.md b/CI_CD.md
deleted file mode 100644
index 8d798ba..0000000
--- a/CI_CD.md
+++ /dev/null
@@ -1,218 +0,0 @@
-# CI/CD Setup for repolicense-cli
-
-## GitHub Actions Workflow
-
-To set up continuous integration for this project, create `.github/workflows/ci.yml`:
-
-```yaml
-name: CI
-
-on:
- push:
- branches: [ main ]
- pull_request:
- branches: [ main ]
-
-jobs:
- build:
- strategy:
- matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
-
- runs-on: ${{ matrix.os }}
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Setup Zig
- uses: goto-bus-stop/setup-zig@v2
- with:
- version: 0.13.0
-
- - name: Build
- run: zig build
-
- - name: Run tests (when available)
- run: zig build test || echo "No tests configured yet"
-
- - name: Upload artifact
- uses: actions/upload-artifact@v4
- with:
- name: repolicense-${{ matrix.os }}
- path: zig-out/bin/repolicense*
-
- lint:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Setup Zig
- uses: goto-bus-stop/setup-zig@v2
- with:
- version: 0.13.0
-
- - name: Check formatting
- run: zig fmt --check src/
-```
-
-## Release Workflow
-
-Create `.github/workflows/release.yml` for automated releases:
-
-```yaml
-name: Release
-
-on:
- push:
- tags:
- - 'v*'
-
-jobs:
- build-release:
- strategy:
- matrix:
- include:
- - os: ubuntu-latest
- target: x86_64-linux
- - os: macos-latest
- target: x86_64-macos
- - os: windows-latest
- target: x86_64-windows
-
- runs-on: ${{ matrix.os }}
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Setup Zig
- uses: goto-bus-stop/setup-zig@v2
- with:
- version: 0.13.0
-
- - name: Build Release
- run: zig build -Doptimize=ReleaseSafe
-
- - name: Create Archive
- shell: bash
- run: |
- cd zig-out/bin
- if [ "${{ runner.os }}" = "Windows" ]; then
- 7z a ../../repolicense-${{ matrix.target }}.zip repolicense.exe
- else
- tar czf ../../repolicense-${{ matrix.target }}.tar.gz repolicense
- fi
-
- - name: Upload Release Asset
- uses: actions/upload-artifact@v4
- with:
- name: repolicense-${{ matrix.target }}
- path: repolicense-*.*
-
- create-release:
- needs: build-release
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Download Artifacts
- uses: actions/download-artifact@v4
-
- - name: Create Release
- uses: softprops/action-gh-release@v1
- with:
- files: |
- **/repolicense-*
- draft: false
- prerelease: false
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-```
-
-## Local Development Scripts
-
-### Format Check
-```bash
-#!/bin/bash
-zig fmt --check src/
-```
-
-### Build and Test
-```bash
-#!/bin/bash
-set -e
-echo "Building..."
-zig build
-echo "Running tests..."
-zig build test || echo "No tests yet"
-echo "Success!"
-```
-
-### Cross-compilation Example
-```bash
-#!/bin/bash
-# Build for multiple targets
-zig build -Dtarget=x86_64-linux
-zig build -Dtarget=x86_64-macos
-zig build -Dtarget=x86_64-windows
-zig build -Dtarget=aarch64-linux
-zig build -Dtarget=aarch64-macos
-```
-
-## Docker Support
-
-Create a `Dockerfile` for containerized builds:
-
-```dockerfile
-FROM alpine:latest
-
-# Install Zig
-RUN apk add --no-cache wget xz && \
- wget https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz && \
- tar xf zig-linux-x86_64-0.13.0.tar.xz && \
- mv zig-linux-x86_64-0.13.0 /usr/local/zig && \
- ln -s /usr/local/zig/zig /usr/local/bin/zig && \
- rm zig-linux-x86_64-0.13.0.tar.xz
-
-WORKDIR /app
-COPY . .
-
-RUN zig build
-
-ENTRYPOINT ["/app/zig-out/bin/repolicense"]
-```
-
-Build and run:
-```bash
-docker build -t repolicense-cli .
-docker run -it repolicense-cli
-```
-
-## Pre-commit Hooks
-
-Create `.git/hooks/pre-commit`:
-
-```bash
-#!/bin/bash
-echo "Running pre-commit checks..."
-
-# Format check
-if ! zig fmt --check src/; then
- echo "Error: Code is not formatted. Run 'zig fmt src/' to fix."
- exit 1
-fi
-
-# Build check
-if ! zig build; then
- echo "Error: Build failed."
- exit 1
-fi
-
-echo "Pre-commit checks passed!"
-```
-
-Make it executable:
-```bash
-chmod +x .git/hooks/pre-commit
-```
diff --git a/COMPARISON.md b/COMPARISON.md
index c366a6e..1a6a7a5 100644
--- a/COMPARISON.md
+++ b/COMPARISON.md
@@ -17,6 +17,7 @@ This CLI tool (`repolicense-cli`) is a command-line implementation of the web-ba
| Mermaid Diagram Export | ✅ Copy to clipboard | ❌ Not implemented |
| Download Tree | ✅ JSON download | ❌ Not implemented |
| Visual Design | ✅ Modern UI with Shoelace | ✅ Clean text output |
+| License Compatibility Checker | ❌ Not implemented | ✅ New feature for checking compatible licenses when forking/combining projects |
| Platform | 🌐 Browser-based | 💻 Native CLI |
## Architecture Differences
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 87c8127..4dc5a28 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,7 +10,7 @@ Thank you for your interest in contributing to repolicense-cli!
## Prerequisites
-- Zig 0.13.0 or later: [Download Zig](https://ziglang.org/download/)
+- Zig 0.15.2 or later: [Download Zig](https://ziglang.org/download/)
## Building
@@ -27,7 +27,7 @@ zig build test
## Code Style
- Follow Zig standard library conventions
-- Use 4 spaces for indentation
+- Use 2 spaces for indentation
- Keep functions focused and concise
- Add comments for complex logic
@@ -35,7 +35,7 @@ zig build test
To add a new license to the decision tree:
-1. Open `src/main.zig`
+1. Open `src/tree.zig`
2. Add a new `Node` constant in the `buildDecisionTree()` function
3. Update the tree structure to include your new license
4. Update the README with the new license information
diff --git a/EXAMPLES.md b/EXAMPLES.md
deleted file mode 100644
index 525af03..0000000
--- a/EXAMPLES.md
+++ /dev/null
@@ -1,162 +0,0 @@
-# Usage Examples
-
-## Example 1: Choosing the MIT License
-
-```
-=== Repolicense CLI ===
-Welcome to Repolicense! This tool helps you choose the right open-source license.
-Answer the questions with 'yes', 'no', 'back', 'reset', or 'quit'.
-
---- Question ---
-Do you want to open-source your project?
-
-Elaboration: Do you want to make your source code publicly available and allow others to use, modify, and distribute it.
-
-Your answer (yes/no/back/reset/quit): yes
-
---- Question ---
-Do you want a permissive license?
-
-Elaboration: Do you want a more lenient license and allow others to use, modify, and distribute your code with minimal restrictions. These licenses are generally business-friendly and encourage wider use.
-
-Your answer (yes/no/back/reset/quit): yes
-
---- Question ---
-Do you require minimal conditions?
-
-Elaboration: Do you want minimal conditions meaning that there are very few requirements placed on the use of your code. This typically includes providing attribution to the original authors.
-
-Your answer (yes/no/back/reset/quit): yes
-
---- Question ---
-Do you want the simplest and most permissive license possible?
-
-Elaboration: Do you want a license that is very permissive and has minimal requirements, making it as simple as possible for others to use your code.
-
-Your answer (yes/no/back/reset/quit): yes
-
-=== RESULT ===
-License: MIT
-
-For more information, visit:
-https://api.github.com/licenses/MIT
-https://docs.github.com/en/rest/licenses/licenses
-
-Options: back/reset/quit: quit
-
-Thank you for using Repolicense!
-```
-
-## Example 2: Exploring with Back and Reset
-
-```
---- Question ---
-Do you want to open-source your project?
-
-Your answer (yes/no/back/reset/quit): yes
-
---- Question ---
-Do you want a permissive license?
-
-Your answer (yes/no/back/reset/quit): no
-
---- Question ---
-Do you want a copyleft license?
-
-Your answer (yes/no/back/reset/quit): back
-
-[Moved back]
-
---- Question ---
-Do you want a permissive license?
-
-Your answer (yes/no/back/reset/quit): reset
-
-[Reset to beginning]
-
---- Question ---
-Do you want to open-source your project?
-
-Your answer (yes/no/back/reset/quit): quit
-
-Thank you for using Repolicense!
-```
-
-## Example 3: Choosing GPL-3.0
-
-```
---- Question ---
-Do you want to open-source your project?
-Your answer: yes
-
---- Question ---
-Do you want a permissive license?
-Your answer: no
-
---- Question ---
-Do you want a copyleft license?
-Your answer: yes
-
---- Question ---
-Do you want strong copyleft?
-Your answer: yes
-
---- Question ---
-Do you want network server protection?
-Your answer: no
-
---- Question ---
-Do you want to use the latest version of the GPL license?
-Your answer: yes
-
-=== RESULT ===
-License: GPL-3.0
-
-For more information, visit:
-https://api.github.com/licenses/GPL-3.0
-https://docs.github.com/en/rest/licenses/licenses
-```
-
-## Example 4: Deciding Not to Open Source
-
-```
---- Question ---
-Do you want to open-source your project?
-
-Elaboration: Do you want to make your source code publicly available and allow others to use, modify, and distribute it.
-
-Your answer (yes/no/back/reset/quit): no
-
-=== RESULT ===
-License: You should consider keeping your project closed-source.
-
-You should consider keeping your project closed-source to protect your intellectual property by not sharing the source code.
-
-Options: back/reset/quit: quit
-
-Thank you for using Repolicense!
-```
-
-## Quick Reference: License Decision Paths
-
-### Permissive Licenses
-- **MIT**: yes → yes → yes → yes
-- **BSD-2-Clause**: yes → yes → yes → no
-- **Apache-2.0**: yes → yes → no → yes
-- **BSD-3-Clause**: yes → yes → no → no → yes
-- **0BSD**: yes → yes → no → no → no → yes
-- **ISC**: yes → yes → no → no → no → no
-
-### Copyleft Licenses
-- **AGPL-3.0**: yes → no → yes → yes → yes
-- **GPL-3.0**: yes → no → yes → yes → no → yes
-- **GPL-2.0**: yes → no → yes → yes → no → no
-- **LGPL-3.0**: yes → no → yes → no → yes
-- **MPL-2.0**: yes → no → yes → no → no → yes
-- **EPL-2.0**: yes → no → yes → no → no → no → yes
-- **EPL-1.0**: yes → no → yes → no → no → no → no
-
-### Other
-- **Unlicense**: yes → no → no → yes
-- **OFL-1.1**: yes → no → no → no → yes
-- **Closed Source**: no
diff --git a/PROJECT_SUMMARY.txt b/PROJECT_SUMMARY.txt
deleted file mode 100644
index 36fa5f8..0000000
--- a/PROJECT_SUMMARY.txt
+++ /dev/null
@@ -1,167 +0,0 @@
-================================================================================
- REPOLICENSE-CLI PROJECT SUMMARY
-================================================================================
-
-PROJECT: repolicense-cli
-DESCRIPTION: Command-line tool to help choose open-source licenses
-LANGUAGE: Zig
-VERSION: 1.0.0
-MINIMUM ZIG VERSION: 0.13.0
-
-================================================================================
-IMPLEMENTATION STATUS: ✅ COMPLETE
-================================================================================
-
-The entire logic from the repolicense web application (github.com/lucasfth/repolicense)
-has been successfully transferred to this Zig CLI application.
-
-KEY FEATURES:
-- Interactive question/answer interface
-- Complete decision tree with 17+ licenses
-- Navigation commands (back, reset, quit)
-- Elaborations for each question
-- GitHub API links for license information
-- No external dependencies
-- Cross-platform support (Linux, macOS, Windows)
-
-================================================================================
-PROJECT STRUCTURE
-================================================================================
-
-repolicense-cli/
-├── src/
-│ └── main.zig (380 lines) - Main application code
-├── build.zig (25 lines) - Build configuration
-├── build.zig.zon (10 lines) - Package manifest
-├── install.sh (40 lines) - Installation helper
-├── .gitignore (15 lines) - Git ignore rules
-├── README.md (93 lines) - Main documentation
-├── CONTRIBUTING.md (47 lines) - Contribution guide
-├── TEST_PLAN.md (120 lines) - Testing checklist
-├── EXAMPLES.md (160 lines) - Usage examples
-├── COMPARISON.md (130 lines) - Web vs CLI comparison
-├── TREE_STRUCTURE.md (86 lines) - Decision tree visual
-├── CI_CD.md (218 lines) - CI/CD workflows
-└── LICENSE (674 lines) - GNU GPL v3.0
-
-Total: 12 files, ~2,000 lines
-
-================================================================================
-SUPPORTED LICENSES
-================================================================================
-
-PERMISSIVE (6):
-✓ MIT
-✓ BSD-2-Clause
-✓ BSD-3-Clause
-✓ Apache-2.0
-✓ 0BSD
-✓ ISC
-
-STRONG COPYLEFT (3):
-✓ AGPL-3.0
-✓ GPL-3.0
-✓ GPL-2.0
-
-WEAK COPYLEFT (4):
-✓ LGPL-3.0
-✓ MPL-2.0
-✓ EPL-2.0
-✓ EPL-1.0
-
-OTHER (3):
-✓ Unlicense (public domain)
-✓ OFL-1.1 (fonts)
-✓ Advisory results (proprietary/closed-source)
-
-================================================================================
-BUILD & RUN
-================================================================================
-
-Prerequisites:
- - Zig 0.13.0 or later
-
-Build:
- $ zig build
-
-Run:
- $ ./zig-out/bin/repolicense
-
-Or build and run:
- $ zig build run
-
-================================================================================
-DOCUMENTATION HIGHLIGHTS
-================================================================================
-
-✓ Comprehensive README with installation instructions
-✓ Detailed usage examples with screenshots
-✓ Complete test plan for manual and automated testing
-✓ Contribution guidelines for open-source contributors
-✓ CI/CD workflow examples for GitHub Actions
-✓ Decision tree visualization
-✓ Comparison document explaining web vs CLI differences
-
-================================================================================
-CODE QUALITY
-================================================================================
-
-✓ Clean, idiomatic Zig code
-✓ No external dependencies (Zig stdlib only)
-✓ Proper memory management with allocator
-✓ Compile-time guarantees for tree structure
-✓ No unused imports or dead code
-✓ Code review completed and feedback addressed
-
-================================================================================
-TESTING
-================================================================================
-
-Manual testing checklist provided in TEST_PLAN.md covering:
-- All 17 license decision paths
-- Navigation commands
-- Input validation
-- Edge cases
-- Output formatting
-
-Users can verify the tool by following the test plan.
-
-================================================================================
-NEXT STEPS FOR USERS
-================================================================================
-
-1. Install Zig from https://ziglang.org/download/
-2. Clone this repository
-3. Run ./install.sh or manually run: zig build
-4. Execute: ./zig-out/bin/repolicense
-5. Follow the interactive prompts
-6. Choose your license!
-
-================================================================================
-COMPATIBILITY
-================================================================================
-
-✓ Linux (x86_64, ARM64)
-✓ macOS (Intel, Apple Silicon)
-✓ Windows (x86_64)
-✓ Any platform supported by Zig
-
-================================================================================
-LICENSE
-================================================================================
-
-This project is licensed under GNU General Public License v3.0
-Same as the original repolicense web application.
-
-================================================================================
-ACKNOWLEDGMENTS
-================================================================================
-
-Based on the original repolicense web application:
-https://github.com/lucasfth/repolicense
-
-Decision tree logic faithfully ported from JavaScript to Zig.
-
-================================================================================
- END OF SUMMARY
-================================================================================
diff --git a/README.md b/README.md
index de695c3..3c8945f 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,10 @@
# repolicense-cli
+
+
+
+
+
A command-line tool to help you choose the right open-source license for your project, implemented in Zig.
This is a CLI version of [repolicense](https://github.com/lucasfth/repolicense), which guides you through a series of questions to determine the most suitable license for your needs.
@@ -11,10 +16,12 @@ This is a CLI version of [repolicense](https://github.com/lucasfth/repolicense),
- Reset at any time to start over
- Get detailed information about each license
- Links to GitHub API for license details
+- **NEW**: License compatibility checker for forking projects
+ This feature allows you to input a list of existing licenses and find out which licenses are compatible with all of them when combining code from multiple projects.
## Prerequisites
-- [Zig](https://ziglang.org/download/) version 0.13.0 or later
+- [Zig](https://ziglang.org/download/) version 0.15.2 or later
## Building
@@ -36,38 +43,54 @@ Or build and run in one command:
zig build run
```
+### License Compatibility
+
+To check which licenses you can use when forking or combining projects:
+
+```bash
+./zig-out/bin/repolicense --compat
+```
+
+This mode helps you determine compatible licenses when you want to combine code from multiple projects with different licenses.
+
## Usage
+### License Selection Mode (Default)
+
The tool will ask you a series of yes/no questions about your project requirements. Based on your answers, it will recommend an appropriate open-source license.
Available commands:
+
- `yes` or `y` - Answer yes to the current question
- `no` or `n` - Answer no to the current question
- `back` or `b` - Go back to the previous question
- `reset` or `r` - Start over from the beginning
- `quit` or `q` - Exit the program
-## Example
-
-```
-=== Repolicense CLI ===
-Welcome to Repolicense! This tool helps you choose the right open-source license.
-Answer the questions with 'yes', 'no', 'back', 'reset', or 'quit'.
+### License Compatibility Mode
---- Question ---
-Do you want to open-source your project?
+Run with the `--compat` flag (or `-c`) to check license compatibility:
-Elaboration: Do you want to make your source code publicly available and allow others to use, modify, and distribute it.
+```bash
+./zig-out/bin/repolicense --compat
+```
-Your answer (yes/no/back/reset/quit): yes
+This mode allows you to:
---- Question ---
-Do you want a permissive license?
+- Enter a comma-separated list of licenses from projects you want to combine
+- See which licenses are compatible with all of them
+- Understand why certain combinations are compatible or not
-Elaboration: Do you want a more lenient license and allow others to use, modify, and distribute your code with minimal restrictions. These licenses are generally business-friendly and encourage wider use.
+Example:
-Your answer (yes/no/back/reset/quit): yes
-...
+```bash
+Enter licenses: MIT, Apache-2.0
+You can use any of these licenses for your combined work:
+ • MIT
+ • BSD-2-Clause
+ • Apache-2.0
+ • GPL-3.0
+ ...
```
## Supported Licenses
@@ -79,9 +102,25 @@ The tool helps you choose from the following licenses:
- **Public Domain**: Unlicense
- **Specialized**: OFL-1.1 (for fonts)
+> ℹ️ Note: If you wish to add more licenses, please submit a Pull Request with the new license details and update the decision tree accordingly.
+
+## Testing
+
+Run the test suite:
+
+```bash
+zig build test
+```
+
+The test suite includes:
+
+- Decision tree structure validation
+- License compatibility logic tests
+- Navigation and path verification
+
## Contributing
-Contributions are welcome! Please feel free to submit a Pull Request.
+Contributions are welcome! Please feel free to submit a Pull Request and follow the guidelines in [CONTRIBUTING.md](CONTRIBUTING.md).
## Acknowledgments
diff --git a/build.zig b/build.zig
index 4e0bb9c..19de171 100644
--- a/build.zig
+++ b/build.zig
@@ -4,13 +4,17 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
- const exe = b.addExecutable(.{
- .name = "repolicense",
+ const exe_module = b.addModule("repolicense", .{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
+ const exe = b.addExecutable(.{
+ .name = "repolicense",
+ .root_module = exe_module,
+ });
+
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
@@ -22,4 +26,47 @@ pub fn build(b: *std.Build) void {
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
+
+ // Test step
+ const tree_test_module = b.addModule("tree_test", .{
+ .root_source_file = b.path("src/tree_test.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ const tree_tests = b.addTest(.{
+ .name = "tree_test",
+ .root_module = tree_test_module,
+ });
+
+ const compatibility_test_module = b.addModule("compatibility_test", .{
+ .root_source_file = b.path("src/compatibility_test.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ const compatibility_tests = b.addTest(.{
+ .name = "compatibility_test",
+ .root_module = compatibility_test_module,
+ });
+
+ const ui_test_module = b.addModule("ui_test", .{
+ .root_source_file = b.path("src/ui_test.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ const ui_tests = b.addTest(.{
+ .name = "ui_test",
+ .root_module = ui_test_module,
+ });
+
+ const run_tree_tests = b.addRunArtifact(tree_tests);
+ const run_compatibility_tests = b.addRunArtifact(compatibility_tests);
+ const run_ui_tests = b.addRunArtifact(ui_tests);
+
+ const test_step = b.step("test", "Run unit tests");
+ test_step.dependOn(&run_tree_tests.step);
+ test_step.dependOn(&run_compatibility_tests.step);
+ test_step.dependOn(&run_ui_tests.step);
}
diff --git a/build.zig.zon b/build.zig.zon
index e993b3b..a13b558 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,13 +1,13 @@
.{
- .name = "repolicense-cli",
- .version = "1.0.0",
- .minimum_zig_version = "0.13.0",
- .dependencies = .{},
+ .name = .repolicense_cli,
+ .version = "0.0.0",
+ .fingerprint = 0x38a819a8db54f0a5, // Changing this has security and trust implications.
+ .minimum_zig_version = "0.15.2",
+ .dependencies = .{
+ },
.paths = .{
"build.zig",
"build.zig.zon",
"src",
- "README.md",
- "LICENSE",
},
}
diff --git a/src/compatibility.zig b/src/compatibility.zig
new file mode 100644
index 0000000..a575184
--- /dev/null
+++ b/src/compatibility.zig
@@ -0,0 +1,244 @@
+const std = @import("std");
+
+/// License compatibility checker for determining which licenses can be used
+/// when forking/combining projects with existing licenses.
+///
+/// Compatibility rules are based on common open-source license compatibility:
+/// - Permissive licenses (MIT, BSD, Apache) are generally compatible with everything
+/// - Copyleft licenses (GPL, AGPL) require derivatives to use the same or compatible license
+/// - Some licenses (Apache-2.0 + GPL-2.0) have known incompatibilities
+pub const License = enum {
+ MIT,
+ BSD_2_Clause,
+ BSD_3_Clause,
+ Apache_2_0,
+ @"0BSD",
+ ISC,
+ AGPL_3_0,
+ GPL_3_0,
+ GPL_2_0,
+ LGPL_3_0,
+ MPL_2_0,
+ EPL_2_0,
+ EPL_1_0,
+ Unlicense,
+ OFL_1_1,
+
+ pub fn fromString(s: []const u8) ?License {
+ // Convert to uppercase for case-insensitive matching
+ var buf: [32]u8 = undefined;
+ if (s.len > buf.len) return null;
+
+ const upper = std.ascii.upperString(&buf, s);
+
+ const table = [_]struct { name: []const u8, value: License }{
+ .{ .name = "MIT", .value = .MIT },
+ .{ .name = "BSD-2-CLAUSE", .value = .BSD_2_Clause },
+ .{ .name = "BSD-3-CLAUSE", .value = .BSD_3_Clause },
+ .{ .name = "APACHE-2.0", .value = .Apache_2_0 },
+ .{ .name = "0BSD", .value = .@"0BSD" },
+ .{ .name = "ISC", .value = .ISC },
+ .{ .name = "AGPL-3.0", .value = .AGPL_3_0 },
+ .{ .name = "GPL-3.0", .value = .GPL_3_0 },
+ .{ .name = "GPL-2.0", .value = .GPL_2_0 },
+ .{ .name = "LGPL-3.0", .value = .LGPL_3_0 },
+ .{ .name = "MPL-2.0", .value = .MPL_2_0 },
+ .{ .name = "EPL-2.0", .value = .EPL_2_0 },
+ .{ .name = "EPL-1.0", .value = .EPL_1_0 },
+ .{ .name = "UNLICENSE", .value = .Unlicense },
+ .{ .name = "OFL-1.1", .value = .OFL_1_1 },
+ };
+
+ for (table) |entry| {
+ if (std.mem.eql(u8, upper, entry.name)) return entry.value;
+ }
+
+ return null;
+ }
+
+ pub fn toString(self: License) []const u8 {
+ return switch (self) {
+ .MIT => "MIT",
+ .BSD_2_Clause => "BSD-2-Clause",
+ .BSD_3_Clause => "BSD-3-Clause",
+ .Apache_2_0 => "Apache-2.0",
+ .@"0BSD" => "0BSD",
+ .ISC => "ISC",
+ .AGPL_3_0 => "AGPL-3.0",
+ .GPL_3_0 => "GPL-3.0",
+ .GPL_2_0 => "GPL-2.0",
+ .LGPL_3_0 => "LGPL-3.0",
+ .MPL_2_0 => "MPL-2.0",
+ .EPL_2_0 => "EPL-2.0",
+ .EPL_1_0 => "EPL-1.0",
+ .Unlicense => "Unlicense",
+ .OFL_1_1 => "OFL-1.1",
+ };
+ }
+
+ pub fn getCategory(self: License) LicenseCategory {
+ return switch (self) {
+ .MIT, .BSD_2_Clause, .BSD_3_Clause, .Apache_2_0, .@"0BSD", .ISC => .Permissive,
+ .AGPL_3_0, .GPL_3_0, .GPL_2_0 => .StrongCopyleft,
+ .LGPL_3_0, .MPL_2_0, .EPL_2_0, .EPL_1_0 => .WeakCopyleft,
+ .Unlicense => .PublicDomain,
+ .OFL_1_1 => .Specialized,
+ };
+ }
+};
+
+pub const LicenseCategory = enum {
+ Permissive,
+ StrongCopyleft,
+ WeakCopyleft,
+ PublicDomain,
+ Specialized,
+};
+
+/// Checks if a new license can be used when combining with existing licenses
+/// Returns true if the combination is compatible
+pub fn isCompatible(existing: License, new_license: License) bool {
+ // Public domain and permissive licenses can be combined with anything
+ if (existing.getCategory() == .PublicDomain or existing == .Unlicense) {
+ return true;
+ }
+
+ // Same license is always compatible
+ if (existing == new_license) {
+ return true;
+ }
+
+ // Check compatibility based on existing license
+ return switch (existing) {
+ // Permissive licenses accept most things, but derivatives must respect their terms
+ .MIT, .BSD_2_Clause, .BSD_3_Clause, .ISC, .@"0BSD" => switch (new_license.getCategory()) {
+ .Permissive, .PublicDomain, .WeakCopyleft, .StrongCopyleft => true,
+ .Specialized => false, // Font licenses are specialized
+ },
+
+ // Apache-2.0 has patent grant issues with GPL-2.0
+ .Apache_2_0 => switch (new_license) {
+ .GPL_2_0 => false, // Known incompatibility
+ else => switch (new_license.getCategory()) {
+ .Permissive, .PublicDomain => true,
+ .WeakCopyleft => true,
+ .StrongCopyleft => new_license == .GPL_3_0 or new_license == .AGPL_3_0,
+ .Specialized => false,
+ },
+ },
+
+ // Strong copyleft licenses require derivative works to be under compatible copyleft
+ .GPL_3_0 => switch (new_license) {
+ .GPL_3_0, .AGPL_3_0 => true,
+ else => false,
+ },
+
+ .GPL_2_0 => switch (new_license) {
+ .GPL_2_0, .GPL_3_0 => true,
+ else => false,
+ },
+
+ .AGPL_3_0 => switch (new_license) {
+ .AGPL_3_0 => true,
+ else => false,
+ },
+
+ // LGPL allows linking with other software
+ .LGPL_3_0 => switch (new_license.getCategory()) {
+ .Permissive, .PublicDomain, .WeakCopyleft => true,
+ .StrongCopyleft => new_license == .GPL_3_0 or new_license == .AGPL_3_0,
+ .Specialized => false,
+ },
+
+ // Weak copyleft licenses are file-level copyleft
+ .MPL_2_0 => switch (new_license.getCategory()) {
+ .Permissive, .PublicDomain, .WeakCopyleft => true,
+ .StrongCopyleft => new_license == .GPL_3_0 or new_license == .AGPL_3_0,
+ .Specialized => false,
+ },
+
+ .EPL_2_0, .EPL_1_0 => switch (new_license.getCategory()) {
+ .Permissive, .PublicDomain, .WeakCopyleft => true,
+ .StrongCopyleft => false,
+ .Specialized => false,
+ },
+
+ .Unlicense => true, // Public domain
+
+ .OFL_1_1 => switch (new_license) {
+ .OFL_1_1 => true,
+ else => false,
+ },
+ };
+}
+
+/// Find all compatible licenses given a list of existing licenses
+/// Allocates and returns a list of compatible licenses
+pub fn findCompatibleLicenses(allocator: std.mem.Allocator, existing_licenses: []const License) ![]const License {
+ var compatible = try std.ArrayList(License).initCapacity(allocator, 0);
+ defer compatible.deinit(allocator);
+
+ // All possible licenses to check
+ const all_licenses = [_]License{
+ .MIT,
+ .BSD_2_Clause,
+ .BSD_3_Clause,
+ .Apache_2_0,
+ .@"0BSD",
+ .ISC,
+ .AGPL_3_0,
+ .GPL_3_0,
+ .GPL_2_0,
+ .LGPL_3_0,
+ .MPL_2_0,
+ .EPL_2_0,
+ .EPL_1_0,
+ .Unlicense,
+ .OFL_1_1,
+ };
+
+ // Check each possible license against all existing licenses
+ for (all_licenses) |candidate| {
+ var is_compatible_with_all = true;
+ for (existing_licenses) |existing| {
+ if (!isCompatible(existing, candidate)) {
+ is_compatible_with_all = false;
+ break;
+ }
+ }
+
+ if (is_compatible_with_all) {
+ try compatible.append(allocator, candidate);
+ }
+ }
+
+ return compatible.toOwnedSlice(allocator);
+}
+
+/// Get a brief explanation of why licenses are compatible or not
+pub fn getCompatibilityReason(existing: License, new_license: License) []const u8 {
+ if (existing == new_license) {
+ return "Same license - fully compatible";
+ }
+
+ const compatible = isCompatible(existing, new_license);
+
+ if (compatible) {
+ return switch (existing.getCategory()) {
+ .PublicDomain => "Public domain allows any license",
+ .Permissive => "Permissive licenses are broadly compatible",
+ .WeakCopyleft => "Weak copyleft allows compatible combinations",
+ .StrongCopyleft => "Strong copyleft requires compatible copyleft license",
+ .Specialized => "Specialized licenses have specific rules",
+ };
+ } else {
+ if (existing == .Apache_2_0 and new_license == .GPL_2_0) {
+ return "Apache-2.0 and GPL-2.0 have patent grant incompatibility";
+ }
+ return switch (existing.getCategory()) {
+ .StrongCopyleft => "Strong copyleft requires derivative under same/compatible license",
+ .Specialized => "Specialized licenses restrict combination with other types",
+ else => "License terms are incompatible",
+ };
+ }
+}
diff --git a/src/compatibility_test.zig b/src/compatibility_test.zig
new file mode 100644
index 0000000..dded970
--- /dev/null
+++ b/src/compatibility_test.zig
@@ -0,0 +1,187 @@
+const std = @import("std");
+const testing = std.testing;
+const compatibility = @import("compatibility.zig");
+
+test "License fromString and toString roundtrip" {
+ const licenses = [_][]const u8{
+ "MIT",
+ "BSD-2-Clause",
+ "Apache-2.0",
+ "GPL-3.0",
+ "LGPL-3.0",
+ };
+
+ for (licenses) |license_str| {
+ const lic = compatibility.License.fromString(license_str);
+ try testing.expect(lic != null);
+ try testing.expectEqualStrings(license_str, lic.?.toString());
+ }
+}
+
+test "License fromString returns null for invalid input" {
+ try testing.expect(compatibility.License.fromString("Invalid") == null);
+ try testing.expect(compatibility.License.fromString("") == null);
+}
+
+test "License fromString is case-insensitive" {
+ try testing.expect(compatibility.License.fromString("mit") != null);
+ try testing.expect(compatibility.License.fromString("MIT") != null);
+ try testing.expect(compatibility.License.fromString("Mit") != null);
+ try testing.expectEqual(compatibility.License.MIT, compatibility.License.fromString("mit").?);
+ try testing.expectEqual(compatibility.License.Apache_2_0, compatibility.License.fromString("apache-2.0").?);
+}
+
+test "getCategory returns correct categories" {
+ try testing.expect(compatibility.License.MIT.getCategory() == .Permissive);
+ try testing.expect(compatibility.License.Apache_2_0.getCategory() == .Permissive);
+ try testing.expect(compatibility.License.GPL_3_0.getCategory() == .StrongCopyleft);
+ try testing.expect(compatibility.License.AGPL_3_0.getCategory() == .StrongCopyleft);
+ try testing.expect(compatibility.License.LGPL_3_0.getCategory() == .WeakCopyleft);
+ try testing.expect(compatibility.License.MPL_2_0.getCategory() == .WeakCopyleft);
+ try testing.expect(compatibility.License.Unlicense.getCategory() == .PublicDomain);
+ try testing.expect(compatibility.License.OFL_1_1.getCategory() == .Specialized);
+}
+
+test "same license is always compatible" {
+ try testing.expect(compatibility.isCompatible(.MIT, .MIT));
+ try testing.expect(compatibility.isCompatible(.GPL_3_0, .GPL_3_0));
+ try testing.expect(compatibility.isCompatible(.Apache_2_0, .Apache_2_0));
+}
+
+test "Unlicense is compatible with everything" {
+ try testing.expect(compatibility.isCompatible(.Unlicense, .MIT));
+ try testing.expect(compatibility.isCompatible(.Unlicense, .GPL_3_0));
+ try testing.expect(compatibility.isCompatible(.Unlicense, .Apache_2_0));
+ try testing.expect(compatibility.isCompatible(.Unlicense, .LGPL_3_0));
+}
+
+test "MIT is broadly compatible" {
+ try testing.expect(compatibility.isCompatible(.MIT, .MIT));
+ try testing.expect(compatibility.isCompatible(.MIT, .Apache_2_0));
+ try testing.expect(compatibility.isCompatible(.MIT, .GPL_3_0));
+ try testing.expect(compatibility.isCompatible(.MIT, .BSD_3_Clause));
+ try testing.expect(!compatibility.isCompatible(.MIT, .OFL_1_1)); // Font license is specialized
+}
+
+test "Apache-2.0 and GPL-2.0 are incompatible" {
+ try testing.expect(!compatibility.isCompatible(.Apache_2_0, .GPL_2_0));
+}
+
+test "Apache-2.0 is compatible with GPL-3.0" {
+ try testing.expect(compatibility.isCompatible(.Apache_2_0, .GPL_3_0));
+}
+
+test "GPL-3.0 is restrictive in compatibility" {
+ try testing.expect(compatibility.isCompatible(.GPL_3_0, .GPL_3_0));
+ try testing.expect(compatibility.isCompatible(.GPL_3_0, .AGPL_3_0));
+ try testing.expect(!compatibility.isCompatible(.GPL_3_0, .MIT));
+ try testing.expect(!compatibility.isCompatible(.GPL_3_0, .Apache_2_0));
+ try testing.expect(!compatibility.isCompatible(.GPL_3_0, .LGPL_3_0));
+}
+
+test "AGPL-3.0 is most restrictive" {
+ try testing.expect(compatibility.isCompatible(.AGPL_3_0, .AGPL_3_0));
+ try testing.expect(!compatibility.isCompatible(.AGPL_3_0, .GPL_3_0));
+ try testing.expect(!compatibility.isCompatible(.AGPL_3_0, .MIT));
+}
+
+test "LGPL-3.0 allows more combinations" {
+ try testing.expect(compatibility.isCompatible(.LGPL_3_0, .MIT));
+ try testing.expect(compatibility.isCompatible(.LGPL_3_0, .Apache_2_0));
+ try testing.expect(compatibility.isCompatible(.LGPL_3_0, .GPL_3_0));
+ try testing.expect(compatibility.isCompatible(.LGPL_3_0, .MPL_2_0));
+}
+
+test "MPL-2.0 weak copyleft compatibility" {
+ try testing.expect(compatibility.isCompatible(.MPL_2_0, .MIT));
+ try testing.expect(compatibility.isCompatible(.MPL_2_0, .Apache_2_0));
+ try testing.expect(compatibility.isCompatible(.MPL_2_0, .GPL_3_0));
+ try testing.expect(!compatibility.isCompatible(.MPL_2_0, .OFL_1_1));
+}
+
+test "OFL-1.1 is specialized and restrictive" {
+ try testing.expect(compatibility.isCompatible(.OFL_1_1, .OFL_1_1));
+ try testing.expect(!compatibility.isCompatible(.OFL_1_1, .MIT));
+ try testing.expect(!compatibility.isCompatible(.OFL_1_1, .GPL_3_0));
+}
+
+test "findCompatibleLicenses with single MIT license" {
+ const allocator = testing.allocator;
+ const existing = [_]compatibility.License{.MIT};
+
+ const compatible = try compatibility.findCompatibleLicenses(allocator, &existing);
+ defer allocator.free(compatible);
+
+ // MIT should allow many licenses
+ try testing.expect(compatible.len > 0);
+
+ // Should include common permissive licenses
+ var found_mit = false;
+ var found_apache = false;
+ var found_gpl3 = false;
+
+ for (compatible) |lic| {
+ if (lic == .MIT) found_mit = true;
+ if (lic == .Apache_2_0) found_apache = true;
+ if (lic == .GPL_3_0) found_gpl3 = true;
+ }
+
+ try testing.expect(found_mit);
+ try testing.expect(found_apache);
+ try testing.expect(found_gpl3);
+}
+
+test "findCompatibleLicenses with GPL-3.0" {
+ const allocator = testing.allocator;
+ const existing = [_]compatibility.License{.GPL_3_0};
+
+ const compatible = try compatibility.findCompatibleLicenses(allocator, &existing);
+ defer allocator.free(compatible);
+
+ // GPL-3.0 is restrictive, should only allow GPL-3.0 and AGPL-3.0
+ try testing.expect(compatible.len == 2);
+
+ var found_gpl3 = false;
+ var found_agpl3 = false;
+
+ for (compatible) |lic| {
+ if (lic == .GPL_3_0) found_gpl3 = true;
+ if (lic == .AGPL_3_0) found_agpl3 = true;
+ }
+
+ try testing.expect(found_gpl3);
+ try testing.expect(found_agpl3);
+}
+
+test "findCompatibleLicenses with MIT and Apache-2.0" {
+ const allocator = testing.allocator;
+ const existing = [_]compatibility.License{ .MIT, .Apache_2_0 };
+
+ const compatible = try compatibility.findCompatibleLicenses(allocator, &existing);
+ defer allocator.free(compatible);
+
+ // Both are permissive, should allow many licenses
+ try testing.expect(compatible.len > 0);
+}
+
+test "findCompatibleLicenses with conflicting licenses" {
+ const allocator = testing.allocator;
+ const existing = [_]compatibility.License{ .GPL_3_0, .OFL_1_1 };
+
+ const compatible = try compatibility.findCompatibleLicenses(allocator, &existing);
+ defer allocator.free(compatible);
+
+ // These are incompatible, should find no compatible licenses
+ try testing.expect(compatible.len == 0);
+}
+
+test "getCompatibilityReason returns non-empty string" {
+ const reason1 = compatibility.getCompatibilityReason(.MIT, .MIT);
+ try testing.expect(reason1.len > 0);
+
+ const reason2 = compatibility.getCompatibilityReason(.MIT, .GPL_3_0);
+ try testing.expect(reason2.len > 0);
+
+ const reason3 = compatibility.getCompatibilityReason(.Apache_2_0, .GPL_2_0);
+ try testing.expect(reason3.len > 0);
+}
diff --git a/src/main.zig b/src/main.zig
index 1f34e7f..0943e0e 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,341 +1,206 @@
const std = @import("std");
-const io = std.io;
const mem = std.mem;
+const tree = @import("tree.zig");
+const compatibility = @import("compatibility.zig");
-// Node types in the decision tree
-const NodeType = enum {
- Question,
- Answer,
-};
-
-const Node = struct {
- node_type: NodeType,
- content: []const u8,
- elaboration: []const u8,
- yes: ?*const Node,
- no: ?*const Node,
-};
-
-// Decision tree structure matching the original JavaScript logic
-const decision_tree = buildDecisionTree();
-
-fn buildDecisionTree() Node {
- // Leaf nodes (answers)
- const mit_node = Node{
- .node_type = .Answer,
- .content = "MIT",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const bsd_2_clause = Node{
- .node_type = .Answer,
- .content = "BSD-2-Clause",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const apache_2_0 = Node{
- .node_type = .Answer,
- .content = "Apache-2.0",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const bsd_3_clause = Node{
- .node_type = .Answer,
- .content = "BSD-3-Clause",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const zero_bsd = Node{
- .node_type = .Answer,
- .content = "0BSD",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const isc = Node{
- .node_type = .Answer,
- .content = "ISC",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const agpl_3_0 = Node{
- .node_type = .Answer,
- .content = "AGPL-3.0",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const gpl_3_0 = Node{
- .node_type = .Answer,
- .content = "GPL-3.0",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const gpl_2_0 = Node{
- .node_type = .Answer,
- .content = "GPL-2.0",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const lgpl_3_0 = Node{
- .node_type = .Answer,
- .content = "LGPL-3.0",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const mpl_2_0 = Node{
- .node_type = .Answer,
- .content = "MPL-2.0",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const epl_2_0 = Node{
- .node_type = .Answer,
- .content = "EPL-2.0",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const epl_1_0 = Node{
- .node_type = .Answer,
- .content = "EPL-1.0",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const unlicense = Node{
- .node_type = .Answer,
- .content = "Unlicense",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const ofl_1_1 = Node{
- .node_type = .Answer,
- .content = "OFL-1.1",
- .elaboration = "",
- .yes = null,
- .no = null,
- };
-
- const proprietary = Node{
- .node_type = .Answer,
- .content = "Consider using a proprietary license or another specialized license.",
- .elaboration = "You should consider using a proprietary license or another specialized license by consulting with a legal expert.",
- .yes = null,
- .no = null,
- };
-
- const closed_source = Node{
- .node_type = .Answer,
- .content = "You should consider keeping your project closed-source.",
- .elaboration = "You should consider keeping your project closed-source to protect your intellectual property by not sharing the source code.",
- .yes = null,
- .no = null,
- };
-
- // Build tree from bottom up
- const simplest_permissive = Node{
- .node_type = .Question,
- .content = "Do you want the simplest and most permissive license possible?",
- .elaboration = "Do you want a license that is very permissive and has minimal requirements, making it as simple as possible for others to use your code.",
- .yes = &mit_node,
- .no = &bsd_2_clause,
- };
-
- const simplest_no_conditions = Node{
- .node_type = .Question,
- .content = "Do you want the simplest permissive license with no conditions?",
- .elaboration = "Do you want the simplest permissive license with no conditions, providing complete freedom to use the code without any restrictions.",
- .yes = &zero_bsd,
- .no = &isc,
- };
-
- const permissive_with_conditions = Node{
- .node_type = .Question,
- .content = "Do you want a permissive license with some conditions?",
- .elaboration = "Do you want a permissive license that includes some conditions such as providing attribution and not using the name of the project or its contributors for promotion without permission.",
- .yes = &bsd_3_clause,
- .no = &simplest_no_conditions,
- };
-
- const explicit_patent_grants = Node{
- .node_type = .Question,
- .content = "Do you want explicit patent grants?",
- .elaboration = "Do you want to include explicit grants of patent rights, which can protect users from patent litigation. This is an important consideration for projects that may involve patented technology.",
- .yes = &apache_2_0,
- .no = &permissive_with_conditions,
- };
-
- const minimal_conditions = Node{
- .node_type = .Question,
- .content = "Do you require minimal conditions?",
- .elaboration = "Do you want minimal conditions meaning that there are very few requirements placed on the use of your code. This typically includes providing attribution to the original authors.",
- .yes = &simplest_permissive,
- .no = &explicit_patent_grants,
- };
-
- const latest_gpl = Node{
- .node_type = .Question,
- .content = "Do you want to use the latest version of the GPL license?",
- .elaboration = "Do you prefer using the latest version of the GPL license, which includes additional protections and clarifications compared to older versions.",
- .yes = &gpl_3_0,
- .no = &gpl_2_0,
- };
-
- const network_server_protection = Node{
- .node_type = .Question,
- .content = "Do you want network server protection?",
- .elaboration = "Do you want to extend copyleft requirements to software provided over a network. This means that users who interact with the software over a network (e.g., web applications) must also have access to the source code.",
- .yes = &agpl_3_0,
- .no = &latest_gpl,
- };
-
- const business_friendly_copyleft = Node{
- .node_type = .Question,
- .content = "Do you prefer a copyleft license with a focus on business-friendly terms?",
- .elaboration = "Do you prefer a copyleft license that has a focus on business-friendly terms, making it easier for companies to adopt.",
- .yes = &epl_2_0,
- .no = &epl_1_0,
- };
-
- const weaker_copyleft = Node{
- .node_type = .Question,
- .content = "Do you want a copyleft license with weaker requirements?",
- .elaboration = "Do you want a copyleft license that has weaker requirements compared to the GPL, such as allowing proprietary modules in your project.",
- .yes = &mpl_2_0,
- .no = &business_friendly_copyleft,
- };
-
- const allow_linking = Node{
- .node_type = .Question,
- .content = "Do you want to allow linking with non-(L)GPL software?",
- .elaboration = "Do you want to allow linking with non-(L)GPL software, making it easier to use your code as a library in proprietary software while keeping modifications to the library itself open source.",
- .yes = &lgpl_3_0,
- .no = &weaker_copyleft,
- };
-
- const strong_copyleft = Node{
- .node_type = .Question,
- .content = "Do you want strong copyleft?",
- .elaboration = "Do you want to require that any distributed modifications (or in some cases, software that interacts with the copylefted code) also be open-sourced. This ensures that improvements to the code are shared with the community.",
- .yes = &network_server_protection,
- .no = &allow_linking,
- };
-
- const font_license = Node{
- .node_type = .Question,
- .content = "Do you want a license for fonts?",
- .elaboration = "Do you want a license specifically designed for fonts, allowing embedding, modifying, and redistributing the font.",
- .yes = &ofl_1_1,
- .no = &proprietary,
- };
-
- const public_domain = Node{
- .node_type = .Question,
- .content = "Do you want to dedicate your work to the public domain?",
- .elaboration = "Do you want to dedicate your work to the public domain, allowing anyone to use, modify, and distribute your work without any restrictions.",
- .yes = &unlicense,
- .no = &font_license,
- };
-
- const copyleft_license = Node{
- .node_type = .Question,
- .content = "Do you want a copyleft license?",
- .elaboration = "Do you want to require that any modified versions of your code be distributed under the same license, ensuring that the code (and its derivatives) remain open source.",
- .yes = &strong_copyleft,
- .no = &public_domain,
- };
-
- const permissive_license = Node{
- .node_type = .Question,
- .content = "Do you want a permissive license?",
- .elaboration = "Do you want a more lenient license and allow others to use, modify, and distribute your code with minimal restrictions. These licenses are generally business-friendly and encourage wider use.",
- .yes = &minimal_conditions,
- .no = ©left_license,
- };
-
- const root = Node{
- .node_type = .Question,
- .content = "Do you want to open-source your project?",
- .elaboration = "Do you want to make your source code publicly available and allow others to use, modify, and distribute it.",
- .yes = &permissive_license,
- .no = &closed_source,
- };
-
- return root;
-}
+const Node = tree.Node;
+const NodeType = tree.NodeType;
+const decision_tree = tree.decision_tree;
const History = std.ArrayList(*const Node);
+const BOLD = "\x1b[1m";
+const ITALIC = "\x1b[3m";
+const DIM = "\x1b[2m";
+const UNDERLINE = "\x1b[4m";
+const RESET = "\x1b[0m";
+
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
- const stdout = io.getStdOut().writer();
- const stdin = io.getStdIn().reader();
+ const stdout = std.fs.File.stdout().deprecatedWriter();
+
+ // Check for command-line arguments
+ const args = try std.process.argsAlloc(allocator);
+ defer std.process.argsFree(allocator, args);
+
+ // Check if compatibility mode is requested via flag (`--compat` or `-c`)
+ var is_compat: bool = false;
+ for (args[1..]) |a| {
+ if (mem.eql(u8, a, "--compat") or mem.eql(u8, a, "-c")) {
+ is_compat = true;
+ break;
+ }
+ }
+
+ if (is_compat) {
+ try runCompatibilityMode(allocator, stdout);
+ return;
+ }
+
+ // Run normal decision tree mode
+ try runDecisionTree(allocator, stdout);
+}
+
+fn runCompatibilityMode(allocator: std.mem.Allocator, stdout: anytype) !void {
+ const stdin = std.fs.File.stdin().deprecatedReader();
+
+ try stdout.print("\n=== {s}License Compatibility Checker{s} ===\n", .{ BOLD, RESET });
+ try stdout.print("This tool helps you find compatible licenses when forking or combining projects.\n\n", .{});
+ try stdout.print("Enter the licenses of the projects you want to combine (comma-separated).\n", .{});
+ try stdout.print("Example: MIT, Apache-2.0, BSD-3-Clause\n\n", .{});
+ try stdout.print("Supported licenses:\n", .{});
+ try stdout.print(" Permissive: MIT, BSD-2-Clause, BSD-3-Clause, Apache-2.0, 0BSD, ISC\n", .{});
+ try stdout.print(" Copyleft: GPL-2.0, GPL-3.0, AGPL-3.0, LGPL-3.0\n", .{});
+ try stdout.print(" Weak Copyleft: MPL-2.0, EPL-2.0, EPL-1.0\n", .{});
+ try stdout.print(" Other: Unlicense, OFL-1.1\n\n", .{});
+ try stdout.print("Enter licenses (or 'quit' to exit): ", .{});
+
+ var buf: [1024]u8 = undefined;
+
+ while (true) {
+ const line = (try stdin.readUntilDelimiterOrEof(&buf, '\n')) orelse break;
+ const trimmed = mem.trim(u8, line, &std.ascii.whitespace);
+
+ if (trimmed.len == 0) {
+ try stdout.print("\nEnter licenses (or 'quit' to exit): ", .{});
+ continue;
+ }
+
+ if (mem.eql(u8, trimmed, "quit") or mem.eql(u8, trimmed, "q")) {
+ try stdout.print("\nThank you for using the compatibility checker of Repolicense\n", .{});
+ break;
+ }
+
+ // Parse comma-separated licenses
+ var licenses = try std.ArrayList(compatibility.License).initCapacity(allocator, 0);
+ defer licenses.deinit(allocator);
+
+ var iter = std.mem.tokenizeAny(u8, trimmed, ",");
+ var has_error = false;
+
+ while (iter.next()) |license_str| {
+ const clean = mem.trim(u8, license_str, &std.ascii.whitespace);
+ if (clean.len == 0) continue;
+
+ if (compatibility.License.fromString(clean)) |lic| {
+ try licenses.append(allocator, lic);
+ } else {
+ try stdout.print("\nError: Unknown license '{s}'\n", .{clean});
+ has_error = true;
+ break;
+ }
+ }
+
+ if (has_error) {
+ try stdout.print("\nEnter licenses (or 'quit' to exit): ", .{});
+ continue;
+ }
+
+ if (licenses.items.len == 0) {
+ try stdout.print("\nError: No valid licenses entered\n", .{});
+ try stdout.print("\nEnter licenses (or 'quit' to exit): ", .{});
+ continue;
+ }
+
+ // Find compatible licenses
+ const compatible_licenses = try compatibility.findCompatibleLicenses(allocator, licenses.items);
+ defer allocator.free(compatible_licenses);
+
+ try stdout.print("\n--- {s}Results{s} ---\n", .{ BOLD, RESET });
+ try stdout.print("Given licenses: ", .{});
+ for (licenses.items, 0..) |lic, i| {
+ if (i > 0) try stdout.print(", ", .{});
+ try stdout.print("{s}", .{lic.toString()});
+ }
+ try stdout.print("\n\n", .{});
+
+ if (compatible_licenses.len == 0) {
+ try stdout.print("No compatible licenses found!\n", .{});
+ try stdout.print("The licenses you selected have conflicting requirements.\n\n", .{});
+ } else {
+ try stdout.print("You can use any of these licenses for your combined work:\n\n", .{});
+ for (compatible_licenses) |lic| {
+ try stdout.print(" • {s}\n", .{lic.toString()});
+ }
+ try stdout.print("\n", .{});
+ }
+
+ // Show pairwise compatibility details for better understanding
+ if (licenses.items.len > 1) {
+ try stdout.print("Compatibility details:\n", .{});
+ var i: usize = 0;
+ while (i < licenses.items.len - 1) : (i += 1) {
+ var j: usize = i + 1;
+ while (j < licenses.items.len) : (j += 1) {
+ const compat = compatibility.isCompatible(licenses.items[i], licenses.items[j]);
+ const reason = compatibility.getCompatibilityReason(licenses.items[i], licenses.items[j]);
+ const symbol = if (compat) "✓" else "✗";
+ try stdout.print(" {s} {s} + {s}: {s}\n", .{
+ symbol,
+ licenses.items[i].toString(),
+ licenses.items[j].toString(),
+ reason,
+ });
+ }
+ }
+ try stdout.print("\n", .{});
+ }
+
+ try stdout.print("Enter licenses (or 'quit' to exit): ", .{});
+ }
+}
+
+fn runDecisionTree(allocator: std.mem.Allocator, stdout: anytype) !void {
+ const stdin = std.fs.File.stdin().deprecatedReader();
+ const ui = @import("ui.zig");
- var history = History.init(allocator);
- defer history.deinit();
+ var screen = ui.Screen.init();
+
+ var history = try History.initCapacity(allocator, 0);
+ defer history.deinit(allocator);
var current_node = &decision_tree;
- try history.append(current_node);
+ try history.append(allocator, current_node);
- // Print welcome message
- try stdout.print("\n=== Repolicense CLI ===\n", .{});
- try stdout.print("Welcome to Repolicense! This tool helps you choose the right open-source license.\n", .{});
- try stdout.print("Answer the questions with 'yes', 'no', 'back', 'reset', or 'quit'.\n\n", .{});
+ // Print welcome message (kept as regular prints so it doesn't get cleared)
+ try stdout.print("\n=== {s}Repolicense CLI{s} ===\n", .{ BOLD, RESET });
+ try stdout.print("Answer the questions with 'yes', 'no', 'back', 'reset', or 'quit'\n to find the best license for your project.\n", .{});
+ try stdout.print("\nRun with {s}'--compat'{s} flag (or {s}'-c'{s}) to check license compatibility for forking projects.\n\n", .{ UNDERLINE, RESET, UNDERLINE, RESET });
var buf: [256]u8 = undefined;
while (true) {
if (current_node.node_type == .Question) {
- try stdout.print("\n--- Question ---\n", .{});
- try stdout.print("{s}\n", .{current_node.content});
- if (current_node.elaboration.len > 0) {
- try stdout.print("\nElaboration: {s}\n", .{current_node.elaboration});
- }
- try stdout.print("\nYour answer (yes/no/back/reset/quit): ", .{});
+ const content = if (current_node.elaboration.len > 0)
+ try std.fmt.allocPrint(allocator, "\n--- {s}Question{s} ---\n{s}\n\n{s}Elaboration: {s}\n{s}\nYour answer (yes/no/back/reset/quit): ", .{ ITALIC, RESET, current_node.content, DIM, current_node.elaboration, RESET })
+ else
+ try std.fmt.allocPrint(allocator, "\n--- {s}Question{s} ---\n{s}\nYour answer (yes/no/back/reset/quit): ", .{ ITALIC, RESET, current_node.content });
+
+ try screen.render(stdout, content);
+ allocator.free(content);
} else {
- try stdout.print("\n=== RESULT ===\n", .{});
- try stdout.print("License: {s}\n", .{current_node.content});
- if (current_node.elaboration.len > 0) {
- try stdout.print("\n{s}\n", .{current_node.elaboration});
- }
-
- // For official licenses, provide GitHub API link
- if (!mem.startsWith(u8, current_node.content, "Consider") and
- !mem.startsWith(u8, current_node.content, "You should")) {
- try stdout.print("\nFor more information, visit:\n", .{});
- try stdout.print("https://api.github.com/licenses/{s}\n", .{current_node.content});
- try stdout.print("https://docs.github.com/en/rest/licenses/licenses\n", .{});
+ var content = if (current_node.elaboration.len > 0)
+ try std.fmt.allocPrint(allocator, "\n--- {s}RESULT{s} ---\nLicense: {s}{s}{s}\n\n{s}{s}{s}\n", .{ BOLD, RESET, BOLD, current_node.content, RESET, DIM, current_node.elaboration, RESET })
+ else
+ try std.fmt.allocPrint(allocator, "\n--- {s}RESULT{s} ---\nLicense: {s}{s}{s}\n", .{ BOLD, RESET, BOLD, current_node.content, RESET });
+
+ if (!mem.startsWith(u8, current_node.content, "Consider") and !mem.startsWith(u8, current_node.content, "You should")) {
+ const links = try std.fmt.allocPrint(allocator, "\nFor more information, visit:\nhttps://api.github.com/licenses/{s}\nhttps://docs.github.com/en/rest/licenses/licenses\n", .{current_node.content});
+ const combined = try std.fmt.allocPrint(allocator, "{s}{s}", .{ content, links });
+ allocator.free(content);
+ allocator.free(links);
+ content = combined;
}
-
- try stdout.print("\nOptions: back/reset/quit: ", .{});
+
+ const content_with_options = try std.fmt.allocPrint(allocator, "{s}\nOptions: back/reset/quit: ", .{content});
+ allocator.free(content);
+ content = content_with_options;
+
+ try screen.render(stdout, content);
+ allocator.free(content);
}
const line = (try stdin.readUntilDelimiterOrEof(&buf, '\n')) orelse break;
@@ -343,38 +208,44 @@ pub fn main() !void {
const lower = try std.ascii.allocLowerString(allocator, trimmed);
defer allocator.free(lower);
+ // Clear previous rendered block and the input line the user just entered
+ try screen.clearIncludingInput(stdout);
+
if (mem.eql(u8, lower, "quit") or mem.eql(u8, lower, "q") or mem.eql(u8, lower, "exit")) {
- try stdout.print("\nThank you for using Repolicense!\n", .{});
+ // Clear last rendered block before exiting
+ try screen.clear(stdout);
+ try stdout.print("\nThank you for using Repolicense\n", .{});
break;
} else if (mem.eql(u8, lower, "back") or mem.eql(u8, lower, "b")) {
if (history.items.len > 1) {
_ = history.pop();
current_node = history.items[history.items.len - 1];
- try stdout.print("\n[Moved back]\n", .{});
+ // Show a short status message inside the same screen block so it will be overwritten next render
+ try screen.render(stdout, "[Moved back]\n");
} else {
- try stdout.print("\n[Already at the beginning]\n", .{});
+ try screen.render(stdout, "[Already at the beginning]\n");
}
} else if (mem.eql(u8, lower, "reset") or mem.eql(u8, lower, "r")) {
history.clearRetainingCapacity();
current_node = &decision_tree;
- try history.append(current_node);
- try stdout.print("\n[Reset to beginning]\n", .{});
+ try history.append(allocator, current_node);
+ try screen.render(stdout, "[Reset to beginning]\n");
} else if (current_node.node_type == .Question) {
if (mem.eql(u8, lower, "yes") or mem.eql(u8, lower, "y")) {
if (current_node.yes) |next_node| {
current_node = next_node;
- try history.append(current_node);
+ try history.append(allocator, current_node);
}
} else if (mem.eql(u8, lower, "no") or mem.eql(u8, lower, "n")) {
if (current_node.no) |next_node| {
current_node = next_node;
- try history.append(current_node);
+ try history.append(allocator, current_node);
}
} else {
- try stdout.print("\n[Invalid input. Please enter yes, no, back, reset, or quit]\n", .{});
+ try screen.render(stdout, "[Invalid input. Please enter yes, no, back, reset, or quit]\n");
}
} else {
- try stdout.print("\n[Invalid command. Use back, reset, or quit]\n", .{});
+ try screen.render(stdout, "[Invalid command. Use back, reset, or quit]\n");
}
}
}
diff --git a/src/tree.zig b/src/tree.zig
new file mode 100644
index 0000000..7b9c959
--- /dev/null
+++ b/src/tree.zig
@@ -0,0 +1,288 @@
+const std = @import("std");
+
+// Node types in the decision tree
+pub const NodeType = enum {
+ Question,
+ Answer,
+};
+
+pub const Node = struct {
+ node_type: NodeType,
+ content: []const u8,
+ elaboration: []const u8,
+ yes: ?*const Node,
+ no: ?*const Node,
+};
+
+// Decision tree structure matching the original JavaScript logic
+pub const decision_tree = buildDecisionTree();
+
+fn buildDecisionTree() Node {
+ // Leaf nodes (answers)
+ const mit_node = Node{
+ .node_type = .Answer,
+ .content = "MIT",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const bsd_2_clause = Node{
+ .node_type = .Answer,
+ .content = "BSD-2-Clause",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const apache_2_0 = Node{
+ .node_type = .Answer,
+ .content = "Apache-2.0",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const bsd_3_clause = Node{
+ .node_type = .Answer,
+ .content = "BSD-3-Clause",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const zero_bsd = Node{
+ .node_type = .Answer,
+ .content = "0BSD",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const isc = Node{
+ .node_type = .Answer,
+ .content = "ISC",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const agpl_3_0 = Node{
+ .node_type = .Answer,
+ .content = "AGPL-3.0",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const gpl_3_0 = Node{
+ .node_type = .Answer,
+ .content = "GPL-3.0",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const gpl_2_0 = Node{
+ .node_type = .Answer,
+ .content = "GPL-2.0",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const lgpl_3_0 = Node{
+ .node_type = .Answer,
+ .content = "LGPL-3.0",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const mpl_2_0 = Node{
+ .node_type = .Answer,
+ .content = "MPL-2.0",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const epl_2_0 = Node{
+ .node_type = .Answer,
+ .content = "EPL-2.0",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const epl_1_0 = Node{
+ .node_type = .Answer,
+ .content = "EPL-1.0",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const unlicense = Node{
+ .node_type = .Answer,
+ .content = "Unlicense",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const ofl_1_1 = Node{
+ .node_type = .Answer,
+ .content = "OFL-1.1",
+ .elaboration = "",
+ .yes = null,
+ .no = null,
+ };
+
+ const proprietary = Node{
+ .node_type = .Answer,
+ .content = "Consider using a proprietary license or another specialized license.",
+ .elaboration = "You should consider using a proprietary license or another specialized license by consulting with a legal expert.",
+ .yes = null,
+ .no = null,
+ };
+
+ const closed_source = Node{
+ .node_type = .Answer,
+ .content = "You should consider keeping your project closed-source.",
+ .elaboration = "You should consider keeping your project closed-source to protect your intellectual property by not sharing the source code.",
+ .yes = null,
+ .no = null,
+ };
+
+ // Build tree from bottom up
+ const simplest_permissive = Node{
+ .node_type = .Question,
+ .content = "Do you want the simplest and most permissive license possible?",
+ .elaboration = "Do you want a license that is very permissive and has minimal requirements, making it as simple as possible for others to use your code.",
+ .yes = &mit_node,
+ .no = &bsd_2_clause,
+ };
+
+ const simplest_no_conditions = Node{
+ .node_type = .Question,
+ .content = "Do you want the simplest permissive license with no conditions?",
+ .elaboration = "Do you want the simplest permissive license with no conditions, providing complete freedom to use the code without any restrictions.",
+ .yes = &zero_bsd,
+ .no = &isc,
+ };
+
+ const permissive_with_conditions = Node{
+ .node_type = .Question,
+ .content = "Do you want a permissive license with some conditions?",
+ .elaboration = "Do you want a permissive license that includes some conditions such as providing attribution and not using the name of the project or its contributors for promotion without permission.",
+ .yes = &bsd_3_clause,
+ .no = &simplest_no_conditions,
+ };
+
+ const explicit_patent_grants = Node{
+ .node_type = .Question,
+ .content = "Do you want explicit patent grants?",
+ .elaboration = "Do you want to include explicit grants of patent rights, which can protect users from patent litigation. This is an important consideration for projects that may involve patented technology.",
+ .yes = &apache_2_0,
+ .no = &permissive_with_conditions,
+ };
+
+ const minimal_conditions = Node{
+ .node_type = .Question,
+ .content = "Do you require minimal conditions?",
+ .elaboration = "Do you want minimal conditions meaning that there are very few requirements placed on the use of your code. This typically includes providing attribution to the original authors.",
+ .yes = &simplest_permissive,
+ .no = &explicit_patent_grants,
+ };
+
+ const latest_gpl = Node{
+ .node_type = .Question,
+ .content = "Do you want to use the latest version of the GPL license?",
+ .elaboration = "Do you prefer using the latest version of the GPL license, which includes additional protections and clarifications compared to older versions.",
+ .yes = &gpl_3_0,
+ .no = &gpl_2_0,
+ };
+
+ const network_server_protection = Node{
+ .node_type = .Question,
+ .content = "Do you want network server protection?",
+ .elaboration = "Do you want to extend copyleft requirements to software provided over a network. This means that users who interact with the software over a network (e.g., web applications) must also have access to the source code.",
+ .yes = &agpl_3_0,
+ .no = &latest_gpl,
+ };
+
+ const business_friendly_copyleft = Node{
+ .node_type = .Question,
+ .content = "Do you prefer a copyleft license with a focus on business-friendly terms?",
+ .elaboration = "Do you prefer a copyleft license that has a focus on business-friendly terms, making it easier for companies to adopt.",
+ .yes = &epl_2_0,
+ .no = &epl_1_0,
+ };
+
+ const weaker_copyleft = Node{
+ .node_type = .Question,
+ .content = "Do you want a copyleft license with weaker requirements?",
+ .elaboration = "Do you want a copyleft license that has weaker requirements compared to the GPL, such as allowing proprietary modules in your project.",
+ .yes = &mpl_2_0,
+ .no = &business_friendly_copyleft,
+ };
+
+ const allow_linking = Node{
+ .node_type = .Question,
+ .content = "Do you want to allow linking with non-(L)GPL software?",
+ .elaboration = "Do you want to allow linking with non-(L)GPL software, making it easier to use your code as a library in proprietary software while keeping modifications to the library itself open source.",
+ .yes = &lgpl_3_0,
+ .no = &weaker_copyleft,
+ };
+
+ const strong_copyleft = Node{
+ .node_type = .Question,
+ .content = "Do you want strong copyleft?",
+ .elaboration = "Do you want to require that any distributed modifications (or in some cases, software that interacts with the copylefted code) also be open-sourced. This ensures that improvements to the code are shared with the community.",
+ .yes = &network_server_protection,
+ .no = &allow_linking,
+ };
+
+ const font_license = Node{
+ .node_type = .Question,
+ .content = "Do you want a license for fonts?",
+ .elaboration = "Do you want a license specifically designed for fonts, allowing embedding, modifying, and redistributing the font.",
+ .yes = &ofl_1_1,
+ .no = &proprietary,
+ };
+
+ const public_domain = Node{
+ .node_type = .Question,
+ .content = "Do you want to dedicate your work to the public domain?",
+ .elaboration = "Do you want to dedicate your work to the public domain, allowing anyone to use, modify, and distribute your work without any restrictions.",
+ .yes = &unlicense,
+ .no = &font_license,
+ };
+
+ const copyleft_license = Node{
+ .node_type = .Question,
+ .content = "Do you want a copyleft license?",
+ .elaboration = "Do you want to require that any modified versions of your code be distributed under the same license, ensuring that the code (and its derivatives) remain open source.",
+ .yes = &strong_copyleft,
+ .no = &public_domain,
+ };
+
+ const permissive_license = Node{
+ .node_type = .Question,
+ .content = "Do you want a permissive license?",
+ .elaboration = "Do you want a more lenient license and allow others to use, modify, and distribute your code with minimal restrictions. These licenses are generally business-friendly and encourage wider use.",
+ .yes = &minimal_conditions,
+ .no = ©left_license,
+ };
+
+ const root = Node{
+ .node_type = .Question,
+ .content = "Do you want to open-source your project?",
+ .elaboration = "Do you want to make your source code publicly available and allow others to use, modify, and distribute it.",
+ .yes = &permissive_license,
+ .no = &closed_source,
+ };
+
+ return root;
+}
diff --git a/src/tree_test.zig b/src/tree_test.zig
new file mode 100644
index 0000000..e9f344c
--- /dev/null
+++ b/src/tree_test.zig
@@ -0,0 +1,114 @@
+const std = @import("std");
+const testing = std.testing;
+const tree = @import("tree.zig");
+
+test "decision tree root exists and is a question" {
+ try testing.expect(tree.decision_tree.node_type == .Question);
+ try testing.expect(tree.decision_tree.content.len > 0);
+}
+
+test "decision tree root has both yes and no branches" {
+ try testing.expect(tree.decision_tree.yes != null);
+ try testing.expect(tree.decision_tree.no != null);
+}
+
+test "all answer nodes have null yes/no pointers" {
+ // Test some known answer nodes by traversing the tree
+ const closed_source = tree.decision_tree.no.?;
+ try testing.expect(closed_source.node_type == .Answer);
+ try testing.expect(closed_source.yes == null);
+ try testing.expect(closed_source.no == null);
+}
+
+test "MIT license is reachable from root" {
+ // Path: yes > yes > yes > yes
+ const permissive = tree.decision_tree.yes.?;
+ try testing.expect(permissive.node_type == .Question);
+
+ const minimal = permissive.yes.?;
+ try testing.expect(minimal.node_type == .Question);
+
+ const simplest = minimal.yes.?;
+ try testing.expect(simplest.node_type == .Question);
+
+ const mit = simplest.yes.?;
+ try testing.expect(mit.node_type == .Answer);
+ try testing.expect(std.mem.eql(u8, mit.content, "MIT"));
+}
+
+test "Apache-2.0 license is reachable from root" {
+ // Path: yes > yes > no > yes
+ const permissive = tree.decision_tree.yes.?;
+ const minimal = permissive.yes.?;
+ const explicit_patents = minimal.no.?;
+ try testing.expect(explicit_patents.node_type == .Question);
+
+ const apache = explicit_patents.yes.?;
+ try testing.expect(apache.node_type == .Answer);
+ try testing.expect(std.mem.eql(u8, apache.content, "Apache-2.0"));
+}
+
+test "GPL-3.0 license is reachable from root" {
+ // Path: yes > no > yes > yes > no > yes
+ const permissive = tree.decision_tree.yes.?;
+ const copyleft = permissive.no.?;
+ try testing.expect(copyleft.node_type == .Question);
+
+ const strong = copyleft.yes.?;
+ try testing.expect(strong.node_type == .Question);
+
+ const network = strong.yes.?;
+ try testing.expect(network.node_type == .Question);
+
+ const latest_gpl = network.no.?;
+ try testing.expect(latest_gpl.node_type == .Question);
+
+ const gpl3 = latest_gpl.yes.?;
+ try testing.expect(gpl3.node_type == .Answer);
+ try testing.expect(std.mem.eql(u8, gpl3.content, "GPL-3.0"));
+}
+
+test "tree has proper structure with no null dereferencing" {
+ // Walk through several paths to ensure no crashes
+ var paths_tested: usize = 0;
+
+ // Test permissive branch
+ if (tree.decision_tree.yes) |permissive| {
+ paths_tested += 1;
+ try testing.expect(permissive.node_type == .Question);
+
+ if (permissive.yes) |minimal| {
+ paths_tested += 1;
+ try testing.expect(minimal.node_type == .Question);
+ }
+
+ if (permissive.no) |copyleft| {
+ paths_tested += 1;
+ try testing.expect(copyleft.node_type == .Question);
+ }
+ }
+
+ // Test closed source branch
+ if (tree.decision_tree.no) |closed| {
+ paths_tested += 1;
+ try testing.expect(closed.node_type == .Answer);
+ }
+
+ try testing.expect(paths_tested == 4);
+}
+
+test "all question nodes have content and elaboration" {
+ // Test root node
+ try testing.expect(tree.decision_tree.content.len > 0);
+ try testing.expect(tree.decision_tree.elaboration.len > 0);
+
+ // Test a few more nodes
+ const permissive = tree.decision_tree.yes.?;
+ try testing.expect(permissive.content.len > 0);
+ try testing.expect(permissive.elaboration.len > 0);
+}
+
+test "NodeType enum has correct values" {
+ try testing.expect(@TypeOf(tree.NodeType.Question) == tree.NodeType);
+ try testing.expect(@TypeOf(tree.NodeType.Answer) == tree.NodeType);
+}
diff --git a/src/ui.zig b/src/ui.zig
new file mode 100644
index 0000000..fa5caf4
--- /dev/null
+++ b/src/ui.zig
@@ -0,0 +1,58 @@
+const std = @import("std");
+
+pub const Screen = struct {
+ lines_printed: usize,
+
+ pub fn init() Screen {
+ return Screen{ .lines_printed = 0 };
+ }
+
+ /// Render the given content by clearing the previously-rendered
+ /// block and printing the new content. This uses simple ANSI
+ /// escape sequences and assumes a terminal supporting them.
+ pub fn render(self: *Screen, stdout: anytype, content: []const u8) !void {
+ // Clear previous block by moving the cursor up and clearing lines
+ if (self.lines_printed > 0) {
+ var i: usize = 0;
+ while (i < self.lines_printed) : (i += 1) {
+ // Move cursor up 1 line, then clear the entire line
+ try stdout.print("\x1b[1A\x1b[2K", .{});
+ }
+ }
+
+ // Print new content
+ try stdout.print("{s}", .{content});
+
+ // Count lines in content to know how many to clear next time.
+ var count: usize = 1;
+ for (content) |c| {
+ if (c == '\n') count += 1;
+ }
+ self.lines_printed = count;
+ }
+
+ /// Explicitly clear the currently-rendered block.
+ pub fn clear(self: *Screen, stdout: anytype) !void {
+ if (self.lines_printed > 0) {
+ var i: usize = 0;
+ while (i < self.lines_printed) : (i += 1) {
+ try stdout.print("\x1b[1A\x1b[2K", .{});
+ }
+ self.lines_printed = 0;
+ }
+ }
+
+ /// Clear the rendered block and the user input line that followed it.
+ pub fn clearIncludingInput(self: *Screen, stdout: anytype) !void {
+ // Always clear the previously-rendered block lines first
+ if (self.lines_printed > 0) {
+ var i: usize = 0;
+ while (i < self.lines_printed) : (i += 1) {
+ try stdout.print("\x1b[1A\x1b[2K", .{});
+ }
+ self.lines_printed = 0;
+ }
+ // Also clear the user's input line (the line after the rendered block)
+ try stdout.print("\x1b[1A\x1b[2K", .{});
+ }
+};
diff --git a/src/ui_test.zig b/src/ui_test.zig
new file mode 100644
index 0000000..c465891
--- /dev/null
+++ b/src/ui_test.zig
@@ -0,0 +1,48 @@
+const std = @import("std");
+const testing = std.testing;
+const ui = @import("ui.zig");
+
+// A tiny writer that collects printed bytes into an ArrayList(u8).
+const CaptureWriter = struct {
+ allocator: std.mem.Allocator,
+ buf: *std.ArrayList(u8),
+
+ pub fn init(allocator: std.mem.Allocator, buf: *std.ArrayList(u8)) CaptureWriter {
+ return CaptureWriter{ .allocator = allocator, .buf = buf };
+ }
+
+ pub fn print(self: *CaptureWriter, comptime fmt: []const u8, args: anytype) !void {
+ const s = try std.fmt.allocPrint(self.allocator, fmt, args);
+ defer self.allocator.free(s);
+ for (s) |c| {
+ try self.buf.append(self.allocator, c);
+ }
+ }
+};
+
+test "Screen.render overwrites previous content" {
+ const allocator = testing.allocator;
+
+ var buf = try std.ArrayList(u8).initCapacity(allocator, 0);
+ defer buf.deinit(allocator);
+
+ var writer = CaptureWriter.init(allocator, &buf);
+
+ var screen = ui.Screen.init();
+
+ // First render: should simply print the content (no clear sequences)
+ try screen.render(&writer, "first line\nsecond line\n");
+ const out1 = try buf.toOwnedSlice(allocator);
+ defer allocator.free(out1);
+
+ try testing.expect(std.mem.indexOf(u8, out1, "first line\n") != null);
+
+ // Second render: should include clear sequences before printing new content
+ try screen.render(&writer, "only line\n");
+ const out2 = try buf.toOwnedSlice(allocator);
+ defer allocator.free(out2);
+
+ const esc = "\x1b[1A\x1b[2K";
+ try testing.expect(std.mem.indexOf(u8, out2, esc) != null);
+ try testing.expect(std.mem.indexOf(u8, out2, "only line\n") != null);
+}