diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..755ec86 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,155 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +env: + DOTNET_VERSION: '10.0.x' + SOLUTION_FILE: ${{ github.workspace }}/src/Todo.CLI.sln + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET ${{ env.DOTNET_VERSION }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore dependencies + run: dotnet restore ${{ env.SOLUTION_FILE }} + + - name: Build solution + run: dotnet build ${{ env.SOLUTION_FILE }} --configuration Release + + - name: Run tests + run: dotnet test ${{ env.SOLUTION_FILE }} --configuration Release --no-build --verbosity normal + + package-linux: + name: Package Linux x64 + needs: build + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET ${{ env.DOTNET_VERSION }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Build Linux x64 (self-contained) + run: | + dotnet publish src/Todo.CLI/Todo.CLI.csproj \ + --configuration Release \ + --runtime linux-x64 \ + --framework net10.0 \ + --self-contained true \ + --output ${{ github.workspace }}/publish/linux-x64 + + - name: Create tarball + run: | + cd ${{ github.workspace }}/publish/linux-x64 + tar -czf todo-linux-x64-${{ github.run_id }}.tar.gz . + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: todo-linux-x64 + path: ${{ github.workspace }}/publish/linux-x64/todo-linux-x64-${{ github.run_id }}.tar.gz + + package-windows: + name: Package Windows x64 + needs: build + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET ${{ env.DOTNET_VERSION }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Build Windows x64 (self-contained) + run: | + dotnet publish src/Todo.CLI/Todo.CLI.csproj ` + --configuration Release ` + --runtime win-x64 ` + --framework net10.0 ` + --self-contained true ` + --output ${{ github.workspace }}/publish/win-x64 + + - name: Create zip + working-directory: ${{ github.workspace }}/publish/win-x64 + run: Compress-Archive -Path * -DestinationPath todo-win-x64-${{ github.run_id }}.zip + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: todo-win-x64 + path: ${{ github.workspace }}/publish/win-x64/todo-win-x64-${{ github.run_id }}.zip + + package-osx: + name: Package macOS x64 + needs: build + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET ${{ env.DOTNET_VERSION }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Build macOS x64 (self-contained) + run: | + dotnet publish src/Todo.CLI/Todo.CLI.csproj \ + --configuration Release \ + --runtime osx-x64 \ + --framework net10.0 \ + --self-contained true \ + --output ${{ github.workspace }}/publish/osx-x64 + + - name: Create tarball + run: | + cd ${{ github.workspace }}/publish/osx-x64 + tar -czf todo-osx-x64-${{ github.run_id }}.tar.gz . + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: todo-osx-x64 + path: ${{ github.workspace }}/publish/osx-x64/todo-osx-x64-${{ github.run_id }}.tar.gz + + release: + name: Create Release + needs: [package-linux, package-windows, package-osx] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: ${{ github.workspace }}/artifacts + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: | + ${{ github.workspace }}/artifacts/**/*.tar.gz + ${{ github.workspace }}/artifacts/**/*.zip + generate_release_notes: true diff --git a/README.md b/README.md index 634f005..c180d0a 100644 --- a/README.md +++ b/README.md @@ -8,59 +8,134 @@ # Todo CLI -A cross-platform command-line interface to interact with Microsoft To Do, built using .NET 8. +A cross-platform command-line interface to interact with Microsoft To Do, built using .NET 10. ## Build Status | Platform | Status | | ------ | ------------ | -| CI | [![CI build status](https://dev.azure.com/mtseckin/todo-cli/_apis/build/status/CI)](https://dev.azure.com/mtseckin/todo-cli/_build/latest?definitionId=1) | -| CD | [![Windows (x64) build status](https://dev.azure.com/mtseckin/todo-cli/_apis/build/status/CD)](https://dev.azure.com/mtseckin/todo-cli/_build/latest?definitionId=5) +| CI | ![CI](https://github.com/evilz/todo-cli/actions/workflows/ci.yml/badge.svg) | +| Release | ![Release](https://github.com/evilz/todo-cli/actions/workflows/ci.yml/badge.svg?event=release) | -## Getting Started +## Features -### Install +- ✅ Cross-platform (Linux, macOS, Windows) +- ✅ Self-contained binaries (no .NET runtime required) +- ✅ Microsoft To Do integration +- ✅ Interactive mode with Inquirer +- ✅ Command-line mode for scripting +- ✅ CI/CD with GitHub Actions -If you just want to give it a spin, head over to [releases](https://github.com/mehmetseckin/todo-cli/releases/). Download a release and extract to somewhere in your `PATH`, and run `todo --help` to get started. +## Quick Install -### Build +### Linux/macOS (curl) +```bash +# Download the latest Linux x64 binary +curl -L https://github.com/evilz/todo-cli/releases/latest/download/todo-linux-x64.tar.gz -o todo.tar.gz +tar -xzf todo.tar.gz +chmod +x todo +sudo mv todo /usr/local/bin/ +todo --help ``` -# Clone the repository -git clone https://github.com/mehmetseckin/todo-cli.git -# Navigate into the source code folder -cd .\todo-cli\src +### Windows (PowerShell) + +```powershell +# Download the latest Windows x64 zip +Invoke-WebRequest -Uri https://github.com/evilz/todo-cli/releases/latest/download/todo-win-x64.zip -OutFile todo.zip +Expand-Archive -Path todo.zip -DestinationPath todo +.\todo\todo.exe --help +``` + +### Via .NET CLI -# Build the project -dotnet build +```bash +dotnet tool install -g Todo.CLI --source https://www.nuget.org +todo --help ``` -### Run +## Build from Source + +### Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- Linux, macOS, or Windows + +### Build + +```bash +# Clone the repository +git clone https://github.com/evilz/todo-cli.git +cd todo-cli + +# Build the solution +dotnet build src/Todo.CLI.sln --configuration Release -The application will automatically prompt you to sign in with your Microsoft account, and ask for your consent to access your data when needed. +# Run tests +dotnet test src/Todo.CLI.sln --configuration Release +# Publish self-contained for your platform +dotnet publish src/Todo.CLI/Todo.CLI.csproj \ + --configuration Release \ + --runtime linux-x64 \ + --self-contained true \ + --output ./publish ``` -# Run using dotnet run -dotnet run -p .\Todo.CLI -- --help -# Run from build output (?) -.\Todo.CLI\bin\Debug\netcoreapp3.0\todo --help +## Usage + +First run will prompt you to sign in with your Microsoft account. + +```bash +# Show help +todo --help + +# Add a task +todo add "Buy milk" + +# List tasks +todo list +todo list --all + +# Complete a task +todo complete + +# Remove a task +todo remove ``` -## Contributing +## Authentication + +The application uses Microsoft OAuth 2.0 for authentication. On first run, it will: +1. Open your default browser +2. Ask you to sign in with your Microsoft account +3. Request consent to access Microsoft To Do +4. Store tokens securely in your system's keyring + +## CI/CD -Interested? You are awesome. Feel free to fork, do your thing and send a PR! Everything is appreciated. +GitHub Actions workflows handle: +- Building and testing on multiple platforms +- Creating self-contained binaries for Linux, macOS, and Windows +- Publishing releases automatically on tag + +## Contributing -## Code of Conduct +1. Fork the repository +2. Create a feature branch +3. Add tests for your changes +4. Ensure all tests pass +5. Submit a pull request -Be nice to people, give constructive feedback, and have fun! +## License -## Stack +MIT License -This project is built using the following nuggets of awesomeness, and many more. Many thanks to the folks who are working on and maintaining these products. +## Acknowledgments -- [.NET 8](https://github.com/dotnet/core) +Built with: +- [.NET 10](https://github.com/dotnet/core) - [System.CommandLine](https://github.com/dotnet/command-line-api) -- [Microsoft Graph Beta SDK](https://github.com/microsoftgraph/msgraph-beta-sdk-dotnet) +- [Microsoft Graph SDK](https://github.com/microsoftgraph/msgraph-sdk-dotnet) - [Inquirer.cs](https://github.com/hayer/Inquirer.cs) diff --git a/src/Todo.CLI.Tests/Commands/CommandTests.cs b/src/Todo.CLI.Tests/Commands/CommandTests.cs new file mode 100644 index 0000000..bf62c3a --- /dev/null +++ b/src/Todo.CLI.Tests/Commands/CommandTests.cs @@ -0,0 +1,156 @@ +using System.CommandLine; +using Todo.CLI.Commands; +using Todo.CLI.Tests.Handlers; +using Todo.Core; +using Xunit; + +namespace Todo.CLI.Tests.Commands; + +public abstract class CommandTestsBase +{ + protected readonly ServiceProvider _serviceProvider; + + protected CommandTestsBase() + { + _serviceProvider = new ServiceCollection() + .AddSingleton() + .AddTodoRepositories() + .AddSingleton(new MockUserInteraction()) + .BuildServiceProvider(); + } +} + +public class AddCommandTests : CommandTestsBase +{ + + [Fact] + public void AddCommand_ShouldHaveCorrectName() + { + // Arrange & Act + var command = new AddCommand(_serviceProvider); + + // Assert + Assert.Equal("add", command.Name); + } + + [Fact] + public void AddCommand_ShouldHaveDescription() + { + // Arrange & Act + var command = new AddCommand(_serviceProvider); + + // Assert + Assert.False(string.IsNullOrEmpty(command.Description)); + } + + [Fact] + public void AddItemCommand_ShouldAcceptSubjectArgument() + { + // Arrange + var command = new AddCommand(_serviceProvider); + var itemSubCommand = command.Subcommands.Single(c => c.Name == "item"); + + // Act + var subjectArgument = itemSubCommand.Arguments.SingleOrDefault(a => a.Name == "subject"); + + // Assert + Assert.NotNull(subjectArgument); + } + + [Fact] + public void AddItemCommand_ShouldAcceptListAndStarOptions() + { + // Arrange + var command = new AddCommand(_serviceProvider); + var itemSubCommand = command.Subcommands.Single(c => c.Name == "item"); + + // Act + var listOption = itemSubCommand.Options.SingleOrDefault(o => o.Name == "list"); + var starOption = itemSubCommand.Options.SingleOrDefault(o => o.Name == "star"); + + // Assert + Assert.NotNull(listOption); + Assert.NotNull(starOption); + } +} + +public class ListCommandTests : CommandTestsBase +{ + + [Fact] + public void ListCommand_ShouldHaveCorrectName() + { + // Arrange & Act + var command = new ListCommand(_serviceProvider); + + // Assert + Assert.Equal("list", command.Name); + } + + [Fact] + public void ListCommand_ShouldAcceptAllFlag() + { + // Arrange + var command = new ListCommand(_serviceProvider); + + // Act + var allOption = command.Children.OfType