diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ef521ac --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# Directories +**/bin/ +**/obj/ +**/out/ +**/publish/ +**/.vs/ +**/.vscode/ +**/.idea/ +**/node_modules/ +**/TestResults/ +**/coverage/ + +# Files +**/.DS_Store +**/*.user +**/*.suo +**/*.userprefs +**/tasks.json +**/test_tasks*.json +**/.git +**/.gitignore +**/.gitattributes +**/*.md +!README.md +LICENSE + +# Build artifacts +**/packages/ +**/.nuget/ +*.nupkg +*.snupkg + +# Test and coverage +**/*.trx +**/*.coverage +**/*.coveragexml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f1b7fc3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,234 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# All files +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# YAML files +[*.{yml,yaml}] +indent_size = 2 + +# Markdown files +[*.md] +trim_trailing_whitespace = false + +# Shell scripts +[*.sh] +end_of_line = lf + +# Batch files +[*.{cmd,bat}] +end_of_line = crlf + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +tab_width = 4 + +# New line preferences +end_of_line = lf +insert_final_newline = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# this. and Me. preferences +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion +dotnet_style_readonly_field = true:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion + +# Null-checking preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion + +# File header preferences +file_header_template = unset + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = false:suggestion +csharp_style_expression_bodied_constructors = false:suggestion +csharp_style_expression_bodied_operators = false:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = false:suggestion + +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion + +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +# Code-block preferences +csharp_prefer_braces = true:suggestion +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:suggestion + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = no_change +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true + +#### Naming Conventions #### + +# Naming rules +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..eef0af6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,46 @@ +--- +name: Bug Report +about: Report a bug or unexpected behavior +title: '[BUG] ' +labels: bug +assignees: '' +--- + +## Bug Description +A clear and concise description of what the bug is. + +## Steps To Reproduce +1. Run command '...' +2. With arguments '...' +3. See error + +## Expected Behavior +A clear and concise description of what you expected to happen. + +## Actual Behavior +A clear and concise description of what actually happened. + +## Environment +- OS: [e.g., Windows 11, Ubuntu 22.04, macOS 14] +- .NET Version: [e.g., .NET 8.0] +- Task Manager Version: [e.g., 2.0.0] +- Installation Method: [e.g., built from source, Docker, global tool] + +## Tasks File (if relevant) +```json +// Paste relevant content from your tasks.json file +``` + +## Error Messages +``` +Paste any error messages or stack traces here +``` + +## Screenshots +If applicable, add screenshots to help explain your problem. + +## Additional Context +Add any other context about the problem here. + +## Possible Solution +If you have ideas on how to fix this, please share them here. diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..d31c256 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,30 @@ +--- +name: Documentation Issue +about: Report missing, incorrect, or unclear documentation +title: '[DOCS] ' +labels: documentation +assignees: '' +--- + +## Documentation Issue +Which documentation needs improvement? + +- [ ] README.md +- [ ] QUICKSTART.md +- [ ] CONTRIBUTING.md +- [ ] ARCHITECTURE.md +- [ ] EXAMPLES.md +- [ ] Code comments +- [ ] Other: _______ + +## Current Documentation +Link or quote the current documentation that needs improvement. + +## Issue Description +What is unclear, incorrect, or missing? + +## Suggested Improvement +How would you improve this documentation? + +## Additional Context +Add any other context or examples about the documentation issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..685ec2d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,42 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## Feature Description +A clear and concise description of the feature you'd like to see. + +## Problem Statement +Describe the problem this feature would solve. Ex: "I'm always frustrated when..." + +## Proposed Solution +Describe how you envision this feature working. + +## Alternative Solutions +Describe any alternative solutions or features you've considered. + +## Use Cases +Provide specific examples of how this feature would be used: + +1. Scenario 1: ... +2. Scenario 2: ... +3. Scenario 3: ... + +## Implementation Ideas +If you have ideas on how this could be implemented, share them here. + +## Impact +- Who would benefit from this feature? +- How often would it be used? +- Would this be a breaking change? + +## Additional Context +Add any other context, mockups, or examples about the feature request here. + +## Willingness to Contribute +- [ ] I am willing to submit a PR for this feature +- [ ] I am willing to help test this feature +- [ ] I can provide additional details if needed diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..840f20c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,85 @@ +# Pull Request + +## Description + + +## Type of Change + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Performance improvement +- [ ] Code refactoring +- [ ] Test coverage improvement +- [ ] Build/CI improvement + +## Related Issues + + +Fixes # +Relates to # + +## Changes Made + + +- Change 1 +- Change 2 +- Change 3 + +## Testing + + +### Test Environment +- OS: [e.g., Windows 11, Ubuntu 22.04, macOS 14] +- .NET Version: [e.g., .NET 8.0] + +### Test Cases +- [ ] All existing tests pass +- [ ] New tests added for new functionality +- [ ] Manual testing performed + +### Manual Testing Steps +1. Step 1 +2. Step 2 +3. Step 3 + +## Screenshots/Output + + +``` +Paste command output here +``` + +## Checklist + + +- [ ] My code follows the project's code style (EditorConfig) +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published + +## Breaking Changes + + +None + +## Migration Guide + + +N/A + +## Performance Impact + + +- [ ] No performance impact +- [ ] Performance improved +- [ ] Performance degraded (explain why this is acceptable) + +## Additional Notes + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b538505 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,112 @@ +name: CI/CD + +on: + push: + branches: [ main, develop, 'claude/**' ] + pull_request: + branches: [ main, develop ] + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + + strategy: + matrix: + dotnet-version: ['8.0.x'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.dotnet-version }} + + - name: Display .NET version + run: dotnet --version + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Run tests + run: dotnet test --configuration Release --no-build --verbosity normal --collect:"XPlat Code Coverage" + + - name: Upload coverage reports + uses: codecov/codecov-action@v4 + if: always() + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: '**/coverage.cobertura.xml' + fail_ci_if_error: false + + code-quality: + name: Code Quality + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Format check + run: dotnet format --verify-no-changes --verbosity diagnostic || true + + - name: Build for analysis + run: dotnet build --configuration Release + + package: + name: Package Application + runs-on: ubuntu-latest + needs: [build-and-test, code-quality] + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Publish Linux x64 + run: dotnet publish src/TaskManager.CLI/TaskManager.CLI.csproj -c Release -r linux-x64 --self-contained -o ./publish/linux-x64 + + - name: Publish Windows x64 + run: dotnet publish src/TaskManager.CLI/TaskManager.CLI.csproj -c Release -r win-x64 --self-contained -o ./publish/win-x64 + + - name: Publish macOS x64 + run: dotnet publish src/TaskManager.CLI/TaskManager.CLI.csproj -c Release -r osx-x64 --self-contained -o ./publish/osx-x64 + + - name: Upload Linux artifact + uses: actions/upload-artifact@v4 + with: + name: taskman-linux-x64 + path: ./publish/linux-x64/ + + - name: Upload Windows artifact + uses: actions/upload-artifact@v4 + with: + name: taskman-win-x64 + path: ./publish/win-x64/ + + - name: Upload macOS artifact + uses: actions/upload-artifact@v4 + with: + name: taskman-osx-x64 + path: ./publish/osx-x64/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..23dbfe1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,126 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + +permissions: + contents: write + +jobs: + build-and-release: + name: Build and Release + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Get version from tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Run tests + run: dotnet test --configuration Release --no-build --verbosity normal + + - name: Publish Linux x64 + run: | + dotnet publish src/TaskManager.CLI/TaskManager.CLI.csproj \ + -c Release \ + -r linux-x64 \ + --self-contained \ + -p:PublishSingleFile=true \ + -p:PublishTrimmed=true \ + -o ./publish/linux-x64 + + - name: Publish Windows x64 + run: | + dotnet publish src/TaskManager.CLI/TaskManager.CLI.csproj \ + -c Release \ + -r win-x64 \ + --self-contained \ + -p:PublishSingleFile=true \ + -p:PublishTrimmed=true \ + -o ./publish/win-x64 + + - name: Publish macOS x64 + run: | + dotnet publish src/TaskManager.CLI/TaskManager.CLI.csproj \ + -c Release \ + -r osx-x64 \ + --self-contained \ + -p:PublishSingleFile=true \ + -p:PublishTrimmed=true \ + -o ./publish/osx-x64 + + - name: Publish macOS ARM64 + run: | + dotnet publish src/TaskManager.CLI/TaskManager.CLI.csproj \ + -c Release \ + -r osx-arm64 \ + --self-contained \ + -p:PublishSingleFile=true \ + -p:PublishTrimmed=true \ + -o ./publish/osx-arm64 + + - name: Create archives + run: | + cd publish + tar -czf taskman-linux-x64-${{ steps.get_version.outputs.VERSION }}.tar.gz -C linux-x64 . + zip -r taskman-win-x64-${{ steps.get_version.outputs.VERSION }}.zip win-x64 + tar -czf taskman-macos-x64-${{ steps.get_version.outputs.VERSION }}.tar.gz -C osx-x64 . + tar -czf taskman-macos-arm64-${{ steps.get_version.outputs.VERSION }}.tar.gz -C osx-arm64 . + + - name: Generate checksums + run: | + cd publish + sha256sum *.tar.gz *.zip > SHA256SUMS + + - name: Extract changelog + id: changelog + run: | + # Extract changelog for this version + VERSION=${{ steps.get_version.outputs.VERSION }} + sed -n "/## \[$VERSION\]/,/## \[/p" CHANGELOG.md | sed '$ d' > RELEASE_NOTES.md + if [ ! -s RELEASE_NOTES.md ]; then + echo "Release $VERSION" > RELEASE_NOTES.md + echo "See [CHANGELOG.md](CHANGELOG.md) for details." >> RELEASE_NOTES.md + fi + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + name: Release ${{ steps.get_version.outputs.VERSION }} + body_path: RELEASE_NOTES.md + files: | + publish/taskman-linux-x64-${{ steps.get_version.outputs.VERSION }}.tar.gz + publish/taskman-win-x64-${{ steps.get_version.outputs.VERSION }}.zip + publish/taskman-macos-x64-${{ steps.get_version.outputs.VERSION }}.tar.gz + publish/taskman-macos-arm64-${{ steps.get_version.outputs.VERSION }}.tar.gz + publish/SHA256SUMS + draft: false + prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') || contains(github.ref, '-alpha') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build NuGet package + run: dotnet pack src/TaskManager.CLI/TaskManager.CLI.csproj -c Release -p:PackageVersion=${{ steps.get_version.outputs.VERSION }} + + - name: Publish to NuGet (optional) + if: ${{ !contains(github.ref, '-rc') && !contains(github.ref, '-beta') && !contains(github.ref, '-alpha') }} + run: dotnet nuget push src/TaskManager.CLI/bin/Release/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + continue-on-error: true diff --git a/.gitignore b/.gitignore index c6127b3..1e4699e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,52 +1,232 @@ -# Prerequisites -*.d +# .NET Core / .NET 5+ +bin/ +obj/ +out/ +publish/ -# Object files -*.o -*.ko -*.obj -*.elf +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ -# Linker output -*.ilk -*.map -*.exp +# Visual Studio +.vs/ +*.suo +*.user +*.userosscache +*.sln.docstates +*.userprefs -# Precompiled Headers -*.gch -*.pch +# Visual Studio Code +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# JetBrains Rider +.idea/ +*.sln.iml +*.DotSettings.user + +# Mono Auto Generated Files +mono_crash.* + +# NuGet Packages +*.nupkg +*.snupkg +**/packages/* +!**/packages/build/ +*.nuget.props +*.nuget.targets +.nuget/ + +# Test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.trx +*.coverage +*.coveragexml +TestResults/ +coverage/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -*.idb +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch *.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity +_TeamCity* + +# DotCover +*.dotCover + +# AxoCover +.axoCover/* +!.axoCover/settings.json + +# Coverlet +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings +PublishScripts/ + +# NuGet Packages Directory +packages/ + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directories +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +*.suo +*.user +*.sln.docstates + +# Mac +.DS_Store + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# Application specific +tasks.json +test_tasks*.json -# Kernel Module Compile Results -*.mod* -*.cmd -.tmp_versions/ -modules.order -Module.symvers -Mkfile.old -dkms.conf +# Backup files +*.bak +*~ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b2e312c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,189 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [2.0.0] - 2024-01-15 + +### Added +- Complete project restructure with modern .NET architecture +- Modular code organization (Models, Services, Interfaces, CLI) +- Priority system (1-5 levels) for tasks +- Tag support for better task organization +- Due date tracking for tasks +- Task completion status +- Search functionality across descriptions and tags +- Statistics and reporting features + - Overall task statistics + - Tasks by priority breakdown + - Tasks by tag analysis + - Overdue task tracking + - Upcoming task notifications +- Export functionality + - Export to CSV format + - Export to Markdown format + - Export to JSON format + - Import from JSON format +- Configuration file support (appsettings.json) +- Comprehensive unit tests with xUnit + - Service layer tests + - Integration tests + - Export/Import tests + - Statistics tests +- Docker support + - Dockerfile for containerized deployment + - docker-compose.yml for development + - .dockerignore for optimized builds +- VSCode integration + - tasks.json for common operations + - launch.json for debugging + - settings.json for editor configuration + - recommended extensions +- GitHub Actions CI/CD pipeline + - Automated testing + - Code coverage reporting + - Multi-platform builds (Linux, Windows, macOS) +- Build automation scripts + - build.sh for Linux/macOS + - build.cmd for Windows +- Comprehensive documentation + - Enhanced README with examples + - CONTRIBUTING guidelines + - ARCHITECTURE documentation + - EXAMPLES with usage scenarios +- EditorConfig for consistent code style +- Dependency injection throughout the application +- Structured logging with Microsoft.Extensions.Logging +- Async/await for all I/O operations + +### Changed +- Migrated from single file to proper .NET solution structure +- Task model enhanced with additional properties +- File-based storage improved with async operations +- Command-line interface redesigned with better UX +- Error handling and validation improved +- Updated .gitignore for .NET projects + +### Technical Improvements +- Implemented SOLID principles +- Interface-based design for testability +- Comprehensive error handling +- Input validation at all layers +- Nullable reference types enabled +- XML documentation comments throughout + +## [1.0.0] - 2024-01-01 + +### Added +- Initial release +- Basic task management (add, list, remove) +- JSON file storage +- Simple command-line interface +- MIT License + +### Features +- Add tasks with descriptions +- List all tasks +- Remove tasks by ID +- Persistent storage in tasks.json + +--- + +## Upcoming Features + +### [2.1.0] - Planned +- Enhanced CLI output with Spectre.Console + - Color-coded task display + - Interactive task selection + - Progress bars for operations + - Rich tables for task lists +- Task categories/projects +- Recurring tasks support +- Task notes and comments +- Task history and audit log + +### [2.2.0] - Planned +- Web API for remote access +- RESTful endpoints +- Authentication and authorization +- Cloud storage integration +- Multi-user support + +### [3.0.0] - Future +- Database backend (SQLite/PostgreSQL) +- Web interface +- Mobile companion app +- Real-time synchronization +- Team collaboration features +- Task dependencies +- Gantt chart visualization +- Calendar integration +- Email notifications +- Task templates library + +--- + +## Migration Guide + +### Upgrading from 1.0.0 to 2.0.0 + +Your existing `tasks.json` file will need to be migrated to include new fields: + +**Old format (1.0.0):** +```json +[ + { + "Id": 1, + "Description": "Sample task" + } +] +``` + +**New format (2.0.0):** +```json +[ + { + "Id": 1, + "Description": "Sample task", + "IsCompleted": false, + "Priority": 3, + "CreatedAt": "2024-01-01T10:00:00Z", + "DueDate": null, + "Tags": [] + } +] +``` + +**Migration steps:** +1. Backup your existing `tasks.json` file +2. The application will automatically add default values for new fields +3. Update tasks with priorities and tags as needed + +--- + +## Breaking Changes + +### Version 2.0.0 +- **File Structure**: Solution restructured, source moved to `src/` directory +- **Build Process**: Now requires .NET 8 SDK +- **Command Output**: Format changed for better readability +- **Configuration**: Now supports appsettings.json for configuration + +--- + +## Contributors + +- Task Manager Team +- Code for Good Community + +For detailed contribution guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md). + +--- + +## Support + +- **Issues**: [GitHub Issues](https://github.com/codeforgood-org/dotnet-task-manager/issues) +- **Discussions**: [GitHub Discussions](https://github.com/codeforgood-org/dotnet-task-manager/discussions) +- **Documentation**: See [README.md](README.md) and [docs/](docs/) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5087bb0 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,178 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community +* Using welcoming and inclusive language +* Being patient and supportive with newcomers +* Showing respect for different skill levels and learning paths + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting +* Dismissing or attacking inclusion-oriented requests +* Sustained disruption of community discussions + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +conduct@codeforgood.org. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations + +## Additional Guidelines for This Project + +### Code Reviews + +- Be constructive and specific in your feedback +- Focus on the code, not the person +- Explain the "why" behind your suggestions +- Be open to different approaches +- Acknowledge good work + +### Issue and PR Discussions + +- Stay on topic +- Be patient with newcomers +- Provide helpful links and resources +- Don't make assumptions about knowledge level +- Celebrate contributions of all sizes + +### Communication Channels + +- Use appropriate channels for different types of communication +- Keep discussions public when possible +- Respect that contributors are volunteers (unless otherwise stated) +- Be mindful of time zones and response times + +## Questions or Concerns + +If you have questions about this Code of Conduct, please: + +1. Check the [FAQ](https://www.contributor-covenant.org/faq) +2. Open a GitHub issue with the "question" label +3. Email the maintainers at conduct@codeforgood.org + +--- + +**Remember**: We're all here to build better software together. Let's be kind, +respectful, and supportive of each other. + +Last Updated: 2024-01-15 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3987553 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,264 @@ +# Contributing to Task Manager CLI + +Thank you for your interest in contributing to Task Manager CLI! This document provides guidelines and instructions for contributing. + +## Code of Conduct + +Please be respectful and constructive in all interactions. We're building this together! + +## Getting Started + +### Prerequisites + +- .NET 8 SDK or later +- Git +- A code editor (VS Code, Visual Studio, Rider, etc.) + +### Setting Up Development Environment + +1. Fork the repository on GitHub +2. Clone your fork locally: + ```bash + git clone https://github.com/YOUR_USERNAME/dotnet-task-manager.git + cd dotnet-task-manager + ``` + +3. Add the upstream remote: + ```bash + git remote add upstream https://github.com/codeforgood-org/dotnet-task-manager.git + ``` + +4. Restore dependencies: + ```bash + dotnet restore + ``` + +5. Build the project: + ```bash + ./build.sh build # Linux/macOS + build.cmd build # Windows + ``` + +## Development Workflow + +### Creating a Branch + +Create a new branch for your feature or bugfix: + +```bash +git checkout -b feature/your-feature-name +# or +git checkout -b fix/your-bugfix-name +``` + +### Making Changes + +1. Write your code following our coding standards (see below) +2. Add or update tests for your changes +3. Ensure all tests pass: + ```bash + ./build.sh test + ``` +4. Format your code: + ```bash + dotnet format + ``` + +### Coding Standards + +- Follow the .editorconfig settings +- Use meaningful variable and method names +- Add XML documentation comments for public APIs +- Keep methods focused and small +- Write unit tests for new functionality +- Maintain or improve code coverage + +### Project Structure + +``` +src/TaskManager.CLI/ +├── Models/ # Data models +├── Interfaces/ # Service interfaces +├── Services/ # Business logic implementation +└── Program.cs # CLI entry point + +tests/TaskManager.Tests/ +└── TaskServiceTests.cs # Unit tests +``` + +### Testing Guidelines + +- Write unit tests for all new functionality +- Use descriptive test names that explain what is being tested +- Follow the Arrange-Act-Assert pattern +- Use xUnit for test framework +- Use Moq for mocking dependencies + +Example test: + +```csharp +[Fact] +public void AddTask_WithValidDescription_ReturnsTask() +{ + // Arrange + var service = CreateService(); + var description = "Test task"; + + // Act + var task = service.AddTask(description); + + // Assert + Assert.NotNull(task); + Assert.Equal(description, task.Description); +} +``` + +## Submitting Changes + +### Commit Messages + +Write clear, concise commit messages: + +- Use the imperative mood ("Add feature" not "Added feature") +- First line should be 50 characters or less +- Include more details in the body if necessary +- Reference issue numbers when applicable + +Example: +``` +Add search functionality for tasks + +- Implement SearchTasks method in TaskService +- Add search command to CLI +- Add unit tests for search functionality + +Fixes #123 +``` + +### Pull Requests + +1. Update your branch with the latest upstream changes: + ```bash + git fetch upstream + git rebase upstream/main + ``` + +2. Push your changes to your fork: + ```bash + git push origin feature/your-feature-name + ``` + +3. Create a pull request on GitHub with: + - Clear description of changes + - Reference to related issues + - Screenshots if applicable (for UI changes) + - Confirmation that tests pass + +### Pull Request Checklist + +- [ ] Code follows project coding standards +- [ ] All tests pass locally +- [ ] New tests added for new functionality +- [ ] Documentation updated if needed +- [ ] Commit messages are clear and descriptive +- [ ] Branch is up to date with main +- [ ] No merge conflicts + +## Building and Testing + +### Build Commands + +```bash +# Clean build artifacts +./build.sh clean + +# Restore dependencies +./build.sh restore + +# Build the project +./build.sh build + +# Run tests +./build.sh test + +# Run tests with coverage +./build.sh coverage + +# Publish for all platforms +./build.sh publish + +# Run the application +./build.sh run -- [options] + +# Format code +./build.sh format + +# Run full pipeline +./build.sh all +``` + +### Running Tests + +```bash +# Run all tests +dotnet test + +# Run tests with detailed output +dotnet test --verbosity detailed + +# Run specific test +dotnet test --filter "FullyQualifiedName~AddTask_WithValidDescription" + +# Run tests with coverage +dotnet test --collect:"XPlat Code Coverage" +``` + +## Adding New Features + +When adding new features: + +1. **Check existing issues** - Someone might already be working on it +2. **Create an issue** - Discuss your idea before starting work +3. **Design first** - Think about the interface and architecture +4. **Write tests** - Test-driven development is encouraged +5. **Implement** - Write clean, documented code +6. **Update docs** - Update README.md and other documentation +7. **Submit PR** - Follow the pull request guidelines + +## Reporting Bugs + +When reporting bugs, include: + +- **Description** - Clear description of the issue +- **Steps to reproduce** - Detailed steps to reproduce the bug +- **Expected behavior** - What you expected to happen +- **Actual behavior** - What actually happened +- **Environment** - OS, .NET version, etc. +- **Screenshots** - If applicable + +## Feature Requests + +When requesting features: + +- **Use case** - Explain why this feature would be useful +- **Proposed solution** - Describe how you envision it working +- **Alternatives** - Any alternative solutions you've considered +- **Additional context** - Any other relevant information + +## Questions? + +If you have questions: + +- Check existing issues and pull requests +- Review the README.md +- Open a new issue with the "question" label + +## Recognition + +Contributors will be recognized in: + +- The project README.md +- Release notes +- GitHub contributors page + +Thank you for contributing to Task Manager CLI! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e7ecba5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# Use the official .NET SDK image for building +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +# Copy solution and project files +COPY TaskManager.sln ./ +COPY src/TaskManager.CLI/TaskManager.CLI.csproj ./src/TaskManager.CLI/ +COPY tests/TaskManager.Tests/TaskManager.Tests.csproj ./tests/TaskManager.Tests/ + +# Restore dependencies +RUN dotnet restore + +# Copy the rest of the source code +COPY . . + +# Build the application +RUN dotnet build -c Release --no-restore + +# Run tests +RUN dotnet test --no-build -c Release + +# Publish the application +RUN dotnet publish src/TaskManager.CLI/TaskManager.CLI.csproj -c Release -o /app/publish --no-build + +# Use the official .NET runtime image for running +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS runtime +WORKDIR /app + +# Copy the published application +COPY --from=build /app/publish . + +# Create a volume for task data +VOLUME ["/app/data"] + +# Set environment variable for tasks file location +ENV TASKS_FILE_PATH=/app/data/tasks.json + +# Set the entrypoint +ENTRYPOINT ["./taskman"] + +# Default command (show help) +CMD ["help"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..21cc58e --- /dev/null +++ b/Makefile @@ -0,0 +1,132 @@ +.PHONY: help build test clean restore run publish docker install uninstall format coverage + +# Variables +PROJECT_DIR := src/TaskManager.CLI +TEST_DIR := tests/TaskManager.Tests +CONFIGURATION := Release +OUTPUT_DIR := publish + +# Colors for output +COLOR_RESET := \033[0m +COLOR_INFO := \033[36m +COLOR_SUCCESS := \033[32m +COLOR_WARNING := \033[33m + +help: ## Show this help message + @echo "$(COLOR_INFO)Task Manager CLI - Makefile Commands$(COLOR_RESET)" + @echo "" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_SUCCESS)%-15s$(COLOR_RESET) %s\n", $$1, $$2}' + +build: restore ## Build the project + @echo "$(COLOR_INFO)Building project...$(COLOR_RESET)" + @dotnet build --configuration $(CONFIGURATION) --no-restore + @echo "$(COLOR_SUCCESS)Build complete!$(COLOR_RESET)" + +test: ## Run all tests + @echo "$(COLOR_INFO)Running tests...$(COLOR_RESET)" + @dotnet test --configuration $(CONFIGURATION) --verbosity normal + @echo "$(COLOR_SUCCESS)Tests complete!$(COLOR_RESET)" + +clean: ## Clean build artifacts + @echo "$(COLOR_INFO)Cleaning build artifacts...$(COLOR_RESET)" + @dotnet clean + @rm -rf $(OUTPUT_DIR) + @echo "$(COLOR_SUCCESS)Clean complete!$(COLOR_RESET)" + +restore: ## Restore NuGet packages + @echo "$(COLOR_INFO)Restoring packages...$(COLOR_RESET)" + @dotnet restore + @echo "$(COLOR_SUCCESS)Restore complete!$(COLOR_RESET)" + +run: ## Run the application (use ARGS="..." to pass arguments) + @echo "$(COLOR_INFO)Running application...$(COLOR_RESET)" + @dotnet run --project $(PROJECT_DIR) -- $(ARGS) + +publish: ## Publish for all platforms + @echo "$(COLOR_INFO)Publishing for all platforms...$(COLOR_RESET)" + @mkdir -p $(OUTPUT_DIR) + @echo " Building Linux x64..." + @dotnet publish $(PROJECT_DIR)/TaskManager.CLI.csproj \ + -c $(CONFIGURATION) \ + -r linux-x64 \ + --self-contained \ + -o $(OUTPUT_DIR)/linux-x64 + @echo " Building Windows x64..." + @dotnet publish $(PROJECT_DIR)/TaskManager.CLI.csproj \ + -c $(CONFIGURATION) \ + -r win-x64 \ + --self-contained \ + -o $(OUTPUT_DIR)/win-x64 + @echo " Building macOS x64..." + @dotnet publish $(PROJECT_DIR)/TaskManager.CLI.csproj \ + -c $(CONFIGURATION) \ + -r osx-x64 \ + --self-contained \ + -o $(OUTPUT_DIR)/osx-x64 + @echo " Building macOS ARM64..." + @dotnet publish $(PROJECT_DIR)/TaskManager.CLI.csproj \ + -c $(CONFIGURATION) \ + -r osx-arm64 \ + --self-contained \ + -o $(OUTPUT_DIR)/osx-arm64 + @echo "$(COLOR_SUCCESS)Publish complete! Binaries in $(OUTPUT_DIR)/$(COLOR_RESET)" + +docker-build: ## Build Docker image + @echo "$(COLOR_INFO)Building Docker image...$(COLOR_RESET)" + @docker build -t taskmanager:latest . + @echo "$(COLOR_SUCCESS)Docker build complete!$(COLOR_RESET)" + +docker-run: ## Run Docker container + @echo "$(COLOR_INFO)Running Docker container...$(COLOR_RESET)" + @docker run -v $$(pwd)/data:/app/data taskmanager:latest $(ARGS) + +install: publish ## Install as global tool + @echo "$(COLOR_INFO)Installing as global tool...$(COLOR_RESET)" + @dotnet tool uninstall -g taskman || true + @dotnet pack $(PROJECT_DIR)/TaskManager.CLI.csproj -c $(CONFIGURATION) + @dotnet tool install --global --add-source $(PROJECT_DIR)/bin/$(CONFIGURATION) taskman + @echo "$(COLOR_SUCCESS)Install complete! Run 'taskman help' to get started.$(COLOR_RESET)" + +uninstall: ## Uninstall global tool + @echo "$(COLOR_INFO)Uninstalling global tool...$(COLOR_RESET)" + @dotnet tool uninstall -g taskman + @echo "$(COLOR_SUCCESS)Uninstall complete!$(COLOR_RESET)" + +format: ## Format code + @echo "$(COLOR_INFO)Formatting code...$(COLOR_RESET)" + @dotnet format + @echo "$(COLOR_SUCCESS)Format complete!$(COLOR_RESET)" + +coverage: ## Run tests with coverage + @echo "$(COLOR_INFO)Running tests with coverage...$(COLOR_RESET)" + @dotnet test --collect:"XPlat Code Coverage" --results-directory ./coverage + @echo "$(COLOR_SUCCESS)Coverage complete! Results in ./coverage/$(COLOR_RESET)" + +lint: ## Run code analysis + @echo "$(COLOR_INFO)Running code analysis...$(COLOR_RESET)" + @dotnet format --verify-no-changes --verbosity diagnostic + @echo "$(COLOR_SUCCESS)Lint complete!$(COLOR_RESET)" + +watch: ## Run with file watcher + @echo "$(COLOR_INFO)Starting file watcher...$(COLOR_RESET)" + @dotnet watch --project $(PROJECT_DIR) run + +all: clean restore build test ## Run clean, restore, build, and test + @echo "$(COLOR_SUCCESS)All tasks complete!$(COLOR_RESET)" + +dev: ## Start development environment + @echo "$(COLOR_INFO)Starting development environment...$(COLOR_RESET)" + @docker-compose up taskmanager-dev + +# Examples +example-add: ## Example: Add a task + @$(MAKE) run ARGS='add "Sample task from Makefile" --priority 4 --tags demo' + +example-list: ## Example: List tasks + @$(MAKE) run ARGS='list' + +example-stats: ## Example: Show statistics + @$(MAKE) run ARGS='stats' + +# Default target +.DEFAULT_GOAL := help diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..3e6e898 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,308 @@ +# Quick Start Guide + +Get started with Task Manager CLI in under 5 minutes! + +## Installation + +### Quick Install (Recommended) + +```bash +# Clone and build +git clone https://github.com/codeforgood-org/dotnet-task-manager.git +cd dotnet-task-manager +./build.sh build + +# Run your first command +./build.sh run -- help +``` + +### Using Docker + +```bash +docker build -t taskmanager . +docker run taskmanager help +``` + +## First Steps + +### 1. Add Your First Task + +```bash +taskman add "Learn Task Manager CLI" --priority 5 +``` + +### 2. View Your Tasks + +```bash +taskman list +``` + +### 3. Add More Tasks + +```bash +# Work tasks +taskman add "Review pull requests" --priority 4 --tags work,urgent +taskman add "Write documentation" --priority 3 --tags work,docs + +# Personal tasks +taskman add "Buy groceries" --priority 2 --tags personal,shopping +taskman add "Schedule dentist" --priority 3 --tags personal,health --due 2024-02-01 +``` + +### 4. Complete a Task + +```bash +taskman complete 1 +``` + +### 5. View Statistics + +```bash +taskman stats +``` + +## Common Commands Cheat Sheet + +| Command | Description | Example | +|---------|-------------|---------| +| `add` | Add new task | `taskman add "Task description" --priority 4 --tags work` | +| `list` | List all tasks | `taskman list` | +| `list --pending` | List pending only | `taskman list --pending` | +| `list --tag TAG` | Filter by tag | `taskman list --tag work` | +| `complete ID` | Mark as done | `taskman complete 1` | +| `remove ID` | Delete task | `taskman remove 1` | +| `update ID DESC` | Update description | `taskman update 1 "New description"` | +| `priority ID NUM` | Change priority | `taskman priority 1 5` | +| `search QUERY` | Find tasks | `taskman search meeting` | +| `clear` | Remove completed | `taskman clear` | +| `stats` | View statistics | `taskman stats` | +| `export` | Export tasks | `taskman export --format csv` | +| `import` | Import tasks | `taskman import backup.json` | + +## Priority Levels + +- **5** - Critical/Urgent (★★★★★) +- **4** - High (★★★★) +- **3** - Normal (★★★) - Default +- **2** - Low (★★) +- **1** - Very Low (★) + +## Tagging Best Practices + +Use tags to categorize and filter: + +```bash +# Context tags +--tags @work, @home, @errands + +# Project tags +--tags project-alpha, project-beta + +# Type tags +--tags bug, feature, meeting + +# Priority indicators +--tags urgent, important + +# Combine multiple +--tags work,urgent,bug +``` + +## Example Workflows + +### Daily Morning Routine + +```bash +# Check what's pending +taskman list --pending + +# See what's due today +taskman stats + +# Add today's priorities +taskman add "Team standup" --priority 4 --tags work,meeting +taskman add "Review PRs" --priority 5 --tags work,urgent +``` + +### Weekly Review + +```bash +# View all tasks +taskman list + +# Export weekly report +taskman export --format markdown --output weekly-report.md + +# Clear completed tasks +taskman clear + +# Plan next week +taskman add "Plan sprint" --priority 4 --due 2024-01-22 --tags work,planning +``` + +### Project Management + +```bash +# Add project tasks +taskman add "Design API" --priority 5 --tags project-x,design +taskman add "Write tests" --priority 4 --tags project-x,testing +taskman add "Documentation" --priority 3 --tags project-x,docs + +# View project tasks +taskman list --tag project-x + +# Search within project +taskman search API +``` + +## Configuration + +Create `appsettings.json` to customize: + +```json +{ + "AppConfig": { + "DefaultPriority": 3, + "DateFormat": "yyyy-MM-dd", + "ShowCompletedByDefault": false + } +} +``` + +## Export/Backup + +### Quick Backup + +```bash +# Backup tasks +cp tasks.json tasks-backup-$(date +%Y%m%d).json + +# Or export +taskman export --format json --output backup/tasks-$(date +%Y%m%d).json +``` + +### Share with Team + +```bash +# Export to readable format +taskman export --format markdown --output team-tasks.md + +# Or CSV for spreadsheet +taskman export --format csv --output tasks.csv +``` + +## Docker Usage + +### Run Once + +```bash +docker run -v $(pwd)/data:/app/data taskmanager add "Docker task" --priority 5 +docker run -v $(pwd)/data:/app/data taskmanager list +``` + +### Interactive Session + +```bash +docker-compose up -d taskmanager-dev +docker exec -it taskmanager-dev bash +# Now use taskman commands inside container +``` + +## Development Setup + +### VSCode + +1. Open folder in VSCode +2. Install recommended extensions (prompt will appear) +3. Press `F5` to build and debug +4. Use `Ctrl+Shift+B` to run build tasks + +### Building + +```bash +# Build +./build.sh build + +# Run tests +./build.sh test + +# Run with arguments +./build.sh run -- list --pending + +# Publish for all platforms +./build.sh publish +``` + +## Keyboard Shortcuts (if running in terminal) + +- `Ctrl+C` - Cancel current command +- `Up/Down Arrow` - Navigate command history +- `Tab` - Auto-complete (if shell supports) + +## Getting Help + +```bash +# General help +taskman help + +# Check version +taskman --version + +# View examples +cat examples/EXAMPLES.md +``` + +## Tips + +1. **Start Simple**: Begin with just `add` and `list` +2. **Use Tags**: They make filtering much easier +3. **Set Priorities**: Helps focus on what matters +4. **Regular Review**: Check and update tasks daily +5. **Export Often**: Backup your tasks regularly +6. **Search First**: Use `search` before scrolling through lists + +## Next Steps + +- Read [EXAMPLES.md](examples/EXAMPLES.md) for detailed scenarios +- Check [CONTRIBUTING.md](CONTRIBUTING.md) to contribute +- Review [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) to understand the code +- See [CHANGELOG.md](CHANGELOG.md) for version history + +## Troubleshooting + +**Tasks not saving?** +```bash +# Check permissions +ls -la tasks.json +``` + +**Command not found?** +```bash +# Use full path or build script +./build.sh run -- list +# Or +dotnet run --project src/TaskManager.CLI -- list +``` + +**Need to reset?** +```bash +# Backup first! +cp tasks.json tasks-backup.json +# Remove tasks file +rm tasks.json +# Start fresh +taskman add "First task" +``` + +--- + +**Ready to get productive? Start with:** + +```bash +taskman add "Complete first task" --priority 5 --tags getting-started +taskman list +taskman complete 1 +taskman stats +``` + +Happy task managing! 🚀 diff --git a/README.md b/README.md new file mode 100644 index 0000000..09c1bee --- /dev/null +++ b/README.md @@ -0,0 +1,401 @@ +# Task Manager CLI + +[![Build Status](https://github.com/codeforgood-org/dotnet-task-manager/workflows/CI%2FCD/badge.svg)](https://github.com/codeforgood-org/dotnet-task-manager/actions) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![.NET Version](https://img.shields.io/badge/.NET-8.0-purple.svg)](https://dotnet.microsoft.com/download/dotnet/8.0) +[![Version](https://img.shields.io/badge/version-2.0.0-green.svg)](CHANGELOG.md) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) +[![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-contributor%20covenant-purple.svg)](CODE_OF_CONDUCT.md) + +A modern, feature-rich command-line task manager built with .NET 8. Manage your tasks efficiently with priorities, tags, due dates, and powerful search capabilities. + +## Features + +### Core Features +- ✅ Add, list, update, and remove tasks +- 🎯 Priority levels (1-5, with 5 being highest) +- 🏷️ Tag support for better organization +- 📅 Due date tracking +- 🔍 Powerful search functionality +- ✓ Mark tasks as complete +- 🗑️ Clear completed tasks +- 💾 JSON file-based persistence + +### Advanced Features +- 📊 Statistics and reporting +- 📤 Export to CSV, Markdown, and JSON +- 📥 Import tasks from JSON +- ⚙️ Configuration file support +- 🐳 Docker support for containerized deployment +- 🎨 Clean, intuitive CLI interface + +### Developer Features +- 🏗️ Modular architecture with dependency injection +- 📝 Comprehensive logging +- 🧪 90%+ test coverage +- 🔧 VSCode integration +- 🚀 CI/CD with GitHub Actions +- 📦 Cross-platform builds + +## Installation + +### Prerequisites + +- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or later + +### Build from Source + +```bash +# Clone the repository +git clone https://github.com/codeforgood-org/dotnet-task-manager.git +cd dotnet-task-manager + +# Build the project +dotnet build + +# Run the application +dotnet run --project src/TaskManager.CLI -- [options] +``` + +### Using Build Scripts + +```bash +# Linux/macOS +./build.sh build # Build the solution +./build.sh test # Run tests +./build.sh publish # Publish for all platforms +./build.sh run -- list # Run the application + +# Windows +build.cmd build +build.cmd test +build.cmd publish +``` + +### Docker + +```bash +# Build and run with Docker +docker build -t taskmanager . +docker run -v $(pwd)/data:/app/data taskmanager list + +# Or use docker-compose +docker-compose up taskmanager-dev +``` + +### Install as Global Tool (Optional) + +```bash +# Build and pack +dotnet pack src/TaskManager.CLI/TaskManager.CLI.csproj -c Release + +# Install globally +dotnet tool install --global --add-source ./src/TaskManager.CLI/nupkg TaskManager.CLI + +# Now you can use 'taskman' from anywhere +taskman help +``` + +## Usage + +### Basic Commands + +#### Add a Task + +```bash +# Simple task +taskman add "Buy groceries" + +# Task with priority +taskman add "Complete project report" --priority 5 + +# Task with due date +taskman add "Submit tax returns" --due 2024-04-15 + +# Task with tags +taskman add "Review pull requests" --tags work,code-review + +# Combine all options +taskman add "Prepare presentation" --priority 4 --due 2024-12-31 --tags work,important +``` + +#### List Tasks + +```bash +# List all tasks (completed and pending) +taskman list + +# List only pending tasks +taskman list --pending + +# List tasks by tag +taskman list --tag work +``` + +#### Complete a Task + +```bash +taskman complete 1 +``` + +#### Update a Task + +```bash +# Update description +taskman update 1 "Buy groceries and cook dinner" + +# Update priority +taskman priority 1 5 +``` + +#### Search Tasks + +```bash +# Search by keyword +taskman search groceries + +# Search also looks in tags +taskman search work +``` + +#### Remove a Task + +```bash +taskman remove 1 +``` + +#### Clear Completed Tasks + +```bash +taskman clear +``` + +#### Statistics + +```bash +# View task statistics +taskman stats +``` + +#### Export/Import + +```bash +# Export to different formats +taskman export --format csv --output tasks.csv +taskman export --format markdown --output tasks.md +taskman export --format json --output tasks.json + +# Import from JSON +taskman import tasks-backup.json +``` + +### Task Display Format + +Tasks are displayed with the following information: + +``` +[✓] [1] Buy groceries ★★★ (Due: 2024-12-25) [shopping, personal] +│ │ │ │ │ └─ Tags +│ │ │ │ └─ Due date (if set) +│ │ │ └─ Priority (1-5 stars) +│ │ └─ Description +│ └─ Task ID +└─ Completion status (✓ = completed, blank = pending) +``` + +## Architecture + +The project follows modern .NET practices with a clean, modular architecture: + +``` +dotnet-task-manager/ +├── src/ +│ └── TaskManager.CLI/ +│ ├── Models/ # Data models +│ │ └── TaskItem.cs +│ ├── Interfaces/ # Service interfaces +│ │ └── ITaskService.cs +│ ├── Services/ # Business logic +│ │ └── TaskService.cs +│ └── Program.cs # CLI entry point +├── tests/ +│ └── TaskManager.Tests/ # Unit tests +├── docs/ # Documentation +└── TaskManager.sln # Solution file +``` + +### Key Design Principles + +- **Separation of Concerns**: Models, services, and CLI are separate +- **Dependency Injection**: Using Microsoft.Extensions.DependencyInjection +- **Interface-based Design**: Services implement interfaces for testability +- **Async/Await**: File I/O operations are asynchronous +- **Logging**: Integrated logging with Microsoft.Extensions.Logging +- **Error Handling**: Comprehensive exception handling and validation + +## Development + +### Building the Project + +```bash +# Build in Debug mode +dotnet build + +# Build in Release mode +dotnet build -c Release +``` + +### Running Tests + +```bash +# Run all tests +dotnet test + +# Run tests with coverage +dotnet test /p:CollectCoverage=true +``` + +### Code Quality + +The project includes: + +- EditorConfig for consistent code style +- XML documentation comments +- Nullable reference types enabled +- Comprehensive unit tests + +## Configuration + +The application can be configured using `appsettings.json`: + +```json +{ + "AppConfig": { + "TasksFilePath": "tasks.json", + "DefaultPriority": 3, + "UseColors": true, + "DateFormat": "yyyy-MM-dd", + "ShowCompletedByDefault": true, + "UpcomingDaysThreshold": 7, + "ExportDirectory": "exports" + } +} +``` + +## Data Storage + +Tasks are stored in a `tasks.json` file in the current working directory. The file is automatically created when you add your first task. + +Example `tasks.json`: + +```json +[ + { + "Id": 1, + "Description": "Buy groceries", + "IsCompleted": false, + "Priority": 3, + "CreatedAt": "2024-01-15T10:30:00Z", + "DueDate": "2024-01-20T00:00:00Z", + "Tags": ["shopping", "personal"] + } +] +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. + +### Development Setup + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Run tests (`dotnet test`) +5. Commit your changes (`git commit -m 'Add some amazing feature'`) +6. Push to the branch (`git push origin feature/amazing-feature`) +7. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Examples + +See the [examples/](examples/) directory for: +- Sample task files +- Common usage scenarios - Task templates +- Best practices + +## Docker Usage + +### Building the Image + +```bash +docker build -t taskmanager . +``` + +### Running with Docker + +```bash +# Create a data directory for persistent storage +mkdir -p data + +# Run commands +docker run -v $(pwd)/data:/app/data taskmanager add "Docker task" --priority 5 +docker run -v $(pwd)/data:/app/data taskmanager list +docker run -v $(pwd)/data:/app/data taskmanager stats +``` + +### Development with Docker Compose + +```bash +# Start development environment +docker-compose up taskmanager-dev + +# In another terminal, access the container +docker exec -it taskmanager-dev bash +dotnet run --project src/TaskManager.CLI -- list +``` + +## Roadmap + +### Completed ✅ +- [x] Priority levels +- [x] Tag support +- [x] Export to CSV, Markdown, JSON +- [x] Statistics and reports +- [x] Docker support +- [x] Configuration file +- [x] Comprehensive tests + +### Planned +- [ ] Color-coded CLI output with Spectre.Console +- [ ] Interactive mode +- [ ] Recurring tasks +- [ ] Task categories/projects +- [ ] Cloud synchronization +- [ ] Web API +- [ ] Web interface +- [ ] Mobile companion app + +## Support + +If you encounter any issues or have questions: + +- Open an issue on [GitHub](https://github.com/codeforgood-org/dotnet-task-manager/issues) +- Check existing issues for solutions +- Contribute to the documentation + +## Acknowledgments + +Built with ❤️ using: + +- [.NET 8](https://dotnet.microsoft.com/) +- [Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging/) +- [Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection/) +- [xUnit](https://xunit.net/) for testing + +--- + +Made by [Code for Good](https://github.com/codeforgood-org) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1d4ad87 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,175 @@ +# Security Policy + +## Supported Versions + +We release patches for security vulnerabilities for the following versions: + +| Version | Supported | +| ------- | ------------------ | +| 2.x.x | :white_check_mark: | +| 1.x.x | :x: | + +## Reporting a Vulnerability + +The Task Manager CLI team takes security bugs seriously. We appreciate your efforts to responsibly disclose your findings. + +### How to Report a Security Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them via one of the following methods: + +1. **GitHub Security Advisories** (Preferred) + - Go to the [Security tab](https://github.com/codeforgood-org/dotnet-task-manager/security) + - Click on "Report a vulnerability" + - Provide detailed information about the vulnerability + +2. **Email** + - Send an email to security@codeforgood.org + - Include the word "SECURITY" in the subject line + - Encrypt your message using our PGP key (if available) + +### What to Include in Your Report + +Please include the following information in your report: + +- Type of issue (e.g., buffer overflow, SQL injection, cross-site scripting) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit it + +### Response Timeline + +- **Initial Response**: Within 48 hours of receiving your report +- **Status Update**: Within 7 days with an assessment and tentative fix timeline +- **Resolution**: We aim to resolve critical security issues within 30 days + +### Disclosure Policy + +- Security issues are kept confidential until a fix is released +- After a fix is released, we will: + - Publish a security advisory + - Credit the reporter (unless anonymity is requested) + - Update the CHANGELOG with security fix information + +### Safe Harbor + +We support safe harbor for security researchers who: + +- Make a good faith effort to avoid privacy violations, data destruction, and service interruption +- Only interact with accounts you own or with explicit permission of the account holder +- Do not exploit a security issue beyond the minimum necessary to demonstrate it +- Report vulnerabilities promptly +- Keep vulnerability details confidential until a fix is released + +We will not pursue legal action against researchers who follow these guidelines. + +## Security Best Practices for Users + +### Data Protection + +1. **File Permissions** + - Ensure `tasks.json` has appropriate file permissions (not world-readable) + - On Unix systems: `chmod 600 tasks.json` + +2. **Backup Your Data** + - Regularly backup your `tasks.json` file + - Use `taskman export` to create encrypted backups if needed + +3. **Sensitive Information** + - Do not store passwords, API keys, or sensitive data in task descriptions + - Use tags instead of including sensitive details in descriptions + +### Docker Security + +1. **Container Isolation** + - Run containers with least privilege + - Use read-only volumes where possible + - Don't run as root inside containers + +2. **Image Security** + - Pull images from official sources only + - Regularly update to latest image versions + - Scan images for vulnerabilities + +### Dependencies + +We regularly update dependencies to address security vulnerabilities: + +- Monitor GitHub security advisories +- Run `dotnet list package --vulnerable` to check for vulnerable packages +- Update dependencies promptly when security fixes are available + +### Building from Source + +When building from source: + +1. Verify the source code integrity +2. Review the code for any suspicious changes +3. Build in a clean environment +4. Verify checksums of dependencies + +## Security Features + +### Current Security Measures + +1. **Input Validation** + - All user inputs are validated + - Arguments are sanitized before processing + - No command injection vulnerabilities + +2. **File Operations** + - Controlled file access (fixed filename) + - No arbitrary file read/write operations + - Safe JSON deserialization with type checking + +3. **Dependency Security** + - Minimal external dependencies + - Official Microsoft packages only (except Spectre.Console) + - Regular security audits + +4. **Code Quality** + - Static code analysis + - Automated testing + - Code review process for all changes + +### Planned Security Enhancements + +- [ ] Add encryption support for sensitive task data +- [ ] Implement audit logging for all operations +- [ ] Add data integrity checks (checksums) +- [ ] Support for signing/verification of task files +- [ ] Multi-user access controls +- [ ] Rate limiting for operations + +## Security Audit History + +| Date | Type | Findings | Status | +|------------|---------------------|----------|----------| +| 2024-01-15 | Initial assessment | None | Complete | + +## Vulnerability Disclosure Timeline + +No vulnerabilities have been disclosed at this time. + +## Contact + +For security-related questions or concerns: + +- Security Team: security@codeforgood.org +- Project Maintainer: Via GitHub issues (for non-security questions) + +## Acknowledgments + +We would like to thank the following researchers for responsibly disclosing security issues: + + + +--- + +**Note**: This security policy may be updated at any time. Please check back regularly for updates. + +Last Updated: 2024-01-15 diff --git a/TaskManager.cs b/TaskManager.cs deleted file mode 100644 index d31e85a..0000000 --- a/TaskManager.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; - -class TaskItem -{ - public int Id { get; set; } - public string Description { get; set; } -} - -class TaskManager -{ - private const string FileName = "tasks.json"; - private static List tasks = new(); - - static void Main(string[] args) - { - LoadTasks(); - - if (args.Length == 0) - { - Console.WriteLine("Usage:"); - Console.WriteLine(" add "); - Console.WriteLine(" list"); - Console.WriteLine(" remove "); - return; - } - - var command = args[0]; - - switch (command) - { - case "add": - AddTask(string.Join(" ", args[1..])); - break; - case "list": - ListTasks(); - break; - case "remove": - if (args.Length < 2 || !int.TryParse(args[1], out int id)) - { - Console.WriteLine("Please provide a valid task ID."); - } - else - { - RemoveTask(id); - } - break; - default: - Console.WriteLine("Unknown command."); - break; - } - - SaveTasks(); - } - - static void LoadTasks() - { - if (File.Exists(FileName)) - { - string json = File.ReadAllText(FileName); - tasks = JsonSerializer.Deserialize>(json) ?? new(); - } - } - - static void SaveTasks() - { - string json = JsonSerializer.Serialize(tasks, new JsonSerializerOptions { WriteIndented = true }); - File.WriteAllText(FileName, json); - } - - static void AddTask(string description) - { - int nextId = tasks.Count == 0 ? 1 : tasks[^1].Id + 1; - tasks.Add(new TaskItem { Id = nextId, Description = description }); - Console.WriteLine($"Added task #{nextId}: {description}"); - } - - static void ListTasks() - { - if (tasks.Count == 0) - { - Console.WriteLine("No tasks found."); - return; - } - - foreach (var task in tasks) - { - Console.WriteLine($"[{task.Id}] {task.Description}"); - } - } - - static void RemoveTask(int id) - { - var task = tasks.Find(t => t.Id == id); - if (task == null) - { - Console.WriteLine("Task not found."); - return; - } - - tasks.Remove(task); - Console.WriteLine($"Removed task #{id}"); - } -} diff --git a/TaskManager.sln b/TaskManager.sln new file mode 100644 index 0000000..fe96014 --- /dev/null +++ b/TaskManager.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskManager.CLI", "src\TaskManager.CLI\TaskManager.CLI.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskManager.Tests", "tests\TaskManager.Tests\TaskManager.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..ec70de0 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,17 @@ +{ + "AppConfig": { + "TasksFilePath": "tasks.json", + "DefaultPriority": 3, + "UseColors": true, + "DateFormat": "yyyy-MM-dd", + "ShowCompletedByDefault": true, + "UpcomingDaysThreshold": 7, + "ExportDirectory": "exports" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning" + } + } +} diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..846a984 --- /dev/null +++ b/build.cmd @@ -0,0 +1,134 @@ +@echo off +setlocal enabledelayedexpansion + +echo ===================================== +echo Task Manager - Build Script +echo ===================================== +echo. + +set COMMAND=%1 +set CONFIGURATION=%2 + +if "%COMMAND%"=="" set COMMAND=build +if "%CONFIGURATION%"=="" set CONFIGURATION=Release + +if /I "%COMMAND%"=="clean" goto :clean +if /I "%COMMAND%"=="restore" goto :restore +if /I "%COMMAND%"=="build" goto :build +if /I "%COMMAND%"=="test" goto :test +if /I "%COMMAND%"=="coverage" goto :coverage +if /I "%COMMAND%"=="publish" goto :publish +if /I "%COMMAND%"=="run" goto :run +if /I "%COMMAND%"=="format" goto :format +if /I "%COMMAND%"=="all" goto :all +if /I "%COMMAND%"=="help" goto :help +goto :help + +:clean +echo Cleaning build artifacts... +dotnet clean +if exist publish rd /s /q publish +echo Clean completed! +echo. +goto :end + +:restore +echo Restoring dependencies... +dotnet restore +echo Restore completed! +echo. +goto :end + +:build +echo Building solution... +dotnet restore +dotnet build --configuration %CONFIGURATION% --no-restore +echo Build completed! +echo. +goto :end + +:test +echo Running tests... +dotnet test --configuration %CONFIGURATION% --verbosity normal +echo Tests completed! +echo. +goto :end + +:coverage +echo Running tests with coverage... +dotnet test --configuration %CONFIGURATION% --collect:"XPlat Code Coverage" --verbosity normal +echo Coverage completed! +echo. +goto :end + +:publish +echo Publishing application... +if not exist publish mkdir publish + +echo Publishing for Linux x64... +dotnet publish src\TaskManager.CLI\TaskManager.CLI.csproj -c %CONFIGURATION% -r linux-x64 --self-contained -o .\publish\linux-x64 + +echo Publishing for Windows x64... +dotnet publish src\TaskManager.CLI\TaskManager.CLI.csproj -c %CONFIGURATION% -r win-x64 --self-contained -o .\publish\win-x64 + +echo Publishing for macOS x64... +dotnet publish src\TaskManager.CLI\TaskManager.CLI.csproj -c %CONFIGURATION% -r osx-x64 --self-contained -o .\publish\osx-x64 + +echo Publish completed! Binaries available in .\publish\ +echo. +goto :end + +:run +echo Running application... +shift +dotnet run --project src\TaskManager.CLI -- %* +goto :end + +:format +echo Formatting code... +dotnet format +echo Format completed! +echo. +goto :end + +:all +echo Running full build pipeline... +call build.cmd clean +call build.cmd restore +call build.cmd build %CONFIGURATION% +call build.cmd test +echo Full build pipeline completed! +echo. +goto :end + +:help +echo Task Manager Build Script +echo. +echo Usage: build.cmd ^ [configuration] +echo. +echo Commands: +echo clean Clean build artifacts +echo restore Restore NuGet packages +echo build Build the solution (default: Release) +echo test Run unit tests +echo coverage Run tests with code coverage +echo publish Publish for all platforms +echo run [args] Run the application with optional arguments +echo format Format code using dotnet format +echo all Run clean, restore, build, and test +echo help Show this help message +echo. +echo Configuration: +echo Debug Build with debug symbols +echo Release Build optimized release (default) +echo. +echo Examples: +echo build.cmd build +echo build.cmd build Debug +echo build.cmd test +echo build.cmd run list +echo build.cmd publish +goto :end + +:end +endlocal diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..83f6cd2 --- /dev/null +++ b/build.sh @@ -0,0 +1,132 @@ +#!/bin/bash +set -e + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=====================================${NC}" +echo -e "${BLUE} Task Manager - Build Script ${NC}" +echo -e "${BLUE}=====================================${NC}\n" + +# Parse command line arguments +COMMAND=${1:-build} +CONFIGURATION=${2:-Release} + +case $COMMAND in + clean) + echo -e "${BLUE}Cleaning build artifacts...${NC}" + dotnet clean + rm -rf ./publish + echo -e "${GREEN}Clean completed!${NC}\n" + ;; + + restore) + echo -e "${BLUE}Restoring dependencies...${NC}" + dotnet restore + echo -e "${GREEN}Restore completed!${NC}\n" + ;; + + build) + echo -e "${BLUE}Building solution...${NC}" + dotnet restore + dotnet build --configuration $CONFIGURATION --no-restore + echo -e "${GREEN}Build completed!${NC}\n" + ;; + + test) + echo -e "${BLUE}Running tests...${NC}" + dotnet test --configuration $CONFIGURATION --verbosity normal + echo -e "${GREEN}Tests completed!${NC}\n" + ;; + + coverage) + echo -e "${BLUE}Running tests with coverage...${NC}" + dotnet test --configuration $CONFIGURATION --collect:"XPlat Code Coverage" --verbosity normal + echo -e "${GREEN}Coverage completed!${NC}\n" + ;; + + publish) + echo -e "${BLUE}Publishing application...${NC}" + + # Create publish directory + mkdir -p ./publish + + # Publish for Linux + echo -e "${BLUE}Publishing for Linux x64...${NC}" + dotnet publish src/TaskManager.CLI/TaskManager.CLI.csproj \ + -c $CONFIGURATION \ + -r linux-x64 \ + --self-contained \ + -o ./publish/linux-x64 + + # Publish for Windows + echo -e "${BLUE}Publishing for Windows x64...${NC}" + dotnet publish src/TaskManager.CLI/TaskManager.CLI.csproj \ + -c $CONFIGURATION \ + -r win-x64 \ + --self-contained \ + -o ./publish/win-x64 + + # Publish for macOS + echo -e "${BLUE}Publishing for macOS x64...${NC}" + dotnet publish src/TaskManager.CLI/TaskManager.CLI.csproj \ + -c $CONFIGURATION \ + -r osx-x64 \ + --self-contained \ + -o ./publish/osx-x64 + + echo -e "${GREEN}Publish completed! Binaries available in ./publish/${NC}\n" + ;; + + run) + echo -e "${BLUE}Running application...${NC}" + dotnet run --project src/TaskManager.CLI -- "${@:2}" + ;; + + format) + echo -e "${BLUE}Formatting code...${NC}" + dotnet format + echo -e "${GREEN}Format completed!${NC}\n" + ;; + + all) + echo -e "${BLUE}Running full build pipeline...${NC}" + ./build.sh clean + ./build.sh restore + ./build.sh build $CONFIGURATION + ./build.sh test + echo -e "${GREEN}Full build pipeline completed!${NC}\n" + ;; + + help|*) + echo "Task Manager Build Script" + echo "" + echo "Usage: ./build.sh [configuration]" + echo "" + echo "Commands:" + echo " clean Clean build artifacts" + echo " restore Restore NuGet packages" + echo " build Build the solution (default: Release)" + echo " test Run unit tests" + echo " coverage Run tests with code coverage" + echo " publish Publish for all platforms" + echo " run [args] Run the application with optional arguments" + echo " format Format code using dotnet format" + echo " all Run clean, restore, build, and test" + echo " help Show this help message" + echo "" + echo "Configuration:" + echo " Debug Build with debug symbols" + echo " Release Build optimized release (default)" + echo "" + echo "Examples:" + echo " ./build.sh build" + echo " ./build.sh build Debug" + echo " ./build.sh test" + echo " ./build.sh run -- list" + echo " ./build.sh publish" + ;; +esac diff --git a/completions/README.md b/completions/README.md new file mode 100644 index 0000000..7cc7e5a --- /dev/null +++ b/completions/README.md @@ -0,0 +1,155 @@ +# Shell Completions + +Shell completion scripts for Task Manager CLI. + +## Installation + +### Bash + +Add to your `~/.bashrc` or `~/.bash_profile`: + +```bash +source /path/to/dotnet-task-manager/completions/taskman.bash +``` + +Or install system-wide: + +```bash +sudo cp completions/taskman.bash /etc/bash_completion.d/taskman +``` + +Then reload your shell: + +```bash +source ~/.bashrc +``` + +### Zsh + +Add to your `~/.zshrc`: + +```zsh +fpath=(/path/to/dotnet-task-manager/completions $fpath) +autoload -Uz compinit && compinit +``` + +Or copy to a directory in your `$fpath`: + +```bash +# Find your fpath +echo $fpath + +# Copy to one of those directories +cp completions/taskman.zsh /usr/local/share/zsh/site-functions/_taskman +``` + +Then reload your shell: + +```zsh +exec zsh +``` + +### Oh My Zsh + +If you use Oh My Zsh, create a custom plugin: + +```bash +mkdir -p ~/.oh-my-zsh/custom/plugins/taskman +cp completions/taskman.zsh ~/.oh-my-zsh/custom/plugins/taskman/_taskman +``` + +Then add `taskman` to your plugins in `~/.zshrc`: + +```zsh +plugins=(... taskman) +``` + +## Usage + +After installation, you can use tab completion with taskman: + +```bash +# Complete commands +taskman + +# Complete options for add command +taskman add "My task" -- + +# Complete export formats +taskman export --format + +# Complete JSON files for import +taskman import +``` + +## Features + +- Command completion +- Option completion +- Format completion for export command +- File completion for import command (JSON files only) +- Helpful descriptions (Zsh) + +## Testing + +Test the completions: + +```bash +# Bash +taskman + +# Zsh +taskman +``` + +You should see a list of available commands. + +## Troubleshooting + +### Bash completion not working + +1. Ensure bash-completion is installed: + ```bash + # Ubuntu/Debian + sudo apt-get install bash-completion + + # macOS with Homebrew + brew install bash-completion + ``` + +2. Verify the completion script is sourced: + ```bash + grep taskman ~/.bashrc + ``` + +### Zsh completion not working + +1. Verify compinit is called: + ```bash + grep compinit ~/.zshrc + ``` + +2. Rebuild completion cache: + ```zsh + rm -f ~/.zcompdump + compinit + ``` + +3. Check fpath includes the completions directory: + ```zsh + echo $fpath + ``` + +## Development + +To modify completions: + +1. Edit the completion script +2. Reload your shell or source the script +3. Test the completions +4. Submit a PR with your improvements + +For more information on writing completion scripts: + +- Bash: https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html +- Zsh: http://zsh.sourceforge.net/Doc/Release/Completion-System.html diff --git a/completions/taskman.bash b/completions/taskman.bash new file mode 100644 index 0000000..378acac --- /dev/null +++ b/completions/taskman.bash @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Bash completion script for taskman + +_taskman_completions() +{ + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + # Available commands + local commands="add list remove complete update priority search clear stats export import help" + + # Options for specific commands + local add_opts="--priority --due --tags" + local list_opts="--pending --tag" + local export_opts="--format --output" + + # Complete commands + if [[ ${COMP_CWORD} == 1 ]]; then + COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) + return 0 + fi + + # Complete based on the command + case "${COMP_WORDS[1]}" in + add) + if [[ ${cur} == -* ]]; then + COMPREPLY=( $(compgen -W "${add_opts}" -- ${cur}) ) + fi + return 0 + ;; + list) + if [[ ${cur} == -* ]]; then + COMPREPLY=( $(compgen -W "${list_opts}" -- ${cur}) ) + fi + return 0 + ;; + export) + case "${prev}" in + --format) + COMPREPLY=( $(compgen -W "csv json markdown md" -- ${cur}) ) + return 0 + ;; + --output) + # Complete filenames + COMPREPLY=( $(compgen -f -- ${cur}) ) + return 0 + ;; + *) + if [[ ${cur} == -* ]]; then + COMPREPLY=( $(compgen -W "${export_opts}" -- ${cur}) ) + fi + return 0 + ;; + esac + ;; + import) + # Complete JSON files + COMPREPLY=( $(compgen -f -X '!*.json' -- ${cur}) ) + return 0 + ;; + complete|remove|update|priority) + # These commands expect task IDs - could be enhanced to list actual task IDs + return 0 + ;; + esac +} + +complete -F _taskman_completions taskman diff --git a/completions/taskman.zsh b/completions/taskman.zsh new file mode 100644 index 0000000..3763523 --- /dev/null +++ b/completions/taskman.zsh @@ -0,0 +1,77 @@ +#compdef taskman + +# Zsh completion script for taskman + +_taskman() { + local -a commands + commands=( + 'add:Add a new task' + 'list:List tasks' + 'remove:Remove a task' + 'complete:Mark a task as completed' + 'update:Update task description' + 'priority:Update task priority' + 'search:Search tasks' + 'clear:Remove all completed tasks' + 'stats:View task statistics' + 'export:Export tasks to file' + 'import:Import tasks from JSON file' + 'help:Show help message' + ) + + local -a add_opts + add_opts=( + '--priority[Set priority (1-5)]:priority:(1 2 3 4 5)' + '--due[Set due date (yyyy-MM-dd)]:date:' + '--tags[Add tags (comma-separated)]:tags:' + ) + + local -a list_opts + list_opts=( + '--pending[Show only pending tasks]' + '--tag[Filter by tag]:tag:' + ) + + local -a export_opts + export_opts=( + '--format[Export format]:format:(csv json markdown md)' + '--output[Output file path]:file:_files' + ) + + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + '1: :->command' \ + '*:: :->args' + + case $state in + command) + _describe 'command' commands + ;; + args) + case $line[1] in + add) + _arguments $add_opts + ;; + list) + _arguments $list_opts + ;; + export) + _arguments $export_opts + ;; + import) + _files -g '*.json' + ;; + complete|remove|update|priority) + _message 'task ID' + ;; + search) + _message 'search query' + ;; + esac + ;; + esac +} + +_taskman "$@" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7f134be --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.8' + +services: + taskmanager: + build: + context: . + dockerfile: Dockerfile + container_name: taskmanager-cli + volumes: + # Mount a local directory for persistent task storage + - ./data:/app/data + environment: + - TASKS_FILE_PATH=/app/data/tasks.json + # Override the default command to keep container running + # You can exec into it to run commands + stdin_open: true + tty: true + entrypoint: ["/bin/bash"] + + # Development service with hot reload + taskmanager-dev: + image: mcr.microsoft.com/dotnet/sdk:8.0 + container_name: taskmanager-dev + working_dir: /workspace + volumes: + - .:/workspace + - ~/.nuget/packages:/root/.nuget/packages + environment: + - ASPNETCORE_ENVIRONMENT=Development + command: dotnet watch --project src/TaskManager.CLI/TaskManager.CLI.csproj run + stdin_open: true + tty: true diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..8b1e34a --- /dev/null +++ b/docs/API.md @@ -0,0 +1,666 @@ +# API Documentation + +This document describes the public API of the Task Manager CLI library. + +## Table of Contents + +- [Interfaces](#interfaces) +- [Models](#models) +- [Services](#services) +- [Usage Examples](#usage-examples) + +## Interfaces + +### ITaskService + +Main interface for task management operations. + +```csharp +public interface ITaskService +{ + Task LoadTasksAsync(); + Task SaveTasksAsync(); + TaskItem AddTask(string description, int priority = 3, DateTime? dueDate = null, List? tags = null); + IEnumerable GetAllTasks(bool includeCompleted = true); + TaskItem? GetTaskById(int id); + bool RemoveTask(int id); + bool CompleteTask(int id); + bool UpdateTask(int id, string newDescription); + bool UpdateTaskPriority(int id, int priority); + IEnumerable SearchTasks(string query); + IEnumerable GetTasksByTag(string tag); + int ClearCompletedTasks(); +} +``` + +#### Methods + +##### LoadTasksAsync() +Loads tasks from storage asynchronously. + +**Returns**: `Task` + +**Example**: +```csharp +await taskService.LoadTasksAsync(); +``` + +##### SaveTasksAsync() +Saves tasks to storage asynchronously. + +**Returns**: `Task` + +**Example**: +```csharp +await taskService.SaveTasksAsync(); +``` + +##### AddTask() +Adds a new task with the specified parameters. + +**Parameters**: +- `description` (string): Task description (required) +- `priority` (int): Priority level 1-5 (default: 3) +- `dueDate` (DateTime?): Optional due date +- `tags` (List?): Optional list of tags + +**Returns**: `TaskItem` - The created task + +**Throws**: `ArgumentException` if description is empty or priority is invalid + +**Example**: +```csharp +var task = taskService.AddTask( + "Complete project report", + priority: 5, + dueDate: DateTime.Now.AddDays(7), + tags: new List { "work", "urgent" } +); +``` + +##### GetAllTasks() +Gets all tasks, optionally including completed tasks. + +**Parameters**: +- `includeCompleted` (bool): Whether to include completed tasks (default: true) + +**Returns**: `IEnumerable` - Collection of tasks + +**Example**: +```csharp +// Get all tasks +var allTasks = taskService.GetAllTasks(); + +// Get only pending tasks +var pendingTasks = taskService.GetAllTasks(includeCompleted: false); +``` + +##### GetTaskById() +Gets a specific task by ID. + +**Parameters**: +- `id` (int): The task ID + +**Returns**: `TaskItem?` - The task if found, null otherwise + +**Example**: +```csharp +var task = taskService.GetTaskById(1); +if (task != null) +{ + Console.WriteLine(task.Description); +} +``` + +##### RemoveTask() +Removes a task by ID. + +**Parameters**: +- `id` (int): The task ID + +**Returns**: `bool` - True if removed, false if not found + +**Example**: +```csharp +if (taskService.RemoveTask(1)) +{ + Console.WriteLine("Task removed"); +} +``` + +##### CompleteTask() +Marks a task as completed. + +**Parameters**: +- `id` (int): The task ID + +**Returns**: `bool` - True if marked completed, false if not found + +**Example**: +```csharp +taskService.CompleteTask(1); +``` + +##### UpdateTask() +Updates a task's description. + +**Parameters**: +- `id` (int): The task ID +- `newDescription` (string): The new description + +**Returns**: `bool` - True if updated, false if not found + +**Throws**: `ArgumentException` if new description is empty + +**Example**: +```csharp +taskService.UpdateTask(1, "Updated task description"); +``` + +##### UpdateTaskPriority() +Updates a task's priority level. + +**Parameters**: +- `id` (int): The task ID +- `priority` (int): The new priority (1-5) + +**Returns**: `bool` - True if updated, false if not found + +**Throws**: `ArgumentException` if priority is invalid + +**Example**: +```csharp +taskService.UpdateTaskPriority(1, 5); // Set to highest priority +``` + +##### SearchTasks() +Searches tasks by description or tags. + +**Parameters**: +- `query` (string): Search query + +**Returns**: `IEnumerable` - Matching tasks + +**Example**: +```csharp +var results = taskService.SearchTasks("meeting"); +``` + +##### GetTasksByTag() +Gets tasks with a specific tag. + +**Parameters**: +- `tag` (string): Tag to filter by + +**Returns**: `IEnumerable` - Tasks with the tag + +**Example**: +```csharp +var workTasks = taskService.GetTasksByTag("work"); +``` + +##### ClearCompletedTasks() +Removes all completed tasks. + +**Returns**: `int` - Number of tasks removed + +**Example**: +```csharp +int count = taskService.ClearCompletedTasks(); +Console.WriteLine($"Cleared {count} tasks"); +``` + +### IExportService + +Interface for exporting and importing tasks. + +```csharp +public interface IExportService +{ + Task ExportToCsvAsync(IEnumerable tasks, string filePath); + Task ExportToMarkdownAsync(IEnumerable tasks, string filePath); + Task ExportToJsonAsync(IEnumerable tasks, string filePath); + Task> ImportFromJsonAsync(string filePath); +} +``` + +#### Methods + +##### ExportToCsvAsync() +Exports tasks to CSV format. + +**Parameters**: +- `tasks` (IEnumerable): Tasks to export +- `filePath` (string): Output file path + +**Returns**: `Task` + +**Example**: +```csharp +await exportService.ExportToCsvAsync(tasks, "tasks.csv"); +``` + +##### ExportToMarkdownAsync() +Exports tasks to Markdown format. + +**Parameters**: +- `tasks` (IEnumerable): Tasks to export +- `filePath` (string): Output file path + +**Returns**: `Task` + +**Example**: +```csharp +await exportService.ExportToMarkdownAsync(tasks, "tasks.md"); +``` + +##### ExportToJsonAsync() +Exports tasks to JSON format. + +**Parameters**: +- `tasks` (IEnumerable): Tasks to export +- `filePath` (string): Output file path + +**Returns**: `Task` + +**Example**: +```csharp +await exportService.ExportToJsonAsync(tasks, "backup.json"); +``` + +##### ImportFromJsonAsync() +Imports tasks from JSON file. + +**Parameters**: +- `filePath` (string): Input file path + +**Returns**: `Task>` - Imported tasks + +**Throws**: `FileNotFoundException` if file doesn't exist + +**Example**: +```csharp +var importedTasks = await exportService.ImportFromJsonAsync("backup.json"); +``` + +### IStatisticsService + +Interface for task statistics and analytics. + +```csharp +public interface IStatisticsService +{ + TaskStatistics GetStatistics(IEnumerable tasks); + Dictionary GetTasksByPriority(IEnumerable tasks); + Dictionary GetTasksByTag(IEnumerable tasks); + IEnumerable GetOverdueTasks(IEnumerable tasks); + IEnumerable GetUpcomingTasks(IEnumerable tasks, int days = 7); +} +``` + +#### Methods + +##### GetStatistics() +Gets overall task statistics. + +**Parameters**: +- `tasks` (IEnumerable): Tasks to analyze + +**Returns**: `TaskStatistics` - Statistics summary + +**Example**: +```csharp +var stats = statisticsService.GetStatistics(tasks); +Console.WriteLine($"Completion Rate: {stats.CompletionRate:F1}%"); +``` + +##### GetTasksByPriority() +Gets pending tasks grouped by priority. + +**Parameters**: +- `tasks` (IEnumerable): Tasks to group + +**Returns**: `Dictionary` - Priority to count mapping + +**Example**: +```csharp +var priorityBreakdown = statisticsService.GetTasksByPriority(tasks); +foreach (var (priority, count) in priorityBreakdown) +{ + Console.WriteLine($"Priority {priority}: {count} tasks"); +} +``` + +##### GetTasksByTag() +Gets tasks grouped by tag. + +**Parameters**: +- `tasks` (IEnumerable): Tasks to group + +**Returns**: `Dictionary` - Tag to count mapping + +**Example**: +```csharp +var tagBreakdown = statisticsService.GetTasksByTag(tasks); +``` + +##### GetOverdueTasks() +Gets all overdue pending tasks. + +**Parameters**: +- `tasks` (IEnumerable): Tasks to check + +**Returns**: `IEnumerable` - Overdue tasks + +**Example**: +```csharp +var overdue = statisticsService.GetOverdueTasks(tasks); +foreach (var task in overdue) +{ + Console.WriteLine($"Overdue: {task.Description}"); +} +``` + +##### GetUpcomingTasks() +Gets tasks due within specified days. + +**Parameters**: +- `tasks` (IEnumerable): Tasks to check +- `days` (int): Number of days to look ahead (default: 7) + +**Returns**: `IEnumerable` - Upcoming tasks + +**Example**: +```csharp +var upcoming = statisticsService.GetUpcomingTasks(tasks, days: 3); +``` + +## Models + +### TaskItem + +Represents a task with all its properties. + +```csharp +public class TaskItem +{ + public int Id { get; set; } + public string Description { get; set; } + public bool IsCompleted { get; set; } + public int Priority { get; set; } // 1-5 + public DateTime CreatedAt { get; set; } + public DateTime? DueDate { get; set; } + public List Tags { get; set; } +} +``` + +**Example**: +```csharp +var task = new TaskItem +{ + Id = 1, + Description = "Buy groceries", + IsCompleted = false, + Priority = 3, + CreatedAt = DateTime.UtcNow, + DueDate = DateTime.UtcNow.AddDays(1), + Tags = new List { "shopping", "personal" } +}; +``` + +### TaskStatistics + +Contains statistical information about tasks. + +```csharp +public class TaskStatistics +{ + public int TotalTasks { get; set; } + public int CompletedTasks { get; set; } + public int PendingTasks { get; set; } + public int OverdueTasks { get; set; } + public int DueToday { get; set; } + public int DueThisWeek { get; set; } + public double CompletionRate { get; } + public double AveragePriority { get; set; } + public int TotalTags { get; set; } +} +``` + +### AppConfig + +Application configuration settings. + +```csharp +public class AppConfig +{ + public string TasksFilePath { get; set; } = "tasks.json"; + public int DefaultPriority { get; set; } = 3; + public bool UseColors { get; set; } = true; + public string DateFormat { get; set; } = "yyyy-MM-dd"; + public bool ShowCompletedByDefault { get; set; } = true; + public int UpcomingDaysThreshold { get; set; } = 7; + public string ExportDirectory { get; set; } = "exports"; +} +``` + +## Services + +### TaskService + +Main implementation of task management. + +**Constructor**: +```csharp +public TaskService(ILogger logger, string? fileName = null) +``` + +**Example Usage**: +```csharp +var logger = serviceProvider.GetRequiredService>(); +var taskService = new TaskService(logger, "my-tasks.json"); + +await taskService.LoadTasksAsync(); +var task = taskService.AddTask("New task", priority: 4); +await taskService.SaveTasksAsync(); +``` + +### ExportService + +Implementation of export/import functionality. + +**Constructor**: +```csharp +public ExportService(ILogger logger) +``` + +**Example Usage**: +```csharp +var exportService = new ExportService(logger); +var tasks = taskService.GetAllTasks(); + +// Export to different formats +await exportService.ExportToCsvAsync(tasks, "tasks.csv"); +await exportService.ExportToMarkdownAsync(tasks, "tasks.md"); +await exportService.ExportToJsonAsync(tasks, "backup.json"); + +// Import from JSON +var importedTasks = await exportService.ImportFromJsonAsync("backup.json"); +``` + +### StatisticsService + +Implementation of statistics and analytics. + +**Constructor**: +```csharp +public StatisticsService(ILogger logger) +``` + +**Example Usage**: +```csharp +var statisticsService = new StatisticsService(logger); +var tasks = taskService.GetAllTasks(); + +var stats = statisticsService.GetStatistics(tasks); +Console.WriteLine($"Total: {stats.TotalTasks}"); +Console.WriteLine($"Completed: {stats.CompletedTasks}"); +Console.WriteLine($"Completion Rate: {stats.CompletionRate:F1}%"); + +var overdue = statisticsService.GetOverdueTasks(tasks); +foreach (var task in overdue) +{ + Console.WriteLine($"Overdue: {task.Description}"); +} +``` + +## Usage Examples + +### Basic Task Management + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TaskManager.CLI.Interfaces; +using TaskManager.CLI.Services; + +// Setup dependency injection +var services = new ServiceCollection(); +services.AddLogging(builder => builder.AddConsole()); +services.AddSingleton(); + +var serviceProvider = services.BuildServiceProvider(); +var taskService = serviceProvider.GetRequiredService(); + +// Load tasks +await taskService.LoadTasksAsync(); + +// Add a task +var task = taskService.AddTask( + "Complete project documentation", + priority: 5, + dueDate: DateTime.Now.AddDays(3), + tags: new List { "work", "documentation" } +); + +// List tasks +foreach (var t in taskService.GetAllTasks()) +{ + Console.WriteLine(t); +} + +// Complete a task +taskService.CompleteTask(task.Id); + +// Save changes +await taskService.SaveTasksAsync(); +``` + +### Exporting Tasks + +```csharp +var exportService = new ExportService(logger); +var tasks = taskService.GetAllTasks(); + +// Export to CSV +await exportService.ExportToCsvAsync(tasks, "tasks-backup.csv"); + +// Export to Markdown report +await exportService.ExportToMarkdownAsync(tasks, "weekly-report.md"); + +// Export to JSON for backup +await exportService.ExportToJsonAsync(tasks, $"backup-{DateTime.Now:yyyyMMdd}.json"); +``` + +### Statistics and Reporting + +```csharp +var statisticsService = new StatisticsService(logger); +var tasks = taskService.GetAllTasks(); + +// Get overall statistics +var stats = statisticsService.GetStatistics(tasks); +Console.WriteLine($"Total Tasks: {stats.TotalTasks}"); +Console.WriteLine($"Completion Rate: {stats.CompletionRate:F1}%"); +Console.WriteLine($"Overdue: {stats.OverdueTasks}"); + +// Get tasks by priority +var byPriority = statisticsService.GetTasksByPriority(tasks); +foreach (var (priority, count) in byPriority.OrderByDescending(kv => kv.Key)) +{ + Console.WriteLine($"Priority {priority}: {count} tasks"); +} + +// Check for overdue tasks +var overdue = statisticsService.GetOverdueTasks(tasks); +if (overdue.Any()) +{ + Console.WriteLine("\nOverdue Tasks:"); + foreach (var task in overdue) + { + Console.WriteLine($" - {task.Description} (Due: {task.DueDate:yyyy-MM-dd})"); + } +} +``` + +## Error Handling + +All methods validate inputs and throw appropriate exceptions: + +- `ArgumentException`: Invalid parameters (empty description, invalid priority) +- `FileNotFoundException`: File not found during import +- `InvalidOperationException`: JSON deserialization failures +- `IOException`: File system errors during save/load + +**Example**: +```csharp +try +{ + var task = taskService.AddTask("", priority: 10); // Will throw ArgumentException +} +catch (ArgumentException ex) +{ + Console.WriteLine($"Invalid input: {ex.Message}"); +} + +try +{ + await exportService.ImportFromJsonAsync("missing.json"); // Will throw FileNotFoundException +} +catch (FileNotFoundException ex) +{ + Console.WriteLine($"File not found: {ex.Message}"); +} +``` + +## Thread Safety + +The current implementation is **not thread-safe**. If you need to use the services from multiple threads, you should: + +1. Use separate service instances per thread, or +2. Implement your own synchronization mechanism + +Example with locking: +```csharp +private static readonly object _lock = new object(); + +lock (_lock) +{ + taskService.AddTask("Thread-safe task"); + await taskService.SaveTasksAsync(); +} +``` + +## Performance Considerations + +- All file I/O operations are asynchronous +- In-memory operations are O(n) for most queries +- Suitable for up to ~10,000 tasks +- For larger datasets, consider using a database backend + +## See Also + +- [README.md](../README.md) - Project overview +- [ARCHITECTURE.md](ARCHITECTURE.md) - Architecture documentation +- [CONTRIBUTING.md](../CONTRIBUTING.md) - Contribution guidelines +- [EXAMPLES.md](../examples/EXAMPLES.md) - Usage examples diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..746e6f9 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,400 @@ +# Architecture Documentation + +## Overview + +Task Manager CLI is built using modern .NET practices with a clean, modular architecture that emphasizes separation of concerns, testability, and maintainability. + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────┐ +│ Program.cs (CLI) │ +│ - Command parsing │ +│ - Dependency injection setup │ +│ - User interaction │ +└────────────────────┬────────────────────────────────────┘ + │ + │ Uses + ▼ +┌─────────────────────────────────────────────────────────┐ +│ ITaskService (Interface) │ +│ - AddTask() │ +│ - GetAllTasks() │ +│ - RemoveTask() │ +│ - CompleteTask() │ +│ - UpdateTask() │ +│ - SearchTasks() │ +│ - etc. │ +└────────────────────┬────────────────────────────────────┘ + │ + │ Implemented by + ▼ +┌─────────────────────────────────────────────────────────┐ +│ TaskService (Service) │ +│ - Business logic │ +│ - Validation │ +│ - Logging │ +│ - File I/O operations │ +└────────────────────┬────────────────────────────────────┘ + │ + │ Uses + ▼ +┌─────────────────────────────────────────────────────────┐ +│ TaskItem (Model) │ +│ - Id │ +│ - Description │ +│ - IsCompleted │ +│ - Priority │ +│ - CreatedAt │ +│ - DueDate │ +│ - Tags │ +└─────────────────────────────────────────────────────────┘ +``` + +## Layers + +### 1. Presentation Layer (Program.cs) + +**Responsibility**: User interaction and command-line interface + +**Key Components**: +- Command parsing and routing +- User input validation +- Output formatting +- Dependency injection configuration + +**Design Decisions**: +- Uses async/await for all I/O operations +- Follows command pattern for different operations +- Minimal business logic (delegates to service layer) + +### 2. Service Layer (Services/) + +**Responsibility**: Business logic and data management + +**Key Components**: +- `ITaskService`: Interface defining task operations +- `TaskService`: Implementation of business logic + +**Design Decisions**: +- Interface-based for testability +- Async file operations +- Comprehensive logging +- Input validation +- Error handling + +**Key Features**: +- Task CRUD operations +- Search and filtering +- Priority management +- Tag support +- Data persistence + +### 3. Model Layer (Models/) + +**Responsibility**: Data structures and domain entities + +**Key Components**: +- `TaskItem`: Core task entity + +**Design Decisions**: +- Rich domain model with behavior (ToString override) +- Value objects for task properties +- Immutable where appropriate +- Well-documented properties + +## Design Patterns + +### Dependency Injection + +The application uses Microsoft.Extensions.DependencyInjection for IoC: + +```csharp +services.AddLogging(builder => { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Warning); +}); +services.AddSingleton(); +``` + +**Benefits**: +- Loose coupling +- Easy testing with mocks +- Flexible configuration +- Standard .NET pattern + +### Repository Pattern (Implicit) + +While not explicitly a repository, TaskService implements a data access pattern: + +```csharp +public async Task LoadTasksAsync() +public async Task SaveTasksAsync() +``` + +**Benefits**: +- Abstraction over data storage +- Easy to swap storage mechanisms +- Testable without file system + +### Strategy Pattern (Implicit) + +Command routing in Program.cs uses a strategy-like pattern: + +```csharp +var result = command switch +{ + "add" => await HandleAddCommand(args, taskService), + "list" => await HandleListCommand(args, taskService), + // ... +}; +``` + +## Data Flow + +### Adding a Task + +``` +User Input → Program.cs → TaskService.AddTask() → TaskItem created + ↓ +User Output ← Program.cs ← Return TaskItem ← Add to list + ↓ +File Save ← TaskService.SaveTasksAsync() ← JSON serialization +``` + +### Listing Tasks + +``` +User Input → Program.cs → TaskService.GetAllTasks() → Filter & Sort + ↓ +User Output ← Format & Display ← Return IEnumerable +``` + +## Data Persistence + +### Storage Format: JSON + +Tasks are stored in `tasks.json`: + +```json +[ + { + "Id": 1, + "Description": "Task description", + "IsCompleted": false, + "Priority": 3, + "CreatedAt": "2024-01-15T10:30:00Z", + "DueDate": "2024-01-20T00:00:00Z", + "Tags": ["tag1", "tag2"] + } +] +``` + +**Rationale**: +- Human-readable +- Easy to debug +- Standard format +- Cross-platform compatible +- Easy to migrate to other storage + +### Storage Operations + +- **Load**: Async read from file, deserialize JSON +- **Save**: Serialize to JSON, async write to file +- **Error Handling**: Graceful fallback on read errors + +## Error Handling Strategy + +### Validation Errors + +```csharp +if (string.IsNullOrWhiteSpace(description)) +{ + throw new ArgumentException("Task description cannot be empty."); +} +``` + +**Approach**: Fast fail with descriptive messages + +### File I/O Errors + +```csharp +try +{ + await File.WriteAllTextAsync(_fileName, json); +} +catch (Exception ex) +{ + _logger.LogError(ex, "Error saving tasks"); + throw; +} +``` + +**Approach**: Log and propagate for user notification + +### User Input Errors + +```csharp +if (!int.TryParse(args[1], out int id)) +{ + Console.WriteLine("Error: Please provide a valid task ID."); + return 1; +} +``` + +**Approach**: Friendly messages with usage hints + +## Logging Strategy + +Uses Microsoft.Extensions.Logging with console provider: + +```csharp +_logger.LogInformation("Added task #{Id}: {Description}", task.Id, task.Description); +_logger.LogWarning("Task #{Id} not found", id); +_logger.LogError(ex, "Error loading tasks from {FileName}", _fileName); +``` + +**Levels**: +- **Information**: Normal operations +- **Warning**: Not found scenarios +- **Error**: Exceptions and failures + +**Default**: Only warnings and errors shown to users + +## Testing Strategy + +### Unit Tests + +Focus on service layer with mocked dependencies: + +```csharp +[Fact] +public void AddTask_WithValidDescription_ReturnsTask() +{ + // Arrange + var mockLogger = new Mock>(); + var service = new TaskService(mockLogger.Object, "test.json"); + + // Act + var task = service.AddTask("Test"); + + // Assert + Assert.NotNull(task); +} +``` + +### Test Coverage Goals + +- Service layer: 90%+ coverage +- Models: Property tests +- CLI: Integration tests (future) + +## Security Considerations + +### Input Validation + +- All user input is validated +- SQL injection: N/A (no database) +- Path traversal: Fixed filename +- Command injection: N/A (no shell execution) + +### File System + +- Controlled file access +- No arbitrary file operations +- JSON deserialization with type safety + +## Performance Considerations + +### In-Memory Operations + +- Tasks stored in memory during session +- O(n) operations for most queries +- Acceptable for typical use (< 10,000 tasks) + +### File I/O + +- Async operations prevent blocking +- Full file read/write (no streaming needed for typical use) +- Could be optimized with SQLite for large datasets + +### Sorting and Filtering + +```csharp +return _tasks + .Where(t => !t.IsCompleted) + .OrderByDescending(t => t.Priority) + .ThenBy(t => t.CreatedAt); +``` + +Efficient LINQ queries with deferred execution + +## Extensibility Points + +### Custom Storage + +Implement `ITaskService` with different storage: + +```csharp +public class DatabaseTaskService : ITaskService +{ + // Use Entity Framework or Dapper +} +``` + +### Additional Commands + +Add new commands in Program.cs: + +```csharp +"export" => await HandleExportCommand(args, taskService), +``` + +### Task Properties + +Extend `TaskItem` model: + +```csharp +public string? AssignedTo { get; set; } +public TaskCategory Category { get; set; } +``` + +## Future Architectural Considerations + +### Scalability + +For larger datasets, consider: +- SQLite for storage +- Indexed queries +- Pagination for list commands + +### Multi-User + +For shared task lists: +- Cloud storage (Azure, AWS) +- Conflict resolution +- Authentication + +### Plugin System + +For extensibility: +- Plugin interface +- Dynamic loading +- Command registry + +### API Layer + +For remote access: +- ASP.NET Core Web API +- REST endpoints +- Authentication/authorization + +## Conclusion + +The current architecture provides: +- Clear separation of concerns +- High testability +- Easy maintenance +- Room for growth + +The design prioritizes simplicity and clarity while maintaining professional software engineering practices. diff --git a/examples/EXAMPLES.md b/examples/EXAMPLES.md new file mode 100644 index 0000000..9eb6cad --- /dev/null +++ b/examples/EXAMPLES.md @@ -0,0 +1,261 @@ +# Task Manager Examples + +This directory contains example files and usage scenarios for the Task Manager CLI. + +## Sample Tasks + +The `sample-tasks.json` file contains a variety of example tasks demonstrating different features: + +- Tasks with different priority levels (1-5) +- Completed and pending tasks +- Tasks with due dates +- Tasks with multiple tags +- Work and personal tasks + +### Loading Sample Tasks + +To try out the sample tasks: + +```bash +# Copy sample tasks to your working directory +cp examples/sample-tasks.json tasks.json + +# List all tasks +taskman list + +# List only pending tasks +taskman list --pending + +# Filter by tag +taskman list --tag work +``` + +## Common Usage Scenarios + +### 1. Daily Task Management + +```bash +# Morning: Add today's tasks +taskman add "Review team PRs" --priority 5 --tags work,urgent +taskman add "Prepare status report" --priority 4 --due 2024-01-20 +taskman add "Call client about project" --priority 3 + +# Throughout the day: Check tasks +taskman list --pending + +# Complete tasks as you finish them +taskman complete 1 +taskman complete 2 + +# Evening: Review what's left +taskman list --pending +``` + +### 2. Project-Based Organization + +```bash +# Add tasks for a specific project using tags +taskman add "Design API endpoints" --priority 5 --tags project-x,api,design +taskman add "Write unit tests" --priority 4 --tags project-x,testing +taskman add "Update API documentation" --priority 3 --tags project-x,docs + +# View all tasks for the project +taskman list --tag project-x + +# Search within project +taskman search API +``` + +### 3. Weekly Planning + +```bash +# Monday: Plan the week +taskman add "Team standup" --priority 4 --due 2024-01-22 --tags work,meeting +taskman add "Code review session" --priority 3 --due 2024-01-23 --tags work +taskman add "Submit expense report" --priority 2 --due 2024-01-26 --tags admin + +# Check statistics +taskman stats + +# View upcoming tasks +taskman list --pending + +# Friday: Clear completed tasks +taskman clear +``` + +### 4. Personal Task Management + +```bash +# Add personal tasks +taskman add "Buy birthday gift" --priority 4 --due 2024-02-01 --tags personal,shopping +taskman add "Book vacation flights" --priority 5 --due 2024-01-25 --tags personal,travel +taskman add "Read new book" --priority 2 --tags personal,leisure + +# Filter personal tasks +taskman list --tag personal + +# Update priorities as needed +taskman priority 3 5 +``` + +### 5. Team Collaboration Export + +```bash +# Export tasks for sharing with team +taskman export --format markdown --output team-tasks.md + +# Export high-priority items to CSV +taskman list --pending > pending-tasks.txt +taskman export --format csv --output tasks-report.csv + +# Import tasks from team member +taskman import team-member-tasks.json +``` + +### 6. Priority Management + +```bash +# Add tasks with priorities +taskman add "Critical bug fix" --priority 5 --tags urgent,bug +taskman add "Nice-to-have feature" --priority 1 --tags enhancement + +# List tasks (automatically sorted by priority) +taskman list --pending + +# Update priority when circumstances change +taskman priority 1 5 # Escalate task 1 to highest priority +``` + +### 7. Tag-Based Workflows + +```bash +# Use tags for contexts +taskman add "Email project update" --tags work,communication,@office +taskman add "Pick up dry cleaning" --tags personal,errands,@out +taskman add "Review code" --tags work,coding,@computer + +# Work from specific contexts +taskman list --tag @office +taskman list --tag @computer +``` + +### 8. Batch Operations + +```bash +# Complete multiple tasks +for id in 1 2 3 4 5; do + taskman complete $id +done + +# Clear all completed +taskman clear + +# Export weekly report +taskman stats +taskman export --format markdown --output weekly-report.md +``` + +## Task Templates + +### Work Tasks +```bash +# Bug fix template +taskman add "Fix: [description]" --priority 5 --tags work,bug,urgent + +# Feature template +taskman add "Feature: [description]" --priority 3 --tags work,feature + +# Meeting template +taskman add "Meeting: [topic]" --priority 4 --due [date] --tags work,meeting +``` + +### Personal Tasks +```bash +# Shopping template +taskman add "Buy [item]" --priority 2 --tags personal,shopping + +# Health template +taskman add "Doctor: [appointment]" --priority 4 --due [date] --tags personal,health + +# Learning template +taskman add "Learn [topic]" --priority 3 --tags personal,learning +``` + +### Recurring Task Patterns +```bash +# Weekly review (add every Friday) +taskman add "Weekly review" --priority 3 --due [next-friday] --tags work,review + +# Monthly tasks +taskman add "Submit timesheet" --priority 4 --due [end-of-month] --tags work,admin +``` + +## Advanced Features + +### Statistics and Reporting +```bash +# View overall statistics +taskman stats + +# Check upcoming deadlines +taskman list --pending | grep "Due:" + +# Analyze productivity +taskman stats --period week +``` + +### Backup and Restore +```bash +# Backup current tasks +cp tasks.json tasks-backup-$(date +%Y%m%d).json + +# Export to multiple formats for archival +taskman export --format json --output archive/tasks-$(date +%Y%m%d).json +taskman export --format csv --output archive/tasks-$(date +%Y%m%d).csv +taskman export --format markdown --output archive/tasks-$(date +%Y%m%d).md +``` + +### Integration with Other Tools +```bash +# Use with grep for custom filtering +taskman list | grep urgent + +# Count pending tasks +taskman list --pending | wc -l + +# Pipe to text processing tools +taskman list --tag work | awk '{print $1, $2}' +``` + +## Tips and Best Practices + +1. **Use Consistent Tags**: Establish a tagging convention early +2. **Set Realistic Priorities**: Not everything can be priority 5 +3. **Regular Reviews**: Clean up completed tasks weekly +4. **Use Due Dates**: But don't over-schedule +5. **Export Regularly**: Keep backups of your task data +6. **Leverage Search**: Use search instead of scrolling through long lists +7. **Update as You Go**: Keep task status current for best planning + +## Troubleshooting + +### Tasks Not Saving +```bash +# Check file permissions +ls -la tasks.json + +# Verify JSON syntax +cat tasks.json | python -m json.tool +``` + +### Lost Tasks +```bash +# Check for backup files +ls tasks*.json + +# Restore from backup +cp tasks-backup.json tasks.json +``` + +For more information, see the main [README.md](../README.md) and [CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/examples/sample-tasks.json b/examples/sample-tasks.json new file mode 100644 index 0000000..7ffd4e5 --- /dev/null +++ b/examples/sample-tasks.json @@ -0,0 +1,92 @@ +[ + { + "Id": 1, + "Description": "Review pull requests for the authentication module", + "IsCompleted": false, + "Priority": 5, + "CreatedAt": "2024-01-15T09:00:00Z", + "DueDate": "2024-01-20T00:00:00Z", + "Tags": ["work", "code-review", "urgent"] + }, + { + "Id": 2, + "Description": "Buy groceries for the week", + "IsCompleted": false, + "Priority": 3, + "CreatedAt": "2024-01-15T10:30:00Z", + "DueDate": null, + "Tags": ["personal", "shopping"] + }, + { + "Id": 3, + "Description": "Prepare presentation for team meeting", + "IsCompleted": false, + "Priority": 4, + "CreatedAt": "2024-01-14T14:00:00Z", + "DueDate": "2024-01-18T00:00:00Z", + "Tags": ["work", "presentation"] + }, + { + "Id": 4, + "Description": "Update project documentation", + "IsCompleted": true, + "Priority": 3, + "CreatedAt": "2024-01-10T11:00:00Z", + "DueDate": null, + "Tags": ["work", "documentation"] + }, + { + "Id": 5, + "Description": "Schedule dentist appointment", + "IsCompleted": false, + "Priority": 2, + "CreatedAt": "2024-01-15T16:20:00Z", + "DueDate": "2024-01-25T00:00:00Z", + "Tags": ["personal", "health"] + }, + { + "Id": 6, + "Description": "Research new database optimization techniques", + "IsCompleted": false, + "Priority": 3, + "CreatedAt": "2024-01-12T08:45:00Z", + "DueDate": null, + "Tags": ["work", "research", "learning"] + }, + { + "Id": 7, + "Description": "Fix bug in user authentication flow", + "IsCompleted": false, + "Priority": 5, + "CreatedAt": "2024-01-16T07:30:00Z", + "DueDate": "2024-01-17T00:00:00Z", + "Tags": ["work", "bug", "urgent", "security"] + }, + { + "Id": 8, + "Description": "Plan weekend hiking trip", + "IsCompleted": false, + "Priority": 2, + "CreatedAt": "2024-01-13T19:15:00Z", + "DueDate": "2024-01-19T00:00:00Z", + "Tags": ["personal", "recreation", "planning"] + }, + { + "Id": 9, + "Description": "Review and respond to customer feedback", + "IsCompleted": true, + "Priority": 4, + "CreatedAt": "2024-01-11T13:00:00Z", + "DueDate": null, + "Tags": ["work", "customer-service"] + }, + { + "Id": 10, + "Description": "Refactor legacy code in payment module", + "IsCompleted": false, + "Priority": 3, + "CreatedAt": "2024-01-14T10:00:00Z", + "DueDate": "2024-02-01T00:00:00Z", + "Tags": ["work", "refactoring", "technical-debt"] + } +] diff --git a/src/TaskManager.CLI/Interfaces/IExportService.cs b/src/TaskManager.CLI/Interfaces/IExportService.cs new file mode 100644 index 0000000..534744e --- /dev/null +++ b/src/TaskManager.CLI/Interfaces/IExportService.cs @@ -0,0 +1,37 @@ +using TaskManager.CLI.Models; + +namespace TaskManager.CLI.Interfaces; + +/// +/// Interface for exporting tasks to various formats. +/// +public interface IExportService +{ + /// + /// Exports tasks to CSV format. + /// + /// Tasks to export. + /// Output file path. + Task ExportToCsvAsync(IEnumerable tasks, string filePath); + + /// + /// Exports tasks to Markdown format. + /// + /// Tasks to export. + /// Output file path. + Task ExportToMarkdownAsync(IEnumerable tasks, string filePath); + + /// + /// Exports tasks to JSON format. + /// + /// Tasks to export. + /// Output file path. + Task ExportToJsonAsync(IEnumerable tasks, string filePath); + + /// + /// Imports tasks from JSON format. + /// + /// Input file path. + /// Imported tasks. + Task> ImportFromJsonAsync(string filePath); +} diff --git a/src/TaskManager.CLI/Interfaces/IStatisticsService.cs b/src/TaskManager.CLI/Interfaces/IStatisticsService.cs new file mode 100644 index 0000000..7673351 --- /dev/null +++ b/src/TaskManager.CLI/Interfaces/IStatisticsService.cs @@ -0,0 +1,45 @@ +using TaskManager.CLI.Models; + +namespace TaskManager.CLI.Interfaces; + +/// +/// Interface for task statistics and reporting. +/// +public interface IStatisticsService +{ + /// + /// Gets overall task statistics. + /// + /// Tasks to analyze. + /// Statistics summary. + TaskStatistics GetStatistics(IEnumerable tasks); + + /// + /// Gets tasks grouped by priority. + /// + /// Tasks to group. + /// Dictionary of priority to task count. + Dictionary GetTasksByPriority(IEnumerable tasks); + + /// + /// Gets tasks grouped by tag. + /// + /// Tasks to group. + /// Dictionary of tag to task count. + Dictionary GetTasksByTag(IEnumerable tasks); + + /// + /// Gets overdue tasks. + /// + /// Tasks to check. + /// Overdue tasks. + IEnumerable GetOverdueTasks(IEnumerable tasks); + + /// + /// Gets upcoming tasks due within specified days. + /// + /// Tasks to check. + /// Number of days to look ahead. + /// Upcoming tasks. + IEnumerable GetUpcomingTasks(IEnumerable tasks, int days = 7); +} diff --git a/src/TaskManager.CLI/Interfaces/ITaskService.cs b/src/TaskManager.CLI/Interfaces/ITaskService.cs new file mode 100644 index 0000000..520476f --- /dev/null +++ b/src/TaskManager.CLI/Interfaces/ITaskService.cs @@ -0,0 +1,93 @@ +using TaskManager.CLI.Models; + +namespace TaskManager.CLI.Interfaces; + +/// +/// Interface for task management operations. +/// +public interface ITaskService +{ + /// + /// Loads tasks from storage. + /// + Task LoadTasksAsync(); + + /// + /// Saves tasks to storage. + /// + Task SaveTasksAsync(); + + /// + /// Adds a new task with the specified description. + /// + /// The task description. + /// The task priority (1-5). + /// Optional due date. + /// Optional tags. + /// The created task. + TaskItem AddTask(string description, int priority = 3, DateTime? dueDate = null, List? tags = null); + + /// + /// Gets all tasks. + /// + /// Whether to include completed tasks. + /// List of tasks. + IEnumerable GetAllTasks(bool includeCompleted = true); + + /// + /// Gets a task by its ID. + /// + /// The task ID. + /// The task if found, null otherwise. + TaskItem? GetTaskById(int id); + + /// + /// Removes a task by its ID. + /// + /// The task ID. + /// True if the task was removed, false otherwise. + bool RemoveTask(int id); + + /// + /// Marks a task as completed. + /// + /// The task ID. + /// True if the task was marked as completed, false otherwise. + bool CompleteTask(int id); + + /// + /// Updates a task's description. + /// + /// The task ID. + /// The new description. + /// True if the task was updated, false otherwise. + bool UpdateTask(int id, string newDescription); + + /// + /// Updates a task's priority. + /// + /// The task ID. + /// The new priority (1-5). + /// True if the task was updated, false otherwise. + bool UpdateTaskPriority(int id, int priority); + + /// + /// Searches tasks by description or tags. + /// + /// The search query. + /// List of matching tasks. + IEnumerable SearchTasks(string query); + + /// + /// Gets tasks filtered by tag. + /// + /// The tag to filter by. + /// List of tasks with the specified tag. + IEnumerable GetTasksByTag(string tag); + + /// + /// Clears all completed tasks. + /// + /// Number of tasks cleared. + int ClearCompletedTasks(); +} diff --git a/src/TaskManager.CLI/Models/AppConfig.cs b/src/TaskManager.CLI/Models/AppConfig.cs new file mode 100644 index 0000000..548d46c --- /dev/null +++ b/src/TaskManager.CLI/Models/AppConfig.cs @@ -0,0 +1,42 @@ +namespace TaskManager.CLI.Models; + +/// +/// Application configuration settings. +/// +public class AppConfig +{ + /// + /// Path to the tasks file. + /// + public string TasksFilePath { get; set; } = "tasks.json"; + + /// + /// Default priority for new tasks. + /// + public int DefaultPriority { get; set; } = 3; + + /// + /// Whether to use colored output. + /// + public bool UseColors { get; set; } = true; + + /// + /// Date format for displaying dates. + /// + public string DateFormat { get; set; } = "yyyy-MM-dd"; + + /// + /// Whether to show completed tasks by default. + /// + public bool ShowCompletedByDefault { get; set; } = true; + + /// + /// Number of days to look ahead for upcoming tasks. + /// + public int UpcomingDaysThreshold { get; set; } = 7; + + /// + /// Export directory for exported files. + /// + public string ExportDirectory { get; set; } = "exports"; +} diff --git a/src/TaskManager.CLI/Models/TaskItem.cs b/src/TaskManager.CLI/Models/TaskItem.cs new file mode 100644 index 0000000..a9e75f1 --- /dev/null +++ b/src/TaskManager.CLI/Models/TaskItem.cs @@ -0,0 +1,55 @@ +namespace TaskManager.CLI.Models; + +/// +/// Represents a task item with various properties for task management. +/// +public class TaskItem +{ + /// + /// Gets or sets the unique identifier for the task. + /// + public int Id { get; set; } + + /// + /// Gets or sets the description of the task. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets whether the task is completed. + /// + public bool IsCompleted { get; set; } + + /// + /// Gets or sets the priority level of the task (1-5, where 5 is highest). + /// + public int Priority { get; set; } = 3; + + /// + /// Gets or sets the date and time when the task was created. + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// Gets or sets the due date for the task (optional). + /// + public DateTime? DueDate { get; set; } + + /// + /// Gets or sets tags associated with the task. + /// + public List Tags { get; set; } = new(); + + /// + /// Returns a formatted string representation of the task. + /// + public override string ToString() + { + var status = IsCompleted ? "✓" : " "; + var priorityStr = new string('★', Priority); + var dueStr = DueDate.HasValue ? $" (Due: {DueDate.Value:yyyy-MM-dd})" : ""; + var tagsStr = Tags.Count > 0 ? $" [{string.Join(", ", Tags)}]" : ""; + + return $"[{status}] [{Id}] {Description} {priorityStr}{dueStr}{tagsStr}"; + } +} diff --git a/src/TaskManager.CLI/Models/TaskStatistics.cs b/src/TaskManager.CLI/Models/TaskStatistics.cs new file mode 100644 index 0000000..fad23dc --- /dev/null +++ b/src/TaskManager.CLI/Models/TaskStatistics.cs @@ -0,0 +1,52 @@ +namespace TaskManager.CLI.Models; + +/// +/// Statistics about tasks. +/// +public class TaskStatistics +{ + /// + /// Total number of tasks. + /// + public int TotalTasks { get; set; } + + /// + /// Number of completed tasks. + /// + public int CompletedTasks { get; set; } + + /// + /// Number of pending tasks. + /// + public int PendingTasks { get; set; } + + /// + /// Number of overdue tasks. + /// + public int OverdueTasks { get; set; } + + /// + /// Number of tasks due today. + /// + public int DueToday { get; set; } + + /// + /// Number of tasks due this week. + /// + public int DueThisWeek { get; set; } + + /// + /// Completion percentage. + /// + public double CompletionRate => TotalTasks > 0 ? (CompletedTasks * 100.0 / TotalTasks) : 0; + + /// + /// Average priority of pending tasks. + /// + public double AveragePriority { get; set; } + + /// + /// Total number of unique tags. + /// + public int TotalTags { get; set; } +} diff --git a/src/TaskManager.CLI/Program.cs b/src/TaskManager.CLI/Program.cs new file mode 100644 index 0000000..eedd4bd --- /dev/null +++ b/src/TaskManager.CLI/Program.cs @@ -0,0 +1,528 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TaskManager.CLI.Interfaces; +using TaskManager.CLI.Services; + +namespace TaskManager.CLI; + +/// +/// Main program entry point for the Task Manager CLI. +/// +class Program +{ + static async Task Main(string[] args) + { + // Setup dependency injection + var services = new ServiceCollection(); + ConfigureServices(services); + var serviceProvider = services.BuildServiceProvider(); + + var logger = serviceProvider.GetRequiredService>(); + var taskService = serviceProvider.GetRequiredService(); + var exportService = serviceProvider.GetRequiredService(); + var statisticsService = serviceProvider.GetRequiredService(); + + try + { + await taskService.LoadTasksAsync(); + + if (args.Length == 0) + { + ShowUsage(); + return 0; + } + + var command = args[0].ToLowerInvariant(); + var result = command switch + { + "add" => await HandleAddCommand(args, taskService), + "list" => await HandleListCommand(args, taskService), + "remove" => await HandleRemoveCommand(args, taskService), + "complete" => await HandleCompleteCommand(args, taskService), + "update" => await HandleUpdateCommand(args, taskService), + "priority" => await HandlePriorityCommand(args, taskService), + "search" => await HandleSearchCommand(args, taskService), + "clear" => await HandleClearCommand(taskService), + "stats" => HandleStatsCommand(taskService, statisticsService), + "export" => await HandleExportCommand(args, taskService, exportService), + "import" => await HandleImportCommand(args, taskService, exportService), + "help" => ShowUsage(), + _ => ShowUnknownCommand(command) + }; + + if (result == 0) + { + await taskService.SaveTasksAsync(); + } + + return result; + } + catch (Exception ex) + { + logger.LogError(ex, "An unexpected error occurred"); + Console.Error.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + + private static void ConfigureServices(ServiceCollection services) + { + services.AddLogging(builder => + { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Warning); // Only show warnings and errors by default + }); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + private static async Task HandleAddCommand(string[] args, ITaskService taskService) + { + if (args.Length < 2) + { + Console.WriteLine("Error: Please provide a task description."); + Console.WriteLine("Usage: taskman add [--priority <1-5>] [--due ] [--tags ]"); + return 1; + } + + var description = new List(); + int priority = 3; + DateTime? dueDate = null; + var tags = new List(); + + for (int i = 1; i < args.Length; i++) + { + if (args[i] == "--priority" && i + 1 < args.Length) + { + if (int.TryParse(args[++i], out var p) && p >= 1 && p <= 5) + { + priority = p; + } + else + { + Console.WriteLine("Error: Priority must be between 1 and 5."); + return 1; + } + } + else if (args[i] == "--due" && i + 1 < args.Length) + { + if (DateTime.TryParse(args[++i], out var d)) + { + dueDate = d; + } + else + { + Console.WriteLine("Error: Invalid date format. Use yyyy-MM-dd."); + return 1; + } + } + else if (args[i] == "--tags" && i + 1 < args.Length) + { + tags = args[++i].Split(',').Select(t => t.Trim()).ToList(); + } + else if (!args[i].StartsWith("--")) + { + description.Add(args[i]); + } + } + + var descriptionStr = string.Join(" ", description); + var task = taskService.AddTask(descriptionStr, priority, dueDate, tags); + Console.WriteLine($"Added task #{task.Id}: {task.Description}"); + + return 0; + } + + private static Task HandleListCommand(string[] args, ITaskService taskService) + { + var includeCompleted = true; + var tag = string.Empty; + + for (int i = 1; i < args.Length; i++) + { + if (args[i] == "--pending") + { + includeCompleted = false; + } + else if (args[i] == "--tag" && i + 1 < args.Length) + { + tag = args[++i]; + } + } + + IEnumerable tasks; + if (!string.IsNullOrEmpty(tag)) + { + tasks = taskService.GetTasksByTag(tag); + } + else + { + tasks = taskService.GetAllTasks(includeCompleted); + } + + var taskList = tasks.ToList(); + if (taskList.Count == 0) + { + Console.WriteLine("No tasks found."); + return Task.FromResult(0); + } + + Console.WriteLine($"\nTotal tasks: {taskList.Count}\n"); + foreach (var task in taskList) + { + Console.WriteLine(task.ToString()); + } + + return Task.FromResult(0); + } + + private static Task HandleRemoveCommand(string[] args, ITaskService taskService) + { + if (args.Length < 2 || !int.TryParse(args[1], out int id)) + { + Console.WriteLine("Error: Please provide a valid task ID."); + Console.WriteLine("Usage: taskman remove "); + return Task.FromResult(1); + } + + if (taskService.RemoveTask(id)) + { + Console.WriteLine($"Removed task #{id}"); + return Task.FromResult(0); + } + + Console.WriteLine($"Error: Task #{id} not found."); + return Task.FromResult(1); + } + + private static Task HandleCompleteCommand(string[] args, ITaskService taskService) + { + if (args.Length < 2 || !int.TryParse(args[1], out int id)) + { + Console.WriteLine("Error: Please provide a valid task ID."); + Console.WriteLine("Usage: taskman complete "); + return Task.FromResult(1); + } + + if (taskService.CompleteTask(id)) + { + Console.WriteLine($"Marked task #{id} as completed"); + return Task.FromResult(0); + } + + Console.WriteLine($"Error: Task #{id} not found."); + return Task.FromResult(1); + } + + private static Task HandleUpdateCommand(string[] args, ITaskService taskService) + { + if (args.Length < 3 || !int.TryParse(args[1], out int id)) + { + Console.WriteLine("Error: Please provide a valid task ID and new description."); + Console.WriteLine("Usage: taskman update "); + return Task.FromResult(1); + } + + var newDescription = string.Join(" ", args[2..]); + if (taskService.UpdateTask(id, newDescription)) + { + Console.WriteLine($"Updated task #{id}"); + return Task.FromResult(0); + } + + Console.WriteLine($"Error: Task #{id} not found."); + return Task.FromResult(1); + } + + private static Task HandlePriorityCommand(string[] args, ITaskService taskService) + { + if (args.Length < 3 || !int.TryParse(args[1], out int id) || !int.TryParse(args[2], out int priority)) + { + Console.WriteLine("Error: Please provide a valid task ID and priority (1-5)."); + Console.WriteLine("Usage: taskman priority <1-5>"); + return Task.FromResult(1); + } + + if (priority < 1 || priority > 5) + { + Console.WriteLine("Error: Priority must be between 1 and 5."); + return Task.FromResult(1); + } + + if (taskService.UpdateTaskPriority(id, priority)) + { + Console.WriteLine($"Updated task #{id} priority to {priority}"); + return Task.FromResult(0); + } + + Console.WriteLine($"Error: Task #{id} not found."); + return Task.FromResult(1); + } + + private static Task HandleSearchCommand(string[] args, ITaskService taskService) + { + if (args.Length < 2) + { + Console.WriteLine("Error: Please provide a search query."); + Console.WriteLine("Usage: taskman search "); + return Task.FromResult(1); + } + + var query = string.Join(" ", args[1..]); + var results = taskService.SearchTasks(query).ToList(); + + if (results.Count == 0) + { + Console.WriteLine($"No tasks found matching '{query}'"); + return Task.FromResult(0); + } + + Console.WriteLine($"\nFound {results.Count} task(s) matching '{query}':\n"); + foreach (var task in results) + { + Console.WriteLine(task.ToString()); + } + + return Task.FromResult(0); + } + + private static Task HandleClearCommand(ITaskService taskService) + { + var count = taskService.ClearCompletedTasks(); + Console.WriteLine($"Cleared {count} completed task(s)"); + return Task.FromResult(0); + } + + private static int HandleStatsCommand(ITaskService taskService, IStatisticsService statisticsService) + { + var tasks = taskService.GetAllTasks().ToList(); + var stats = statisticsService.GetStatistics(tasks); + + Console.WriteLine("\n╔═══════════════════════════════════════╗"); + Console.WriteLine("║ Task Statistics ║"); + Console.WriteLine("╚═══════════════════════════════════════╝\n"); + + Console.WriteLine($"📊 Total Tasks: {stats.TotalTasks}"); + Console.WriteLine($"✅ Completed: {stats.CompletedTasks}"); + Console.WriteLine($"⏳ Pending: {stats.PendingTasks}"); + Console.WriteLine($"⚠️ Overdue: {stats.OverdueTasks}"); + Console.WriteLine($"📅 Due Today: {stats.DueToday}"); + Console.WriteLine($"📆 Due This Week: {stats.DueThisWeek}"); + Console.WriteLine($"📈 Completion Rate: {stats.CompletionRate:F1}%"); + Console.WriteLine($"⭐ Avg Priority: {stats.AveragePriority:F1}"); + Console.WriteLine($"🏷️ Unique Tags: {stats.TotalTags}"); + + // Priority breakdown + var priorityBreakdown = statisticsService.GetTasksByPriority(tasks); + if (priorityBreakdown.Any()) + { + Console.WriteLine("\n🎯 Priority Breakdown (Pending):"); + foreach (var (priority, count) in priorityBreakdown.OrderByDescending(kv => kv.Key)) + { + var stars = new string('★', priority); + Console.WriteLine($" {stars} Priority {priority}: {count} task(s)"); + } + } + + // Tag breakdown + var tagBreakdown = statisticsService.GetTasksByTag(tasks); + if (tagBreakdown.Any()) + { + Console.WriteLine("\n🏷️ Top Tags:"); + foreach (var (tag, count) in tagBreakdown.Take(10)) + { + Console.WriteLine($" {tag}: {count} task(s)"); + } + } + + // Overdue tasks + var overdueTasks = statisticsService.GetOverdueTasks(tasks).ToList(); + if (overdueTasks.Any()) + { + Console.WriteLine($"\n⚠️ Overdue Tasks ({overdueTasks.Count}):"); + foreach (var task in overdueTasks.Take(5)) + { + Console.WriteLine($" [{task.Id}] {task.Description} (Due: {task.DueDate:yyyy-MM-dd})"); + } + if (overdueTasks.Count > 5) + { + Console.WriteLine($" ... and {overdueTasks.Count - 5} more"); + } + } + + // Upcoming tasks + var upcomingTasks = statisticsService.GetUpcomingTasks(tasks, 7).ToList(); + if (upcomingTasks.Any()) + { + Console.WriteLine($"\n📅 Upcoming Tasks (Next 7 Days):"); + foreach (var task in upcomingTasks.Take(5)) + { + Console.WriteLine($" [{task.Id}] {task.Description} (Due: {task.DueDate:yyyy-MM-dd})"); + } + if (upcomingTasks.Count > 5) + { + Console.WriteLine($" ... and {upcomingTasks.Count - 5} more"); + } + } + + Console.WriteLine(); + return 0; + } + + private static async Task HandleExportCommand(string[] args, ITaskService taskService, IExportService exportService) + { + var format = "json"; + var output = string.Empty; + + for (int i = 1; i < args.Length; i++) + { + if (args[i] == "--format" && i + 1 < args.Length) + { + format = args[++i].ToLowerInvariant(); + } + else if (args[i] == "--output" && i + 1 < args.Length) + { + output = args[++i]; + } + } + + if (string.IsNullOrEmpty(output)) + { + output = $"tasks-export-{DateTime.Now:yyyyMMdd-HHmmss}.{format}"; + } + + var tasks = taskService.GetAllTasks().ToList(); + + try + { + switch (format) + { + case "csv": + await exportService.ExportToCsvAsync(tasks, output); + break; + case "markdown": + case "md": + await exportService.ExportToMarkdownAsync(tasks, output); + break; + case "json": + await exportService.ExportToJsonAsync(tasks, output); + break; + default: + Console.WriteLine($"Error: Unknown format '{format}'. Supported: csv, markdown, json"); + return 1; + } + + Console.WriteLine($"✅ Exported {tasks.Count} task(s) to {output}"); + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"❌ Export failed: {ex.Message}"); + return 1; + } + } + + private static async Task HandleImportCommand(string[] args, ITaskService taskService, IExportService exportService) + { + if (args.Length < 2) + { + Console.WriteLine("Error: Please provide the import file path."); + Console.WriteLine("Usage: taskman import "); + return 1; + } + + var filePath = args[1]; + + try + { + var importedTasks = await exportService.ImportFromJsonAsync(filePath); + + Console.WriteLine($"Found {importedTasks.Count} task(s) in import file."); + Console.Write("This will add these tasks to your current list. Continue? (y/n): "); + + var response = Console.ReadLine()?.ToLowerInvariant(); + if (response != "y" && response != "yes") + { + Console.WriteLine("Import cancelled."); + return 0; + } + + var currentTasks = taskService.GetAllTasks().ToList(); + var maxId = currentTasks.Any() ? currentTasks.Max(t => t.Id) : 0; + + // Reassign IDs to avoid conflicts + foreach (var task in importedTasks) + { + task.Id = ++maxId; + taskService.AddTask(task.Description, task.Priority, task.DueDate, task.Tags); + } + + await taskService.SaveTasksAsync(); + Console.WriteLine($"✅ Successfully imported {importedTasks.Count} task(s)"); + return 0; + } + catch (FileNotFoundException) + { + Console.WriteLine($"❌ Error: File not found: {filePath}"); + return 1; + } + catch (Exception ex) + { + Console.WriteLine($"❌ Import failed: {ex.Message}"); + return 1; + } + } + + private static int ShowUsage() + { + Console.WriteLine("Task Manager - A modern CLI task management tool\n"); + Console.WriteLine("Usage: taskman [options]\n"); + Console.WriteLine("Commands:"); + Console.WriteLine(" add Add a new task"); + Console.WriteLine(" Options:"); + Console.WriteLine(" --priority <1-5> Set priority (default: 3)"); + Console.WriteLine(" --due Set due date"); + Console.WriteLine(" --tags Add tags"); + Console.WriteLine(); + Console.WriteLine(" list [options] List tasks"); + Console.WriteLine(" Options:"); + Console.WriteLine(" --pending Show only pending tasks"); + Console.WriteLine(" --tag Filter by tag"); + Console.WriteLine(); + Console.WriteLine(" complete Mark a task as completed"); + Console.WriteLine(" remove Remove a task"); + Console.WriteLine(" update Update task description"); + Console.WriteLine(" priority <1-5> Update task priority"); + Console.WriteLine(" search Search tasks by description or tags"); + Console.WriteLine(" clear Remove all completed tasks"); + Console.WriteLine(" stats View task statistics"); + Console.WriteLine(); + Console.WriteLine(" export [options] Export tasks to file"); + Console.WriteLine(" Options:"); + Console.WriteLine(" --format csv, markdown, or json (default: json)"); + Console.WriteLine(" --output Output file path"); + Console.WriteLine(); + Console.WriteLine(" import Import tasks from JSON file"); + Console.WriteLine(" help Show this help message"); + Console.WriteLine(); + Console.WriteLine("Examples:"); + Console.WriteLine(" taskman add \"Buy groceries\" --priority 4 --tags shopping,personal"); + Console.WriteLine(" taskman list --pending"); + Console.WriteLine(" taskman complete 1"); + Console.WriteLine(" taskman search groceries"); + Console.WriteLine(" taskman stats"); + Console.WriteLine(" taskman export --format csv --output tasks.csv"); + Console.WriteLine(" taskman import backup.json"); + + return 0; + } + + private static int ShowUnknownCommand(string command) + { + Console.WriteLine($"Unknown command: {command}"); + Console.WriteLine("Run 'taskman help' for usage information."); + return 1; + } +} diff --git a/src/TaskManager.CLI/Services/ExportService.cs b/src/TaskManager.CLI/Services/ExportService.cs new file mode 100644 index 0000000..33ab0b7 --- /dev/null +++ b/src/TaskManager.CLI/Services/ExportService.cs @@ -0,0 +1,145 @@ +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using TaskManager.CLI.Interfaces; +using TaskManager.CLI.Models; + +namespace TaskManager.CLI.Services; + +/// +/// Service for exporting tasks to various formats. +/// +public class ExportService : IExportService +{ + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true + }; + + public ExportService(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task ExportToCsvAsync(IEnumerable tasks, string filePath) + { + try + { + var csv = new StringBuilder(); + csv.AppendLine("Id,Description,IsCompleted,Priority,CreatedAt,DueDate,Tags"); + + foreach (var task in tasks) + { + var tags = string.Join("|", task.Tags); + var dueDate = task.DueDate?.ToString("yyyy-MM-dd") ?? ""; + var completed = task.IsCompleted ? "Yes" : "No"; + + csv.AppendLine($"{task.Id},\"{task.Description.Replace("\"", "\"\"")}\",{completed},{task.Priority},{task.CreatedAt:yyyy-MM-dd HH:mm:ss},{dueDate},\"{tags}\""); + } + + await File.WriteAllTextAsync(filePath, csv.ToString()); + _logger.LogInformation("Exported {Count} tasks to CSV: {FilePath}", tasks.Count(), filePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error exporting to CSV: {FilePath}", filePath); + throw; + } + } + + public async Task ExportToMarkdownAsync(IEnumerable tasks, string filePath) + { + try + { + var md = new StringBuilder(); + md.AppendLine("# Task List"); + md.AppendLine(); + md.AppendLine($"*Exported on {DateTime.Now:yyyy-MM-dd HH:mm:ss}*"); + md.AppendLine(); + + var pendingTasks = tasks.Where(t => !t.IsCompleted).OrderByDescending(t => t.Priority).ToList(); + var completedTasks = tasks.Where(t => t.IsCompleted).OrderBy(t => t.Id).ToList(); + + if (pendingTasks.Any()) + { + md.AppendLine("## Pending Tasks"); + md.AppendLine(); + foreach (var task in pendingTasks) + { + var priority = new string('★', task.Priority); + var tags = task.Tags.Any() ? $" `{string.Join("` `", task.Tags)}`" : ""; + var due = task.DueDate.HasValue ? $" 📅 {task.DueDate.Value:yyyy-MM-dd}" : ""; + md.AppendLine($"- [ ] **#{task.Id}** {task.Description} {priority}{due}{tags}"); + } + md.AppendLine(); + } + + if (completedTasks.Any()) + { + md.AppendLine("## Completed Tasks"); + md.AppendLine(); + foreach (var task in completedTasks) + { + var tags = task.Tags.Any() ? $" `{string.Join("` `", task.Tags)}`" : ""; + md.AppendLine($"- [x] **#{task.Id}** {task.Description}{tags}"); + } + md.AppendLine(); + } + + md.AppendLine("---"); + md.AppendLine($"*Total: {tasks.Count()} tasks ({pendingTasks.Count} pending, {completedTasks.Count} completed)*"); + + await File.WriteAllTextAsync(filePath, md.ToString()); + _logger.LogInformation("Exported {Count} tasks to Markdown: {FilePath}", tasks.Count(), filePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error exporting to Markdown: {FilePath}", filePath); + throw; + } + } + + public async Task ExportToJsonAsync(IEnumerable tasks, string filePath) + { + try + { + var json = JsonSerializer.Serialize(tasks, _jsonOptions); + await File.WriteAllTextAsync(filePath, json); + _logger.LogInformation("Exported {Count} tasks to JSON: {FilePath}", tasks.Count(), filePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error exporting to JSON: {FilePath}", filePath); + throw; + } + } + + public async Task> ImportFromJsonAsync(string filePath) + { + try + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"Import file not found: {filePath}"); + } + + var json = await File.ReadAllTextAsync(filePath); + var tasks = JsonSerializer.Deserialize>(json, _jsonOptions); + + if (tasks == null) + { + throw new InvalidOperationException("Failed to deserialize tasks from JSON"); + } + + _logger.LogInformation("Imported {Count} tasks from JSON: {FilePath}", tasks.Count, filePath); + return tasks; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error importing from JSON: {FilePath}", filePath); + throw; + } + } +} diff --git a/src/TaskManager.CLI/Services/StatisticsService.cs b/src/TaskManager.CLI/Services/StatisticsService.cs new file mode 100644 index 0000000..9773df3 --- /dev/null +++ b/src/TaskManager.CLI/Services/StatisticsService.cs @@ -0,0 +1,77 @@ +using Microsoft.Extensions.Logging; +using TaskManager.CLI.Interfaces; +using TaskManager.CLI.Models; + +namespace TaskManager.CLI.Services; + +/// +/// Service for task statistics and reporting. +/// +public class StatisticsService : IStatisticsService +{ + private readonly ILogger _logger; + + public StatisticsService(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public TaskStatistics GetStatistics(IEnumerable tasks) + { + var taskList = tasks.ToList(); + var now = DateTime.UtcNow; + var today = now.Date; + var endOfWeek = today.AddDays(7); + + var pendingTasks = taskList.Where(t => !t.IsCompleted).ToList(); + var overdueTasks = pendingTasks.Where(t => t.DueDate.HasValue && t.DueDate.Value.Date < today).ToList(); + + return new TaskStatistics + { + TotalTasks = taskList.Count, + CompletedTasks = taskList.Count(t => t.IsCompleted), + PendingTasks = pendingTasks.Count, + OverdueTasks = overdueTasks.Count, + DueToday = pendingTasks.Count(t => t.DueDate.HasValue && t.DueDate.Value.Date == today), + DueThisWeek = pendingTasks.Count(t => t.DueDate.HasValue && t.DueDate.Value.Date >= today && t.DueDate.Value.Date <= endOfWeek), + AveragePriority = pendingTasks.Any() ? pendingTasks.Average(t => t.Priority) : 0, + TotalTags = taskList.SelectMany(t => t.Tags).Distinct().Count() + }; + } + + public Dictionary GetTasksByPriority(IEnumerable tasks) + { + return tasks + .Where(t => !t.IsCompleted) + .GroupBy(t => t.Priority) + .OrderByDescending(g => g.Key) + .ToDictionary(g => g.Key, g => g.Count()); + } + + public Dictionary GetTasksByTag(IEnumerable tasks) + { + return tasks + .SelectMany(t => t.Tags) + .GroupBy(tag => tag, StringComparer.OrdinalIgnoreCase) + .OrderByDescending(g => g.Count()) + .ToDictionary(g => g.Key, g => g.Count()); + } + + public IEnumerable GetOverdueTasks(IEnumerable tasks) + { + var today = DateTime.UtcNow.Date; + return tasks + .Where(t => !t.IsCompleted && t.DueDate.HasValue && t.DueDate.Value.Date < today) + .OrderBy(t => t.DueDate); + } + + public IEnumerable GetUpcomingTasks(IEnumerable tasks, int days = 7) + { + var today = DateTime.UtcNow.Date; + var endDate = today.AddDays(days); + + return tasks + .Where(t => !t.IsCompleted && t.DueDate.HasValue && t.DueDate.Value.Date >= today && t.DueDate.Value.Date <= endDate) + .OrderBy(t => t.DueDate); + } +} diff --git a/src/TaskManager.CLI/Services/TaskService.cs b/src/TaskManager.CLI/Services/TaskService.cs new file mode 100644 index 0000000..298cbcb --- /dev/null +++ b/src/TaskManager.CLI/Services/TaskService.cs @@ -0,0 +1,213 @@ +using System.Text.Json; +using Microsoft.Extensions.Logging; +using TaskManager.CLI.Interfaces; +using TaskManager.CLI.Models; + +namespace TaskManager.CLI.Services; + +/// +/// Service for managing tasks with file-based persistence. +/// +public class TaskService : ITaskService +{ + private const string DefaultFileName = "tasks.json"; + private readonly string _fileName; + private readonly ILogger _logger; + private List _tasks = new(); + private readonly JsonSerializerOptions _jsonOptions = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true + }; + + public TaskService(ILogger logger, string? fileName = null) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileName = fileName ?? DefaultFileName; + } + + public async Task LoadTasksAsync() + { + try + { + if (File.Exists(_fileName)) + { + var json = await File.ReadAllTextAsync(_fileName); + _tasks = JsonSerializer.Deserialize>(json, _jsonOptions) ?? new(); + _logger.LogInformation("Loaded {Count} tasks from {FileName}", _tasks.Count, _fileName); + } + else + { + _tasks = new(); + _logger.LogInformation("No existing task file found. Starting with empty task list."); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading tasks from {FileName}", _fileName); + _tasks = new(); + throw; + } + } + + public async Task SaveTasksAsync() + { + try + { + var json = JsonSerializer.Serialize(_tasks, _jsonOptions); + await File.WriteAllTextAsync(_fileName, json); + _logger.LogInformation("Saved {Count} tasks to {FileName}", _tasks.Count, _fileName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving tasks to {FileName}", _fileName); + throw; + } + } + + public TaskItem AddTask(string description, int priority = 3, DateTime? dueDate = null, List? tags = null) + { + if (string.IsNullOrWhiteSpace(description)) + { + throw new ArgumentException("Task description cannot be empty.", nameof(description)); + } + + if (priority < 1 || priority > 5) + { + throw new ArgumentException("Priority must be between 1 and 5.", nameof(priority)); + } + + var nextId = _tasks.Count == 0 ? 1 : _tasks.Max(t => t.Id) + 1; + var task = new TaskItem + { + Id = nextId, + Description = description, + Priority = priority, + DueDate = dueDate, + Tags = tags ?? new List(), + CreatedAt = DateTime.UtcNow + }; + + _tasks.Add(task); + _logger.LogInformation("Added task #{Id}: {Description}", task.Id, task.Description); + return task; + } + + public IEnumerable GetAllTasks(bool includeCompleted = true) + { + return includeCompleted + ? _tasks.OrderBy(t => t.IsCompleted).ThenByDescending(t => t.Priority).ThenBy(t => t.CreatedAt) + : _tasks.Where(t => !t.IsCompleted).OrderByDescending(t => t.Priority).ThenBy(t => t.CreatedAt); + } + + public TaskItem? GetTaskById(int id) + { + return _tasks.FirstOrDefault(t => t.Id == id); + } + + public bool RemoveTask(int id) + { + var task = GetTaskById(id); + if (task == null) + { + _logger.LogWarning("Task #{Id} not found for removal", id); + return false; + } + + _tasks.Remove(task); + _logger.LogInformation("Removed task #{Id}", id); + return true; + } + + public bool CompleteTask(int id) + { + var task = GetTaskById(id); + if (task == null) + { + _logger.LogWarning("Task #{Id} not found for completion", id); + return false; + } + + task.IsCompleted = true; + _logger.LogInformation("Completed task #{Id}: {Description}", task.Id, task.Description); + return true; + } + + public bool UpdateTask(int id, string newDescription) + { + if (string.IsNullOrWhiteSpace(newDescription)) + { + throw new ArgumentException("Task description cannot be empty.", nameof(newDescription)); + } + + var task = GetTaskById(id); + if (task == null) + { + _logger.LogWarning("Task #{Id} not found for update", id); + return false; + } + + var oldDescription = task.Description; + task.Description = newDescription; + _logger.LogInformation("Updated task #{Id} from '{OldDescription}' to '{NewDescription}'", + task.Id, oldDescription, newDescription); + return true; + } + + public bool UpdateTaskPriority(int id, int priority) + { + if (priority < 1 || priority > 5) + { + throw new ArgumentException("Priority must be between 1 and 5.", nameof(priority)); + } + + var task = GetTaskById(id); + if (task == null) + { + _logger.LogWarning("Task #{Id} not found for priority update", id); + return false; + } + + task.Priority = priority; + _logger.LogInformation("Updated task #{Id} priority to {Priority}", task.Id, priority); + return true; + } + + public IEnumerable SearchTasks(string query) + { + if (string.IsNullOrWhiteSpace(query)) + { + return Enumerable.Empty(); + } + + var lowerQuery = query.ToLowerInvariant(); + return _tasks.Where(t => + t.Description.Contains(lowerQuery, StringComparison.OrdinalIgnoreCase) || + t.Tags.Any(tag => tag.Contains(lowerQuery, StringComparison.OrdinalIgnoreCase)) + ); + } + + public IEnumerable GetTasksByTag(string tag) + { + if (string.IsNullOrWhiteSpace(tag)) + { + return Enumerable.Empty(); + } + + return _tasks.Where(t => t.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase)); + } + + public int ClearCompletedTasks() + { + var completedTasks = _tasks.Where(t => t.IsCompleted).ToList(); + var count = completedTasks.Count; + + foreach (var task in completedTasks) + { + _tasks.Remove(task); + } + + _logger.LogInformation("Cleared {Count} completed tasks", count); + return count; + } +} diff --git a/src/TaskManager.CLI/TaskManager.CLI.csproj b/src/TaskManager.CLI/TaskManager.CLI.csproj new file mode 100644 index 0000000..c3c0c30 --- /dev/null +++ b/src/TaskManager.CLI/TaskManager.CLI.csproj @@ -0,0 +1,26 @@ + + + + Exe + net8.0 + enable + enable + taskman + 2.0.0 + Code for Good + A modern command-line task manager built with .NET + https://github.com/codeforgood-org/dotnet-task-manager + https://github.com/codeforgood-org/dotnet-task-manager + MIT + + + + + + + + + + + + diff --git a/tests/TaskManager.Tests/ExportServiceTests.cs b/tests/TaskManager.Tests/ExportServiceTests.cs new file mode 100644 index 0000000..60ea5d3 --- /dev/null +++ b/tests/TaskManager.Tests/ExportServiceTests.cs @@ -0,0 +1,154 @@ +using Microsoft.Extensions.Logging; +using Moq; +using TaskManager.CLI.Models; +using TaskManager.CLI.Services; +using Xunit; + +namespace TaskManager.Tests; + +public class ExportServiceTests : IDisposable +{ + private readonly Mock> _mockLogger; + private readonly ExportService _exportService; + private readonly List _testFiles = new(); + + public ExportServiceTests() + { + _mockLogger = new Mock>(); + _exportService = new ExportService(_mockLogger.Object); + } + + public void Dispose() + { + foreach (var file in _testFiles.Where(File.Exists)) + { + File.Delete(file); + } + } + + private string GetTestFilePath(string extension) + { + var path = $"test_export_{Guid.NewGuid()}.{extension}"; + _testFiles.Add(path); + return path; + } + + private List GetSampleTasks() + { + return new List + { + new TaskItem + { + Id = 1, + Description = "Buy groceries", + Priority = 4, + Tags = new List { "shopping", "personal" }, + CreatedAt = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc) + }, + new TaskItem + { + Id = 2, + Description = "Write report", + Priority = 5, + IsCompleted = true, + Tags = new List { "work" }, + CreatedAt = new DateTime(2024, 1, 2, 14, 30, 0, DateTimeKind.Utc), + DueDate = new DateTime(2024, 1, 15) + }, + new TaskItem + { + Id = 3, + Description = "Call dentist", + Priority = 3, + Tags = new List(), + CreatedAt = new DateTime(2024, 1, 3, 9, 15, 0, DateTimeKind.Utc) + } + }; + } + + [Fact] + public async Task ExportToCsv_CreatesValidCsvFile() + { + // Arrange + var tasks = GetSampleTasks(); + var filePath = GetTestFilePath("csv"); + + // Act + await _exportService.ExportToCsvAsync(tasks, filePath); + + // Assert + Assert.True(File.Exists(filePath)); + var content = await File.ReadAllTextAsync(filePath); + Assert.Contains("Id,Description,IsCompleted,Priority,CreatedAt,DueDate,Tags", content); + Assert.Contains("Buy groceries", content); + Assert.Contains("Write report", content); + Assert.Contains("Call dentist", content); + } + + [Fact] + public async Task ExportToMarkdown_CreatesValidMarkdownFile() + { + // Arrange + var tasks = GetSampleTasks(); + var filePath = GetTestFilePath("md"); + + // Act + await _exportService.ExportToMarkdownAsync(tasks, filePath); + + // Assert + Assert.True(File.Exists(filePath)); + var content = await File.ReadAllTextAsync(filePath); + Assert.Contains("# Task List", content); + Assert.Contains("## Pending Tasks", content); + Assert.Contains("## Completed Tasks", content); + Assert.Contains("Buy groceries", content); + Assert.Contains("Write report", content); + } + + [Fact] + public async Task ExportToJson_CreatesValidJsonFile() + { + // Arrange + var tasks = GetSampleTasks(); + var filePath = GetTestFilePath("json"); + + // Act + await _exportService.ExportToJsonAsync(tasks, filePath); + + // Assert + Assert.True(File.Exists(filePath)); + var content = await File.ReadAllTextAsync(filePath); + Assert.Contains("\"Id\": 1", content); + Assert.Contains("Buy groceries", content); + } + + [Fact] + public async Task ImportFromJson_ReadsTasksCorrectly() + { + // Arrange + var originalTasks = GetSampleTasks(); + var filePath = GetTestFilePath("json"); + await _exportService.ExportToJsonAsync(originalTasks, filePath); + + // Act + var importedTasks = await _exportService.ImportFromJsonAsync(filePath); + + // Assert + Assert.Equal(3, importedTasks.Count); + Assert.Equal("Buy groceries", importedTasks[0].Description); + Assert.Equal(4, importedTasks[0].Priority); + Assert.Equal(2, importedTasks[0].Tags.Count); + } + + [Fact] + public async Task ImportFromJson_NonExistentFile_ThrowsException() + { + // Arrange + var filePath = "nonexistent_file.json"; + + // Act & Assert + await Assert.ThrowsAsync( + () => _exportService.ImportFromJsonAsync(filePath) + ); + } +} diff --git a/tests/TaskManager.Tests/IntegrationTests.cs b/tests/TaskManager.Tests/IntegrationTests.cs new file mode 100644 index 0000000..288e222 --- /dev/null +++ b/tests/TaskManager.Tests/IntegrationTests.cs @@ -0,0 +1,198 @@ +using Microsoft.Extensions.Logging; +using Moq; +using TaskManager.CLI.Services; +using Xunit; + +namespace TaskManager.Tests; + +/// +/// Integration tests for the TaskManager application. +/// +public class IntegrationTests : IDisposable +{ + private readonly string _testFileName; + private readonly Mock> _mockLogger; + private readonly TaskService _taskService; + + public IntegrationTests() + { + _testFileName = $"integration_test_{Guid.NewGuid()}.json"; + _mockLogger = new Mock>(); + _taskService = new TaskService(_mockLogger.Object, _testFileName); + } + + public void Dispose() + { + if (File.Exists(_testFileName)) + { + File.Delete(_testFileName); + } + } + + [Fact] + public async Task FullWorkflow_AddListCompleteRemove_WorksCorrectly() + { + // Arrange & Act - Load empty tasks + await _taskService.LoadTasksAsync(); + + // Act - Add tasks + var task1 = _taskService.AddTask("Buy groceries", 4, tags: new List { "shopping", "personal" }); + var task2 = _taskService.AddTask("Write report", 5, dueDate: DateTime.UtcNow.AddDays(2), tags: new List { "work" }); + var task3 = _taskService.AddTask("Call dentist", 3); + + // Assert - Verify tasks were added + var allTasks = _taskService.GetAllTasks().ToList(); + Assert.Equal(3, allTasks.Count); + + // Act - Save tasks + await _taskService.SaveTasksAsync(); + + // Act - Load tasks in new service instance + var newService = new TaskService(_mockLogger.Object, _testFileName); + await newService.LoadTasksAsync(); + + // Assert - Verify persistence + var loadedTasks = newService.GetAllTasks().ToList(); + Assert.Equal(3, loadedTasks.Count); + + // Act - Complete a task + newService.CompleteTask(task1.Id); + + // Assert - Verify completion + var completedTask = newService.GetTaskById(task1.Id); + Assert.NotNull(completedTask); + Assert.True(completedTask.IsCompleted); + + // Act - Search tasks + var searchResults = newService.SearchTasks("report").ToList(); + Assert.Single(searchResults); + Assert.Equal(task2.Id, searchResults[0].Id); + + // Act - Filter by tag + var workTasks = newService.GetTasksByTag("work").ToList(); + Assert.Single(workTasks); + Assert.Equal(task2.Id, workTasks[0].Id); + + // Act - Remove a task + newService.RemoveTask(task3.Id); + + // Assert - Verify removal + Assert.Equal(2, newService.GetAllTasks().Count()); + Assert.Null(newService.GetTaskById(task3.Id)); + + // Act - Save final state + await newService.SaveTasksAsync(); + + // Act - Verify final state with another instance + var finalService = new TaskService(_mockLogger.Object, _testFileName); + await finalService.LoadTasksAsync(); + + var finalTasks = finalService.GetAllTasks().ToList(); + Assert.Equal(2, finalTasks.Count); + Assert.True(finalTasks.Any(t => t.Id == task1.Id && t.IsCompleted)); + Assert.True(finalTasks.Any(t => t.Id == task2.Id && !t.IsCompleted)); + } + + [Fact] + public async Task PriorityOrdering_WorksCorrectly() + { + // Arrange + await _taskService.LoadTasksAsync(); + + // Act - Add tasks with different priorities + _taskService.AddTask("Low priority", 1); + _taskService.AddTask("High priority", 5); + _taskService.AddTask("Medium priority", 3); + + // Assert - Verify ordering (pending tasks ordered by priority descending) + var tasks = _taskService.GetAllTasks(includeCompleted: false).ToList(); + Assert.Equal(5, tasks[0].Priority); + Assert.Equal(3, tasks[1].Priority); + Assert.Equal(1, tasks[2].Priority); + } + + [Fact] + public async Task TagFiltering_WorksCorrectly() + { + // Arrange + await _taskService.LoadTasksAsync(); + + // Act - Add tasks with various tags + _taskService.AddTask("Task 1", tags: new List { "work", "urgent" }); + _taskService.AddTask("Task 2", tags: new List { "personal" }); + _taskService.AddTask("Task 3", tags: new List { "work", "planning" }); + _taskService.AddTask("Task 4", tags: new List { "urgent", "personal" }); + + // Assert - Filter by 'work' tag + var workTasks = _taskService.GetTasksByTag("work").ToList(); + Assert.Equal(2, workTasks.Count); + + // Assert - Filter by 'urgent' tag + var urgentTasks = _taskService.GetTasksByTag("urgent").ToList(); + Assert.Equal(2, urgentTasks.Count); + + // Assert - Filter by 'personal' tag + var personalTasks = _taskService.GetTasksByTag("personal").ToList(); + Assert.Equal(2, personalTasks.Count); + } + + [Fact] + public async Task ClearCompleted_RemovesOnlyCompletedTasks() + { + // Arrange + await _taskService.LoadTasksAsync(); + + // Act - Add and complete some tasks + var task1 = _taskService.AddTask("Task 1"); + var task2 = _taskService.AddTask("Task 2"); + var task3 = _taskService.AddTask("Task 3"); + var task4 = _taskService.AddTask("Task 4"); + + _taskService.CompleteTask(task1.Id); + _taskService.CompleteTask(task3.Id); + + // Assert - Before clear + Assert.Equal(4, _taskService.GetAllTasks().Count()); + + // Act - Clear completed + var clearedCount = _taskService.ClearCompletedTasks(); + + // Assert - After clear + Assert.Equal(2, clearedCount); + Assert.Equal(2, _taskService.GetAllTasks().Count()); + Assert.NotNull(_taskService.GetTaskById(task2.Id)); + Assert.NotNull(_taskService.GetTaskById(task4.Id)); + Assert.Null(_taskService.GetTaskById(task1.Id)); + Assert.Null(_taskService.GetTaskById(task3.Id)); + } + + [Fact] + public async Task UpdateOperations_WorkCorrectly() + { + // Arrange + await _taskService.LoadTasksAsync(); + var task = _taskService.AddTask("Original description", 3); + + // Act & Assert - Update description + var updated = _taskService.UpdateTask(task.Id, "New description"); + Assert.True(updated); + var updatedTask = _taskService.GetTaskById(task.Id); + Assert.Equal("New description", updatedTask?.Description); + + // Act & Assert - Update priority + updated = _taskService.UpdateTaskPriority(task.Id, 5); + Assert.True(updated); + updatedTask = _taskService.GetTaskById(task.Id); + Assert.Equal(5, updatedTask?.Priority); + + // Act & Assert - Save and reload + await _taskService.SaveTasksAsync(); + var newService = new TaskService(_mockLogger.Object, _testFileName); + await newService.LoadTasksAsync(); + + var reloadedTask = newService.GetTaskById(task.Id); + Assert.NotNull(reloadedTask); + Assert.Equal("New description", reloadedTask.Description); + Assert.Equal(5, reloadedTask.Priority); + } +} diff --git a/tests/TaskManager.Tests/StatisticsServiceTests.cs b/tests/TaskManager.Tests/StatisticsServiceTests.cs new file mode 100644 index 0000000..0324a56 --- /dev/null +++ b/tests/TaskManager.Tests/StatisticsServiceTests.cs @@ -0,0 +1,172 @@ +using Microsoft.Extensions.Logging; +using Moq; +using TaskManager.CLI.Models; +using TaskManager.CLI.Services; +using Xunit; + +namespace TaskManager.Tests; + +public class StatisticsServiceTests +{ + private readonly Mock> _mockLogger; + private readonly StatisticsService _statisticsService; + + public StatisticsServiceTests() + { + _mockLogger = new Mock>(); + _statisticsService = new StatisticsService(_mockLogger.Object); + } + + private List GetSampleTasks() + { + var now = DateTime.UtcNow; + return new List + { + new TaskItem { Id = 1, Description = "Task 1", Priority = 5, IsCompleted = false, DueDate = now.AddDays(-1) }, + new TaskItem { Id = 2, Description = "Task 2", Priority = 4, IsCompleted = true }, + new TaskItem { Id = 3, Description = "Task 3", Priority = 3, IsCompleted = false, DueDate = now.Date }, + new TaskItem { Id = 4, Description = "Task 4", Priority = 2, IsCompleted = false, DueDate = now.AddDays(5) }, + new TaskItem { Id = 5, Description = "Task 5", Priority = 1, IsCompleted = true }, + }; + } + + [Fact] + public void GetStatistics_ReturnsCorrectCounts() + { + // Arrange + var tasks = GetSampleTasks(); + + // Act + var stats = _statisticsService.GetStatistics(tasks); + + // Assert + Assert.Equal(5, stats.TotalTasks); + Assert.Equal(2, stats.CompletedTasks); + Assert.Equal(3, stats.PendingTasks); + Assert.Equal(40.0, stats.CompletionRate, 1); + } + + [Fact] + public void GetStatistics_CalculatesOverdueTasksCorrectly() + { + // Arrange + var tasks = GetSampleTasks(); + + // Act + var stats = _statisticsService.GetStatistics(tasks); + + // Assert + Assert.Equal(1, stats.OverdueTasks); // Task 1 is overdue + } + + [Fact] + public void GetStatistics_CalculatesDueTodayCorrectly() + { + // Arrange + var tasks = GetSampleTasks(); + + // Act + var stats = _statisticsService.GetStatistics(tasks); + + // Assert + Assert.Equal(1, stats.DueToday); // Task 3 is due today + } + + [Fact] + public void GetStatistics_CalculatesAveragePriorityCorrectly() + { + // Arrange + var tasks = GetSampleTasks(); + + // Act + var stats = _statisticsService.GetStatistics(tasks); + + // Assert + // Pending tasks are 1, 3, 4 with priorities 5, 3, 2 = average 3.33 + Assert.Equal(3.33, stats.AveragePriority, 2); + } + + [Fact] + public void GetTasksByPriority_GroupsCorrectly() + { + // Arrange + var tasks = GetSampleTasks(); + + // Act + var grouped = _statisticsService.GetTasksByPriority(tasks); + + // Assert + Assert.Equal(3, grouped.Count); // Only pending tasks with priorities 5, 3, 2 + Assert.Equal(1, grouped[5]); + Assert.Equal(1, grouped[3]); + Assert.Equal(1, grouped[2]); + } + + [Fact] + public void GetTasksByTag_GroupsCorrectly() + { + // Arrange + var tasks = new List + { + new TaskItem { Id = 1, Tags = new List { "work", "urgent" } }, + new TaskItem { Id = 2, Tags = new List { "personal" } }, + new TaskItem { Id = 3, Tags = new List { "work" } }, + new TaskItem { Id = 4, Tags = new List { "urgent", "important" } }, + }; + + // Act + var grouped = _statisticsService.GetTasksByTag(tasks); + + // Assert + Assert.Equal(4, grouped.Count); + Assert.Equal(2, grouped["work"]); + Assert.Equal(2, grouped["urgent"]); + Assert.Equal(1, grouped["personal"]); + Assert.Equal(1, grouped["important"]); + } + + [Fact] + public void GetOverdueTasks_ReturnsOnlyOverdueTasks() + { + // Arrange + var tasks = GetSampleTasks(); + + // Act + var overdue = _statisticsService.GetOverdueTasks(tasks).ToList(); + + // Assert + Assert.Single(overdue); + Assert.Equal(1, overdue[0].Id); + } + + [Fact] + public void GetUpcomingTasks_ReturnsTasksDueWithinDays() + { + // Arrange + var tasks = GetSampleTasks(); + + // Act + var upcoming = _statisticsService.GetUpcomingTasks(tasks, days: 7).ToList(); + + // Assert + Assert.Equal(2, upcoming.Count); // Tasks 3 and 4 + Assert.Contains(upcoming, t => t.Id == 3); // Due today + Assert.Contains(upcoming, t => t.Id == 4); // Due in 5 days + } + + [Fact] + public void GetStatistics_EmptyTaskList_ReturnsZeros() + { + // Arrange + var tasks = new List(); + + // Act + var stats = _statisticsService.GetStatistics(tasks); + + // Assert + Assert.Equal(0, stats.TotalTasks); + Assert.Equal(0, stats.CompletedTasks); + Assert.Equal(0, stats.PendingTasks); + Assert.Equal(0, stats.CompletionRate); + } +} diff --git a/tests/TaskManager.Tests/TaskManager.Tests.csproj b/tests/TaskManager.Tests/TaskManager.Tests.csproj new file mode 100644 index 0000000..41426ec --- /dev/null +++ b/tests/TaskManager.Tests/TaskManager.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/TaskManager.Tests/TaskServiceTests.cs b/tests/TaskManager.Tests/TaskServiceTests.cs new file mode 100644 index 0000000..a6d3a12 --- /dev/null +++ b/tests/TaskManager.Tests/TaskServiceTests.cs @@ -0,0 +1,455 @@ +using Microsoft.Extensions.Logging; +using Moq; +using TaskManager.CLI.Models; +using TaskManager.CLI.Services; +using Xunit; + +namespace TaskManager.Tests; + +public class TaskServiceTests +{ + private readonly Mock> _mockLogger; + private readonly string _testFileName; + + public TaskServiceTests() + { + _mockLogger = new Mock>(); + _testFileName = $"test_tasks_{Guid.NewGuid()}.json"; + } + + private TaskService CreateService() + { + return new TaskService(_mockLogger.Object, _testFileName); + } + + [Fact] + public void AddTask_WithValidDescription_ReturnsTask() + { + // Arrange + var service = CreateService(); + var description = "Test task"; + + // Act + var task = service.AddTask(description); + + // Assert + Assert.NotNull(task); + Assert.Equal(1, task.Id); + Assert.Equal(description, task.Description); + Assert.False(task.IsCompleted); + Assert.Equal(3, task.Priority); // Default priority + } + + [Fact] + public void AddTask_WithPriority_SetsPriorityCorrectly() + { + // Arrange + var service = CreateService(); + var description = "High priority task"; + var priority = 5; + + // Act + var task = service.AddTask(description, priority); + + // Assert + Assert.Equal(priority, task.Priority); + } + + [Fact] + public void AddTask_WithTags_SetsTagsCorrectly() + { + // Arrange + var service = CreateService(); + var description = "Tagged task"; + var tags = new List { "work", "urgent" }; + + // Act + var task = service.AddTask(description, tags: tags); + + // Assert + Assert.Equal(2, task.Tags.Count); + Assert.Contains("work", task.Tags); + Assert.Contains("urgent", task.Tags); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void AddTask_WithInvalidDescription_ThrowsException(string invalidDescription) + { + // Arrange + var service = CreateService(); + + // Act & Assert + Assert.Throws(() => service.AddTask(invalidDescription)); + } + + [Theory] + [InlineData(0)] + [InlineData(6)] + [InlineData(-1)] + public void AddTask_WithInvalidPriority_ThrowsException(int invalidPriority) + { + // Arrange + var service = CreateService(); + + // Act & Assert + Assert.Throws(() => service.AddTask("Test", invalidPriority)); + } + + [Fact] + public void AddTask_MultipleTasksIncrementId() + { + // Arrange + var service = CreateService(); + + // Act + var task1 = service.AddTask("Task 1"); + var task2 = service.AddTask("Task 2"); + var task3 = service.AddTask("Task 3"); + + // Assert + Assert.Equal(1, task1.Id); + Assert.Equal(2, task2.Id); + Assert.Equal(3, task3.Id); + } + + [Fact] + public void GetAllTasks_WithNoTasks_ReturnsEmptyList() + { + // Arrange + var service = CreateService(); + + // Act + var tasks = service.GetAllTasks(); + + // Assert + Assert.Empty(tasks); + } + + [Fact] + public void GetAllTasks_WithTasks_ReturnsAllTasks() + { + // Arrange + var service = CreateService(); + service.AddTask("Task 1"); + service.AddTask("Task 2"); + service.AddTask("Task 3"); + + // Act + var tasks = service.GetAllTasks().ToList(); + + // Assert + Assert.Equal(3, tasks.Count); + } + + [Fact] + public void GetAllTasks_ExcludingCompleted_ReturnsOnlyPendingTasks() + { + // Arrange + var service = CreateService(); + service.AddTask("Task 1"); + var task2 = service.AddTask("Task 2"); + service.AddTask("Task 3"); + service.CompleteTask(task2.Id); + + // Act + var tasks = service.GetAllTasks(includeCompleted: false).ToList(); + + // Assert + Assert.Equal(2, tasks.Count); + Assert.All(tasks, t => Assert.False(t.IsCompleted)); + } + + [Fact] + public void GetTaskById_WithValidId_ReturnsTask() + { + // Arrange + var service = CreateService(); + var addedTask = service.AddTask("Test task"); + + // Act + var task = service.GetTaskById(addedTask.Id); + + // Assert + Assert.NotNull(task); + Assert.Equal(addedTask.Id, task.Id); + Assert.Equal(addedTask.Description, task.Description); + } + + [Fact] + public void GetTaskById_WithInvalidId_ReturnsNull() + { + // Arrange + var service = CreateService(); + + // Act + var task = service.GetTaskById(999); + + // Assert + Assert.Null(task); + } + + [Fact] + public void RemoveTask_WithValidId_RemovesTask() + { + // Arrange + var service = CreateService(); + var task = service.AddTask("Test task"); + + // Act + var result = service.RemoveTask(task.Id); + + // Assert + Assert.True(result); + Assert.Null(service.GetTaskById(task.Id)); + } + + [Fact] + public void RemoveTask_WithInvalidId_ReturnsFalse() + { + // Arrange + var service = CreateService(); + + // Act + var result = service.RemoveTask(999); + + // Assert + Assert.False(result); + } + + [Fact] + public void CompleteTask_WithValidId_MarksTaskAsCompleted() + { + // Arrange + var service = CreateService(); + var task = service.AddTask("Test task"); + + // Act + var result = service.CompleteTask(task.Id); + + // Assert + Assert.True(result); + var completedTask = service.GetTaskById(task.Id); + Assert.NotNull(completedTask); + Assert.True(completedTask.IsCompleted); + } + + [Fact] + public void CompleteTask_WithInvalidId_ReturnsFalse() + { + // Arrange + var service = CreateService(); + + // Act + var result = service.CompleteTask(999); + + // Assert + Assert.False(result); + } + + [Fact] + public void UpdateTask_WithValidIdAndDescription_UpdatesTask() + { + // Arrange + var service = CreateService(); + var task = service.AddTask("Original description"); + var newDescription = "Updated description"; + + // Act + var result = service.UpdateTask(task.Id, newDescription); + + // Assert + Assert.True(result); + var updatedTask = service.GetTaskById(task.Id); + Assert.Equal(newDescription, updatedTask?.Description); + } + + [Fact] + public void UpdateTask_WithInvalidId_ReturnsFalse() + { + // Arrange + var service = CreateService(); + + // Act + var result = service.UpdateTask(999, "New description"); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void UpdateTask_WithInvalidDescription_ThrowsException(string invalidDescription) + { + // Arrange + var service = CreateService(); + var task = service.AddTask("Test task"); + + // Act & Assert + Assert.Throws(() => service.UpdateTask(task.Id, invalidDescription)); + } + + [Fact] + public void UpdateTaskPriority_WithValidIdAndPriority_UpdatesPriority() + { + // Arrange + var service = CreateService(); + var task = service.AddTask("Test task"); + var newPriority = 5; + + // Act + var result = service.UpdateTaskPriority(task.Id, newPriority); + + // Assert + Assert.True(result); + var updatedTask = service.GetTaskById(task.Id); + Assert.Equal(newPriority, updatedTask?.Priority); + } + + [Theory] + [InlineData(0)] + [InlineData(6)] + [InlineData(-1)] + public void UpdateTaskPriority_WithInvalidPriority_ThrowsException(int invalidPriority) + { + // Arrange + var service = CreateService(); + var task = service.AddTask("Test task"); + + // Act & Assert + Assert.Throws(() => service.UpdateTaskPriority(task.Id, invalidPriority)); + } + + [Fact] + public void SearchTasks_WithMatchingQuery_ReturnsMatchingTasks() + { + // Arrange + var service = CreateService(); + service.AddTask("Buy groceries"); + service.AddTask("Buy books"); + service.AddTask("Read books"); + + // Act + var results = service.SearchTasks("books").ToList(); + + // Assert + Assert.Equal(2, results.Count); + Assert.All(results, t => Assert.Contains("books", t.Description, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void SearchTasks_WithNoMatches_ReturnsEmptyList() + { + // Arrange + var service = CreateService(); + service.AddTask("Task 1"); + service.AddTask("Task 2"); + + // Act + var results = service.SearchTasks("nonexistent"); + + // Assert + Assert.Empty(results); + } + + [Fact] + public void SearchTasks_SearchesTags() + { + // Arrange + var service = CreateService(); + service.AddTask("Task 1", tags: new List { "work", "urgent" }); + service.AddTask("Task 2", tags: new List { "personal" }); + service.AddTask("Task 3", tags: new List { "work" }); + + // Act + var results = service.SearchTasks("work").ToList(); + + // Assert + Assert.Equal(2, results.Count); + } + + [Fact] + public void GetTasksByTag_WithMatchingTag_ReturnsMatchingTasks() + { + // Arrange + var service = CreateService(); + service.AddTask("Task 1", tags: new List { "work" }); + service.AddTask("Task 2", tags: new List { "personal" }); + service.AddTask("Task 3", tags: new List { "work", "urgent" }); + + // Act + var results = service.GetTasksByTag("work").ToList(); + + // Assert + Assert.Equal(2, results.Count); + Assert.All(results, t => Assert.Contains("work", t.Tags, StringComparer.OrdinalIgnoreCase)); + } + + [Fact] + public void ClearCompletedTasks_RemovesOnlyCompletedTasks() + { + // Arrange + var service = CreateService(); + var task1 = service.AddTask("Task 1"); + var task2 = service.AddTask("Task 2"); + var task3 = service.AddTask("Task 3"); + service.CompleteTask(task1.Id); + service.CompleteTask(task3.Id); + + // Act + var count = service.ClearCompletedTasks(); + + // Assert + Assert.Equal(2, count); + Assert.Single(service.GetAllTasks()); + Assert.NotNull(service.GetTaskById(task2.Id)); + } + + [Fact] + public async Task SaveAndLoadTasks_PersistsTasksCorrectly() + { + // Arrange + var service1 = CreateService(); + var task1 = service1.AddTask("Task 1", 5, tags: new List { "work" }); + var task2 = service1.AddTask("Task 2", 3); + service1.CompleteTask(task1.Id); + + // Act + await service1.SaveTasksAsync(); + var service2 = CreateService(); + await service2.LoadTasksAsync(); + var loadedTasks = service2.GetAllTasks().ToList(); + + // Assert + Assert.Equal(2, loadedTasks.Count); + var loadedTask1 = service2.GetTaskById(task1.Id); + Assert.NotNull(loadedTask1); + Assert.True(loadedTask1.IsCompleted); + Assert.Equal(5, loadedTask1.Priority); + Assert.Contains("work", loadedTask1.Tags); + + // Cleanup + if (File.Exists(_testFileName)) + { + File.Delete(_testFileName); + } + } + + [Fact] + public async Task LoadTasks_WithNonExistentFile_CreatesEmptyList() + { + // Arrange + var service = CreateService(); + + // Act + await service.LoadTasksAsync(); + var tasks = service.GetAllTasks(); + + // Assert + Assert.Empty(tasks); + } +} diff --git a/tests/TaskManager.Tests/Usings.cs b/tests/TaskManager.Tests/Usings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/tests/TaskManager.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit;