diff --git a/go-version/CHANGELOG_JIRA_READER.md b/go-version/CHANGELOG_JIRA_READER.md deleted file mode 100644 index e5c073a..0000000 --- a/go-version/CHANGELOG_JIRA_READER.md +++ /dev/null @@ -1,297 +0,0 @@ -# Jira Reader Feature - Implementation Summary - -## 🎉 New Feature: Jira Issue Reader for Cursor AI - -Added comprehensive Jira issue reading and exporting capabilities, specifically optimized for use with Cursor AI. - -## ✨ What's New - -### New Commands - -1. **`qkflow jira show`** - Quick terminal view - - Basic metadata display - - `--full` flag for complete details - - Fast, no file creation - -2. **`qkflow jira export`** - Complete export to files - - Markdown format output - - `--with-images` flag to download attachments - - Custom output directory support - - Includes README for Cursor usage - -3. **`qkflow jira read`** ⭐️ - Intelligent mode (RECOMMENDED) - - Automatically decides best format - - Cursor AI optimized output - - Smart image detection - -4. **`qkflow jira clean`** - Cleanup utility - - Remove single or all exports - - Dry-run support - - Disk space reporting - -### New Internal Packages - -#### `internal/jira/client.go` (Extended) -- Added `GetIssueDetailed()` method -- Support for attachments and comments -- Enhanced Issue struct with metadata - -#### `internal/jira/formatter.go` (New) -- Terminal formatting (simple and full) -- Markdown generation -- Cursor-optimized output - -#### `internal/jira/exporter.go` (New) -- File export functionality -- Image/attachment downloading -- README generation - -#### `internal/jira/cleaner.go` (New) -- Export cleanup utilities -- Directory size calculation -- Batch operations - -## 🎯 Key Features - -### 1. Multiple Output Formats - -- **Terminal**: Quick view, no files -- **Markdown**: Complete structured export -- **Smart Mode**: Automatic format selection - -### 2. Cursor AI Integration - -Special output markers that Cursor recognizes: - -``` -💡 CURSOR: Please read the following files: -1. /tmp/qkflow/jira/NA-9245/content.md -2. All images in /tmp/qkflow/jira/NA-9245/attachments/ -``` - -### 3. Image Support - -- Downloads all attachments with `--with-images` -- Supports: PNG, JPG, GIF, WebP, SVG, PDF, DOC -- Local image references in Markdown -- Cursor can read and analyze images - -### 4. Complete Metadata - -Exports include: -- Issue key, title, type, status -- Priority, assignee, reporter -- Created/updated timestamps -- Full description -- All comments -- All attachments -- Direct Jira URL - -## 📊 Usage Statistics - -### File Structure - -``` -/tmp/qkflow/jira// -├── README.md # Usage guide for Cursor -├── content.md # Main content (5-50 KB typical) -└── attachments/ # Downloaded files (if --with-images) - ├── screenshot.png - └── diagram.jpg -``` - -### Command Comparison - -| Command | Files Created | Download Images | Speed | Use Case | -|---------|---------------|-----------------|-------|----------| -| `show` | 0 | No | ⚡️ Instant | Quick peek | -| `show --full` | 0 | No | ⚡️ Fast | Read text | -| `export` | 2 | No | 🚀 Fast | Save text | -| `export --with-images` | 2+ | Yes | 🐌 Slow | Complete | -| `read` | Smart | Smart | ⚡️ Smart | **Cursor** | - -## 🔧 Technical Details - -### Dependencies - -No new external dependencies added. Uses existing: -- `github.com/andygrunwald/go-jira` (already used) -- Standard library only for new features - -### Performance - -- Basic read: < 1s -- Export with images: 2-10s (depends on attachment sizes) -- Clean operations: < 100ms - -### Storage - -- Default export location: `/tmp/qkflow/jira/` -- Typical export size: 10-500 KB (text only) -- With images: 1-50 MB (varies by attachments) - -## 🎨 Output Examples - -### Terminal Output (show --full) - -``` -╔═══════════════════════════════════════════════════════╗ -║ 🎫 NA-9245: Implement user authentication ║ -╚═══════════════════════════════════════════════════════╝ - -📋 Type: Story -📊 Status: In Progress -🏷️ Priority: High -👤 Assignee: John Doe -📝 Reporter: Jane Smith -📅 Created: 2025-01-15 10:30 -🔄 Updated: 2025-01-18 14:20 - -📝 Description: -────────────────────────────────────────────────────── -We need to implement a secure authentication system... -────────────────────────────────────────────────────── - -📎 Attachments: (3) - - screenshot.png (245 KB) - - architecture.jpg (512 KB) - - requirements.pdf (128 KB) - -💬 Comments: (5) -[Shows latest 5 comments with authors and timestamps] - -🔗 View in Jira: https://brain-ai.atlassian.net/browse/NA-9245 -``` - -### Markdown Output (export) - -```markdown ---- -issue_key: NA-9245 -title: Implement user authentication -type: Story -status: In Progress -priority: High -assignee: John Doe -jira_url: https://brain-ai.atlassian.net/browse/NA-9245 -exported_at: 2025-01-18T15:00:00Z -export_includes_images: true ---- - -# NA-9245: Implement user authentication - -## 📊 Metadata -[Metadata table] - -## 📝 Description -[Full description] - -## 📎 Attachments (3) -1. **screenshot.png** (245 KB) - ![screenshot.png](./attachments/screenshot.png) - ... - -## 💬 Comments (5) -[All comments with full content] -``` - -## 🚀 Usage in Cursor - -### Example 1: Simple Summary - -``` -User: "通过 qkflow 读取 NA-9245" - -Cursor executes: qkflow jira read NA-9245 -Cursor output: [Comprehensive summary with all details] -``` - -### Example 2: With Images - -``` -User: "用 qkflow 读取 NA-9245 包括图片,分析架构" - -Cursor executes: qkflow jira export NA-9245 --with-images -Cursor reads: content.md + all images -Cursor output: [Architecture analysis based on diagrams] -``` - -## 📝 Files Modified/Created - -### New Files - -- `internal/jira/formatter.go` (277 lines) -- `internal/jira/exporter.go` (209 lines) -- `internal/jira/cleaner.go` (214 lines) -- `cmd/qkflow/commands/jira_show.go` (69 lines) -- `cmd/qkflow/commands/jira_export.go` (120 lines) -- `cmd/qkflow/commands/jira_read.go` (96 lines) -- `cmd/qkflow/commands/jira_clean.go` (151 lines) -- `JIRA_READER.md` (documentation) -- `CHANGELOG_JIRA_READER.md` (this file) - -### Modified Files - -- `internal/jira/client.go` - Extended with detailed issue fetching -- `cmd/qkflow/commands/jira.go` - Registered new commands -- `README.md` - Added new feature documentation - -### Total Lines Added - -- Code: ~1,200 lines -- Documentation: ~600 lines -- Total: ~1,800 lines - -## 🎯 Future Enhancements - -Potential improvements for future versions: - -1. **Search functionality** - Search Jira issues -2. **Batch export** - Export multiple issues at once -3. **Custom templates** - User-defined export formats -4. **Inline image rendering** - Embed images directly in Markdown -5. **Comment filtering** - Export only recent comments -6. **JQL support** - Query issues with JQL -7. **Issue creation** - Create new Jira issues from CLI - -## 🐛 Known Limitations - -1. Images must be downloaded to be viewed by Cursor -2. Export location is temporary (`/tmp/qkflow/jira/`) -3. Large attachments may take time to download -4. Requires valid Jira API token - -## ✅ Testing - -All commands tested with: -- ✅ Valid issue keys -- ✅ Invalid issue keys (error handling) -- ✅ Issues with/without attachments -- ✅ Issues with/without comments -- ✅ Various attachment types (images, PDFs, docs) -- ✅ Clean operations (single, all, dry-run) -- ✅ All help text and flags - -## 📚 Documentation - -Complete documentation available in: -- [JIRA_READER.md](JIRA_READER.md) - Full user guide -- [README.md](README.md) - Quick start -- Command help: `qkflow jira --help` - -## 🎉 Conclusion - -This feature enables seamless integration between Jira and Cursor AI, allowing developers to: -- Quickly access Jira issue content -- Include images in their analysis -- Work efficiently with AI assistance -- Keep their workspace clean - -The implementation is production-ready, well-documented, and optimized for the Cursor AI workflow. - ---- - -**Implementation Date**: 2024-11-18 -**Version**: 1.0.0 -**Status**: ✅ Complete and Tested - diff --git a/go-version/CHANGELOG_PR_APPROVE.md b/go-version/CHANGELOG_PR_APPROVE.md deleted file mode 100644 index ba0bc81..0000000 --- a/go-version/CHANGELOG_PR_APPROVE.md +++ /dev/null @@ -1,249 +0,0 @@ -# Changelog - PR Approve Feature - -## [New Feature] PR Approve Command with URL Support - -Added `qkflow pr approve` command to streamline PR approval workflow, with full support for GitHub PR URLs! - -### What's New - -#### 🎯 PR Approve Command - -Quickly approve pull requests with optional auto-merge capability. - -**Basic Usage:** -```bash -# Approve PR by number (default comment: 👍) -qkflow pr approve 123 - -# 🆕 Approve PR by URL (works from anywhere!) -qkflow pr approve https://github.com/brain/planning-api/pull/2001 - -# 🆕 Works with /files, /commits, /checks URLs too! -qkflow pr approve https://github.com/brain/planning-api/pull/2001/files - -# Auto-detect from current branch -qkflow pr approve - -# Custom comment -qkflow pr approve 123 --comment "LGTM! 🎉" -qkflow pr approve 123 -c "Looks good!" - -# 🆕 Approve by URL with comment -qkflow pr approve https://github.com/owner/repo/pull/456 -c "Great work!" - -# Approve and auto-merge (with default 👍) -qkflow pr approve 123 --merge -qkflow pr approve 123 -m - -# 🆕 Approve by URL and merge -qkflow pr approve https://github.com/owner/repo/pull/789 -c "Ship it! 🚀" -m -``` - -### Features - -1. **🆕 URL Support** - - Approve PRs from **any repository** using GitHub URLs - - No need to be in the git directory - - Copy URL from browser and approve instantly - - Works with: `https://github.com/owner/repo/pull/NUMBER` - - **🆕 Also works with tab-specific URLs:** - - `/files` - Files tab - - `/commits` - Commits tab - - `/checks` - Checks tab - - Also supports PR numbers for local repos - -2. **🆕 Default Comment** - - All approvals use 👍 as default comment - - Customize with `-c` flag if needed - - No more empty approvals! - -3. **Auto-Detection** - - Automatically finds PR from current branch if no argument provided - - Interactive selection if multiple PRs exist - -4. **Smart Approval** - - Approves PR on GitHub - - Optional comment customization - - Validates PR state before approval - -5. **Auto-Merge (Optional)** - - `--merge` flag enables automatic merging after approval - - Checks if PR is mergeable before proceeding - - Confirms with user before merging - - Cleans up branches after successful merge - -6. **Interactive Mode** - - Asks for confirmation before merge - - Lists all open PRs for selection - -### Command Flags - -- `-c, --comment string`: Add a comment with the approval -- `-m, --merge`: Automatically merge the PR after approval -- `-h, --help`: Display help information - -### Workflow Examples - -**Quick Approve:** -```bash -# In a branch with PR -qkflow pr approve -# → Finds PR automatically → Approves → Done! -``` - -**🆕 Approve by URL (Cross-Repo):** -```bash -# Someone shares a PR link in Slack/Email -qkflow pr approve https://github.com/brain/planning-api/pull/2001 -c "LGTM!" -# → Works from anywhere! → Approves → Done! -``` - -**Approve with Comment:** -```bash -qkflow pr approve 123 -c "Great work! Ship it! 🚀" -# → Approves with comment → Done! -``` - -**Approve and Merge:** -```bash -qkflow pr approve 123 --merge -# → Approves → Checks mergeability → Confirms → Merges → Cleans up branches -``` - -**🆕 Cross-Repository Workflow:** -```bash -# Approve multiple PRs from different repos -qkflow pr approve https://github.com/team/frontend/pull/100 -c "Approved" -qkflow pr approve https://github.com/team/backend/pull/200 -c "Approved" -qkflow pr approve https://github.com/team/mobile/pull/300 -c "Approved" -# → No directory changes needed! -``` - -**Code Review Workflow:** -```bash -# Review someone's PR -qkflow pr approve 456 -c "Reviewed and approved. Minor suggestions in comments." - -# Or review and merge immediately -qkflow pr approve 456 -c "LGTM!" --merge -``` - -### Technical Details - -#### New Functions in `internal/github/client.go` - -- `ApprovePullRequest(owner, repo, number, body)`: Approve a PR with optional comment -- `IsPRMergeable(owner, repo, number)`: Check if a PR can be merged -- `ParsePRFromURL(url)`: 🆕 Parse owner, repo, and PR number from GitHub URL -- `IsPRURL(s)`: 🆕 Check if a string is a GitHub PR URL - -#### Updated Functions - -- `pr_approve.go`: Added URL parsing support at the start of command execution -- `pr_merge.go`: Added URL parsing support for consistency - -#### New Files - -- `cmd/qkflow/commands/pr_approve.go`: PR approve command implementation -- `internal/git/operations.go`: Added `CheckoutBranch()` function - -#### Supported URL Formats - -```bash -# Full HTTPS URL -https://github.com/owner/repo/pull/123 - -# With tab suffixes (NEW!) -https://github.com/brain/planning-api/pull/2001/files -https://github.com/owner/repo/pull/123/commits -https://github.com/owner/repo/pull/123/checks - -# HTTP URL -http://github.com/owner/repo/pull/123 - -# Without protocol -github.com/owner/repo/pull/123 - -# With query parameters (parsed correctly) -https://github.com/owner/repo/pull/123?comments=all -https://github.com/owner/repo/pull/123/files?file-filters%5B%5D=.js - -# With URL fragments (parsed correctly) -https://github.com/owner/repo/pull/123#discussion_r123456 -``` - -**Pro Tip:** Copy URL from any PR tab and it just works! 🎉 - -### Integration - -The approve command integrates seamlessly with existing PR workflow: - -```bash -# Full PR workflow -qkflow pr create # Create PR -qkflow pr approve 123 -m # Review and merge -# Done! 🎉 -``` - -### Error Handling - -- ✅ Validates PR exists and is open -- ✅ Checks GitHub authentication -- ✅ Verifies PR is mergeable before merge -- ✅ Handles branch cleanup failures gracefully -- ✅ Provides clear error messages - -### Comparison with GitHub CLI - -**Before (with gh):** -```bash -# For local repo PR -gh pr review 123 --approve --body "LGTM" -gh pr merge 123 -git checkout main -git branch -D feature-branch - -# For different repo (need to specify owner/repo) -gh pr review 123 --approve --body "LGTM" --repo owner/repo -gh pr merge 123 --repo owner/repo -``` - -**After (with qkflow):** -```bash -# For local repo PR -qkflow pr approve 123 -c "LGTM" -m -# Everything done automatically! 🚀 - -# 🆕 For different repo (just copy the URL!) -qkflow pr approve https://github.com/owner/repo/pull/123 -c "LGTM" -m -# Works from anywhere! No --repo flag needed! 🎉 -``` - -**Key Advantages:** -- ✅ URL support - no need to specify owner/repo separately -- ✅ Copy-paste friendly from browser -- ✅ Auto cleanup after merge -- ✅ Single command for approve + merge -- ✅ Works cross-repository seamlessly - -### Future Enhancements - -Potential improvements for future versions: - -- [ ] Request changes workflow -- [ ] Bulk approve multiple PRs -- [ ] Conditional merge (wait for CI) -- [ ] Custom merge strategies (squash, rebase) -- [ ] Slack/notification integration - ---- - -**Related Commands:** -- `qkflow pr create` - Create a new PR -- `qkflow pr merge` - Merge an existing PR -- `qkflow jira read` - Read Jira issues - -**Documentation:** -- See [README.md](README.md) for general usage -- See [CONTRIBUTING.md](CONTRIBUTING.md) for development guide - diff --git a/go-version/CONTRIBUTING.md b/go-version/CONTRIBUTING.md index 5b4b4f0..b4baab5 100644 --- a/go-version/CONTRIBUTING.md +++ b/go-version/CONTRIBUTING.md @@ -1,71 +1,71 @@ -# Contributing to Quick Workflow +# 贡献指南 -Thank you for your interest in contributing to Quick Workflow! 🎉 +感谢你对 Quick Workflow 项目的贡献兴趣!🎉 -## 🚀 Quick Start +## 🚀 快速开始 -1. Fork the repository -2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/quick-workflow.git` -3. Create a branch: `git checkout -b feature/amazing-feature` -4. Make your changes -5. Test your changes: `make test` -6. Commit: `git commit -m "Add amazing feature"` -7. Push: `git push origin feature/amazing-feature` -8. Open a Pull Request +1. Fork 仓库 +2. 克隆你的 Fork:`git clone https://github.com/YOUR_USERNAME/quick-workflow.git` +3. 创建分支:`git checkout -b feature/amazing-feature` +4. 进行更改 +5. 测试更改:`make test` +6. 提交:`git commit -m "Add amazing feature"` +7. 推送:`git push origin feature/amazing-feature` +8. 打开 Pull Request -## 🏗️ Development Setup +## 🏗️ 开发环境设置 -### Prerequisites +### 前置要求 -- Go 1.21 or higher +- Go 1.21 或更高版本 - Make - Git -- GitHub CLI (`gh`) for testing -- Jira account for integration testing (optional) +- GitHub CLI (`gh`) 用于测试 +- Jira 账户用于集成测试(可选) -### Local Setup +### 本地设置 ```bash -# Clone the repository +# 克隆仓库 git clone https://github.com/Wangggym/quick-workflow.git cd quick-workflow/go-version -# Install dependencies +# 安装依赖 make deps -# Build +# 构建 make build -# Run tests +# 运行测试 make test -# Run linters +# 运行代码检查 make lint ``` -## 📝 Code Style +## 📝 代码风格 -### Go Code +### Go 代码 -- Follow [Effective Go](https://golang.org/doc/effective_go.html) -- Use `gofmt` to format code (run `make fmt`) -- Keep functions small and focused -- Write meaningful variable names -- Add comments for exported functions and types +- 遵循 [Effective Go](https://golang.org/doc/effective_go.html) +- 使用 `gofmt` 格式化代码(运行 `make fmt`) +- 保持函数小而专注 +- 使用有意义的变量名 +- 为导出的函数和类型添加注释 -### Commit Messages +### 提交信息 -Follow [Conventional Commits](https://www.conventionalcommits.org/): +遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范: -- `feat:` New feature -- `fix:` Bug fix -- `docs:` Documentation changes -- `style:` Code style changes (formatting, etc.) -- `refactor:` Code refactoring -- `test:` Adding or updating tests -- `chore:` Maintenance tasks +- `feat:` 新功能 +- `fix:` Bug 修复 +- `docs:` 文档更改 +- `style:` 代码风格更改(格式化等) +- `refactor:` 代码重构 +- `test:` 添加或更新测试 +- `chore:` 维护任务 -Examples: +示例: ``` feat: add support for GitLab integration fix: handle empty Jira ticket gracefully @@ -73,33 +73,33 @@ docs: update installation instructions test: add tests for GitHub client ``` -## 🧪 Testing +## 🧪 测试 -### Running Tests +### 运行测试 ```bash -# Run all tests +# 运行所有测试 make test -# Run specific package tests +# 运行特定包的测试 go test ./internal/github/... go test ./internal/jira/... -# Run with coverage +# 运行测试并生成覆盖率报告 make coverage -# Run with race detector +# 使用竞态检测器运行测试 go test -race ./... ``` -### Writing Tests +### 编写测试 -- Place tests in `*_test.go` files -- Use table-driven tests for multiple test cases -- Mock external dependencies (GitHub API, Jira API) -- Test both success and error cases +- 将测试放在 `*_test.go` 文件中 +- 对多个测试用例使用表驱动测试 +- 模拟外部依赖(GitHub API、Jira API) +- 测试成功和错误两种情况 -Example: +示例: ```go func TestSanitizeBranchName(t *testing.T) { tests := []struct { @@ -123,128 +123,128 @@ func TestSanitizeBranchName(t *testing.T) { } ``` -## 📚 Documentation +## 📚 文档 -- Update README.md for user-facing changes -- Update MIGRATION.md for migration-related changes -- Add godoc comments for exported types and functions -- Update CHANGELOG.md (we'll add this) +- 为用户面向的更改更新 README.md +- 为迁移相关更改更新 MIGRATION.md +- 为导出的类型和函数添加 godoc 注释 +- 更新 CHANGELOG.md -## 🐛 Bug Reports +## 🐛 Bug 报告 -When reporting bugs, please include: +报告 Bug 时,请包含: -1. **Description**: Clear description of the issue -2. **Steps to Reproduce**: Detailed steps -3. **Expected Behavior**: What should happen -4. **Actual Behavior**: What actually happens -5. **Environment**: - - OS and version - - Go version - - qk version (`qk version`) -6. **Logs**: Any relevant error messages +1. **描述**:问题的清晰描述 +2. **复现步骤**:详细步骤 +3. **预期行为**:应该发生什么 +4. **实际行为**:实际发生了什么 +5. **环境信息**: + - 操作系统和版本 + - Go 版本 + - qkflow 版本(`qkflow version`) +6. **日志**:任何相关的错误消息 -Use the bug report template when creating an issue. +创建 Issue 时请使用 Bug 报告模板。 -## 💡 Feature Requests +## 💡 功能请求 -When requesting features: +请求功能时: -1. **Use Case**: Describe the problem you're trying to solve -2. **Proposed Solution**: Your idea for solving it -3. **Alternatives**: Other solutions you've considered -4. **Additional Context**: Any other relevant information +1. **使用场景**:描述你试图解决的问题 +2. **提议的解决方案**:你的解决思路 +3. **替代方案**:你考虑过的其他解决方案 +4. **额外上下文**:任何其他相关信息 -## 🔍 Code Review +## 🔍 代码审查 -All submissions require review. We use GitHub Pull Requests for this purpose. +所有提交都需要审查。我们使用 GitHub Pull Requests 进行代码审查。 -### PR Checklist +### PR 检查清单 -Before submitting a PR, ensure: +提交 PR 前,请确保: -- [ ] Tests pass (`make test`) -- [ ] Linters pass (`make lint`) -- [ ] Code is formatted (`make fmt`) -- [ ] Documentation is updated -- [ ] Commit messages follow conventions -- [ ] PR description explains the changes -- [ ] No sensitive information (tokens, passwords) in code +- [ ] 测试通过(`make test`) +- [ ] 代码检查通过(`make lint`) +- [ ] 代码已格式化(`make fmt`) +- [ ] 文档已更新 +- [ ] 提交信息遵循规范 +- [ ] PR 描述说明了更改内容 +- [ ] 代码中没有敏感信息(令牌、密码等) -### PR Review Process +### PR 审查流程 -1. Automated checks run (tests, linters) -2. Maintainer reviews code -3. Requested changes are addressed -4. PR is approved and merged +1. 自动检查运行(测试、代码检查) +2. 维护者审查代码 +3. 处理请求的更改 +4. PR 被批准并合并 -## 🏷️ Issue Labels +## 🏷️ Issue 标签 -- `bug`: Something isn't working -- `enhancement`: New feature or request -- `documentation`: Documentation improvements -- `good first issue`: Good for newcomers -- `help wanted`: Extra attention needed -- `question`: Further information requested +- `bug`:某些功能不工作 +- `enhancement`:新功能或请求 +- `documentation`:文档改进 +- `good first issue`:适合新手的任务 +- `help wanted`:需要额外关注 +- `question`:需要更多信息 -## 📋 Project Structure +## 📋 项目结构 ``` go-version/ -├── cmd/qkflow/ # Main application entry point +├── cmd/qkflow/ # 主应用程序入口点 │ ├── main.go -│ └── commands/ # CLI commands -├── internal/ # Internal packages -│ ├── github/ # GitHub client -│ ├── jira/ # Jira client -│ ├── git/ # Git operations -│ └── ui/ # User interface -├── pkg/ # Public packages -│ └── config/ # Configuration management -├── scripts/ # Build and release scripts -└── docs/ # Additional documentation +│ └── commands/ # CLI 命令 +├── internal/ # 内部包 +│ ├── github/ # GitHub 客户端 +│ ├── jira/ # Jira 客户端 +│ ├── git/ # Git 操作 +│ └── ui/ # 用户界面 +├── pkg/ # 公共包 +│ └── config/ # 配置管理 +├── scripts/ # 构建和发布脚本 +└── docs/ # 额外文档 ``` -## 🎯 Areas for Contribution +## 🎯 贡献领域 -We welcome contributions in these areas: +我们欢迎在这些领域的贡献: -### High Priority -- [ ] Windows support improvements -- [ ] GitLab support -- [ ] Bitbucket support -- [ ] More comprehensive tests -- [ ] Performance optimizations +### 高优先级 +- [ ] Windows 支持改进 +- [ ] GitLab 支持 +- [ ] Bitbucket 支持 +- [ ] 更全面的测试 +- [ ] 性能优化 -### Medium Priority -- [ ] Configuration validation -- [ ] Better error messages -- [ ] Template support for PR bodies -- [ ] Custom workflows -- [ ] Integration with other issue trackers +### 中优先级 +- [ ] 配置验证 +- [ ] 更好的错误消息 +- [ ] PR 正文模板支持 +- [ ] 自定义工作流 +- [ ] 与其他问题跟踪器的集成 -### Documentation -- [ ] Video tutorials -- [ ] More examples -- [ ] Troubleshooting guide -- [ ] API documentation +### 文档 +- [ ] 视频教程 +- [ ] 更多示例 +- [ ] 故障排除指南 +- [ ] API 文档 -## 🤝 Community +## 🤝 社区 -- **Discussions**: Use GitHub Discussions for questions and ideas -- **Issues**: Report bugs and request features -- **PRs**: Submit code contributions -- **Code of Conduct**: Be respectful and inclusive +- **讨论**:使用 GitHub Discussions 提问和分享想法 +- **Issues**:报告 Bug 和请求功能 +- **PRs**:提交代码贡献 +- **行为准则**:保持尊重和包容 -## 📄 License +## 📄 许可证 -By contributing, you agree that your contributions will be licensed under the MIT License. +通过贡献,你同意你的贡献将在 MIT 许可证下授权。 -## 🙏 Thank You! +## 🙏 感谢! -Your contributions make Quick Workflow better for everyone. Thank you for taking the time to contribute! +你的贡献让 Quick Workflow 对每个人都更好。感谢你花时间贡献! --- -If you have questions, feel free to open an issue or start a discussion. +如有问题,请随时打开 Issue 或开始讨论。 diff --git a/go-version/GETTING_STARTED.md b/go-version/GETTING_STARTED.md deleted file mode 100644 index 60739d0..0000000 --- a/go-version/GETTING_STARTED.md +++ /dev/null @@ -1,245 +0,0 @@ -# 🚀 Getting Started with qkflow - -## 📥 安装 - -### 方式 1: 下载预编译二进制 (推荐) - -访问 [Releases 页面](https://github.com/Wangggym/quick-workflow/releases) 下载适合你系统的版本: - -```bash -# macOS Apple Silicon (M1/M2/M3) -curl -L https://github.com/Wangggym/quick-workflow/releases/download/v1.0.0/qkflow-darwin-arm64 -o qkflow -chmod +x qkflow -sudo mv qkflow /usr/local/bin/ - -# macOS Intel -curl -L https://github.com/Wangggym/quick-workflow/releases/download/v1.0.0/qkflow-darwin-amd64 -o qkflow -chmod +x qkflow -sudo mv qkflow /usr/local/bin/ - -# Linux -curl -L https://github.com/Wangggym/quick-workflow/releases/download/v1.0.0/qkflow-linux-amd64 -o qkflow -chmod +x qkflow -sudo mv qkflow /usr/local/bin/ - -# Windows (PowerShell) -Invoke-WebRequest -Uri https://github.com/Wangggym/quick-workflow/releases/download/v1.0.0/qkflow-windows-amd64.exe -OutFile qkflow.exe -# 将 qkflow.exe 移动到 PATH 中的目录 -``` - -### 方式 2: 从源码构建 - -```bash -git clone https://github.com/Wangggym/quick-workflow.git -cd quick-workflow/go-version -make gen # 初始化依赖 -make build # 构建 -make install # 安装到 GOPATH/bin -``` - -## ⚙️ 初始化配置 - -首次使用需要配置 GitHub 和 Jira 信息: - -```bash -qkflow init -``` - -按提示输入: -- GitHub Token (从 https://github.com/settings/tokens 获取) -- GitHub Owner (你的用户名或组织名) -- GitHub Repo (仓库名) -- Jira URL (如 https://your-company.atlassian.net) -- Jira Email -- Jira API Token (从 Jira 账户设置获取) - -### 📱 iCloud 同步 (macOS) - -配置会自动保存到 iCloud Drive(如果可用),实现多设备同步! - -查看配置位置: -```bash -qkflow config -``` - -## 🎯 核心功能 - -### 1. 创建 PR - -```bash -# 在当前分支创建 PR -qkflow pr create - -# 交互式选择 AI 生成标题和描述 -``` - -### 2. 合并 PR - -```bash -# 合并当前分支的 PR -qkflow pr merge - -# 自动更新 Jira 状态 -``` - -### 3. 快速更新 (qkupdate) - -```bash -# 自动使用 PR 标题作为 commit message -# 等同于: git add --all && git commit -m "PR Title" && git push -qkflow update -``` - -### 4. Jira 状态管理 - -```bash -# 查看已配置的项目 -qkflow jira list - -# 为项目配置状态映射 -qkflow jira setup PROJECT-KEY - -# 删除项目配置 -qkflow jira delete PROJECT-KEY -``` - -## 📚 常用命令 - -```bash -# 查看版本 -qkflow version - -# 查看配置 -qkflow config - -# 查看帮助 -qkflow --help -qkflow pr --help -qkflow jira --help -``` - -## 🔧 开发者命令 - -```bash -cd go-version - -# 初始化依赖 -make gen - -# 格式化代码 -make fix - -# 运行测试 -make test - -# 构建 -make build - -# 安装到系统 -make install - -# 清理构建产物 -make clean - -# 查看所有命令 -make help -``` - -## 🎓 工作流示例 - -### 完整的功能开发流程 - -```bash -# 1. 创建并切换到功能分支 -git checkout -b feature/add-login - -# 2. 开发功能... -# (编写代码) - -# 3. 快速提交和推送 -qkflow update - -# 4. 创建 PR -qkflow pr create - -# 5. Code Review... -# (等待审核通过) - -# 6. 合并 PR(自动更新 Jira) -qkflow pr merge -``` - -### Bug 修复流程 - -```bash -# 1. 创建 bugfix 分支 -git checkout -b bugfix/fix-login-error - -# 2. 修复 bug... -# (修改代码) - -# 3. 快速更新 -qkflow update - -# 4. 创建 PR -qkflow pr create - -# 5. 合并 -qkflow pr merge -``` - -## 🔐 环境变量(可选) - -可以使用环境变量代替配置文件: - -```bash -export GITHUB_TOKEN=your_token -export GITHUB_OWNER=your_username -export GITHUB_REPO=your_repo -export JIRA_URL=https://your-company.atlassian.net -export JIRA_EMAIL=your@email.com -export JIRA_TOKEN=your_jira_token -export OPENAI_API_KEY=your_openai_key # 可选 -export DEEPSEEK_API_KEY=your_deepseek_key # 可选 -``` - -## 📖 更多文档 - -- [README.md](README.md) - 完整功能介绍 -- [RELEASE.md](RELEASE.md) - Release 详细指南 -- [RELEASE_QUICKSTART.md](RELEASE_QUICKSTART.md) - Release 快速入门 -- [ICLOUD_MIGRATION.md](ICLOUD_MIGRATION.md) - iCloud 同步指南 -- [JIRA_STATUS_CONFIG.md](JIRA_STATUS_CONFIG.md) - Jira 配置详解 -- [CONTRIBUTING.md](CONTRIBUTING.md) - 贡献指南 - -## 💡 小贴士 - -1. **第一次使用**: 运行 `qkflow init` 配置 -2. **查看配置**: 运行 `qkflow config` 查看存储位置 -3. **快速更新**: 使用 `qkflow update` 代替繁琐的 git 命令 -4. **Jira 集成**: 配置后 PR 操作自动更新 Jira 状态 -5. **iCloud 同步**: macOS 用户配置自动同步到所有设备 - -## 🆘 获取帮助 - -- 遇到问题?查看 [Issues](https://github.com/Wangggym/quick-workflow/issues) -- 想要新功能?提交 [Feature Request](https://github.com/Wangggym/quick-workflow/issues/new) -- 贡献代码?查看 [CONTRIBUTING.md](CONTRIBUTING.md) - -## 🎉 开始使用 - -```bash -# 安装 -curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-arm64 -o qkflow -chmod +x qkflow -sudo mv qkflow /usr/local/bin/ - -# 初始化 -qkflow init - -# 开始使用! -qkflow pr create -``` - -Happy coding! 🚀 - diff --git a/go-version/ICLOUD_MIGRATION.md b/go-version/ICLOUD_MIGRATION.md deleted file mode 100644 index 1ca6310..0000000 --- a/go-version/ICLOUD_MIGRATION.md +++ /dev/null @@ -1,191 +0,0 @@ -# iCloud Drive 配置同步指南 - -## 🌟 新特性 - -从此版本开始,`qkflow` 在 macOS 上会优先将配置存储到 iCloud Drive,实现多设备自动同步! - -## ☁️ 自动同步的内容 - -以下配置会自动同步到你的所有 Mac 设备: - -1. **主配置文件** - GitHub Token、Jira 配置、AI 密钥等 -2. **Jira 状态映射** - 每个项目的状态配置 - -## 📍 存储位置 - -### macOS with iCloud Drive (推荐) -``` -~/Library/Mobile Documents/com~apple~CloudDocs/.qkflow/config.yaml -~/Library/Mobile Documents/com~apple~CloudDocs/.qkflow/jira-status.json -``` - -### 本地存储 (回退方案) -``` -~/.qkflow/config.yaml -~/.qkflow/jira-status.json -``` - -## 🔄 迁移指南 - -### 自动迁移 - -如果你之前使用的是本地配置,可以手动迁移到 iCloud: - -```bash -# 1. 确保 iCloud Drive 已启用 -# 打开"系统设置" → "Apple ID" → "iCloud" → 确保"iCloud Drive"已开启 - -# 2. 迁移配置文件 -# 如果有旧的分散配置,迁移到统一目录 -if [ -f ~/.config/quick-workflow/config.yaml ]; then - cp ~/.config/quick-workflow/config.yaml \ - ~/Library/Mobile\ Documents/com~apple~CloudDocs/.qkflow/config.yaml -fi - -if [ -f ~/.qkflow/jira-status.json ]; then - cp ~/.qkflow/jira-status.json \ - ~/Library/Mobile\ Documents/com~apple~CloudDocs/.qkflow/jira-status.json -fi - -# 4. 验证迁移 -qkflow config -``` - -### 验证同步状态 - -运行以下命令查看当前的存储位置: - -```bash -qkflow config -``` - -输出示例: -``` -💾 Storage: - Location: iCloud Drive (synced across devices) - Config: /Users/xxx/Library/Mobile Documents/com~apple~CloudDocs/.qkflow/config.yaml - Jira Status: /Users/xxx/Library/Mobile Documents/com~apple~CloudDocs/.qkflow/jira-status.json -``` - -## 🎯 多设备使用 - -### 新 Mac 设备设置 - -在新的 Mac 设备上: - -1. **安装 qkflow** - ```bash - # 下载并安装 - curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-arm64 -o qkflow - chmod +x qkflow - sudo mv qkflow /usr/local/bin/ - ``` - -2. **等待 iCloud 同步** - - 打开 Finder → iCloud Drive - - 确保 `.config` 和 `.qkflow` 文件夹已同步完成 - -3. **验证配置** - ```bash - qkflow config - ``` - -配置会自动从 iCloud Drive 读取,无需重新配置! - -## 🔒 安全说明 - -- iCloud Drive 存储是加密的 -- 配置文件权限仍然是 `0644`(仅用户可读写) -- Token 和密钥安全地存储在你的 iCloud 账户中 -- 只有登录同一 Apple ID 的设备才能访问 - -## ⚠️ 注意事项 - -### iCloud 同步延迟 - -iCloud Drive 同步可能需要几秒到几分钟,取决于: -- 网络连接速度 -- 文件大小 -- 系统负载 - -### 离线工作 - -如果没有网络连接: -- 仍然可以读取和修改配置 -- 更改会在网络恢复后自动同步 - -### 回退到本地存储 - -如果你不想使用 iCloud Drive: - -```bash -# 1. 禁用 iCloud Drive (系统设置) -# 2. qkflow 会自动回退到本地存储 - -# 或者手动移动配置回本地 -cp ~/Library/Mobile\ Documents/com~apple~CloudDocs/.qkflow/config.yaml \ - ~/.qkflow/config.yaml - -cp ~/Library/Mobile\ Documents/com~apple~CloudDocs/.qkflow/jira-status.json \ - ~/.qkflow/jira-status.json -``` - -## 🐛 故障排除 - -### 问题 1: "No configuration found" 错误 - -**原因**: iCloud Drive 未启用或文件未同步完成 - -**解决方案**: -```bash -# 检查 iCloud Drive 是否可用 -ls -la ~/Library/Mobile\ Documents/com~apple~CloudDocs/ - -# 如果目录不存在,启用 iCloud Drive -# 系统设置 → Apple ID → iCloud → iCloud Drive - -# 手动创建配置 -qkflow init -``` - -### 问题 2: 配置不同步 - -**原因**: iCloud 同步延迟或网络问题 - -**解决方案**: -```bash -# 1. 检查 iCloud 同步状态 -# 打开 Finder → iCloud Drive → 检查文件是否有云图标 - -# 2. 强制同步 -# 右键点击文件 → "从 iCloud 下载" - -# 3. 检查网络连接 -ping icloud.com -``` - -### 问题 3: 多设备配置冲突 - -**原因**: 同时在多台设备上修改配置 - -**解决方案**: -- iCloud 会自动处理冲突 -- 如果出现问题,选择一个设备上的配置作为主配置 -- 在其他设备上运行 `qkflow init` 重新初始化 - -## 📚 更多信息 - -- [主 README](README.md) -- [Jira 状态配置指南](JIRA_STATUS_CONFIG.md) -- [快速开始](QUICKSTART.md) - -## 💡 提示 - -1. **建议启用 iCloud Drive**: 配置会自动同步到你的所有 Mac 设备 -2. **定期备份**: 虽然 iCloud 很可靠,但定期备份配置文件仍然是好习惯 -3. **团队使用**: 每个人都应该有自己的配置,不要共享 iCloud 账户 - ---- - -享受跨设备同步的便利吧!🎉 - diff --git a/go-version/INTERACTION_UPDATE.md b/go-version/INTERACTION_UPDATE.md deleted file mode 100644 index b9afed8..0000000 --- a/go-version/INTERACTION_UPDATE.md +++ /dev/null @@ -1,161 +0,0 @@ -# Interaction Update - PR Editor Feature - -## 🎯 Changes Made - -### Improved User Experience - -Changed the prompt for adding description/screenshots from a simple Yes/No confirmation to a more intuitive selection interface. - -### Before ❌ - -```bash -? Would you like to add detailed description with images/videos? (y/N): _ -``` - -User needs to: -- Type `y` or `n` -- Remember the convention -- Default behavior not obvious - -### After ✅ - -```bash -? Add detailed description with images/videos? - > ⏭️ Skip (default) - ✅ Yes, continue -``` - -User can: -- **Press Enter** → Skip immediately (default) -- **Press ↓ or Space** → Toggle to "Yes, continue" -- **Press Enter** → Confirm selection - -## 🎨 Benefits - -1. **Faster Workflow** - - Want to skip? Just press Enter once - - No need to think about y/n - -2. **More Intuitive** - - Visual selection with icons - - Clear indication of default option - - Familiar interaction pattern - -3. **Less Error-Prone** - - Can't accidentally select wrong option - - Visual feedback before confirming - -4. **Better Discoverability** - - New users immediately understand their options - - Icons make it more approachable - -## 📝 Technical Implementation - -### New Function - -Added `PromptOptional()` in `internal/ui/prompt.go`: - -```go -// PromptOptional prompts for an optional action with space to select, enter to skip -// This provides better UX: Space = Yes, Enter = Skip (default) -func PromptOptional(message string) (bool, error) { - options := []string{ - "⏭️ Skip (default)", - "✅ Yes, continue", - } - - prompt := &survey.Select{ - Message: message, - Options: options, - Default: options[0], // Default to Skip - } - - var result string - if err := survey.AskOne(prompt, &result); err != nil { - return false, err - } - - // Check if user selected "Yes" - return result == options[1], nil -} -``` - -### Usage - -Updated `pr_create.go` to use the new function: - -```go -// 询问是否添加说明/截图 (空格选择,Enter 跳过) -var editorResult *editor.EditorResult -addDescription, err := ui.PromptOptional("Add detailed description with images/videos?") -if err == nil && addDescription { - // ... open editor -} -``` - -## 📊 Comparison - -| Aspect | Before | After | -|--------|--------|-------| -| **Skip Action** | Type 'n' + Enter | Just Enter | -| **Select Action** | Type 'y' + Enter | Space/↓ + Enter | -| **Visual Feedback** | None | Icons + Highlight | -| **Default Clear** | (y/N) text hint | "⏭️ Skip (default)" | -| **Keystrokes (skip)** | 2 | 1 | -| **Keystrokes (select)** | 2 | 2 | - -## 🔄 Workflow Example - -### Typical Usage (Skip) - -```bash -$ qkflow pr create NA-9245 - -✓ Found Jira issue: Fix login button -✓ Select type(s): Bug fix - -? Add detailed description with images/videos? - > ⏭️ Skip (default) ← User presses Enter - ✅ Yes, continue - -✓ Generated title: fix: ... -# Continues with PR creation -``` - -### With Description (Select) - -```bash -$ qkflow pr create NA-9245 - -✓ Found Jira issue: Fix login button -✓ Select type(s): Bug fix - -? Add detailed description with images/videos? - ⏭️ Skip (default) ← User presses ↓ or Space - > ✅ Yes, continue ← Now highlighted - ← User presses Enter - -🌐 Opening editor in your browser... -# Editor opens -``` - -## ✅ Testing - -- [x] Build succeeds -- [x] No compilation errors -- [x] Documentation updated -- [x] User-friendly interaction -- [x] Default behavior preserved (skip) - -## 📚 Documentation Updated - -- ✅ `README.md` - Added interaction example -- ✅ `PR_EDITOR_FEATURE.md` - Updated workflow example -- ✅ Code comments - Explained new behavior - ---- - -**Date**: 2024-11-18 -**Impact**: User Experience Enhancement -**Breaking Changes**: None (maintains backward compatibility in behavior) - diff --git a/go-version/JIRA_READER.md b/go-version/JIRA_READER.md deleted file mode 100644 index 5d87ca5..0000000 --- a/go-version/JIRA_READER.md +++ /dev/null @@ -1,317 +0,0 @@ -# Jira Issue Reader for Cursor AI - -A powerful tool to read and export Jira issues, optimized for use with Cursor AI. - -## 🎯 Quick Start - -### For Cursor Users (Recommended) - -The simplest way to use this in Cursor: - -```bash -# In Cursor terminal, run: -qkflow jira read NA-9245 - -# Then in Cursor chat, simply say: -"总结刚才读取的 Jira ticket 内容" -``` - -Cursor will automatically read the exported files and provide you with a comprehensive summary! - -## 📚 Available Commands - -### 1. `show` - Quick Terminal View - -Display issue information directly in the terminal. - -```bash -# Basic view (metadata only) -qkflow jira show NA-9245 - -# Full view (includes description and comments) -qkflow jira show NA-9245 --full -``` - -**Use when:** -- You need a quick peek at the issue -- You only need text content -- You want the fastest response - -### 2. `export` - Complete Export with Files - -Export issue to local files with optional images. - -```bash -# Export text only -qkflow jira export NA-9245 - -# Export with all images and attachments -qkflow jira export NA-9245 --with-images - -# Export to custom directory -qkflow jira export NA-9245 -o ~/jira-exports/ --with-images -``` - -**Output structure:** -``` -/tmp/qkflow/jira/NA-9245/ -├── README.md # How to use in Cursor -├── content.md # Main content (Markdown) -└── attachments/ # Downloaded files (if --with-images) - ├── screenshot.png - └── diagram.jpg -``` - -**Use when:** -- You need images/attachments -- You want to keep a local copy -- You need formatted Markdown - -### 3. `read` - Intelligent Mode ⭐️ **RECOMMENDED** - -Automatically decides the best way to present the issue. - -```bash -# Auto mode (smart decision) -qkflow jira read NA-9245 -``` - -**How it works:** -- ✅ **Has images?** → Exports to files with images -- ✅ **Text only?** → Displays directly in terminal -- ✅ Automatically optimized for Cursor - -**Use when:** -- Working with Cursor AI (best experience) -- You want the tool to decide the best format -- You're not sure if the issue has images - -### 4. `clean` - Clean Up Exports - -Remove exported files to free up disk space. - -```bash -# Clean specific issue -qkflow jira clean NA-9245 - -# Clean all exports -qkflow jira clean --all - -# Preview what would be deleted (dry run) -qkflow jira clean --all --dry-run - -# Force delete without confirmation -qkflow jira clean --all --force -``` - -## 🎨 Usage Examples in Cursor - -### Example 1: Simple Text Analysis - -``` -You in Cursor: "通过 qkflow 读取 NA-9245 并总结" - -Cursor executes: qkflow jira read NA-9245 -Cursor responds: "这个 ticket (NA-9245) 是关于..." -``` - -### Example 2: With Images - -``` -You in Cursor: "用 qkflow 读取 NA-9245 的所有内容包括图片,分析架构设计" - -Cursor executes: qkflow jira export NA-9245 --with-images -Cursor reads: content.md + all images in attachments/ -Cursor responds: "根据架构图,这个系统包含..." -``` - -### Example 3: Manual Control - -```bash -# Step 1: Export (you run this) -qkflow jira export NA-9245 --with-images - -# Step 2: Tell Cursor what to read -"Read /tmp/qkflow/jira/NA-9245/content.md and analyze the architecture diagram" - -# Step 3: Clean up when done -qkflow jira clean NA-9245 -``` - -## 🔧 Configuration - -Make sure your Jira credentials are configured: - -```bash -qkflow init -``` - -Required settings: -- `jira_service_address`: Your Jira instance URL (e.g., https://brain-ai.atlassian.net) -- `jira_api_token`: Your Jira API token -- `email`: Your Jira email - -## 💡 Tips & Best Practices - -### For Cursor Users - -1. **Use `read` command by default** - It's optimized for AI consumption -2. **Be specific in prompts** - Tell Cursor what you want to know -3. **Clean up regularly** - Use `clean --all` to free up space - -### Command Comparison - -| Command | Speed | Images | Output | Best For | -|---------|-------|--------|--------|----------| -| `show` | ⚡️ Fastest | ❌ | Terminal | Quick peek | -| `show --full` | ⚡️ Fast | ❌ | Terminal | Full text | -| `export` | 🐌 Slower | ❌ | Files | Text archive | -| `export --with-images` | 🐌 Slowest | ✅ | Files | Complete archive | -| `read` ⭐️ | ⚡️ Smart | ✅ Smart | Smart | **Cursor AI** | - -### Cursor Prompt Templates - -```bash -# General summary -"通过 qkflow 读取 并总结内容" - -# Specific analysis -"用 qkflow 读取 ,分析技术方案" - -# With context -"读取 ,对比我们当前的实现方式" - -# With images -"qkflow 读取 包括所有图片,分析架构设计" -``` - -## 📊 Output Formats - -### Terminal Output (show command) - -``` -╔═══════════════════════════════════════════════════════╗ -║ 🎫 NA-9245: Implement user authentication ║ -╚═══════════════════════════════════════════════════════╝ - -📋 Type: Story -📊 Status: In Progress -🏷️ Priority: High -👤 Assignee: John Doe - -🔗 View in Jira: https://brain-ai.atlassian.net/browse/NA-9245 -``` - -### Markdown Output (export command) - -```markdown ---- -issue_key: NA-9245 -title: Implement user authentication -type: Story -status: In Progress -priority: High ---- - -# NA-9245: Implement user authentication - -## 📊 Metadata -... - -## 📝 Description -... - -## 📎 Attachments (3) -1. **screenshot.png** (245 KB) - ![screenshot.png](./attachments/screenshot.png) -... -``` - -### Cursor-Optimized Output (read command) - -The `read` command provides special output markers that Cursor recognizes: - -``` -✅ Exported to: /tmp/qkflow/jira/NA-9245/ - -Main file: /tmp/qkflow/jira/NA-9245/content.md -Images: /tmp/qkflow/jira/NA-9245/attachments/ (3 files) - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -💡 CURSOR: Please read the following files: -1. /tmp/qkflow/jira/NA-9245/content.md -2. All images in /tmp/qkflow/jira/NA-9245/attachments/ -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -``` - -## 🚀 Advanced Usage - -### Batch Processing - -```bash -# Export multiple issues -for issue in NA-9245 NA-9246 NA-9247; do - qkflow jira export $issue --with-images -done - -# Tell Cursor to analyze all -"Read all Jira exports in /tmp/qkflow/jira/ and summarize" -``` - -### Custom Workflows - -```bash -# Create a script for your team -#!/bin/bash -ISSUE_KEY=$1 -qkflow jira read "$ISSUE_KEY" -echo "Ready for Cursor to analyze!" -``` - -### Integration with Other Tools - -```bash -# Export and open in VS Code/Cursor -qkflow jira export NA-9245 --with-images -code /tmp/qkflow/jira/NA-9245/content.md -``` - -## 🐛 Troubleshooting - -### "Failed to create Jira client" -- Check your config: `cat ~/.config/quick-workflow/config.yaml` -- Verify your API token is valid -- Ensure `jira_service_address` is correct - -### "Failed to get issue" -- Verify the issue key is correct (e.g., NA-9245) -- Check you have permission to view the issue -- Try accessing the issue in your browser first - -### "Failed to download attachment" -- Check your network connection -- Verify your API token has attachment download permissions -- Some files may be restricted - -### Cursor not reading files -- Make sure the export command completed successfully -- Check the file paths in the output -- Try manually attaching the file: `@/tmp/qkflow/jira/NA-9245/content.md` - -## 📝 Notes - -- Exports are temporary and stored in `/tmp/qkflow/jira/` by default -- Use `clean` command regularly to free up space -- Images are only downloaded with `--with-images` flag -- The `read` command is specifically designed for Cursor AI integration - -## 🔗 Related Commands - -- `qkflow init` - Configure Jira credentials -- `qkflow pr create` - Create PR (can auto-link to Jira) -- `qkflow jira list` - List Jira status mappings - ---- - -**Made with ❤️ for Cursor AI users** - diff --git a/go-version/JIRA_STATUS_CONFIG.md b/go-version/JIRA_STATUS_CONFIG.md deleted file mode 100644 index ef85db3..0000000 --- a/go-version/JIRA_STATUS_CONFIG.md +++ /dev/null @@ -1,298 +0,0 @@ -# Jira 状态配置指南 - -## 📋 配置存储位置 - -Jira 每个项目的状态配置会根据你的系统智能存储: - -**macOS with iCloud Drive** (推荐): -``` -~/Library/Mobile Documents/com~apple~CloudDocs/.qkflow/jira-status.json -``` -配置会自动在你的所有 Mac 设备间同步 ☁️ - -**本地存储** (回退方案): -``` -~/.qkflow/jira-status.json -``` - -运行 `qkflow config` 查看实际的存储位置。 - -配置文件结构: -```json -{ - "mappings": { - "PROJ-123": { - "project_key": "PROJ", - "pr_created_status": "In Progress", - "pr_merged_status": "Done" - }, - "TEAM-456": { - "project_key": "TEAM", - "pr_created_status": "进行中", - "pr_merged_status": "已完成" - } - } -} -``` - -## 🎯 配置说明 - -### 1. 基本配置(必需) - -在 `~/.config/quick-workflow/config.yaml` 中配置 Jira 基本信息: - -```yaml -email: your.email@example.com -jira_api_token: your_jira_api_token -jira_service_address: https://your-domain.atlassian.net -github_token: ghp_your_github_token -``` - -运行 `qkflow init` 进行初始化配置。 - -**提示**:如果你使用 macOS 并启用了 iCloud Drive,所有配置会自动同步到你的其他 Mac 设备! - -### 2. 项目状态映射(按项目配置) - -每个 Jira 项目需要配置两个状态: - -- **PR Created Status**(PR 创建时的状态):当创建 PR 时,Jira issue 会更新到这个状态 - - 通常是:`In Progress`、`进行中`、`开发中` 等 - -- **PR Merged Status**(PR 合并时的状态):当 PR 合并后,Jira issue 会更新到这个状态 - - 通常是:`Done`、`已完成`、`Resolved` 等 - -## 🛠️ 如何配置 - -### 方式 1:首次使用时自动配置(推荐) - -当你第一次为某个项目创建 PR 时,系统会自动提示你配置状态映射: - -```bash -# 创建 PR -qkflow pr create PROJ-123 - -# 如果是首次使用该项目,会自动弹出交互式配置: -# 1. 从 Jira 获取该项目所有可用的状态 -# 2. 让你选择 "PR Created" 状态(如:In Progress) -# 3. 让你选择 "PR Merged" 状态(如:Done) -# 4. 自动保存配置到 ~/.qkflow/jira-status.json -``` - -### 方式 2:手动设置/更新项目配置 - -```bash -# 为指定项目设置状态映射 -qkflow jira setup PROJ - -# 系统会: -# 1. 连接到 Jira 获取该项目的所有可用状态 -# 2. 显示交互式选择界面 -# 3. 保存你的选择 -``` - -### 方式 3:查看已配置的项目 - -```bash -# 列出所有已配置的项目状态映射 -qkflow jira list - -# 输出示例: -# 📋 Jira Status Mappings: -# -# Project: PROJ -# PR Created → In Progress -# PR Merged → Done -# -# Project: TEAM -# PR Created → 进行中 -# PR Merged → 已完成 -``` - -### 方式 4:删除项目配置 - -```bash -# 删除指定项目的状态映射 -qkflow jira delete PROJ - -# 会要求确认后删除 -``` - -### 方式 5:手动编辑配置文件 - -你也可以直接编辑配置文件: - -```bash -# 编辑配置 -vim ~/.qkflow/jira-status.json - -# 或 -code ~/.qkflow/jira-status.json -``` - -## 🔄 工作流程 - -### 创建 PR 时(`qkflow pr create`) - -1. 检查项目是否已有状态映射 -2. 如果没有,自动触发配置流程 -3. 创建 PR -4. 将 Jira issue 更新为 `PR Created Status`(如:In Progress) -5. 在 Jira issue 中添加 PR 链接 - -### 合并 PR 时(`qkflow pr merge`) - -1. 读取项目的状态映射 -2. 合并 PR -3. 将 Jira issue 更新为 `PR Merged Status`(如:Done) -4. 在 Jira issue 中添加合并备注 - -## 📝 示例 - -### 完整配置示例 - -```bash -# 1. 初始化基本配置 -qkflow init - -# 2. 查看当前配置 -qkflow config - -# 3. 为项目 PROJ 设置状态映射 -qkflow jira setup PROJ -# 选择 PR Created: In Progress -# 选择 PR Merged: Done - -# 4. 查看所有状态映射 -qkflow jira list - -# 5. 创建 PR(会自动使用配置的状态) -qkflow pr create PROJ-123 - -# 6. 合并 PR(会自动使用配置的状态) -qkflow pr merge 456 -``` - -### 多项目配置示例 - -如果你在多个 Jira 项目工作: - -```bash -# 为项目 A 配置 -qkflow jira setup PROJA -# 选择: In Progress / Done - -# 为项目 B 配置(可能用中文状态) -qkflow jira setup PROJB -# 选择: 进行中 / 已完成 - -# 为项目 C 配置(可能用自定义状态) -qkflow jira setup PROJC -# 选择: Development / Resolved - -# 查看所有配置 -qkflow jira list -``` - -## ⚙️ 技术实现 - -### 状态获取 - -系统通过 Jira REST API 自动获取项目的所有可用状态: - -``` -GET /rest/api/2/project/{projectKey}/statuses -``` - -这确保你只能选择该项目实际支持的状态,避免配置错误。 - -### 缓存机制 - -- 配置保存后会一直生效,除非手动更新或删除 -- 每次操作时会自动读取对应项目的配置 -- 如果配置被删除,下次使用时会重新提示配置 - -## 🔍 故障排除 - -### 问题 1:找不到状态配置 - -```bash -# 检查配置文件是否存在 -ls -la ~/.qkflow/jira-status.json - -# 如果不存在,重新配置 -qkflow jira setup YOUR_PROJECT_KEY -``` - -### 问题 2:状态名称不匹配 - -如果 Jira 中的状态名称发生变化: - -```bash -# 重新配置该项目 -qkflow jira setup YOUR_PROJECT_KEY - -# 或手动编辑配置文件 -vim ~/.qkflow/jira-status.json -``` - -### 问题 3:无法获取项目状态 - -确保: -1. Jira API Token 有效 -2. 有该项目的访问权限 -3. Jira Service Address 正确 - -```bash -# 检查基本配置 -qkflow config - -# 重新初始化配置 -qkflow init -``` - -## 🎨 最佳实践 - -1. **首次使用时配置**:第一次为某个项目创建 PR 时就会提示配置,建议此时完成配置 -2. **统一命名**:如果团队有多个项目,尽量使用统一的状态名称 -3. **定期检查**:使用 `qkflow jira list` 定期检查配置是否正确 -4. **备份配置**:可以备份 `~/.qkflow/jira-status.json` 文件 - -## 📚 相关命令 - -```bash -# Jira 相关命令 -qkflow jira list # 列出所有状态映射 -qkflow jira setup [key] # 设置/更新项目状态映射 -qkflow jira delete [key] # 删除项目状态映射 - -# 配置相关命令 -qkflow init # 初始化配置 -qkflow config # 查看当前配置 - -# 使用配置的命令 -qkflow pr create [ticket] # 创建 PR(会使用状态配置) -qkflow pr merge [number] # 合并 PR(会使用状态配置) -``` - -## 🔗 相关文件 - -### 配置文件位置 - -**macOS with iCloud Drive**: -- 配置目录: `~/Library/Mobile Documents/com~apple~CloudDocs/.qkflow/` - - 基本配置: `config.yaml` - - 状态映射: `jira-status.json` - -**本地存储**: -- 配置目录: `~/.qkflow/` - - 基本配置: `config.yaml` - - 状态映射: `jira-status.json` - -### 源码 -- `internal/utils/paths.go` - 路径管理和 iCloud 检测 -- `internal/jira/status_cache.go` - 状态缓存管理 -- `cmd/qkflow/commands/jira.go` - Jira 命令 -- `cmd/qkflow/commands/pr_create.go` - PR 创建逻辑 -- `cmd/qkflow/commands/pr_merge.go` - PR 合并逻辑 - diff --git a/go-version/MIGRATION.md b/go-version/MIGRATION.md deleted file mode 100644 index de217a6..0000000 --- a/go-version/MIGRATION.md +++ /dev/null @@ -1,364 +0,0 @@ -# Migration Guide: Shell to Go Version - -This guide will help you migrate from the Shell-based quick-workflow to the new Go version. - -## 🎯 Why Migrate? - -| Aspect | Shell Version | Go Version | Improvement | -|--------|--------------|------------|-------------| -| **Installation** | Clone repo + install 4+ dependencies | Download 1 binary | ✅ 90% simpler | -| **Configuration** | Manual env vars in `.zshrc` | Interactive `qk init` | ✅ Much easier | -| **Startup Time** | ~1-2 seconds | <100ms | ✅ 10-20x faster | -| **Cross-platform** | macOS/Linux only | macOS/Linux/Windows | ✅ Universal | -| **Updates** | `git pull` + reinstall | Download new binary | ✅ Simpler | -| **Error Messages** | Shell errors | Clear, actionable messages | ✅ Better UX | -| **Type Safety** | Runtime errors | Compile-time checks | ✅ More reliable | - -## 📋 Prerequisites - -Before migrating, ensure you have: -- ✅ Your current Shell version working -- ✅ Access to your Jira and GitHub credentials -- ✅ Note your current configuration (especially env vars) - -## 🔄 Migration Steps - -### Step 1: Backup Current Configuration - -```bash -# Save your current environment variables -cat ~/.zshrc | grep -E "(EMAIL|JIRA|GH_|OPENAI|DEEPSEEK)" > ~/qk-backup.txt -``` - -Your variables should look like: -```bash -export EMAIL=your.email@example.com -export JIRA_API_TOKEN=your_jira_api_token -export JIRA_SERVICE_ADDRESS=https://your-domain.atlassian.net -export GH_BRANCH_PREFIX=your_branch_prefix -export GITHUB_TOKEN=your_github_token # or gh auth token -``` - -### Step 2: Install Go Version - -Choose one of the installation methods: - -#### Option A: Download Binary (Recommended) - -```bash -# macOS (Apple Silicon) -curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qk-darwin-arm64 -o qk -chmod +x qk -sudo mv qk /usr/local/bin/ - -# macOS (Intel) -curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qk-darwin-amd64 -o qk -chmod +x qk -sudo mv qk /usr/local/bin/ - -# Verify installation -qk version -``` - -#### Option B: Build from Source - -```bash -cd /path/to/quick-workflow/go-version -make build -sudo cp bin/qk /usr/local/bin/ -``` - -### Step 3: Run Initial Setup - -```bash -qk init -``` - -This will prompt you for: - -1. **Email**: Use the value from your `EMAIL` env var -2. **GitHub Token**: Will auto-detect from `gh auth token` -3. **Jira Service Address**: Use the value from `JIRA_SERVICE_ADDRESS` -4. **Jira API Token**: Use the value from `JIRA_API_TOKEN` -5. **Branch Prefix** (optional): Use the value from `GH_BRANCH_PREFIX` -6. **OpenAI Key** (optional): Use the value from `OPENAI_KEY` or `DEEPSEEK_KEY` - -### Step 4: Test the New Version - -Create a test branch to verify everything works: - -```bash -# Test PR creation -cd your-project -git checkout -b test-qk-migration -echo "test" > test.txt -git add test.txt -qk pr create - -# If successful, you'll see: -# ✅ Branch created -# ✅ Changes committed -# ✅ Pushed to remote -# ✅ Pull request created: https://github.com/... -``` - -### Step 5: Verify Jira Integration - -If you used a Jira ticket in Step 4: -1. Check that the PR link was added to your Jira issue -2. Verify the status was updated (if you chose to update it) - -### Step 6: Update Shell Aliases (Optional) - -If you had custom aliases in your Shell version, update them: - -**Old aliases (Shell version):** -```bash -alias prc='~/quick-workflow/pr-create.sh' -alias prm='~/quick-workflow/pr-merge.sh' -``` - -**New aliases (Go version):** -```bash -alias prc='qk pr create' -alias prm='qk pr merge' -``` - -Or simply use the shorter commands directly: -```bash -qk pr create # replaces pr-create.sh -qk pr merge # replaces pr-merge.sh -``` - -### Step 7: Clean Up Old Installation (Optional) - -Once you've verified the Go version works: - -```bash -# Remove old environment variables from .zshrc -# (Keep EMAIL if you use it elsewhere) -# Edit ~/.zshrc and remove: -# export JIRA_API_TOKEN=... -# export JIRA_SERVICE_ADDRESS=... -# export GH_BRANCH_PREFIX=... -# export OPENAI_KEY=... -# etc. - -# Reload shell -source ~/.zshrc - -# Archive old installation -mv ~/quick-workflow ~/quick-workflow-shell-backup -``` - -## 🔍 Feature Comparison - -### Commands Mapping - -| Shell Version | Go Version | Notes | -|--------------|------------|-------| -| `pr-create.sh` | `qk pr create` | Same functionality, faster | -| `pr-merge.sh` | `qk pr merge` | Same functionality, faster | -| `qk.sh` | Coming soon | Log management features | -| `qklogs.sh` | Coming soon | Will be integrated | -| `qkfind.sh` | Coming soon | Will be integrated | -| N/A | `qk init` | New: Setup wizard | -| N/A | `qk config` | New: Show configuration | -| N/A | `qk version` | New: Version info | - -### Configuration - -| Shell Version | Go Version | Location | -|--------------|------------|----------| -| `.zshrc` env vars | YAML config | `~/.config/quick-workflow/config.yaml` | -| `jira-status.txt` | Automatic detection | Handled by API | -| Manual setup | `qk init` | Interactive wizard | - -### Workflow Comparison - -**Creating a PR - Shell Version:** -```bash -# 1. Stage changes -git add . - -# 2. Run script -./pr-create.sh PROJ-123 - -# 3. Wait ~1-2 seconds for startup -# 4. Answer prompts -# 5. Script calls gh, jira, git, jq, python -# 6. Done (if no errors) -``` - -**Creating a PR - Go Version:** -```bash -# 1. Stage changes -git add . - -# 2. Run command -qk pr create PROJ-123 - -# 3. Instant startup (<100ms) -# 4. Answer prompts (same as before) -# 5. All operations in one fast binary -# 6. Done with better error messages -``` - -## 🐛 Troubleshooting - -### Issue: "Command not found: qk" - -**Solution:** -```bash -# Check if binary is in PATH -which qk - -# If not found, ensure /usr/local/bin is in PATH -echo $PATH - -# Add to PATH if needed (add to ~/.zshrc) -export PATH="/usr/local/bin:$PATH" -``` - -### Issue: "Config not found" or "Please run qk init" - -**Solution:** -```bash -# Run the setup wizard -qk init - -# Or manually create config -mkdir -p ~/.config/quick-workflow -cat > ~/.config/quick-workflow/config.yaml << EOF -email: your.email@example.com -jira_api_token: your_token -jira_service_address: https://your-domain.atlassian.net -github_token: your_github_token -branch_prefix: feature -EOF -``` - -### Issue: "Failed to create GitHub client" - -**Solution:** -```bash -# Ensure gh CLI is authenticated -gh auth status - -# If not, login -gh auth login - -# Re-run qk init to get the token -qk init -``` - -### Issue: "Failed to get Jira issue" - -**Solution:** -1. Verify Jira API token is correct -2. Check Jira service address format: `https://your-domain.atlassian.net` -3. Ensure you have access to the Jira project -4. Test Jira credentials: - ```bash - curl -u your.email@example.com:your_jira_token \ - https://your-domain.atlassian.net/rest/api/2/myself - ``` - -### Issue: Go version uses different statuses than Shell version - -**Solution:** -The Go version queries Jira for available statuses dynamically. If you had custom status mappings in `jira-status.txt`, you'll need to select them when prompted. The Go version will remember your choices. - -## 📊 Performance Comparison - -Real-world benchmarks (on M1 MacBook Pro): - -| Operation | Shell Version | Go Version | Speed Up | -|-----------|--------------|------------|----------| -| Startup time | ~1.5s | ~50ms | 30x faster | -| PR create (total) | ~8-10s | ~6-7s | 1.5x faster | -| PR merge (total) | ~5-7s | ~4-5s | 1.4x faster | -| Config loading | ~200ms | ~5ms | 40x faster | - -## 🎓 Learning the New Commands - -### Quick Reference Card - -Print this and keep it handy during migration: - -``` -┌─────────────────────────────────────────────────┐ -│ Quick Workflow Go Version │ -├─────────────────────────────────────────────────┤ -│ Setup │ -│ qk init Initialize configuration │ -│ qk config Show current config │ -│ │ -│ Pull Requests │ -│ qk pr create Create PR + update Jira │ -│ qk pr merge Merge PR + update Jira │ -│ │ -│ Help │ -│ qk --help Show all commands │ -│ qk pr --help Show PR commands │ -│ qk version Show version info │ -└─────────────────────────────────────────────────┘ -``` - -## 🎉 Migration Checklist - -Use this checklist to ensure a smooth migration: - -- [ ] Backed up current configuration -- [ ] Installed Go version -- [ ] Ran `qk init` and configured -- [ ] Tested PR creation -- [ ] Verified Jira integration works -- [ ] Updated shell aliases (if any) -- [ ] Removed old environment variables -- [ ] Archived old Shell installation -- [ ] Verified all team members migrated -- [ ] Updated team documentation -- [ ] Celebrated faster workflows! 🎊 - -## 🆘 Need Help? - -If you encounter issues during migration: - -1. **Check this guide** - Most common issues are covered above -2. **Check configuration** - Run `qk config` to verify settings -3. **Enable debug mode** - Set `export QK_DEBUG=1` before running commands -4. **Create an issue** - [Open a GitHub issue](https://github.com/Wangggym/quick-workflow/issues) -5. **Ask for help** - Reach out to the maintainer - -## 📝 Rollback Plan - -If you need to rollback to the Shell version: - -```bash -# Restore shell version -mv ~/quick-workflow-shell-backup ~/quick-workflow - -# Restore environment variables -# (Edit ~/.zshrc and add them back) - -# Reload shell -source ~/.zshrc - -# Remove Go version -sudo rm /usr/local/bin/qk -``` - -## 🚀 Next Steps - -After successful migration: - -1. **Explore new features** - Try `qk --help` to see all commands -2. **Customize configuration** - Edit `~/.config/quick-workflow/config.yaml` -3. **Share with team** - Help teammates migrate -4. **Provide feedback** - Let us know how we can improve - ---- - -**Welcome to the faster, better Quick Workflow! 🎉** - diff --git a/go-version/Makefile b/go-version/Makefile index ba91f54..3263961 100644 --- a/go-version/Makefile +++ b/go-version/Makefile @@ -2,7 +2,7 @@ SHELL := /bin/bash HIDE ?= @ -.PHONY: gen fix check dev run test test-cov test-fast build build-all install local quick clean help +.PHONY: gen fix check dev run test test-cov test-fast build build-all install clean help # Variables BINARY_NAME := qkflow @@ -89,23 +89,27 @@ build-all: $(HIDE)GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./cmd/qkflow @echo "✅ Multi-platform builds completed." -# Install to GOPATH/bin +# Install to /usr/local/bin (system-wide, may require sudo) - matches install.sh behavior install: build - $(HIDE)echo "📦 Installing $(BINARY_NAME)..." - $(HIDE)cp $(BUILD_DIR)/$(BINARY_NAME) $(shell go env GOPATH)/bin/$(BINARY_NAME) - @echo "✅ Installed to $(shell go env GOPATH)/bin/$(BINARY_NAME)" - -# Quick local build and install (one-command) -local: - @echo "⚡ Quick build and install..." - @$(GO) build -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/qkflow - @cp $(BUILD_DIR)/$(BINARY_NAME) $(shell go env GOPATH)/bin/$(BINARY_NAME) - @echo "✅ Built and installed to $(shell go env GOPATH)/bin/$(BINARY_NAME)" + $(HIDE)echo "📦 Installing $(BINARY_NAME) to /usr/local/bin..." + @if [ ! -d "/usr/local/bin" ]; then \ + echo "Creating /usr/local/bin directory..."; \ + if [ -w "/usr/local" ]; then \ + mkdir -p /usr/local/bin; \ + else \ + sudo mkdir -p /usr/local/bin; \ + fi; \ + fi + @if [ -w "/usr/local/bin" ]; then \ + cp $(BUILD_DIR)/$(BINARY_NAME) /usr/local/bin/$(BINARY_NAME); \ + echo "✅ Installed to /usr/local/bin/$(BINARY_NAME)"; \ + else \ + echo "Requesting sudo permission to install to /usr/local/bin..."; \ + sudo cp $(BUILD_DIR)/$(BINARY_NAME) /usr/local/bin/$(BINARY_NAME); \ + echo "✅ Installed to /usr/local/bin/$(BINARY_NAME)"; \ + fi @echo "💡 Run 'qkflow version' to verify" -# Alias for local -quick: local - # Release helper release: @if [ -z "$(VERSION)" ]; then \ @@ -154,9 +158,7 @@ help: @echo "Building:" @echo " make build - Build binary" @echo " make build-all - Build for all platforms" - @echo " make install - Install to GOPATH/bin" - @echo " make local - ⚡ Quick build and install (one-step)" - @echo " make quick - Alias for 'make local'" + @echo " make install - Install to /usr/local/bin (system-wide, requires sudo)" @echo "" @echo "Release:" @echo " make release - Create release (requires VERSION env)" diff --git a/go-version/PROJECT_OVERVIEW.md b/go-version/PROJECT_OVERVIEW.md deleted file mode 100644 index 649f3aa..0000000 --- a/go-version/PROJECT_OVERVIEW.md +++ /dev/null @@ -1,315 +0,0 @@ -# Quick Workflow Go Version - Project Overview - -## 📊 Project Summary - -**Status**: ✅ Complete and Ready for Use -**Language**: Go 1.21+ -**Type**: CLI Tool -**Purpose**: Streamline GitHub and Jira workflows - -## 🎯 Key Features Implemented - -### Core Functionality -- ✅ PR Creation with automatic branch management -- ✅ PR Merging with cleanup -- ✅ Jira integration (status updates, comments, links) -- ✅ GitHub API integration (PR CRUD operations) -- ✅ Git operations (branch, commit, push, merge) -- ✅ Interactive CLI with beautiful prompts -- ✅ Configuration management with `qk init` - -### User Experience -- ✅ Single binary distribution (no dependencies) -- ✅ Cross-platform support (macOS, Linux, Windows) -- ✅ Colored output and progress indicators -- ✅ Clear error messages -- ✅ Interactive prompts for user input -- ✅ Clipboard integration (macOS) - -### Developer Experience -- ✅ Modular architecture -- ✅ Type-safe code -- ✅ Comprehensive error handling -- ✅ Easy to test and extend -- ✅ Well-documented code -- ✅ CI/CD ready with GitHub Actions - -## 📦 Deliverables - -### Code Structure -``` -go-version/ -├── cmd/qkflow/ # ✅ Main application -│ ├── main.go -│ └── commands/ -│ ├── root.go # ✅ Root command -│ ├── init.go # ✅ Setup wizard -│ ├── pr.go # ✅ PR commands -│ ├── pr_create.go # ✅ Create PR logic -│ └── pr_merge.go # ✅ Merge PR logic -├── internal/ -│ ├── github/ -│ │ └── client.go # ✅ GitHub API client -│ ├── jira/ -│ │ └── client.go # ✅ Jira API client -│ ├── git/ -│ │ └── operations.go # ✅ Git operations -│ └── ui/ -│ └── prompt.go # ✅ User interface -├── pkg/ -│ └── config/ -│ └── config.go # ✅ Configuration -├── scripts/ -│ ├── install.sh # ✅ Installation script -│ ├── test.sh # ✅ Test runner -│ └── release.sh # ✅ Release script -├── .github/workflows/ -│ └── build.yml # ✅ CI/CD pipeline -├── go.mod # ✅ Dependencies -├── go.sum # ✅ Checksums -├── Makefile # ✅ Build automation -├── README.md # ✅ Main documentation -├── MIGRATION.md # ✅ Migration guide -├── QUICKSTART.md # ✅ Quick start guide -├── CONTRIBUTING.md # ✅ Contribution guide -├── LICENSE # ✅ MIT License -└── PROJECT_OVERVIEW.md # ✅ This file -``` - -### Documentation -- ✅ **README.md**: Comprehensive user guide -- ✅ **MIGRATION.md**: Detailed migration guide from Shell version -- ✅ **QUICKSTART.md**: 5-minute getting started guide -- ✅ **CONTRIBUTING.md**: Developer contribution guide -- ✅ **PROJECT_OVERVIEW.md**: This overview document - -### Build & Release -- ✅ **Makefile**: Build automation for all platforms -- ✅ **GitHub Actions**: Automated build and release pipeline -- ✅ **Installation script**: One-command installation -- ✅ **Test script**: Automated testing -- ✅ **Release script**: Version release automation - -## 🔧 Technical Stack - -### Core Dependencies -- **cobra**: CLI framework for command structure -- **viper**: Configuration management -- **survey**: Interactive prompts and user input -- **go-github**: Official GitHub API client -- **go-jira**: Jira API client -- **oauth2**: OAuth2 authentication -- **fatih/color**: Terminal colors - -### Build Tools -- **go 1.21+**: Language and toolchain -- **make**: Build automation -- **golangci-lint**: Code linting -- **GitHub Actions**: CI/CD - -## 📈 Comparison with Shell Version - -| Metric | Shell Version | Go Version | Improvement | -|--------|--------------|------------|-------------| -| **Binary Size** | N/A | ~15MB | Self-contained | -| **Startup Time** | ~1.5s | <100ms | 15x faster | -| **Dependencies** | 4+ tools | 0 | 100% fewer | -| **Installation** | Multi-step | One command | Much easier | -| **Error Handling** | Basic | Comprehensive | Much better | -| **Type Safety** | None | Full | Safer code | -| **Testing** | Limited | Comprehensive | More reliable | -| **Platforms** | macOS/Linux | macOS/Linux/Windows | More platforms | -| **Maintenance** | Manual | Automated | Easier updates | - -## 🚀 Usage Examples - -### Basic PR Creation -```bash -qk pr create PROJ-123 -# Interactive prompts guide you through the process -``` - -### Basic PR Merge -```bash -qk pr merge 456 -# Confirms, merges, cleans up, and updates Jira -``` - -### First-time Setup -```bash -qk init -# Interactive wizard for configuration -``` - -## 🏗️ Architecture - -### Design Principles -1. **Modularity**: Separate concerns into packages -2. **Testability**: Easy to mock and test -3. **User-first**: Prioritize user experience -4. **Type Safety**: Leverage Go's type system -5. **Error Handling**: Clear, actionable errors -6. **Performance**: Fast startup and execution - -### Package Structure - -#### `cmd/qkflow/commands` -- CLI command definitions -- User interaction logic -- Command orchestration - -#### `internal/github` -- GitHub API client wrapper -- PR operations (create, get, merge) -- Repository parsing - -#### `internal/jira` -- Jira API client wrapper -- Issue operations (get, update) -- Status management - -#### `internal/git` -- Git command execution -- Branch management -- Commit and push operations - -#### `internal/ui` -- User prompts and input -- Colored output -- Progress indicators - -#### `pkg/config` -- Configuration loading and saving -- Environment variable support -- Validation - -## 🧪 Testing Strategy - -### Unit Tests -- Test individual functions -- Mock external dependencies -- Use table-driven tests - -### Integration Tests -- Test API clients (with mocks) -- Test command execution -- Test configuration management - -### Manual Testing -- Test complete workflows -- Test error scenarios -- Test on different platforms - -## 📊 Build & Release Process - -### Development Build -```bash -make build # Build for current platform -make test # Run tests -make lint # Run linters -``` - -### Multi-platform Build -```bash -make build-all # Build for macOS, Linux, Windows -``` - -### Release Process -```bash -./scripts/release.sh v1.0.0 -# Creates tag, triggers CI/CD -# GitHub Actions builds and uploads binaries -``` - -## 🎓 Learning Resources - -### For Users -1. Start with **QUICKSTART.md** (5 minutes) -2. Read **README.md** for full features -3. Check **MIGRATION.md** if coming from Shell version - -### For Contributors -1. Read **CONTRIBUTING.md** for guidelines -2. Study the code structure in this document -3. Run tests and explore the codebase -4. Start with "good first issue" labels - -## 🔮 Future Enhancements - -### High Priority -- [ ] GitLab support -- [ ] Bitbucket support -- [ ] Draft PR support -- [ ] PR templates -- [ ] Custom workflows - -### Medium Priority -- [ ] Better Windows integration -- [ ] Shell completion scripts -- [ ] PR review automation -- [ ] Batch operations -- [ ] Webhooks integration - -### Low Priority -- [ ] GUI version -- [ ] VS Code extension -- [ ] Metrics and analytics -- [ ] Team dashboards - -## 📞 Support & Community - -### Getting Help -- 📖 Read documentation first -- 🐛 Report bugs via GitHub Issues -- 💡 Request features via GitHub Issues -- 💬 Ask questions in GitHub Discussions - -### Contributing -- Fork, branch, code, test, PR -- Follow coding standards -- Write tests for new features -- Update documentation - -## 📄 License - -MIT License - See LICENSE file for details. - -## 🙏 Credits - -### Original Project -- Shell version by [Wangggym](https://github.com/Wangggym) - -### Go Version -- Architecture and implementation: AI-assisted development -- Testing and refinement: Community contributors - -### Open Source Libraries -- cobra, viper, survey (CLI framework) -- go-github, go-jira (API clients) -- And many more amazing Go packages - -## 📈 Project Status - -**Current Version**: 1.0.0 (Initial Release) -**Status**: ✅ Production Ready -**Stability**: Stable -**Maintenance**: Active - -## 🎉 Conclusion - -This Go version of Quick Workflow represents a complete modernization of the original Shell-based tool. It brings significant improvements in: - -- **Usability**: Easier installation and setup -- **Performance**: Faster startup and execution -- **Reliability**: Type-safe, well-tested code -- **Maintainability**: Clean architecture, good documentation -- **Extensibility**: Easy to add new features - -The project is ready for production use and welcomes community contributions! - ---- - -**Last Updated**: 2025-11-04 -**Maintainer**: Wangggym -**Repository**: https://github.com/Wangggym/quick-workflow - diff --git a/go-version/PR_APPROVE_GUIDE.md b/go-version/PR_APPROVE_GUIDE.md deleted file mode 100644 index be88e58..0000000 --- a/go-version/PR_APPROVE_GUIDE.md +++ /dev/null @@ -1,410 +0,0 @@ -# PR Approve Command Guide - -Quick guide for using the new `qkflow pr approve` command. - -## 🚀 Quick Start - -### Basic Approval - -```bash -# Approve a specific PR by number (uses default 👍 comment) -qkflow pr approve 123 - -# Approve a PR by URL (works from anywhere!) -qkflow pr approve https://github.com/brain/planning-api/pull/2001 - -# Also works with /files, /commits, /checks URLs -qkflow pr approve https://github.com/brain/planning-api/pull/2001/files - -# Auto-detect PR from current branch -qkflow pr approve -``` - -### With Comment - -By default, all approvals use 👍 as the comment. Customize with `-c` flag: - -```bash -# Default approval (with 👍) -qkflow pr approve 123 - -# Custom comment -qkflow pr approve 123 -c "LGTM! 🎉" - -# Approve by URL with custom comment -qkflow pr approve https://github.com/owner/repo/pull/456 -c "Great work!" - -# Long comment with flag -qkflow pr approve 123 --comment "Great work! All tests passed. Approved for merge." -``` - -### Auto-Merge - -```bash -# Approve and merge in one step -qkflow pr approve 123 --merge - -# Approve by URL and merge -qkflow pr approve https://github.com/owner/repo/pull/789 --merge - -# Short flag -qkflow pr approve 123 -m - -# With comment and merge -qkflow pr approve 123 -c "LGTM!" -m - -# URL with comment and merge -qkflow pr approve https://github.com/owner/repo/pull/789 -c "Ship it! 🚀" -m -``` - -## 🌐 URL Support (NEW!) - -Now you can approve PRs from **any repository** without being in the git directory! - -### Why Use URLs? - -1. **Cross-Repository**: Approve PRs from different repos -2. **No Directory Change**: Work from anywhere -3. **Browser to CLI**: Copy URL from GitHub directly -4. **Batch Operations**: Script approvals across multiple repos - -### URL Examples - -```bash -# Approve a PR from a different repo -qkflow pr approve https://github.com/brain/planning-api/pull/2001 - -# Your colleague shares a PR link, approve it instantly -qkflow pr approve https://github.com/company/frontend/pull/456 -c "Looks good!" - -# Merge someone else's PR by URL -qkflow pr merge https://github.com/team/backend/pull/789 - -# Approve and merge with URL -qkflow pr approve https://github.com/org/project/pull/123 -c "LGTM! 🎉" -m -``` - -### Supported URL Formats - -All these formats work: - -```bash -# HTTPS (most common) -https://github.com/owner/repo/pull/123 - -# With /files suffix (Files tab) -https://github.com/brain/planning-api/pull/2001/files - -# With /commits suffix (Commits tab) -https://github.com/owner/repo/pull/123/commits - -# With /checks suffix (Checks tab) -https://github.com/owner/repo/pull/123/checks - -# HTTP -http://github.com/owner/repo/pull/123 - -# Without protocol -github.com/owner/repo/pull/123 - -# With query params (parsed correctly) -https://github.com/owner/repo/pull/123?comments=all -https://github.com/owner/repo/pull/123/files?file-filters%5B%5D=.js - -# With fragments (parsed correctly) -https://github.com/owner/repo/pull/123#discussion_r123456 -``` - -### When to Use URL vs Number - -| Scenario | Use | Example | -|----------|-----|---------| -| Your repo, in git dir | Number | `qkflow pr approve 123` | -| Your repo, current branch | None | `qkflow pr approve` | -| Different repo | URL | `qkflow pr approve https://...` | -| Shared link from browser | URL | Copy & paste URL | -| Scripting multiple repos | URL | Loop through URLs | - -## 📋 Common Workflows - -### Workflow 1: Quick Code Review - -You're reviewing a colleague's PR: - -```bash -# 1. Check out their branch (optional) -git fetch origin -git checkout feature-branch - -# 2. Review the code... - -# 3. Approve -qkflow pr approve -# Finds PR automatically from branch -# Adds optional comment -# Done! -``` - -### Workflow 2: Approve and Merge - -You have approval rights and want to merge immediately: - -```bash -# One command to approve and merge -qkflow pr approve 123 -c "Approved! Merging now." --merge - -# What happens: -# ✅ Approves PR #123 -# ✅ Adds comment -# ✅ Checks if mergeable -# ✅ Confirms with you -# ✅ Merges PR -# ✅ Deletes remote branch -# ✅ Switches to main -# ✅ Deletes local branch -``` - -### Workflow 3: Batch Approvals - -Multiple PRs to review: - -```bash -# List all open PRs first -gh pr list - -# Approve them one by one -qkflow pr approve 101 -c "Approved" -qkflow pr approve 102 -c "Approved" -qkflow pr approve 103 -c "Approved" -``` - -### Workflow 4: Interactive Mode - -Don't know the PR number? Let the tool help: - -```bash -# Run without arguments -qkflow pr approve - -# What happens: -# 1. Tries to find PR from current branch -# 2. If not found, shows list of all open PRs -# 3. You select one -# 4. Asks for optional comment -# 5. Approves! -``` - -## 🔍 Use Cases - -### Use Case 1: Team Lead Approval - -As a team lead, you need to approve PRs daily: - -```bash -# Morning routine: approve all ready PRs -for pr in 121 122 123; do - qkflow pr approve $pr -c "Reviewed and approved" -done -``` - -### Use Case 2: CI/CD Integration - -Add to your CI pipeline: - -```bash -#!/bin/bash -# Auto-approve dependabot PRs after tests pass -if [[ "$PR_AUTHOR" == "dependabot" ]] && [[ "$TESTS_PASSED" == "true" ]]; then - qkflow pr approve $PR_NUMBER -c "Auto-approved: Tests passed" -m -fi -``` - -### Use Case 3: Hotfix Workflow - -Fast-track urgent fixes: - -```bash -# Create hotfix -git checkout -b hotfix/critical-bug -# ... fix the bug ... -git add . -qkflow pr create - -# Get it approved and merged ASAP -qkflow pr approve 999 -c "Critical hotfix - merging immediately" -m -``` - -## 🎯 Pro Tips - -### Tip 1: Aliases - -Add to your `.bashrc` or `.zshrc`: - -```bash -alias approve='qkflow pr approve' -alias merge='qkflow pr approve --merge' - -# Usage: -approve 123 -c "LGTM" -merge 123 -``` - -### Tip 2: Comment Templates - -Save common comments: - -```bash -# In your shell config -export APPROVE_LGTM="Looks good to me! 👍" -export APPROVE_MINOR="Approved with minor comments. Please address in follow-up." -export APPROVE_EXCELLENT="Excellent work! 🎉" - -# Usage: -qkflow pr approve 123 -c "$APPROVE_LGTM" -``` - -### Tip 3: Check Before Merge - -Before using `--merge`, verify the PR: - -```bash -# View PR details -gh pr view 123 - -# Check CI status -gh pr checks 123 - -# Approve and merge if all good -qkflow pr approve 123 -m -``` - -### Tip 4: Branch Protection - -If branch protection is enabled: - -```bash -# Just approve - let GitHub merge rules handle the rest -qkflow pr approve 123 -c "Approved" - -# Don't use --merge if you need multiple approvals -``` - -## ⚠️ Error Handling - -### PR Not Found - -```bash -$ qkflow pr approve 999 -❌ Failed to get PR: Pull request not found -``` - -**Solution:** Check PR number with `gh pr list` - -### PR Already Closed - -```bash -$ qkflow pr approve 123 -❌ PR is not open (state: closed) -``` - -**Solution:** PR is already merged or closed - -### Not Mergeable - -```bash -$ qkflow pr approve 123 -m -✅ PR approved! -⚠️ Cannot merge PR: PR has conflicts and cannot be merged -``` - -**Solution:** Resolve conflicts first, then merge - -### No PR for Branch - -```bash -$ qkflow pr approve -⚠️ No PR found for branch: feature-xyz -Do you want to select a PR from the list? (Y/n) -``` - -**Solution:** Either select from list or specify PR number - -## 🆚 Comparison - -### GitHub CLI (`gh`) - -```bash -# Approve -gh pr review 123 --approve --body "LGTM" - -# Then merge separately -gh pr merge 123 - -# Then cleanup -git checkout main -git pull -git branch -D feature-branch -``` - -### qkflow (New!) - -```bash -# All in one! -qkflow pr approve 123 -c "LGTM" -m -``` - -**Benefits:** -- ✅ Fewer commands -- ✅ Auto-cleanup -- ✅ Interactive prompts -- ✅ Branch auto-detection -- ✅ Merge validation - -## 🔗 Related Commands - -- `qkflow pr create` - Create a new PR -- `qkflow pr merge` - Merge without approving first -- `gh pr view 123` - View PR details -- `gh pr checks 123` - Check CI status - -## 📚 Full Reference - -### Command Syntax - -``` -qkflow pr approve [pr-number] [flags] -``` - -### Flags - -| Flag | Short | Description | -|------|-------|-------------| -| `--comment` | `-c` | Add a comment with the approval | -| `--merge` | `-m` | Automatically merge after approval | -| `--help` | `-h` | Show help information | - -### Exit Codes - -- `0`: Success -- `1`: Error (PR not found, not mergeable, etc.) - -### Environment - -Requires: -- GitHub token configured (`qkflow config`) -- Git repository with remote -- Write access to repository - ---- - -**Need Help?** - -```bash -qkflow pr approve --help -qkflow help -``` - -**Found a Bug?** - -Open an issue on GitHub with details! - diff --git a/go-version/PR_EDITOR_FEATURE.md b/go-version/PR_EDITOR_FEATURE.md deleted file mode 100644 index ef7e7df..0000000 --- a/go-version/PR_EDITOR_FEATURE.md +++ /dev/null @@ -1,313 +0,0 @@ -# PR Editor Feature - Implementation Summary - -## 🎉 New Feature: Web-Based PR Description Editor - -Added a beautiful web-based editor for adding detailed descriptions and media (images/videos) to pull requests, with automatic upload to both GitHub and Jira. - -## ✨ What's New - -### Enhanced PR Creation Flow - -The `qkflow pr create` command now includes an optional step to add detailed descriptions with rich media: - -``` -Current flow: -1. Get Jira ticket (optional) -2. Get Jira issue details -3. Select change types -4. ⭐ [NEW] Add description & screenshots? (optional) - └─ Opens web editor in browser -5. Generate PR title (can use description for better title) -6. Create branch & commit -7. Push & create GitHub PR -8. Upload files and add comment to GitHub -9. Upload files and add comment to Jira -10. Update Jira status -``` - -### Web Editor Features - -- **GitHub-style Interface**: Familiar dark theme matching GitHub's UI -- **Markdown Editor**: EasyMDE with live preview and toolbar -- **Drag & Drop**: Simply drag images/videos from Finder/Explorer -- **Paste Support**: Paste images directly from clipboard (Cmd+V / Ctrl+V) -- **Real-time Preview**: See formatted output as you type -- **File Management**: Track uploaded files with size information -- **Supported Formats**: - - **Images**: PNG, JPG, JPEG, GIF, WebP, SVG - - **Videos**: MP4, MOV, WebM, AVI - -### Automatic Upload & Commenting - -Once you save in the editor: - -1. **Uploads images** to both GitHub and Jira -2. **Converts local paths** to online URLs in markdown -3. **Adds comment** to GitHub PR with your description -4. **Adds comment** to Jira issue with the same content -5. **Cleans up** temporary files - -### Example Workflow - -```bash -$ qkflow pr create NA-9245 - -✓ Found Jira issue: Fix login button styling -📝 Select type(s) of changes: - ✓ 🐛 Bug fix - -? Add detailed description with images/videos? - > ⏭️ Skip (default) # Just press Enter to skip - ✅ Yes, continue # Use arrow keys or space, then Enter to select - -# User selects "Yes, continue" -🌐 Opening editor in your browser: http://localhost:54321 -📝 Please edit your content in the browser and click 'Save and Continue' - -✅ Content saved! (245 characters, 2 files) -✅ Generated title: fix: Update login button hover state -✅ Creating branch: NA-9245--fix-update-login-button-hover-state -... -✅ Pull request created: https://github.com/owner/repo/pull/123 -📤 Processing description and files... -📤 Uploading 2 file(s)... -✅ Uploaded 2 file(s) -✅ Description added to GitHub PR -✅ Description added to Jira -✅ All done! 🎉 -``` - -## 🎨 Editor UI - -The web editor opens in your default browser with a clean, professional interface: - -``` -┌─────────────────────────────────────────────────────┐ -│ 📝 Add PR Description & Screenshots │ -│ Write your description in Markdown and drag & drop │ -├─────────────────────────────────────────────────────┤ -│ │ -│ [Markdown Editor with Toolbar] │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ ## Description │ │ -│ │ │ │ -│ │ Fixed login button hover state issue. │ │ -│ │ │ │ -│ │ ### Before & After │ │ -│ │ ![Before](./before.png) │ │ -│ │ ![After](./after.png) │ │ -│ └─────────────────────────────────────────────┘ │ -│ │ -│ 📎 Attach Images & Videos │ -│ Drag & drop files here, or click to select │ -│ [Choose Files] │ -│ │ -│ Uploaded files: │ -│ • 🖼️ before.png (125 KB) [Remove] │ -│ • 🖼️ after.png (132 KB) [Remove] │ -│ │ -│ 💡 Tip: You can paste images directly │ -├─────────────────────────────────────────────────────┤ -│ [Cancel] [Save and Continue] │ -└─────────────────────────────────────────────────────┘ -``` - -## 🔧 Technical Implementation - -### New Internal Packages - -#### `internal/editor/` (New Package) - -1. **`server.go`** - HTTP server for the web editor - - Starts local web server on random port - - Handles file uploads - - Receives editor content - - Auto-opens browser - -2. **`html.go`** - Web editor UI - - Complete HTML/CSS/JS for the editor - - EasyMDE markdown editor integration - - Drag & drop file handling - - Clipboard paste support - -3. **`uploader.go`** - File upload logic - - Uploads to GitHub (as base64 data URLs for images) - - Uploads to Jira (as attachments) - - Replaces local paths with URLs - - Handles different file types - -### Enhanced Existing Packages - -#### `internal/github/client.go` -- **New Method**: `AddPRComment(owner, repo string, prNumber int, body string)` - - Adds a comment to a pull request - - Used to post the description after PR creation - -#### `internal/jira/client.go` -- **New Method**: `AddAttachment(issueKey, filename string, content io.Reader)` - - Uploads an attachment to a Jira issue - - Returns attachment URL - - Used for images and videos - -### Modified Files - -#### `cmd/qkflow/commands/pr_create.go` -- Added editor integration after "Select change types" step -- Added file upload and comment logic after PR creation -- Cleans up temporary files after processing - -## 📊 File Structure - -``` -go-version/ -├── internal/ -│ ├── editor/ # NEW -│ │ ├── server.go # HTTP server (280 lines) -│ │ ├── html.go # Web UI (465 lines) -│ │ └── uploader.go # File upload (165 lines) -│ ├── github/ -│ │ └── client.go # + AddPRComment method -│ └── jira/ -│ └── client.go # + AddAttachment method -└── cmd/qkflow/commands/ - └── pr_create.go # Enhanced with editor flow -``` - -## 🎯 Key Features - -### 1. **Non-Intrusive** -- Completely optional step -- Default is "No" - press Enter to skip -- Doesn't break existing workflow - -### 2. **User-Friendly** -- Opens in familiar browser environment -- GitHub-style dark theme -- Drag & drop is intuitive -- Paste from clipboard works - -### 3. **Smart Upload** -- Images → base64 data URLs for GitHub -- Images → attachments for Jira -- Automatic path replacement in markdown -- Error handling with warnings - -### 4. **Clean Implementation** -- Separate package for maintainability -- Reuses existing clients -- Proper error handling -- Cleanup of temporary files - -## 📝 Usage Examples - -### Example 1: Bug Fix with Screenshots - -```markdown -## Description - -Fixed the login button hover state issue where the button -wasn't changing color on hover. - -### Before & After - -![Before](./before.png) -![After](./after.png) - -### Changes Made - -- Updated CSS hover selector -- Added transition animation -- Fixed color contrast - -### Testing - -Tested on: -- ✅ Chrome 120 -- ✅ Firefox 121 -- ✅ Safari 17 -``` - -### Example 2: Feature with Demo Video - -```markdown -## New Feature: User Avatar Upload - -Implemented user profile avatar upload functionality. - -### Demo - -![Demo Video](./demo.mp4) - -### Features - -- Drag & drop support -- Image cropping -- Preview before upload -- Automatic resizing to 256x256 - -### Dependencies - -- Added image processing library -- Updated user model schema -``` - -## 🚀 Future Enhancements - -Potential improvements: - -1. **Video Upload**: Implement proper video hosting (currently only images are fully supported) -2. **Image Optimization**: Compress large images automatically -3. **Templates**: Pre-defined templates for common PR types -4. **AI Suggestions**: Use AI to suggest descriptions based on code changes -5. **Offline Mode**: Local-only markdown file editing -6. **Custom Themes**: Light mode option -7. **Rich Previews**: Better preview for videos - -## 🐛 Known Limitations - -1. **Videos**: Currently only images are uploaded as inline data URLs. Videos need external hosting. -2. **File Size**: Large files (>10MB) may be rejected by GitHub/Jira -3. **Browser**: Requires a default browser to be configured -4. **Temporary Port**: Uses random port - might conflict in rare cases - -## ✅ Testing Checklist - -- [x] Build succeeds -- [x] No lint errors -- [x] Editor opens in browser -- [x] File upload works -- [x] Markdown preview works -- [x] Drag & drop works -- [x] Clipboard paste works -- [x] GitHub comment created -- [x] Jira comment created -- [x] Temporary files cleaned up -- [ ] Manual testing with real PR -- [ ] Cross-platform testing (macOS, Linux, Windows) - -## 📚 Documentation Updates Needed - -1. Update `README.md` with new feature -2. Add screenshots to documentation -3. Update `CHANGELOG.md` -4. Create tutorial video (optional) - -## 🎉 Conclusion - -This feature significantly improves the PR creation experience by: - -- **Reducing friction** in adding rich descriptions -- **Improving documentation** of changes with visual aids -- **Consistent format** across GitHub and Jira -- **Professional appearance** of PRs - -The web-based editor provides a familiar, user-friendly interface that encourages developers to add more context to their PRs, leading to better code reviews and documentation. - ---- - -**Implementation Date**: 2024-11-18 -**Version**: To be included in v1.4.0 -**Status**: ✅ Complete - Ready for Testing - -**Total Lines Added**: ~1,000 lines of code - diff --git a/go-version/PR_URL_SUPPORT.md b/go-version/PR_URL_SUPPORT.md deleted file mode 100644 index 2110670..0000000 --- a/go-version/PR_URL_SUPPORT.md +++ /dev/null @@ -1,308 +0,0 @@ -# PR URL Support - Feature Summary - -## 🎉 What's New - -You can now use **GitHub PR URLs** directly with `pr approve` and `pr merge` commands! No need to be in the repository directory or remember PR numbers. - -## ✨ Quick Examples - -### Before (PR number only) -```bash -# Had to be in the repo directory -cd ~/projects/my-repo -qkflow pr approve 123 -``` - -### After (URL support!) -```bash -# Works from anywhere! 🚀 -qkflow pr approve https://github.com/brain/planning-api/pull/2001 -qkflow pr approve https://github.com/brain/planning-api/pull/2001 -c "LGTM!" -m -``` - -## 📖 Usage - -### Basic Syntax - -Both commands now accept: -1. **PR Number** (requires being in git repo) -2. **Full GitHub URL** (works from anywhere!) -3. **No argument** (auto-detect from current branch) - -### Commands Updated - -#### `qkflow pr approve` - -```bash -# By PR number (in repo) -qkflow pr approve 123 - -# By URL (anywhere) -qkflow pr approve https://github.com/owner/repo/pull/456 - -# With options -qkflow pr approve https://github.com/owner/repo/pull/789 -c "LGTM!" -m -``` - -#### `qkflow pr merge` - -```bash -# By PR number (in repo) -qkflow pr merge 123 - -# By URL (anywhere) -qkflow pr merge https://github.com/owner/repo/pull/456 -``` - -## 🌟 Use Cases - -### 1. Cross-Repository Reviews - -Review PRs from multiple repositories without changing directories: - -```bash -qkflow pr approve https://github.com/team/frontend/pull/100 -c "Approved" -qkflow pr approve https://github.com/team/backend/pull/200 -c "Approved" -qkflow pr approve https://github.com/team/mobile/pull/300 -c "Approved" -``` - -### 2. Browser to CLI Workflow - -1. Open PR in GitHub web -2. Copy URL from address bar -3. Paste into terminal: - -```bash -qkflow pr approve https://github.com/company/project/pull/1234 -c "Looks good!" -``` - -### 3. Slack/Email Integration - -Teammate shares a PR link in Slack? Approve it instantly: - -```bash -# Copy link from Slack -qkflow pr approve -c "Reviewed and approved" -``` - -### 4. Scripting Across Repos - -Automate approvals across multiple repositories: - -```bash -#!/bin/bash -PR_URLS=( - "https://github.com/org/repo1/pull/10" - "https://github.com/org/repo2/pull/20" - "https://github.com/org/repo3/pull/30" -) - -for url in "${PR_URLS[@]}"; do - qkflow pr approve "$url" -c "Auto-approved by bot" -done -``` - -## 🔧 Technical Details - -### Supported URL Formats - -All these formats work: - -``` -✅ https://github.com/owner/repo/pull/123 -✅ https://github.com/owner/repo/pull/123/files (Files tab) -✅ https://github.com/owner/repo/pull/123/commits (Commits tab) -✅ https://github.com/owner/repo/pull/123/checks (Checks tab) -✅ http://github.com/owner/repo/pull/123 -✅ github.com/owner/repo/pull/123 -✅ https://github.com/owner/repo/pull/123?comments=all -✅ https://github.com/owner/repo/pull/123#discussion_r123456 -✅ https://github.com/owner/repo/pull/123/files?file-filters%5B%5D=.js -``` - -**Pro Tip:** Just copy the URL from any PR tab (Overview, Files, Commits, Checks) and it will work! 🎉 - -### URL Parsing - -The tool automatically: -- Detects if argument is a URL or number -- Extracts owner, repo, and PR number from URL -- Handles query parameters and fragments -- Validates URL format - -### Error Handling - -Clear error messages for common issues: - -```bash -$ qkflow pr approve invalid-url -❌ Invalid PR number or URL: invalid-url -ℹ️ Expected: PR number (e.g., '123') or GitHub URL (e.g., 'https://github.com/owner/repo/pull/123') -``` - -## 🧪 Testing - -Comprehensive test coverage for URL parsing: - -```bash -# Run tests -cd go-version -go test -v ./internal/github/ - -# All tests pass! ✅ -# - TestParsePRFromURL (8 test cases) -# - TestIsPRURL (8 test cases) -# - TestParseRepositoryFromURL (5 test cases) -``` - -## 📚 Documentation - -Updated documentation: -- ✅ `README.md` - Main documentation with examples -- ✅ `PR_APPROVE_GUIDE.md` - Detailed usage guide -- ✅ `CHANGELOG_PR_APPROVE.md` - Feature changelog -- ✅ `PR_URL_SUPPORT.md` - This file -- ✅ Command help text (`--help`) - -## 🎯 Benefits - -1. **🌐 Cross-Repository**: Approve PRs from any repo -2. **⚡ Faster**: No need to navigate to repo directory -3. **📋 Copy-Paste Friendly**: Direct from browser -4. **🤖 Scriptable**: Easy batch operations -5. **🔗 Shareable**: Use links from Slack/Email -6. **✨ Backwards Compatible**: Still works with PR numbers - -## 🚀 Get Started - -1. **Update qkflow** (if already installed): - ```bash - qkflow update-cli - ``` - -2. **Try it out**: - ```bash - # Find a PR on GitHub, copy the URL - qkflow pr approve https://github.com/.../pull/123 -c "Testing URL support!" - ``` - -3. **See help**: - ```bash - qkflow pr approve --help - qkflow pr merge --help - ``` - -## 💡 Tips - -### Alias for Quick Access - -Add to your `.bashrc` or `.zshrc`: - -```bash -# Quick approve -alias gha='qkflow pr approve' - -# Usage: -gha https://github.com/owner/repo/pull/123 -c "LGTM!" -``` - -### Use with `pbpaste` (macOS) - -```bash -# Copy URL in browser, then: -qkflow pr approve "$(pbpaste)" -c "Approved!" -``` - -### Integration with GitHub CLI - -Combine with `gh` CLI: - -```bash -# List PRs with gh, approve with qkflow -gh pr list -qkflow pr approve https://github.com/owner/repo/pull/123 -m -``` - -## 📝 Examples in Action - -### Example 1: Quick Review (Default Comment) - -```bash -$ qkflow pr approve https://github.com/brain/planning-api/pull/2001/files - -ℹ️ Detected GitHub PR URL, parsing... -✅ Parsed: brain/planning-api PR #2001 -ℹ️ Fetching PR #2001... -ℹ️ PR: feat: Add user authentication -ℹ️ Branch: feature/auth -> main -ℹ️ State: open -ℹ️ Using default comment: 👍 (use -c flag to customize) -ℹ️ Approving PR #2001... -✅ PR approved with comment: 👍 - -ℹ️ PR approved. Use 'qkg pr merge' to merge it later, or run with --merge flag to auto-merge. -``` - -### Example 1b: Quick Review (Custom Comment) - -```bash -$ qkflow pr approve https://github.com/brain/planning-api/pull/2001 -c "LGTM!" - -ℹ️ Detected GitHub PR URL, parsing... -✅ Parsed: brain/planning-api PR #2001 -ℹ️ Fetching PR #2001... -ℹ️ PR: feat: Add user authentication -ℹ️ Branch: feature/auth -> main -ℹ️ State: open -ℹ️ Approving PR #2001... -✅ PR approved with comment: LGTM! - -ℹ️ PR approved. Use 'qkg pr merge' to merge it later, or run with --merge flag to auto-merge. -``` - -### Example 2: Approve and Merge - -```bash -$ qkflow pr approve https://github.com/team/backend/pull/456 -c "Ship it! 🚀" -m - -ℹ️ Detected GitHub PR URL, parsing... -✅ Parsed: team/backend PR #456 -ℹ️ Fetching PR #456... -ℹ️ PR: fix: Database connection timeout -ℹ️ Branch: fix/db-timeout -> main -ℹ️ State: open -ℹ️ Approving PR #456... -✅ PR approved with comment: Ship it! 🚀 - -ℹ️ Checking if PR is mergeable... -❓ Proceed with merging the PR? (Y/n) y -ℹ️ Merging PR #456... -✅ 🎉 PR merged successfully! -ℹ️ Deleting remote branch fix/db-timeout... -✅ Remote branch deleted - -✅ All done! 🎉 -``` - -## 🤝 Backwards Compatibility - -All existing workflows still work: - -```bash -# PR number (requires being in repo) -qkflow pr approve 123 - -# Auto-detect from current branch -qkflow pr approve - -# Interactive selection -qkflow pr approve -# → Shows list of PRs to choose from -``` - ---- - -**Enjoy the new URL support! 🎉** - -For questions or feedback, please open an issue on GitHub. - diff --git a/go-version/QUICKSTART.md b/go-version/QUICKSTART.md deleted file mode 100644 index 13c70d8..0000000 --- a/go-version/QUICKSTART.md +++ /dev/null @@ -1,233 +0,0 @@ -# Quick Start Guide - -Get up and running with Quick Workflow Go version in 5 minutes! - -## 📦 Installation (30 seconds) - -### macOS (Apple Silicon) -```bash -curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qk-darwin-arm64 -o qk && \ -chmod +x qk && \ -sudo mv qk /usr/local/bin/ -``` - -### macOS (Intel) -```bash -curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qk-darwin-amd64 -o qk && \ -chmod +x qk && \ -sudo mv qk /usr/local/bin/ -``` - -### Linux -```bash -curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qk-linux-amd64 -o qk && \ -chmod +x qk && \ -sudo mv qk /usr/local/bin/ -``` - -## ⚙️ Setup (2 minutes) - -### 1. Ensure Prerequisites - -```bash -# Install and authenticate GitHub CLI -brew install gh -gh auth login - -# Get Jira API token -# Visit: https://id.atlassian.com/manage-profile/security/api-tokens -``` - -### 2. Run Setup Wizard - -```bash -qk init -``` - -Answer the prompts: -- **Email**: Your work email -- **GitHub Token**: Auto-detected from `gh` CLI -- **Jira Address**: `https://your-domain.atlassian.net` -- **Jira Token**: Paste the token from step 1 -- **Branch Prefix**: Optional (e.g., `feature` or your username) - -## 🎯 Your First PR (2 minutes) - -### Step 1: Make Your Changes - -```bash -cd your-project -git checkout -b feature/test - -# Make some changes -echo "# Test" >> README.md -git add README.md -``` - -### Step 2: Create PR - -```bash -qk pr create PROJ-123 -``` - -Follow the prompts: -1. **Title**: Accept suggested or enter custom -2. **Description**: Optional short description -3. **Change Types**: Select applicable types (feat, fix, etc.) -4. **Jira Status**: Choose new status (optional) - -**Done!** Your PR is created and Jira is updated! 🎉 - -## 🔄 Merge a PR (1 minute) - -```bash -qk pr merge 123 -``` - -Follow the prompts: -1. **Confirm merge**: Review PR details -2. **Delete branches**: Choose to clean up -3. **Update Jira**: Set final status - -**Done!** PR merged and cleaned up! 🎉 - -## 💡 Pro Tips - -### Use Without Jira - -```bash -# Skip Jira ticket (press Enter when prompted) -qk pr create -``` - -### Keyboard Shortcuts in Prompts - -- **Arrow keys**: Navigate options -- **Space**: Select/deselect (multi-select) -- **Enter**: Confirm selection -- **Ctrl+C**: Cancel operation - -### Quick Commands - -```bash -# Show config -qk config - -# Show version -qk version - -# Get help -qk --help -qk pr --help -``` - -## 🎨 Example Workflow - -Here's a complete workflow example: - -```bash -# 1. Start new feature -cd ~/projects/my-app -git checkout main -git pull - -# 2. Make changes -git checkout -b feature/awesome-feature -# ... make your changes ... -git add . - -# 3. Create PR -qk pr create PROJ-456 -# Title: "Add awesome feature" -# Description: "This adds X, Y, Z" -# Types: [x] feat: New feature -# Jira Status: In Review - -# Output: -# ✅ Branch created: feature/PROJ-456--Add-awesome-feature -# ✅ Changes committed -# ✅ Pushed to remote -# ✅ Pull request created: https://github.com/org/repo/pull/789 -# ✅ Added PR link to Jira -# ✅ Updated Jira status to: In Review -# ✅ All done! 🎉 - -# 4. Get code review, make changes if needed -# ... after approval ... - -# 5. Merge PR -qk pr merge 789 -# Confirm merge: Yes -# Delete remote branch: Yes -# Delete local branch: Yes -# Update Jira: Yes -# New status: Done - -# Output: -# ✅ Pull request merged! -# ✅ Remote branch deleted -# ✅ Local branch deleted -# ✅ Updated Jira status to: Done -# ✅ All done! 🎉 -``` - -## 🐛 Common Issues - -### "Command not found: qk" - -```bash -# Check if binary exists -ls -l /usr/local/bin/qk - -# Check PATH -echo $PATH | grep -q "/usr/local/bin" && echo "OK" || echo "Add to PATH" - -# Add to PATH if needed (add to ~/.zshrc) -export PATH="/usr/local/bin:$PATH" -``` - -### "Failed to create GitHub client" - -```bash -# Ensure gh is authenticated -gh auth status - -# If not authenticated -gh auth login - -# Re-run qk init -qk init -``` - -### "Failed to get Jira issue" - -```bash -# Verify Jira credentials -curl -u "your.email@example.com:your_jira_token" \ - https://your-domain.atlassian.net/rest/api/2/myself - -# If fails, get new API token and re-run qk init -``` - -## 📚 Learn More - -- **Full Documentation**: [README.md](README.md) -- **Migration Guide**: [MIGRATION.md](MIGRATION.md) -- **GitHub Issues**: [Report bugs or request features](https://github.com/Wangggym/quick-workflow/issues) - -## 🎉 You're Ready! - -Congratulations! You're now set up with Quick Workflow. Enjoy your streamlined workflow! - -**Common Commands to Remember:** -```bash -qk pr create # Create PR -qk pr merge # Merge PR -qk config # Show config -qk --help # Get help -``` - ---- - -**Happy coding! 🚀** - diff --git a/go-version/README.md b/go-version/README.md index 7784cdd..1fa2675 100644 --- a/go-version/README.md +++ b/go-version/README.md @@ -1,656 +1,308 @@ -# Quick Workflow (Go Version) +# Quick Workflow (Go 版本) -> A modern, blazing-fast CLI tool for streamlined GitHub and Jira workflows +> 一个现代化、极速的 CLI 工具,用于简化 GitHub 和 Jira 工作流 [![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://go.dev/) [![Release](https://img.shields.io/github/v/release/Wangggym/quick-workflow?style=flat&logo=github)](https://github.com/Wangggym/quick-workflow/releases) -[![Build Status](https://img.shields.io/github/actions/workflow/status/Wangggym/quick-workflow/build.yml?branch=master&style=flat&logo=github-actions)](https://github.com/Wangggym/quick-workflow/actions) +[![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen?style=flat&logo=github-actions)](https://github.com/Wangggym/quick-workflow/actions) [![License](https://img.shields.io/badge/License-MIT-green.svg?style=flat)](LICENSE) [![Platform](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey?style=flat)](https://github.com/Wangggym/quick-workflow) -[![iCloud Sync](https://img.shields.io/badge/iCloud-Sync%20Enabled-blue?style=flat&logo=icloud)](ICLOUD_MIGRATION.md) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](CONTRIBUTING.md) -## 🚀 What's New in Go Version +## 🚀 快速开始 -This is a complete rewrite of the original Shell-based quick-workflow tool in Go, bringing: +**新用户?** 查看 [📖 快速开始指南](docs/QUICKSTART.md) - 5 分钟快速上手! -- **📦 Single Binary** - No dependencies, just download and run -- **⚡ Faster** - Native performance, instant startup -- **🔒 Type Safe** - Catch errors at compile time -- **🎨 Better UX** - Interactive prompts, colored output -- **🧪 Testable** - Comprehensive test coverage -- **🌍 Cross-platform** - Works on macOS, Linux, and Windows -- **☁️ iCloud Sync** - Automatic config sync across Mac devices (macOS only) +**快速预览:** -## ✨ Features +```bash +# 1. 安装(macOS Apple Silicon) +curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-arm64 -o qkflow +chmod +x qkflow +sudo mv qkflow /usr/local/bin/ -- **PR Creation** - Create PRs with automatic branch creation, commit, and push -- **PR Editor** - 🆕 Web-based editor for adding descriptions with images/videos 🎨 -- **PR Merging** - Merge PRs and clean up branches automatically -- **Quick Update** - Commit and push with PR title as commit message -- **Jira Integration** - Automatically update Jira status and add PR links -- **Jira Reader** - 🆕 Read and export Jira issues (optimized for Cursor AI) 🤖 -- **Watch Daemon** - 🆕 Automatically monitor PRs and update Jira when merged ⚡ -- **Interactive CLI** - Beautiful prompts and progress indicators -- **Configuration Management** - Simple setup with `qkflow init` -- **iCloud Sync** - Seamlessly sync configs across all your Mac devices ☁️ -- **Auto Update** - Automatically check and install updates (24h interval) 🔄 +# 2. 初始化配置 +qkflow init -## 📦 Installation +# 3. 开始使用! +qkflow pr create PROJ-123 +``` -### Option 1: Download Binary (Recommended) +> 💡 **提示**:上面的命令只是快速预览。完整的安装步骤、配置说明和详细示例都在 [快速开始指南](docs/QUICKSTART.md) 中。 -#### macOS Installation +--- -```bash -# macOS (Apple Silicon - M1/M2/M3) -curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-arm64 -o qkflow +## ✨ 核心功能 -# macOS (Intel) -curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-amd64 -o qkflow +- **PR 管理** - 创建、审批、合并 PR,支持 URL 操作 +- **PR 编辑器** - 基于 Web 的编辑器,支持添加图片/视频描述 +- **Jira 集成** - 自动更新 Jira 状态并添加 PR 链接 +- **Jira 阅读器** - 读取和导出 Jira 问题(针对 Cursor AI 优化) +- **监控守护进程** - 自动监控 PR 并在合并时更新 Jira +- **iCloud 同步** - 在所有 Mac 设备间无缝同步配置(仅限 macOS) +- **自动更新** - 自动检查并安装更新(24 小时间隔) + +📖 完整功能列表和使用说明见 [使用指南](docs/README.md)。 + +--- -# 🔑 Important: Remove macOS quarantine attribute to bypass Gatekeeper (if needed) -# Note: If you get "No such xattr: com.apple.quarantine", that's fine - skip this step -xattr -d com.apple.quarantine qkflow 2>/dev/null || echo "No quarantine attribute found (this is fine)" +## 📦 安装 -# Make executable and install +### 方式 1: 下载预编译二进制(推荐) + +#### macOS +```bash +# Apple Silicon (M1/M2/M3) +curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-arm64 -o qkflow chmod +x qkflow sudo mv qkflow /usr/local/bin/ -# Verify installation -qkflow version +# Intel +curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-amd64 -o qkflow +chmod +x qkflow +sudo mv qkflow /usr/local/bin/ ``` -> **⚠️ macOS Security Notice**: If you see "qkflow-darwin-arm64 cannot be opened because Apple cannot verify that it is free of malware", this is normal for unsigned binaries. The `xattr -d com.apple.quarantine` command above will resolve this issue safely. - -#### Linux Installation +> **⚠️ macOS 安全提示**:如果看到安全警告,运行 `xattr -d com.apple.quarantine qkflow` 移除隔离属性。 +#### Linux ```bash -# Linux (x86_64) curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-linux-amd64 -o qkflow chmod +x qkflow sudo mv qkflow /usr/local/bin/ - -# Verify installation -qkflow version ``` -### Option 2: Install with Go +### 方式 2: 使用 Go 安装 ```bash go install github.com/Wangggym/quick-workflow/cmd/qkflow@latest ``` -### Option 3: Build from Source +### 方式 3: 从源码构建 ```bash git clone https://github.com/Wangggym/quick-workflow.git cd quick-workflow/go-version make build -sudo cp bin/qkflow /usr/local/bin/ +make install # 安装到 /usr/local/bin (需要 sudo) +# 或使用快速安装: make local ``` -## ⚙️ Setup +--- -### Prerequisites +## ⚙️ 配置 -- Git installed and configured -- GitHub CLI (`gh`) installed and authenticated: `gh auth login` -- Jira API token: [Get one here](https://id.atlassian.com/manage-profile/security/api-tokens) +### 前置要求 -### Initial Configuration +- 已安装并配置 Git +- 已安装并认证 GitHub CLI (`gh`):`gh auth login` +- Jira API 令牌:[在此获取](https://id.atlassian.com/manage-profile/security/api-tokens) -Run the interactive setup: +### 初始化配置 + +运行交互式设置: ```bash qkflow init ``` -This will prompt you for: -- Email address -- GitHub token (auto-detected from `gh` CLI) -- Jira service address (e.g., https://your-domain.atlassian.net) -- Jira API token -- Optional: Branch prefix -- Optional: OpenAI API key for AI features +这将提示你输入邮箱、GitHub 令牌、Jira 配置等。 -**Configuration Storage:** +**配置存储:** -✨ **NEW**: On macOS, all configs are automatically saved to iCloud Drive in a single directory and synced across all your devices! +在 macOS 上,配置会自动保存到 iCloud Drive,并在所有设备间同步: +- 📂 `~/Library/Mobile Documents/com~apple~CloudDocs/.qkflow/` -- **macOS with iCloud Drive**: Synced across devices ☁️ - - 📂 All configs in: `~/Library/Mobile Documents/com~apple~CloudDocs/.qkflow/` -- **Local Storage** (fallback): - - 📂 All configs in: `~/.qkflow/` +其他系统使用本地存储: +- 📂 `~/.qkflow/` -Both locations contain: -- `config.yaml` - Main configuration -- `jira-status.json` - Jira status mappings +运行 `qkflow config` 查看实际存储位置。 -Run `qkflow config` to see your actual storage location. +📖 更多详情请参阅 [迁移指南](docs/MIGRATION.md)。 -📖 See [iCloud Migration Guide](ICLOUD_MIGRATION.md) for more details. +--- -## 🎯 Usage +## 🎯 常用命令 -### Create a Pull Request +### PR 操作 ```bash -# With Jira ticket +# 创建 PR qkflow pr create PROJ-123 -# Without Jira ticket (will prompt) -qkflow pr create - -# Interactive mode (no arguments) -qkflow pr create -``` - -**What it does:** -1. ✅ Fetches Jira issue details (if ticket provided) -2. ✅ Prompts for PR title and description -3. ✅ Lets you select change types (feat, fix, docs, etc.) -4. ✅ ⭐ **NEW**: Optionally add detailed description with images/videos (web editor) -5. ✅ Creates a new git branch -6. ✅ Commits your staged changes -7. ✅ Pushes to remote -8. ✅ Creates GitHub PR -9. ✅ ⭐ **NEW**: Uploads files and adds comment to GitHub & Jira -10. ✅ Adds PR link to Jira -11. ✅ Updates Jira status (optional) -12. ✅ Copies PR URL to clipboard - -### Merge a Pull Request - -```bash -# Merge PR by number -qkflow pr merge 123 - -# Merge PR by URL (works from anywhere!) -qkflow pr merge https://github.com/brain/planning-api/pull/2001 - -# Interactive mode (auto-detect from current branch) -qkflow pr merge -``` - -**What it does:** -1. ✅ Supports PR number OR full GitHub URL -2. ✅ Fetches PR details -3. ✅ Confirms merge with you -4. ✅ Merges the PR on GitHub -5. ✅ Deletes remote branch (optional) -6. ✅ Switches to main branch -7. ✅ Deletes local branch (optional) -8. ✅ Updates Jira status (optional) -9. ✅ Adds merge comment to Jira - -### Approve a Pull Request - -```bash -# Approve PR by number (with default 👍 comment) +# 审批 PR(支持 URL) qkflow pr approve 123 +qkflow pr approve https://github.com/owner/repo/pull/123 -c "LGTM!" -m -# Approve PR by URL (works from anywhere!) -qkflow pr approve https://github.com/brain/planning-api/pull/2001 - -# URL also works with /files, /commits, /checks suffixes -qkflow pr approve https://github.com/brain/planning-api/pull/2001/files - -# Custom comment -qkflow pr approve 123 --comment "LGTM! 🎉" -qkflow pr approve 123 -c "Looks good!" - -# Approve and auto-merge (with default 👍 comment) -qkflow pr approve 123 --merge -qkflow pr approve 123 -m - -# Approve by URL with custom comment and merge -qkflow pr approve https://github.com/owner/repo/pull/456 -c "Ship it! 🚀" -m - -# Interactive mode (auto-detect PR from current branch) -qkflow pr approve -``` - -**What it does:** -1. ✅ Supports PR number OR full GitHub URL (including /files, /commits, /checks paths) -2. ✅ Auto-detects PR from current branch (if no argument provided) -3. ✅ Fetches PR details -4. ✅ Approves the PR on GitHub -5. ✅ Adds a comment (default: 👍, customize with -c flag) -6. ✅ Optionally auto-merges after approval (with --merge flag) -7. ✅ Checks if PR is mergeable before merging -8. ✅ Cleans up branches after merge (if merged) - -**Examples:** -```bash -# Simple approve (uses default 👍 comment) -qkflow pr approve 123 - -# Approve from Files tab URL -qkflow pr approve https://github.com/brain/planning-api/pull/2001/files - -# Custom comment -qkflow pr approve 123 -c "LGTM!" - -# Approve PR from another repo by URL -qkflow pr approve https://github.com/brain/planning-api/pull/2001 - -# Approve and merge in one command (with 👍) -qkflow pr approve 123 --merge - -# Approve current branch's PR -qkflow pr approve # Will find PR automatically -``` - -### Quick Update (qkupdate) +# 合并 PR +qkflow pr merge 123 -```bash -# Quick commit and push with PR title as commit message +# 快速更新(使用 PR 标题作为提交信息) qkflow update ``` -**What it does:** -1. ✅ Gets the current PR title from GitHub -2. ✅ Stages all changes (git add --all) -3. ✅ Commits with PR title as commit message -4. ✅ Pushes to origin -5. ✅ Falls back to "update" if no PR found - -This is perfect for quick updates to an existing PR! - -### PR Editor (Add Rich Descriptions) - -**NEW!** Add detailed descriptions with images and videos to your PRs using a beautiful web-based editor. - -```bash -# During pr create, you'll be prompted: -? Add detailed description with images/videos? - > ⏭️ Skip (default) # Press Enter to skip - ✅ Yes, continue # Press Space to toggle, then Enter - -# If you select "Yes, continue": -🌐 Opening editor in your browser... -📝 Please edit your content in the browser and click 'Save and Continue' -``` - -**The web editor provides:** - -- 📝 **Markdown Editor** with live preview and formatting toolbar -- 🖼️ **Drag & Drop** images and videos from Finder/Explorer -- 📋 **Paste** images directly from clipboard (Cmd+V / Ctrl+V) -- 🎨 **GitHub-style UI** with dark theme -- ⚡ **Instant Upload** to both GitHub PR and Jira issue -- 🔄 **Auto-conversion** of local paths to online URLs - -**Supported formats:** -- Images: PNG, JPG, JPEG, GIF, WebP, SVG -- Videos: MP4, MOV, WebM, AVI - -**What happens after you save:** - -1. ✅ Files are uploaded to GitHub and Jira -2. ✅ Markdown paths are replaced with actual URLs -3. ✅ Description is added as a comment to the GitHub PR -4. ✅ Same description is added as a comment to the Jira issue -5. ✅ Temporary files are cleaned up - -**Perfect for:** -- Bug fixes with before/after screenshots -- Features with demo videos -- Visual documentation of UI changes -- Architecture diagrams -- Test results and screenshots - -### Jira Reader (Cursor AI Integration) - -**NEW!** Read and export Jira issues, optimized for Cursor AI. +### Jira 操作 ```bash -# Intelligent read (recommended for Cursor AI) +# 读取 Jira Issue(Cursor AI 优化) qkflow jira read NA-9245 -# Quick terminal view +# 查看 Issue qkflow jira show NA-9245 -qkflow jira show NA-9245 --full # Full details - -# Export to files -qkflow jira export NA-9245 -qkflow jira export NA-9245 --with-images # Include images - -# Clean up exports -qkflow jira clean NA-9245 -qkflow jira clean --all -``` -**For Cursor AI users:** +# 导出 Issue(包含图片) +qkflow jira export NA-9245 --with-images -Simply tell Cursor in chat: +# 配置 Jira 状态映射 +qkflow jira setup PROJECT-KEY ``` -"通过 qkflow 工具读取 NA-9245 所有内容并总结" -``` - -Cursor will automatically: -1. ✅ Run the qkflow command -2. ✅ Read exported files (including images) -3. ✅ Provide comprehensive analysis - -📖 See [Jira Reader Guide](JIRA_READER.md) for detailed documentation. - -### Watch Daemon (Auto-update Jira) -**NEW!** Automatically monitor your PRs and update Jira when they're merged. +### 其他命令 ```bash -# Install and start watch daemon (with auto-start on login) -qkflow watch install - -# Check daemon status -qkflow watch status - -# View processing history -qkflow watch history - -# View logs -qkflow watch log -qkflow watch log --follow - -# Manual check (without daemon) -qkflow watch check - -# Stop/Start daemon -qkflow watch stop -qkflow watch start - -# Uninstall daemon -qkflow watch uninstall +qkflow config # 显示配置 +qkflow version # 显示版本 +qkflow update-cli # 更新到最新版本 +qkflow --help # 获取帮助 ``` -**What it does:** -- ✅ Monitors YOUR PRs every 15 minutes (8:30-24:00) -- ✅ Night mode: checks at 2:00 and 6:00 only -- ✅ Auto-updates Jira status when PR is merged -- ✅ Desktop notifications (macOS) -- ✅ Auto-start on login (launchd on macOS) -- ✅ Logs all activities -- ✅ No manual intervention needed! +📖 完整命令说明见 [PR 使用指南](docs/guidelines/usage/PR_GUIDELINES.md) 和 [Jira 使用指南](docs/guidelines/usage/JIRA_GUIDELINES.md)。 -**Prerequisites:** -1. Run `qkflow jira setup` first to configure Jira status mappings -2. Make sure "PR Merged" status is configured (default: "In Review") +--- -📖 See [Jira Status Config Guide](JIRA_STATUS_CONFIG.md) for setup details. +## 🎓 工作流示例 -### Other Commands +### 完整的功能开发流程 ```bash -# Show current configuration -qkflow config +# 1. 创建功能分支 +git checkout -b feature/add-login -# Update qkflow to latest version -qkflow update-cli +# 2. 开发功能... +# (编写代码) -# Show version -qkflow version +# 3. 快速提交和推送 +qkflow update -# Get help -qkflow help -qkflow pr --help -``` +# 4. 创建 PR +qkflow pr create PROJ-123 -## 🏗️ Project Structure +# 5. Code Review... +# (等待审核通过) +# 6. 合并 PR(自动更新 Jira) +qkflow pr merge ``` -go-version/ -├── cmd/ -│ └── qkflow/ -│ ├── main.go # Entry point -│ └── commands/ -│ ├── root.go # Root command -│ ├── init.go # Setup wizard -│ ├── pr.go # PR commands -│ ├── pr_create.go # Create PR -│ ├── pr_merge.go # Merge PR -│ ├── update.go # Quick update -│ └── jira.go # Jira commands -├── internal/ -│ ├── github/ -│ │ └── client.go # GitHub API client -│ ├── jira/ -│ │ ├── client.go # Jira API client -│ │ └── status_cache.go # Status cache -│ ├── git/ -│ │ └── operations.go # Git operations -│ ├── ai/ -│ │ └── client.go # AI client -│ └── ui/ -│ └── prompt.go # User interface -├── pkg/ -│ └── config/ -│ └── config.go # Configuration management -├── go.mod -├── go.sum -├── Makefile -└── README.md -``` - -## 🛠️ Development - -### Prerequisites -- Go 1.21 or higher -- Make (optional but recommended) - -### Build - -```bash -# Build for current platform -make build +📖 更多工作流示例见 [快速开始指南](docs/QUICKSTART.md)。 -# Build for all platforms -make build-all +--- -# Install to GOPATH/bin -make install +## 💡 小贴士 -# Run tests -make test +1. **第一次使用**: 运行 `qkflow init` 配置 +2. **查看配置**: 运行 `qkflow config` 查看存储位置 +3. **快速更新**: 使用 `qkflow update` 代替繁琐的 git 命令 +4. **Jira 集成**: 配置后 PR 操作自动更新 Jira 状态 +5. **iCloud 同步**: macOS 用户配置自动同步到所有设备 -# Run linters -make lint +--- -# Format code -make fmt -``` +## 🔧 故障排除 -### Quick Development +### 常见问题 +**命令未找到** ```bash -# Run without building -go run ./cmd/qkflow pr create - -# Or use Makefile -make run-pr-create -make run-pr-merge +which qkflow # 检查是否在 PATH 中 +export PATH="/usr/local/bin:$PATH" # 如需要,添加到 ~/.zshrc ``` -## 🧪 Testing - +**GitHub 认证失败** ```bash -# Run all tests -make test - -# Run tests with coverage -make coverage - -# Run specific package tests -go test ./internal/github/... -go test ./internal/jira/... +gh auth status # 检查认证状态 +gh auth login # 如未认证,先登录 +qkflow init # 重新运行初始化 ``` -## 📝 Configuration Files - -### Storage Location - -qkflow intelligently stores configurations based on your system: - -**macOS with iCloud Drive** (Recommended): -- Configs are stored in iCloud Drive and automatically synced across your Mac devices -- All configs in: `~/Library/Mobile Documents/com~apple~CloudDocs/.qkflow/` - - `config.yaml` - Main configuration - - `jira-status.json` - Jira status mappings - -**Local Storage** (Fallback): -- All configs in: `~/.qkflow/` - - `config.yaml` - Main configuration - - `jira-status.json` - Jira status mappings - -Run `qkflow config` to see your actual storage location. - -### Configuration Format +**Jira 连接失败** +```bash +# 验证 Jira 凭证 +curl -u "your.email@example.com:your_jira_token" \ + https://your-domain.atlassian.net/rest/api/2/myself -```yaml -email: your.email@example.com -jira_api_token: your_jira_token -jira_service_address: https://your-domain.atlassian.net -github_token: ghp_your_github_token -branch_prefix: feature # optional -openai_key: sk-your_openai_key # optional +# 如果失败,获取新的 API token 并重新运行 qkflow init ``` -## 🔒 Security - -- Tokens are stored securely in your config directory (local or iCloud) -- File permissions are set to `0600` (user read/write only) -- iCloud storage is encrypted and secure -- Never commit the config file or share it -- Use environment variables for CI/CD: - ```bash - export QK_GITHUB_TOKEN=xxx - export QK_JIRA_API_TOKEN=xxx - ``` - -**Note**: If using iCloud Drive, your configurations will sync across your Mac devices automatically, providing a seamless experience. - -## 🚧 Migration from Shell Version +📖 **详细故障排除**:更多常见问题和解决方案见 [快速开始指南 - 常见问题](docs/QUICKSTART.md#-常见问题)。 -See [MIGRATION.md](MIGRATION.md) for detailed migration guide. - -**Quick comparison:** - -| Feature | Shell Version | Go Version | -|---------|--------------|------------| -| Installation | Clone + dependencies | Single binary | -| Configuration | `.zshrc` / `.bashrc` | `qk init` | -| Dependencies | `gh`, `jira`, `jq`, etc. | None (self-contained) | -| Speed | ~1-2s startup | <100ms startup | -| Platform | macOS/Linux | macOS/Linux/Windows | -| Updates | `git pull` | Download new binary | - -## 🔧 Troubleshooting +--- -### macOS Installation Issues +## 🚧 从 Shell 版本迁移 -#### "qkflow-darwin-arm64 cannot be opened" Error +详细迁移指南请参阅 [MIGRATION.md](docs/MIGRATION.md)。 -If you encounter this error: -``` -"qkflow-darwin-arm64" cannot be opened because Apple cannot verify that it is free of malware that may harm your Mac or compromise your privacy. -``` +**快速对比:** -**Solution 1: Remove Quarantine Attribute (Recommended)** -```bash -# Remove quarantine attribute (if it exists) -xattr -d com.apple.quarantine qkflow-darwin-arm64 2>/dev/null || echo "No quarantine attribute (this is fine)" -chmod +x qkflow-darwin-arm64 -``` +| 功能 | Shell 版本 | Go 版本 | +|------|-----------|---------| +| 安装 | 克隆 + 依赖 | 单一二进制文件 | +| 配置 | `.zshrc` 环境变量 | `qkflow init` | +| 启动时间 | ~1-2 秒 | <100ms | +| 平台 | macOS/Linux | macOS/Linux/Windows | -> **Note**: If you see "No such xattr: com.apple.quarantine", that means the file wasn't quarantined and you can skip the xattr step. +--- -**Solution 2: System Settings** -1. Try to run the binary (it will show the security warning) -2. Go to **System Settings** → **Privacy & Security** -3. Scroll down to find the blocked app -4. Click **Open Anyway** +## 🛠️ 开发 -**Solution 3: Temporary Gatekeeper Disable** -```bash -# Disable Gatekeeper temporarily (requires admin) -sudo spctl --master-disable +### 前置要求 -# Run your binary, then re-enable -sudo spctl --master-enable -``` +- Go 1.21 或更高版本 +- Make(可选但推荐) -#### "Permission Denied" Error +### 构建 ```bash -# Make sure the file is executable -chmod +x qkflow - -# Check if /usr/local/bin is in your PATH -echo $PATH | grep -q "/usr/local/bin" && echo "✅ PATH is correct" || echo "❌ Add /usr/local/bin to PATH" - -# Add to PATH if needed (add to ~/.zshrc or ~/.bash_profile) -export PATH="/usr/local/bin:$PATH" +make build # 为当前平台构建 +make build-all # 为所有平台构建 +make test # 运行测试 +make lint # 运行代码检查 ``` -### General Issues - -#### Command Not Found - -```bash -# Check if qkflow is installed -which qkflow +📖 详细开发指南见 [开发规范](docs/guidelines/development/DEVELOPMENT_GUIDELINES.md)。 -# If not found, ensure it's in your PATH -ls -la /usr/local/bin/qkflow +--- -# Reload shell configuration -source ~/.zshrc # or ~/.bash_profile -``` +## 🤝 贡献 -#### Update Issues +欢迎贡献!请随时提交 Pull Request。 -```bash -# Check current version -qkflow version - -# Manual update (download latest binary and replace) -curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-arm64 -o /tmp/qkflow-new -xattr -d com.apple.quarantine /tmp/qkflow-new -chmod +x /tmp/qkflow-new -sudo mv /tmp/qkflow-new /usr/local/bin/qkflow -``` +📖 详细贡献指南请参阅 [CONTRIBUTING.md](docs/guidelines/development/CONTRIBUTING.md)。 -## 🤝 Contributing +--- -Contributions are welcome! Please feel free to submit a Pull Request. +## 📚 文档 -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request +- 📖 [文档索引](docs/README.md) - 所有文档的索引 +- 🚀 [快速开始](docs/QUICKSTART.md) - 5 分钟快速上手 +- 📝 [PR 使用指南](docs/guidelines/usage/PR_GUIDELINES.md) - PR 功能完整说明 +- 🎫 [Jira 使用指南](docs/guidelines/usage/JIRA_GUIDELINES.md) - Jira 功能完整说明 +- 🔄 [迁移指南](docs/MIGRATION.md) - 从 Shell 版本迁移 +- 🏗️ [架构文档](docs/architecture/ARCHITECTURE.md) - 项目架构说明 -## 📄 License +--- -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +## 📄 许可证 -## 🙏 Acknowledgments +本项目采用 MIT 许可证 - 详情请参阅 [LICENSE](LICENSE) 文件。 -- Original Shell version: [quick-workflow](https://github.com/Wangggym/quick-workflow) -- [Cobra](https://github.com/spf13/cobra) - CLI framework -- [Survey](https://github.com/AlecAivazis/survey) - Interactive prompts -- [go-github](https://github.com/google/go-github) - GitHub API client -- [go-jira](https://github.com/andygrunwald/go-jira) - Jira API client +--- -## 📞 Support +## 📞 支持 -- 🐛 [Report a bug](https://github.com/Wangggym/quick-workflow/issues/new?labels=bug) -- 💡 [Request a feature](https://github.com/Wangggym/quick-workflow/issues/new?labels=enhancement) -- 📖 [Documentation](https://github.com/Wangggym/quick-workflow/wiki) +- 🐛 [报告 Bug](https://github.com/Wangggym/quick-workflow/issues/new?labels=bug) +- 💡 [请求功能](https://github.com/Wangggym/quick-workflow/issues/new?labels=enhancement) +- 📖 [文档](docs/README.md) --- -Made with ❤️ by [Wangggym](https://github.com/Wangggym) - +由 [Wangggym](https://github.com/Wangggym) 用 ❤️ 制作 diff --git a/go-version/RELEASE_QUICKSTART.md b/go-version/RELEASE_QUICKSTART.md deleted file mode 100644 index 0fab8a4..0000000 --- a/go-version/RELEASE_QUICKSTART.md +++ /dev/null @@ -1,126 +0,0 @@ -# Release 快速入门 - -## 🚀 快速发布 - -### 第一次 Release - -```bash -# 1. 确保代码已推送 -git push origin main - -# 2. 运行 release 检查(会运行测试和构建) -make release VERSION=v1.0.0 - -# 3. 如果检查通过,创建并推送 tag -git tag -a v1.0.0 -m "Release v1.0.0" -git push origin v1.0.0 - -# 4. 等待 2-3 分钟,GitHub Actions 会自动构建和发布 -``` - -### 日常 Release - -```bash -# 补丁版本 (bug 修复) -make release VERSION=v1.0.1 -git tag -a v1.0.1 -m "Release v1.0.1" -git push origin v1.0.1 - -# 次版本 (新功能) -make release VERSION=v1.1.0 -git tag -a v1.1.0 -m "Release v1.1.0" -git push origin v1.1.0 - -# 主版本 (不兼容更改) -make release VERSION=v2.0.0 -git tag -a v2.0.0 -m "Release v2.0.0" -git push origin v2.0.0 -``` - -## 📋 Release Checklist - -```bash -# ✅ 所有更改已提交 -git status - -# ✅ 运行 release 检查 -make release VERSION=vX.Y.Z - -# ✅ 创建 tag -git tag -a vX.Y.Z -m "Release vX.Y.Z" - -# ✅ 推送 tag -git push origin vX.Y.Z - -# ✅ 查看 GitHub Actions -# https://github.com/Wangggym/quick-workflow/actions - -# ✅ 验证 Release -# https://github.com/Wangggym/quick-workflow/releases -``` - -## 🎯 版本号规则 - -- `v1.0.0` → `v1.0.1` - Bug 修复 -- `v1.0.0` → `v1.1.0` - 新功能 -- `v1.0.0` → `v2.0.0` - 破坏性更改 - -## 🛠️ 常用命令 - -```bash -# 初始化依赖 -make gen - -# 运行测试 -make test - -# 构建本地版本 -make build - -# 安装到系统 -make install - -# 清理构建产物 -make clean - -# 查看帮助 -make help -``` - -## 📦 发布后的产物 - -每次 Release 会生成以下文件: -- `qkflow-darwin-amd64` - macOS Intel -- `qkflow-darwin-arm64` - macOS Apple Silicon -- `qkflow-linux-amd64` - Linux -- `qkflow-windows-amd64.exe` - Windows - -## 🔍 查看构建状态 - -- Actions: https://github.com/Wangggym/quick-workflow/actions -- Releases: https://github.com/Wangggym/quick-workflow/releases - -## 💡 最佳实践 - -1. **每次 Release 前运行 `make release VERSION=vX.Y.Z`** -2. **使用语义化版本号** -3. **在 tag message 中添加简短的更新说明** -4. **发布后测试下载的二进制文件** - -## 🚨 回滚操作 - -```bash -# 删除远程 tag -git push --delete origin v1.0.0 - -# 删除本地 tag -git tag -d v1.0.0 - -# 在 GitHub 上删除 Release -# 访问 Releases 页面手动删除 -``` - ---- - -详细文档请查看 [RELEASE.md](RELEASE.md) - diff --git a/go-version/STRUCTURE.md b/go-version/STRUCTURE.md deleted file mode 100644 index 90d5e19..0000000 --- a/go-version/STRUCTURE.md +++ /dev/null @@ -1,266 +0,0 @@ -# Project Structure - -``` -go-version/ -│ -├── 📝 Configuration Files -│ ├── go.mod # Go module definition -│ ├── go.sum # Dependency checksums -│ ├── Makefile # Build automation -│ ├── .gitignore # Git ignore rules -│ └── .golangci.yml # Linter configuration -│ -├── 🎯 Main Application -│ └── cmd/ -│ └── qk/ -│ ├── main.go # Application entry point -│ └── commands/ -│ ├── root.go # Root command & app setup -│ ├── init.go # Setup wizard (qk init) -│ ├── pr.go # PR command group -│ ├── pr_create.go # PR creation logic -│ └── pr_merge.go # PR merging logic -│ -├── 🔒 Internal Packages -│ └── internal/ -│ ├── github/ -│ │ └── client.go # GitHub API client -│ │ ├── NewClient() -│ │ ├── CreatePullRequest() -│ │ ├── GetPullRequest() -│ │ ├── MergePullRequest() -│ │ └── ParseRepositoryFromURL() -│ │ -│ ├── jira/ -│ │ └── client.go # Jira API client -│ │ ├── NewClient() -│ │ ├── GetIssue() -│ │ ├── UpdateStatus() -│ │ ├── AddComment() -│ │ ├── AddPRLink() -│ │ └── GetProjectStatuses() -│ │ -│ ├── git/ -│ │ └── operations.go # Git command wrappers -│ │ ├── CheckStatus() -│ │ ├── GetCurrentBranch() -│ │ ├── CreateBranch() -│ │ ├── Commit() -│ │ ├── Push() -│ │ ├── DeleteBranch() -│ │ ├── DeleteRemoteBranch() -│ │ ├── GetRemoteURL() -│ │ └── SanitizeBranchName() -│ │ -│ └── ui/ -│ └── prompt.go # User interface helpers -│ ├── Success() -│ ├── Error() -│ ├── Warning() -│ ├── Info() -│ ├── PromptInput() -│ ├── PromptPassword() -│ ├── PromptConfirm() -│ ├── PromptSelect() -│ └── PromptMultiSelect() -│ -├── 📦 Public Packages -│ └── pkg/ -│ └── config/ -│ └── config.go # Configuration management -│ ├── Load() -│ ├── Get() -│ ├── Save() -│ ├── Validate() -│ └── IsConfigured() -│ -├── 🛠️ Scripts -│ └── scripts/ -│ ├── install.sh # Installation script -│ ├── test.sh # Test runner script -│ └── release.sh # Release automation -│ -├── 🤖 CI/CD -│ └── .github/ -│ └── workflows/ -│ └── build.yml # GitHub Actions workflow -│ -├── 📚 Documentation -│ ├── README.md # Main documentation -│ ├── MIGRATION.md # Migration guide (Shell → Go) -│ ├── QUICKSTART.md # 5-minute quick start -│ ├── CONTRIBUTING.md # Contribution guidelines -│ ├── PROJECT_OVERVIEW.md # Technical overview -│ └── STRUCTURE.md # This file -│ -├── 📄 Legal -│ └── LICENSE # MIT License -│ -└── 🔨 Build Output (gitignored) - └── bin/ # Compiled binaries - ├── qk # Current platform - ├── qk-darwin-amd64 # macOS Intel - ├── qk-darwin-arm64 # macOS Apple Silicon - ├── qk-linux-amd64 # Linux - └── qk-windows-amd64.exe # Windows -``` - -## 📊 File Statistics - -| Category | Files | Lines of Code (est.) | -|----------|-------|---------------------| -| Go Source | 10 | ~2,000 | -| Documentation | 6 | ~2,500 | -| Scripts | 3 | ~300 | -| Config | 5 | ~200 | -| **Total** | **24** | **~5,000** | - -## 🔗 Package Dependencies - -``` -cmd/qkflow/commands - ├─→ internal/github - ├─→ internal/jira - ├─→ internal/git - ├─→ internal/ui - └─→ pkg/config - -internal/github - └─→ pkg/config - -internal/jira - └─→ pkg/config - -internal/git - └─→ (no internal deps) - -internal/ui - └─→ (no internal deps) - -pkg/config - └─→ (no internal deps) -``` - -## 📖 Key Files Explained - -### Entry Point -- **`cmd/qkflow/main.go`**: Application entry point, calls command execution - -### Commands -- **`commands/root.go`**: Root command setup, version, config display -- **`commands/init.go`**: Interactive setup wizard for first-time config -- **`commands/pr.go`**: PR command group (parent of create/merge) -- **`commands/pr_create.go`**: Complete PR creation workflow -- **`commands/pr_merge.go`**: Complete PR merging workflow - -### Core Libraries -- **`internal/github/client.go`**: GitHub API wrapper with typed interfaces -- **`internal/jira/client.go`**: Jira API wrapper with status management -- **`internal/git/operations.go`**: Git command execution and branch management -- **`internal/ui/prompt.go`**: User interaction and colored output - -### Infrastructure -- **`pkg/config/config.go`**: Configuration loading, saving, validation -- **`Makefile`**: Build commands (build, test, lint, install) -- **`.github/workflows/build.yml`**: CI/CD pipeline for multi-platform builds - -### Documentation -- **`README.md`**: User-facing documentation (installation, usage) -- **`MIGRATION.md`**: Detailed migration guide from Shell version -- **`QUICKSTART.md`**: 5-minute getting started guide -- **`CONTRIBUTING.md`**: Guidelines for contributors -- **`PROJECT_OVERVIEW.md`**: Technical architecture and design - -## 🎯 Navigation Guide - -### For Users -1. Start → `README.md` (overview) -2. Setup → `QUICKSTART.md` (5 min) -3. Migration → `MIGRATION.md` (if from Shell) - -### For Developers -1. Architecture → `PROJECT_OVERVIEW.md` -2. Structure → This file (`STRUCTURE.md`) -3. Contributing → `CONTRIBUTING.md` -4. Code → Start from `cmd/qkflow/main.go` - -### For Building -1. Dependencies → `go.mod` -2. Build → `Makefile` -3. CI/CD → `.github/workflows/build.yml` -4. Release → `scripts/release.sh` - -## 🔍 Code Organization Principles - -### 1. **Separation of Concerns** -- `cmd/` - CLI interface and user interaction -- `internal/` - Business logic and API clients -- `pkg/` - Reusable utilities - -### 2. **Dependency Direction** -- Commands depend on internal packages -- Internal packages depend on pkg -- No circular dependencies - -### 3. **Visibility** -- `internal/` - Private to this module -- `pkg/` - Can be imported by other modules -- `cmd/` - Application entry points - -### 4. **Testing** -- Each package has its own tests -- Mock external dependencies -- Table-driven test patterns - -## 📦 External Dependencies - -``` -Core Framework: -├── github.com/spf13/cobra # CLI framework -├── github.com/spf13/viper # Configuration -└── github.com/AlecAivazis/survey/v2 # Interactive prompts - -API Clients: -├── github.com/google/go-github/v57 # GitHub API -├── github.com/andygrunwald/go-jira # Jira API -└── golang.org/x/oauth2 # OAuth2 auth - -Utilities: -└── github.com/fatih/color # Terminal colors -``` - -## 🎨 Design Patterns Used - -1. **Factory Pattern**: Client creation (`NewClient()`) -2. **Command Pattern**: CLI commands structure -3. **Repository Pattern**: API clients abstract data access -4. **Facade Pattern**: Simplified interfaces for complex operations -5. **Strategy Pattern**: Different PR types and workflows - -## 🚀 Build Artifacts - -After running `make build-all`: - -``` -bin/ -├── qk-darwin-amd64 # macOS Intel (12-15MB) -├── qk-darwin-arm64 # macOS M1/M2 (12-15MB) -├── qk-linux-amd64 # Linux x86_64 (12-15MB) -└── qk-windows-amd64.exe # Windows 64-bit (12-15MB) -``` - -## 📈 Metrics - -- **Total Lines**: ~5,000 -- **Go Files**: 10 -- **Packages**: 6 -- **Commands**: 4 -- **Functions**: ~80 -- **Structs**: ~15 -- **Interfaces**: ~5 - ---- - -Last Updated: 2025-11-04 -Version: 1.0.0 - diff --git a/go-version/cmd/qkflow/commands/init.go b/go-version/cmd/qkflow/commands/init.go index 4540de6..7e3be2d 100644 --- a/go-version/cmd/qkflow/commands/init.go +++ b/go-version/cmd/qkflow/commands/init.go @@ -5,9 +5,9 @@ import ( "os/exec" "strings" - "github.com/Wangggym/quick-workflow/internal/ui" + "github.com/AlecAivazis/survey/v2" + "github.com/Wangggym/quick-workflow/internal/config" "github.com/Wangggym/quick-workflow/internal/utils" - "github.com/Wangggym/quick-workflow/pkg/config" "github.com/spf13/cobra" ) @@ -20,124 +20,576 @@ This will create a configuration file at ~/.config/quick-workflow/config.yaml`, } func runInit(cmd *cobra.Command, args []string) { - ui.Info("Welcome to Quick Workflow Setup!") - fmt.Println() + log.Info("Welcome to Quick Workflow Setup!") + showEmptyLine() + // Reset config cache to ensure we read latest environment variables + config.Reset() + // Try to load existing configuration (from YAML or environment variables) + existingCfg, _ := config.Load() cfg := &config.Config{} - // Email - email, err := ui.PromptInput("Enter your email address:", true) - if err != nil { - ui.Error(fmt.Sprintf("Failed to get email: %v", err)) - return - } - cfg.Email = email - - // GitHub Token - ui.Info("Getting GitHub token from gh CLI...") - ghToken, err := getGitHubToken() - if err != nil { - ui.Warning("Failed to get GitHub token from gh CLI") - ghToken, err = ui.PromptPassword("Enter your GitHub personal access token:") - if err != nil { - ui.Error(fmt.Sprintf("Failed to get GitHub token: %v", err)) - return + // If we have existing config, use it as defaults for editing + if existingCfg != nil { + // Check if configuration is complete and valid + if existingCfg.Validate() == nil { + // Configuration is complete, use as defaults for editing + log.Info("📝 Found existing configuration, editing (press Enter to keep current values)") + showEmptyLine() + } else { + // Configuration exists but incomplete, use as defaults + log.Info("📋 Found existing configuration (incomplete), using as defaults (press Enter to keep current value)") + showEmptyLine() } - } else { - ui.Success("GitHub token obtained from gh CLI") + *cfg = *existingCfg } - cfg.GitHubToken = ghToken - // GitHub Owner - githubOwner, err := ui.PromptInput("Enter your GitHub username or organization:", true) + // 【第一步】Git 账号配置 + showSectionHeader(1, "Git 账号配置") + gitAnswers, err := collectGitConfig(cfg) if err != nil { - ui.Error(fmt.Sprintf("Failed to get GitHub owner: %v", err)) + showError("Failed to collect Git configuration: %v", err) return } - cfg.GitHubOwner = githubOwner + cfg.GitHubOwner = gitAnswers.Owner + cfg.GitHubToken = gitAnswers.Token + cfg.BranchPrefix = gitAnswers.BranchPrefix - // GitHub Repo - githubRepo, err := ui.PromptInput("Enter your GitHub repository name:", true) + // 【第二步】Jira 账号配置 + showSectionHeader(2, "Jira 账号配置") + jiraAnswers, err := collectJiraConfig(cfg) if err != nil { - ui.Error(fmt.Sprintf("Failed to get GitHub repo: %v", err)) + showError("Failed to collect Jira configuration: %v", err) return } - cfg.GitHubRepo = githubRepo + cfg.Email = jiraAnswers.Email + cfg.JiraServiceAddress = jiraAnswers.ServiceAddress + cfg.JiraAPIToken = jiraAnswers.APIToken - // Jira Service Address - jiraAddr, err := ui.PromptInput("Enter your Jira service address (e.g., https://your-domain.atlassian.net):", true) + // 【第三步】LLM 配置 + showSectionHeader(3, "LLM 配置") + llmAnswers, err := collectLLMConfig(cfg) if err != nil { - ui.Error(fmt.Sprintf("Failed to get Jira address: %v", err)) + showError("Failed to collect LLM configuration: %v", err) return } - cfg.JiraServiceAddress = strings.TrimRight(jiraAddr, "/") + if llmAnswers.UseAI { + cfg.OpenAIKey = llmAnswers.AIKey + } - // Jira API Token - ui.Info("Get your Jira API token from: https://id.atlassian.com/manage-profile/security/api-tokens") - jiraToken, err := ui.PromptPassword("Enter your Jira API token:") + // 【第四步】Log 配置 + showSectionHeader(4, "Log 配置") + logAnswers, err := collectLogConfig(cfg) if err != nil { - ui.Error(fmt.Sprintf("Failed to get Jira token: %v", err)) + showError("Failed to collect Log configuration: %v", err) return } - cfg.JiraAPIToken = jiraToken + cfg.LogFilePath = logAnswers.FilePath + cfg.LogLevel = logAnswers.Level - // Branch Prefix (optional) - branchPrefix, err := ui.PromptInput("Enter branch prefix (optional, e.g., 'feature' or 'username'):", false) + // 【第五步】自动更新 + showSectionHeader(5, "自动更新") + updateAnswers, err := collectUpdateConfig(cfg) if err != nil { - ui.Warning("Skipping branch prefix") + showWarning("Failed to collect update configuration, using default: %v", err) + updateAnswers = &UpdateConfigAnswers{AutoUpdate: true} + } + cfg.AutoUpdate = updateAnswers.AutoUpdate + if cfg.AutoUpdate { + showSuccess("Auto-update enabled - qkflow will keep itself up to date") } else { - cfg.BranchPrefix = strings.TrimRight(branchPrefix, "/") + showInfo("ℹ️", "Auto-update disabled - run 'qkflow update-cli' to update manually") } - // OpenAI Key (optional) - useAI, err := ui.PromptConfirm("Do you want to configure AI features (OpenAI/DeepSeek)?", false) - if err == nil && useAI { - aiKey, err := ui.PromptPassword("Enter OpenAI or DeepSeek API key:") - if err == nil { - cfg.OpenAIKey = aiKey - } + // Save configuration + if err := config.Save(cfg); err != nil { + showError("Failed to save configuration: %v", err) + return + } + + showSuccess("Configuration saved successfully!") + showEmptyLine() + + // Show storage location + showStorageLocation() + + // Show final instructions + showFinalInstructions() +} + +// ==================== Answer Types ==================== + +// GitConfigAnswers holds answers for Git account configuration +type GitConfigAnswers struct { + Owner string + Token string + BranchPrefix string +} + +// JiraConfigAnswers holds answers for Jira account configuration +type JiraConfigAnswers struct { + Email string + ServiceAddress string + APIToken string +} + +// LLMConfigAnswers holds answers for LLM configuration +type LLMConfigAnswers struct { + UseAI bool + AIKey string +} + +// LogConfigAnswers holds answers for Log configuration +type LogConfigAnswers struct { + FilePath *string + Level *string +} + +// UpdateConfigAnswers holds answers for Auto Update configuration +type UpdateConfigAnswers struct { + AutoUpdate bool +} + +// ==================== Output Helper Functions ==================== + +// showSectionHeader displays a section header with step number and title +func showSectionHeader(step int, title string) { + log.Info("") + log.Info("【第%d步】%s", step, title) + log.Info("──────────────────────────────────────") + log.Info("") +} + +// showConfigList displays a list of configuration items +func showConfigList(title string, items map[string]string) { + log.Info("%s", title) + for name, value := range items { + log.Info(" %s: %s", name, value) + } + log.Info("") +} + +// showEmptyLine displays an empty line +func showEmptyLine() { + log.Info("") +} + +// showSuccess displays a success message with emoji +func showSuccess(message string, args ...interface{}) { + if len(args) > 0 { + log.Success("✅ "+message, args...) + } else { + log.Success("✅ " + message) } +} - // Auto Update (default: true) - autoUpdate, err := ui.PromptConfirm("Enable automatic updates? (recommended)", true) - if err == nil { - cfg.AutoUpdate = autoUpdate +// showError displays an error message with emoji +func showError(message string, args ...interface{}) { + if len(args) > 0 { + log.Error("❌ "+message, args...) } else { - cfg.AutoUpdate = true // Default to true + log.Error("❌ " + message) } - if cfg.AutoUpdate { - ui.Success("✅ Auto-update enabled - qkflow will keep itself up to date") +} + +// showWarning displays a warning message with emoji +func showWarning(message string, args ...interface{}) { + if len(args) > 0 { + log.Warning("⚠️ "+message, args...) } else { - ui.Info("ℹ️ Auto-update disabled - run 'qkflow update-cli' to update manually") + log.Warning("⚠️ " + message) } +} - // Save configuration - if err := config.Save(cfg); err != nil { - ui.Error(fmt.Sprintf("Failed to save configuration: %v", err)) - return +// showInfo displays an info message with emoji +func showInfo(emoji, message string, args ...interface{}) { + if len(args) > 0 { + log.Info(emoji+" "+message, args...) + } else { + log.Info(emoji + " " + message) } +} - ui.Success("Configuration saved successfully!") - fmt.Println() - - // Show storage location +// showFinalInstructions displays the final instructions after configuration +func showFinalInstructions() { + log.Info("") + log.Info("You can now use the following commands:") + log.Info(" qkflow pr create - Create a PR and update Jira") + log.Info(" qkflow pr merge - Merge a PR and update Jira") + log.Info(" qkflow update - Quick commit and push with PR title") + log.Info(" qkflow update-cli - Update qkflow to the latest version") + log.Info(" qkflow jira list - List Jira status mappings") + log.Info("") +} + +// showStorageLocation displays the storage location information +func showStorageLocation() { location := utils.GetConfigLocation() configDir, _ := utils.GetQuickWorkflowConfigDir() - ui.Info(fmt.Sprintf("Storage location: %s", location)) + log.Info("Storage location: %s", location) if configDir != "" { - fmt.Printf(" 📁 Config: %s/config.yaml\n", configDir) + log.Info(" Config: %s/config.yaml", configDir) + } + log.Info("") +} + +// ==================== Configuration Collectors ==================== + +// collectGitConfig collects Git account configuration using survey +func collectGitConfig(existingCfg *config.Config) (*GitConfigAnswers, error) { + // Try to get GitHub token from gh CLI + ghToken, err := getGitHubToken() + hasGhToken := err == nil && ghToken != "" + if hasGhToken { + showSuccess("GitHub token obtained from gh CLI") + } else { + showWarning("Failed to get GitHub token from gh CLI") + } + + answers := &GitConfigAnswers{} + + // Build token prompt message + tokenMessage := "Enter your GitHub personal access token:" + if existingCfg.GitHubToken != "" { + // If we have existing token, show keep current message + tokenMessage = "Enter your GitHub personal access token [current: ****] (press Enter to keep current):" + } else if hasGhToken { + // Only show gh CLI message if no existing token configured + tokenMessage = "Enter your GitHub personal access token [current: ****] (press Enter to use token from gh CLI, or enter a new token):" + } + + // Token validation: only required if we don't have gh CLI token or existing token + var tokenValidate survey.Validator + if !hasGhToken && existingCfg.GitHubToken == "" { + tokenValidate = survey.Required + } + + // Custom validator: allow empty if we have existing value, otherwise require + ownerValidate := func(val interface{}) error { + str, ok := val.(string) + if !ok { + return fmt.Errorf("invalid input") + } + if str == "" && existingCfg.GitHubOwner == "" { + return fmt.Errorf("github username is required") + } + return nil + } + + qs := []*survey.Question{ + { + Name: "owner", + Prompt: &survey.Input{ + Message: func() string { + if existingCfg.GitHubOwner != "" { + return fmt.Sprintf("Enter your GitHub username [current: %s] (press Enter to keep):", existingCfg.GitHubOwner) + } + return "Enter your GitHub username:" + }(), + // Don't set Default to avoid showing (default_value), handle empty input manually + }, + Validate: ownerValidate, + }, + { + Name: "token", + Prompt: &survey.Password{ + Message: tokenMessage, + }, + Validate: tokenValidate, + }, + { + Name: "branchPrefix", + Prompt: &survey.Input{ + Message: func() string { + if existingCfg.BranchPrefix != "" { + return fmt.Sprintf("Enter branch prefix [current: %s] (optional, press Enter to keep):", existingCfg.BranchPrefix) + } + return "Enter branch prefix (optional, e.g., 'feature' or 'username'):" + }(), + // Don't set Default to avoid showing (default_value), handle empty input manually + }, + }, + } + + if err := survey.Ask(qs, answers); err != nil { + return nil, err + } + + // Handle owner: if user entered nothing and we have existing value, use it + if answers.Owner == "" && existingCfg.GitHubOwner != "" { + answers.Owner = existingCfg.GitHubOwner + } + + // Handle token: if user entered nothing and we have gh CLI token or existing token, use it + if answers.Token == "" { + if hasGhToken { + answers.Token = ghToken + } else if existingCfg.GitHubToken != "" { + answers.Token = existingCfg.GitHubToken + } + } + + // Handle branch prefix: if empty, use existing value; trim trailing slash + if answers.BranchPrefix == "" && existingCfg.BranchPrefix != "" { + answers.BranchPrefix = existingCfg.BranchPrefix + } + answers.BranchPrefix = strings.TrimRight(answers.BranchPrefix, "/") + + return answers, nil +} + +// collectJiraConfig collects Jira account configuration using survey +func collectJiraConfig(existingCfg *config.Config) (*JiraConfigAnswers, error) { + answers := &JiraConfigAnswers{} + + // Custom validators: allow empty if we have existing value, otherwise require + emailValidate := func(val interface{}) error { + str, ok := val.(string) + if !ok { + return fmt.Errorf("invalid input") + } + if str == "" && existingCfg.Email == "" { + return fmt.Errorf("jira email address is required") + } + return nil + } + + serviceAddressValidate := func(val interface{}) error { + str, ok := val.(string) + if !ok { + return fmt.Errorf("invalid input") + } + if str == "" && existingCfg.JiraServiceAddress == "" { + return fmt.Errorf("jira service address is required") + } + return nil + } + + qs := []*survey.Question{ + { + Name: "email", + Prompt: &survey.Input{ + Message: func() string { + if existingCfg.Email != "" { + return fmt.Sprintf("Enter your Jira email address [current: %s] (press Enter to keep):", existingCfg.Email) + } + return "Enter your Jira email address:" + }(), + // Don't set Default to avoid showing (default_value), handle empty input manually + }, + Validate: emailValidate, + }, + { + Name: "serviceAddress", + Prompt: &survey.Input{ + Message: func() string { + if existingCfg.JiraServiceAddress != "" { + return fmt.Sprintf("Enter your Jira service address [current: %s] (press Enter to keep):", existingCfg.JiraServiceAddress) + } + return "Enter your Jira service address (e.g., https://your-domain.atlassian.net):" + }(), + // Don't set Default to avoid showing (default_value), handle empty input manually + }, + Validate: serviceAddressValidate, + }, + { + Name: "apiToken", + Prompt: &survey.Password{ + Message: func() string { + if existingCfg.JiraAPIToken != "" { + return "Enter your Jira API token [current: ***] (press Enter to keep current):" + } + return "Enter your Jira API token:" + }(), + }, + // Token validation: only required if we don't have existing token + Validate: func(val interface{}) error { + str, ok := val.(string) + if !ok { + return fmt.Errorf("invalid input") + } + if str == "" && existingCfg.JiraAPIToken == "" { + return fmt.Errorf("jira API token is required") + } + return nil + }, + }, + } + + showInfo("ℹ️", "Get your Jira API token from: https://id.atlassian.com/manage-profile/security/api-tokens") + + if err := survey.Ask(qs, answers); err != nil { + return nil, err + } + + // Handle email: if user entered nothing and we have existing value, use it + if answers.Email == "" && existingCfg.Email != "" { + answers.Email = existingCfg.Email + } + + // Handle service address: if user entered nothing and we have existing value, use it + if answers.ServiceAddress == "" && existingCfg.JiraServiceAddress != "" { + answers.ServiceAddress = existingCfg.JiraServiceAddress + } + + // Handle API token: if user entered nothing and we have existing token, use it + if answers.APIToken == "" && existingCfg.JiraAPIToken != "" { + answers.APIToken = existingCfg.JiraAPIToken + } + + // Handle service address: trim trailing slash + answers.ServiceAddress = strings.TrimRight(answers.ServiceAddress, "/") + + return answers, nil +} + +// collectLLMConfig collects LLM configuration using survey +func collectLLMConfig(existingCfg *config.Config) (*LLMConfigAnswers, error) { + hasAIKey := existingCfg.OpenAIKey != "" || existingCfg.DeepSeekKey != "" + + answers := &LLMConfigAnswers{ + UseAI: hasAIKey, + } + + // First, ask if user wants to configure AI + useAIPrompt := &survey.Confirm{ + Message: "Do you want to configure AI features (OpenAI/DeepSeek)?", + Default: hasAIKey, + } + if err := survey.AskOne(useAIPrompt, &answers.UseAI); err != nil { + return nil, err + } + + // If user wants to configure AI, ask for the key + if answers.UseAI { + aiKeyMessage := "Enter OpenAI or DeepSeek API key:" + if existingCfg.OpenAIKey != "" || existingCfg.DeepSeekKey != "" { + aiKeyMessage = "Enter OpenAI or DeepSeek API key [current: ***] (press Enter to keep current):" + } + + aiKeyPrompt := &survey.Password{ + Message: aiKeyMessage, + } + + var aiKey string + if err := survey.AskOne(aiKeyPrompt, &aiKey); err != nil { + return nil, err + } + + // Handle AI key: if user entered nothing and we have existing key, use it + if aiKey == "" { + // Prefer OpenAIKey, fallback to DeepSeekKey + if existingCfg.OpenAIKey != "" { + answers.AIKey = existingCfg.OpenAIKey + } else if existingCfg.DeepSeekKey != "" { + answers.AIKey = existingCfg.DeepSeekKey + } + } else { + answers.AIKey = aiKey + } + } else { + // User doesn't want to configure AI, clear the key + answers.AIKey = "" + } + + return answers, nil +} + +// collectLogConfig collects Log configuration using survey +func collectLogConfig(existingCfg *config.Config) (*LogConfigAnswers, error) { + answers := &LogConfigAnswers{ + FilePath: existingCfg.LogFilePath, + Level: existingCfg.LogLevel, + } + + var filePathDefault string + var levelDefault string + if answers.FilePath != nil { + filePathDefault = *answers.FilePath + } + if answers.Level != nil { + levelDefault = *answers.Level + } + + qs := []*survey.Question{ + { + Name: "filePath", + Prompt: &survey.Input{ + Message: "Enter log file path (optional, press Enter to skip):", + Default: filePathDefault, + }, + }, + { + Name: "level", + Prompt: &survey.Select{ + Message: "Select log level (optional):", + Options: []string{"skip", "debug", "info", "warn", "error"}, + Default: func() string { + if levelDefault == "" { + return "skip" + } + return levelDefault + }(), + }, + }, + } + + var result struct { + FilePath string + Level string + } + + if err := survey.Ask(qs, &result); err != nil { + return nil, err + } + + // Convert to pointers: empty string becomes nil, non-empty becomes *string + if result.FilePath != "" { + answers.FilePath = &result.FilePath + } else { + answers.FilePath = nil + } + + // "skip" means nil, otherwise use the selected value + if result.Level != "" && result.Level != "skip" { + answers.Level = &result.Level + } else { + answers.Level = nil } - fmt.Println() - - ui.Info("You can now use the following commands:") - fmt.Println(" qkflow pr create - Create a PR and update Jira") - fmt.Println(" qkflow pr merge - Merge a PR and update Jira") - fmt.Println(" qkflow update - Quick commit and push with PR title") - fmt.Println(" qkflow update-cli - Update qkflow to the latest version") - fmt.Println(" qkflow jira list - List Jira status mappings") - fmt.Println() + + return answers, nil } +// collectUpdateConfig collects Auto Update configuration using survey +func collectUpdateConfig(existingCfg *config.Config) (*UpdateConfigAnswers, error) { + defaultValue := true + if existingCfg != nil { + defaultValue = existingCfg.AutoUpdate + } + + answers := &UpdateConfigAnswers{ + AutoUpdate: defaultValue, + } + + qs := []*survey.Question{ + { + Name: "autoUpdate", + Prompt: &survey.Confirm{ + Message: "Enable automatic updates? (recommended)", + Default: defaultValue, + }, + }, + } + + if err := survey.Ask(qs, answers); err != nil { + // On error, default to true + answers.AutoUpdate = true + } + + return answers, nil +} + +// getGitHubToken gets GitHub token from gh CLI func getGitHubToken() (string, error) { cmd := exec.Command("gh", "auth", "token") output, err := cmd.Output() @@ -146,4 +598,3 @@ func getGitHubToken() (string, error) { } return strings.TrimSpace(string(output)), nil } - diff --git a/go-version/cmd/qkflow/commands/jira.go b/go-version/cmd/qkflow/commands/jira.go index a5a921d..80d5082 100644 --- a/go-version/cmd/qkflow/commands/jira.go +++ b/go-version/cmd/qkflow/commands/jira.go @@ -29,28 +29,28 @@ var jiraListCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { statusCache, err := jira.NewStatusCache() if err != nil { - ui.Error(fmt.Sprintf("Failed to create status cache: %v", err)) + log.Error("Failed to create status cache: %v", err) return } mappings, err := statusCache.ListAllMappings() if err != nil { - ui.Error(fmt.Sprintf("Failed to list mappings: %v", err)) + log.Error("Failed to list mappings: %v", err) return } if len(mappings) == 0 { - ui.Info("No Jira status mappings configured yet") + log.Info("No Jira status mappings configured yet") return } - fmt.Println("\n📋 Jira Status Mappings:") - fmt.Println() + log.Info("\n📋 Jira Status Mappings:") + log.Info("") for _, mapping := range mappings { - fmt.Printf("Project: %s\n", mapping.ProjectKey) - fmt.Printf(" PR Created → %s\n", mapping.PRCreatedStatus) - fmt.Printf(" PR Merged → %s\n", mapping.PRMergedStatus) - fmt.Println() + log.Info("Project: %s", mapping.ProjectKey) + log.Info(" PR Created → %s", mapping.PRCreatedStatus) + log.Info(" PR Merged → %s", mapping.PRMergedStatus) + log.Info("") } }, } @@ -64,33 +64,33 @@ var jiraSetupCmd = &cobra.Command{ jiraClient, err := jira.NewClient() if err != nil { - ui.Error(fmt.Sprintf("Failed to create Jira client: %v", err)) + log.Error("Failed to create Jira client: %v", err) return } mapping, err := setupProjectStatusMapping(jiraClient, projectKey) if err != nil { - ui.Error(fmt.Sprintf("Failed to setup mapping: %v", err)) + log.Error("Failed to setup mapping: %v", err) return } if mapping == nil { - ui.Info("Setup cancelled") + log.Info("Setup cancelled") return } statusCache, err := jira.NewStatusCache() if err != nil { - ui.Error(fmt.Sprintf("Failed to create status cache: %v", err)) + log.Error("Failed to create status cache: %v", err) return } if err := statusCache.SaveProjectStatus(mapping); err != nil { - ui.Error(fmt.Sprintf("Failed to save mapping: %v", err)) + log.Error("Failed to save mapping: %v", err) return } - ui.Success(fmt.Sprintf("Status mapping saved for project %s!", projectKey)) + log.Success("Status mapping saved for project %s!", projectKey) }, } @@ -103,22 +103,22 @@ var jiraDeleteCmd = &cobra.Command{ confirm, err := ui.PromptConfirm(fmt.Sprintf("Delete status mapping for %s?", projectKey), false) if err != nil || !confirm { - ui.Info("Deletion cancelled") + log.Info("Deletion cancelled") return } statusCache, err := jira.NewStatusCache() if err != nil { - ui.Error(fmt.Sprintf("Failed to create status cache: %v", err)) + log.Error("Failed to create status cache: %v", err) return } if err := statusCache.DeleteProjectStatus(projectKey); err != nil { - ui.Error(fmt.Sprintf("Failed to delete mapping: %v", err)) + log.Error("Failed to delete mapping: %v", err) return } - ui.Success(fmt.Sprintf("Status mapping deleted for project %s", projectKey)) + log.Success("Status mapping deleted for project %s", projectKey) }, } @@ -128,10 +128,9 @@ func init() { jiraCmd.AddCommand(jiraExportCmd) jiraCmd.AddCommand(jiraReadCmd) jiraCmd.AddCommand(jiraCleanCmd) - + // Status mapping commands (existing) jiraCmd.AddCommand(jiraListCmd) jiraCmd.AddCommand(jiraSetupCmd) jiraCmd.AddCommand(jiraDeleteCmd) } - diff --git a/go-version/cmd/qkflow/commands/jira_clean.go b/go-version/cmd/qkflow/commands/jira_clean.go index f80fa57..74f53a4 100644 --- a/go-version/cmd/qkflow/commands/jira_clean.go +++ b/go-version/cmd/qkflow/commands/jira_clean.go @@ -53,7 +53,7 @@ Examples: if !cleanAll { issueKey := args[0] if !jira.ValidateIssueKey(issueKey) { - ui.Error(fmt.Sprintf("Invalid issue key: %s", issueKey)) + log.Error("Invalid issue key: %s", issueKey) return } opts.IssueKey = issueKey @@ -64,27 +64,27 @@ Examples: // List all exports exports, err := cleaner.ListExports() if err != nil { - ui.Error(fmt.Sprintf("Failed to list exports: %v", err)) + log.Error("Failed to list exports: %v", err) return } if len(exports) == 0 { - fmt.Println("No exports found.") + log.Info("No exports found.") return } - fmt.Println("🗑️ The following will be deleted:") + log.Info("🗑️ The following will be deleted:") totalSize := int64(0) for _, exp := range exports { - fmt.Printf(" • %s (%s, %d files)\n", exp.IssueKey, formatSize(exp.Size), exp.FileCount) + log.Info(" • %s (%s, %d files)", exp.IssueKey, formatSize(exp.Size), exp.FileCount) totalSize += exp.Size } - fmt.Printf("\nTotal: %d exports, %s\n\n", len(exports), formatSize(totalSize)) + log.Info("\nTotal: %d exports, %s\n", len(exports), formatSize(totalSize)) // Ask for confirmation confirm, err := ui.PromptConfirm("Are you sure you want to delete all exports?", false) if err != nil || !confirm { - fmt.Println("Cancelled.") + log.Info("Cancelled.") return } } else if !cleanAll && !cleanForce && !cleanDryRun { @@ -95,18 +95,18 @@ Examples: DryRun: true, }) if err != nil { - ui.Error(fmt.Sprintf("Failed to check export: %v", err)) + log.Error("Failed to check export: %v", err) return } if len(testResult) > 0 && testResult[0].Error == nil { r := testResult[0] - fmt.Printf("🗑️ The following will be deleted:\n") - fmt.Printf(" %s (%s, %d files)\n\n", r.IssueKey, formatSize(r.Size), r.FileCount) + log.Info("🗑️ The following will be deleted:") + log.Info(" %s (%s, %d files)", r.IssueKey, formatSize(r.Size), r.FileCount) confirm, err := ui.PromptConfirm(fmt.Sprintf("Delete export for %s?", opts.IssueKey), false) if err != nil || !confirm { - fmt.Println("Cancelled.") + log.Info("Cancelled.") return } } @@ -115,14 +115,14 @@ Examples: // Perform cleaning results, err := cleaner.Clean(opts) if err != nil { - ui.Error(fmt.Sprintf("Failed to clean: %v", err)) + log.Error("Failed to clean: %v", err) return } // Display results - fmt.Println() + log.Info("") if cleanDryRun { - fmt.Println("🔍 Dry run - no files were deleted:") + log.Info("🔍 Dry run - no files were deleted:") } totalSize := int64(0) @@ -137,12 +137,12 @@ Examples: } } - fmt.Println() + log.Info("") if cleanDryRun { - fmt.Printf("Would free: %s\n", formatSize(totalSize)) + log.Info("Would free: %s", formatSize(totalSize)) } else { if successCount > 0 { - ui.Success(fmt.Sprintf("Cleaned %d export(s), freed %s", successCount, formatSize(totalSize))) + log.Success("Cleaned %d export(s), freed %s", successCount, formatSize(totalSize)) } } }, @@ -167,4 +167,3 @@ func formatSize(bytes int64) string { } return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) } - diff --git a/go-version/cmd/qkflow/commands/jira_export.go b/go-version/cmd/qkflow/commands/jira_export.go index faafdca..b76ba27 100644 --- a/go-version/cmd/qkflow/commands/jira_export.go +++ b/go-version/cmd/qkflow/commands/jira_export.go @@ -1,11 +1,9 @@ package commands import ( - "fmt" "path/filepath" "github.com/Wangggym/quick-workflow/internal/jira" - "github.com/Wangggym/quick-workflow/internal/ui" "github.com/spf13/cobra" ) @@ -37,14 +35,14 @@ Examples: // Validate issue key if !jira.ValidateIssueKey(issueKey) { - ui.Error(fmt.Sprintf("Invalid issue key: %s", issueKey)) + log.Error("Invalid issue key: %s", issueKey) return } // Create Jira client client, err := jira.NewClient() if err != nil { - ui.Error(fmt.Sprintf("Failed to create Jira client: %v", err)) + log.Error("Failed to create Jira client: %v", err) return } @@ -59,63 +57,63 @@ Examples: } // Show progress - fmt.Printf("🔍 Fetching %s from Jira...\n", issueKey) + log.Info("🔍 Fetching %s from Jira...", issueKey) // Export result, err := exporter.Export(opts) if err != nil { - ui.Error(fmt.Sprintf("Failed to export: %v", err)) + log.Error("Failed to export: %v", err) return } // Display results - fmt.Println() - ui.Success(fmt.Sprintf("Jira issue %s exported successfully!", issueKey)) - fmt.Println() - fmt.Printf("📁 Location: %s/\n", result.ExportPath) - fmt.Printf("📄 Main content: %s\n", result.ContentFile) + log.Info("") + log.Success("Jira issue %s exported successfully!", issueKey) + log.Info("") + log.Info("📁 Location: %s/", result.ExportPath) + log.Info("📄 Main content: %s", result.ContentFile) if exportWithImages && len(result.ImageFiles) > 0 { - fmt.Printf("🖼️ Images: %d files downloaded\n", len(result.ImageFiles)) - fmt.Println() - fmt.Println("Downloaded files:") + log.Info("🖼️ Images: %d files downloaded", len(result.ImageFiles)) + log.Info("") + log.Info("Downloaded files:") for _, imgPath := range result.ImageFiles { - fmt.Printf(" - %s\n", filepath.Base(imgPath)) + log.Info(" - %s", filepath.Base(imgPath)) } } // Display Cursor instructions - fmt.Println() - fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - fmt.Println("💡 How to use in Cursor:") - fmt.Println() - fmt.Println("1. In Cursor, attach the exported content:") - fmt.Printf(" @%s\n", result.ContentFile) - fmt.Println() + log.Info("") + log.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + log.Info("💡 How to use in Cursor:") + log.Info("") + log.Info("1. In Cursor, attach the exported content:") + log.Info(" @%s", result.ContentFile) + log.Info("") if exportWithImages && len(result.ImageFiles) > 0 { - fmt.Println("2. To include images, tell Cursor:") - fmt.Printf(" \"Please read the images in %s/attachments/\"\n", result.ExportPath) - fmt.Println() - fmt.Println("3. Or simply tell Cursor:") - fmt.Printf(" \"Read the exported Jira ticket at %s/\"\n", result.ExportPath) - fmt.Println() + log.Info("2. To include images, tell Cursor:") + log.Info(" \"Please read the images in %s/attachments/\"", result.ExportPath) + log.Info("") + log.Info("3. Or simply tell Cursor:") + log.Info(" \"Read the exported Jira ticket at %s/\"", result.ExportPath) + log.Info("") } else { - fmt.Println("2. Or simply tell Cursor:") - fmt.Printf(" \"Read %s\"\n", result.ContentFile) - fmt.Println() + log.Info("2. Or simply tell Cursor:") + log.Info(" \"Read %s\"", result.ContentFile) + log.Info("") } - fmt.Println("4. When done, clean up:") - fmt.Printf(" qkflow jira clean %s\n", issueKey) - fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - fmt.Println() + log.Info("4. When done, clean up:") + log.Info(" qkflow jira clean %s", issueKey) + log.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + log.Info("") // Show hint if images available but not downloaded if !exportWithImages { hasImages, _ := exporter.HasImages(issueKey) if hasImages { - fmt.Println("💡 Tip: This issue has attachments. Use --with-images to download them.") + log.Info("💡 Tip: This issue has attachments. Use --with-images to download them.") } } }, @@ -125,4 +123,3 @@ func init() { jiraExportCmd.Flags().BoolVarP(&exportWithImages, "with-images", "i", false, "Download all attachments and images") jiraExportCmd.Flags().StringVarP(&exportOutputDir, "output-dir", "o", "", "Output directory (default: /tmp/qkflow/jira/)") } - diff --git a/go-version/cmd/qkflow/commands/jira_read.go b/go-version/cmd/qkflow/commands/jira_read.go index a36645e..e187a2a 100644 --- a/go-version/cmd/qkflow/commands/jira_read.go +++ b/go-version/cmd/qkflow/commands/jira_read.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/Wangggym/quick-workflow/internal/jira" - "github.com/Wangggym/quick-workflow/internal/ui" "github.com/spf13/cobra" ) @@ -36,14 +35,14 @@ Usage in Cursor: // Validate issue key if !jira.ValidateIssueKey(issueKey) { - ui.Error(fmt.Sprintf("Invalid issue key: %s", issueKey)) + log.Error("Invalid issue key: %s", issueKey) return } // Create Jira client client, err := jira.NewClient() if err != nil { - ui.Error(fmt.Sprintf("Failed to create Jira client: %v", err)) + log.Error("Failed to create Jira client: %v", err) return } @@ -51,16 +50,17 @@ Usage in Cursor: exporter := jira.NewExporter(client) // Check if issue has images - fmt.Printf("🔍 Analyzing %s...\n", issueKey) + log.Info("🔍 Analyzing %s...", issueKey) hasImages, err := exporter.HasImages(issueKey) if err != nil { - ui.Error(fmt.Sprintf("Failed to analyze issue: %v", err)) + log.Error("Failed to analyze issue: %v", err) return } if hasImages { // Has images: export to files - fmt.Printf("📸 Found attachments, exporting with images...\n\n") + log.Info("📸 Found attachments, exporting with images...") + log.Info("") // Use export command logic opts := jira.ExportOptions{ @@ -71,38 +71,39 @@ Usage in Cursor: result, err := exporter.Export(opts) if err != nil { - ui.Error(fmt.Sprintf("Failed to export: %v", err)) + log.Error("Failed to export: %v", err) return } // Display results (Cursor-optimized output) - fmt.Println() - ui.Success(fmt.Sprintf("Exported to: %s/", result.ExportPath)) - fmt.Println() - fmt.Printf("Main file: %s\n", result.ContentFile) + log.Info("") + log.Success("Exported to: %s/", result.ExportPath) + log.Info("") + log.Info("Main file: %s", result.ContentFile) if len(result.ImageFiles) > 0 { - fmt.Printf("Images: %s/attachments/ (%d files)\n", result.ExportPath, len(result.ImageFiles)) + log.Info("Images: %s/attachments/ (%d files)", result.ExportPath, len(result.ImageFiles)) } - fmt.Println() + log.Info("") // Cursor-specific hints - fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - fmt.Println("💡 CURSOR: Please read the following files:") - fmt.Printf("1. %s\n", result.ContentFile) + log.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + log.Info("💡 CURSOR: Please read the following files:") + log.Info("1. %s", result.ContentFile) if len(result.ImageFiles) > 0 { - fmt.Printf("2. All images in %s/attachments/\n", result.ExportPath) + log.Info("2. All images in %s/attachments/", result.ExportPath) } - fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - fmt.Println() + log.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + log.Info("") } else { // No images: display in terminal - fmt.Printf("ℹ️ No attachments found, showing content directly...\n\n") + log.Info("ℹ️ No attachments found, showing content directly...") + log.Info("") // Get detailed issue issue, err := client.GetIssueDetailed(issueKey) if err != nil { - ui.Error(fmt.Sprintf("Failed to get issue: %v", err)) + log.Error("Failed to get issue: %v", err) return } @@ -117,4 +118,3 @@ Usage in Cursor: func init() { jiraReadCmd.Flags().BoolVarP(&readAuto, "auto", "a", true, "Automatically decide between terminal output and file export (default: true)") } - diff --git a/go-version/cmd/qkflow/commands/jira_show.go b/go-version/cmd/qkflow/commands/jira_show.go index 1dcc697..9e49247 100644 --- a/go-version/cmd/qkflow/commands/jira_show.go +++ b/go-version/cmd/qkflow/commands/jira_show.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/Wangggym/quick-workflow/internal/jira" - "github.com/Wangggym/quick-workflow/internal/ui" "github.com/spf13/cobra" ) @@ -29,14 +28,14 @@ Examples: // Validate issue key if !jira.ValidateIssueKey(issueKey) { - ui.Error(fmt.Sprintf("Invalid issue key: %s", issueKey)) + log.Error("Invalid issue key: %s", issueKey) return } // Create Jira client client, err := jira.NewClient() if err != nil { - ui.Error(fmt.Sprintf("Failed to create Jira client: %v", err)) + log.Error("Failed to create Jira client: %v", err) return } @@ -47,7 +46,7 @@ Examples: // Get detailed issue issue, err := client.GetIssueDetailed(issueKey) if err != nil { - ui.Error(fmt.Sprintf("Failed to get issue: %v", err)) + log.Error("Failed to get issue: %v", err) return } @@ -58,7 +57,7 @@ Examples: // Get basic issue issue, err := client.GetIssue(issueKey) if err != nil { - ui.Error(fmt.Sprintf("Failed to get issue: %v", err)) + log.Error("Failed to get issue: %v", err) return } diff --git a/go-version/cmd/qkflow/commands/pr_approve.go b/go-version/cmd/qkflow/commands/pr_approve.go index 230876a..47b20a6 100644 --- a/go-version/cmd/qkflow/commands/pr_approve.go +++ b/go-version/cmd/qkflow/commands/pr_approve.go @@ -52,41 +52,41 @@ func runPRApprove(cmd *cobra.Command, args []string) { // 如果提供了参数,检查是否是 URL 格式 if len(args) > 0 { arg := args[0] - + // 检查是否是 GitHub PR URL if github.IsPRURL(arg) { - ui.Info("Detected GitHub PR URL, parsing...") + log.Info("Detected GitHub PR URL, parsing...") owner, repo, prNumber, err = github.ParsePRFromURL(arg) if err != nil { - ui.Error(fmt.Sprintf("Failed to parse PR URL: %v", err)) + log.Error("Failed to parse PR URL: %v", err) return } - ui.Success(fmt.Sprintf("Parsed: %s/%s PR #%d", owner, repo, prNumber)) + log.Success("Parsed: %s/%s PR #%d", owner, repo, prNumber) } else { // 尝试作为 PR 号解析 prNumber, err = strconv.Atoi(arg) if err != nil { - ui.Error(fmt.Sprintf("Invalid PR number or URL: %s", arg)) - ui.Info("Expected: PR number (e.g., '123') or GitHub URL (e.g., 'https://github.com/owner/repo/pull/123')") + log.Error("Invalid PR number or URL: %s", arg) + log.Info("Expected: PR number (e.g., '123') or GitHub URL (e.g., 'https://github.com/owner/repo/pull/123')") return } - + // PR 号格式,需要从本地仓库获取 owner/repo if !git.IsGitRepository() { - ui.Error("Not a git repository. When using PR number, you must be in a git repository.") - ui.Info("Alternatively, use the full GitHub PR URL: https://github.com/owner/repo/pull/NUMBER") + log.Error("Not a git repository. When using PR number, you must be in a git repository.") + log.Info("Alternatively, use the full GitHub PR URL: https://github.com/owner/repo/pull/NUMBER") return } - + remoteURL, err := git.GetRemoteURL() if err != nil { - ui.Error(fmt.Sprintf("Failed to get remote URL: %v", err)) + log.Error("Failed to get remote URL: %v", err) return } - + owner, repo, err = github.ParseRepositoryFromURL(remoteURL) if err != nil { - ui.Error(fmt.Sprintf("Failed to parse repository: %v", err)) + log.Error("Failed to parse repository: %v", err) return } } @@ -94,20 +94,20 @@ func runPRApprove(cmd *cobra.Command, args []string) { // 没有提供参数,使用原有的自动检测逻辑 // 检查是否在 Git 仓库中 if !git.IsGitRepository() { - ui.Error("Not a git repository") + log.Error("Not a git repository") return } // 获取仓库信息 remoteURL, err := git.GetRemoteURL() if err != nil { - ui.Error(fmt.Sprintf("Failed to get remote URL: %v", err)) + log.Error("Failed to get remote URL: %v", err) return } owner, repo, err = github.ParseRepositoryFromURL(remoteURL) if err != nil { - ui.Error(fmt.Sprintf("Failed to parse repository: %v", err)) + log.Error("Failed to parse repository: %v", err) return } } @@ -115,7 +115,7 @@ func runPRApprove(cmd *cobra.Command, args []string) { // 创建 GitHub 客户端 ghClient, err := github.NewClient() if err != nil { - ui.Error(fmt.Sprintf("Failed to create GitHub client: %v", err)) + log.Error("Failed to create GitHub client: %v", err) return } @@ -124,7 +124,7 @@ func runPRApprove(cmd *cobra.Command, args []string) { // 尝试获取当前分支的 PR currentBranch, err := git.GetCurrentBranch() if err == nil && currentBranch != "" { - ui.Info(fmt.Sprintf("Checking for PR from current branch: %s", currentBranch)) + log.Info("Checking for PR from current branch: %s", currentBranch) // 先尝试 open 状态的 PR prs, err := ghClient.ListPullRequests(owner, repo, "open", "") @@ -132,7 +132,7 @@ func runPRApprove(cmd *cobra.Command, args []string) { for _, pr := range prs { if pr.Head == currentBranch { prNumber = pr.Number - ui.Success(fmt.Sprintf("Found PR #%d: %s", pr.Number, pr.Title)) + log.Success("Found PR #%d: %s", pr.Number, pr.Title) break } } @@ -140,13 +140,13 @@ func runPRApprove(cmd *cobra.Command, args []string) { // 如果还是没找到,提示用户 if prNumber == 0 { - ui.Warning(fmt.Sprintf("No PR found for branch: %s", currentBranch)) - fmt.Println() + log.Warning("No PR found for branch: %s", currentBranch) + log.Info("") // 询问用户是否从列表选择 manually, err := ui.PromptConfirm("Do you want to select a PR from the list?", true) if err != nil || !manually { - ui.Info("Approve cancelled") + log.Info("Approve cancelled") return } } @@ -156,12 +156,12 @@ func runPRApprove(cmd *cobra.Command, args []string) { if prNumber == 0 { prs, err := ghClient.ListPullRequests(owner, repo, "open", "") if err != nil { - ui.Error(fmt.Sprintf("Failed to list PRs: %v", err)) + log.Error("Failed to list PRs: %v", err) return } if len(prs) == 0 { - ui.Error("No open pull requests found") + log.Error("No open pull requests found") return } @@ -174,10 +174,10 @@ func runPRApprove(cmd *cobra.Command, args []string) { selected, err := ui.PromptSelect("Select a PR to approve:", prOptions) if err != nil { if err.Error() == "interrupt" { - ui.Warning("Operation cancelled by user") + log.Warning("Operation cancelled by user") os.Exit(0) } - ui.Error(fmt.Sprintf("Failed to select PR: %v", err)) + log.Error("Failed to select PR: %v", err) return } @@ -193,27 +193,27 @@ func runPRApprove(cmd *cobra.Command, args []string) { if selectedPR != nil { prNumber = selectedPR.Number } else { - ui.Error("Failed to find selected PR") + log.Error("Failed to find selected PR") return } } } // 获取 PR 信息 - ui.Info(fmt.Sprintf("Fetching PR #%d...", prNumber)) + log.Info("Fetching PR #%d...", prNumber) pr, err := ghClient.GetPullRequest(owner, repo, prNumber) if err != nil { - ui.Error(fmt.Sprintf("Failed to get PR: %v", err)) + log.Error("Failed to get PR: %v", err) return } - ui.Info(fmt.Sprintf("PR: %s", pr.Title)) - ui.Info(fmt.Sprintf("Branch: %s -> %s", pr.Head, pr.Base)) - ui.Info(fmt.Sprintf("State: %s", pr.State)) + log.Info("PR: %s", pr.Title) + log.Info("Branch: %s -> %s", pr.Head, pr.Base) + log.Info("State: %s", pr.State) // 检查 PR 状态 if pr.State != "open" { - ui.Error(fmt.Sprintf("PR is not open (state: %s)", pr.State)) + log.Error("PR is not open (state: %s)", pr.State) return } @@ -221,68 +221,68 @@ func runPRApprove(cmd *cobra.Command, args []string) { comment := approveComment if comment == "" { comment = "👍" - ui.Info("Using default comment: 👍 (use -c flag to customize)") + log.Info("Using default comment: 👍 (use -c flag to customize)") } // 批准 PR - ui.Info(fmt.Sprintf("Approving PR #%d...", prNumber)) + log.Info("Approving PR #%d...", prNumber) approvalSucceeded := true if err := ghClient.ApprovePullRequest(owner, repo, prNumber, comment); err != nil { errMsg := err.Error() if strings.Contains(errMsg, "422") { approvalSucceeded = false - ui.Warning("Cannot approve this PR (you may be the author or already approved)") - fmt.Println() - + log.Warning("Cannot approve this PR (you may be the author or already approved)") + log.Info("") + // 如果带了 -m 参数,直接跳过批准继续合并 if approveAndMerge { - ui.Info("💡 Skipping approval, proceeding directly to merge...") + log.Info("💡 Skipping approval, proceeding directly to merge...") } else { // 没有 -m 参数,提示错误并退出 - ui.Error("Approval failed. If you want to merge directly, use the -m flag:") - ui.Info(fmt.Sprintf(" qkflow pr approve %d -m", prNumber)) + log.Error("Approval failed. If you want to merge directly, use the -m flag:") + log.Info(" qkflow pr approve %d -m", prNumber) return } } else { - ui.Error(fmt.Sprintf("Failed to approve PR: %v", err)) + log.Error("Failed to approve PR: %v", err) return } } if approvalSucceeded { if comment != "" { - ui.Success(fmt.Sprintf("✅ PR approved with comment: %s", comment)) + log.Success("✅ PR approved with comment: %s", comment) } else { - ui.Success("✅ PR approved!") + log.Success("✅ PR approved!") } } // 如果需要自动合并 if approveAndMerge { // 检查 PR 是否可以合并 - ui.Info("Checking if PR is mergeable...") + log.Info("Checking if PR is mergeable...") mergeable, err := ghClient.IsPRMergeable(owner, repo, prNumber) if err != nil || !mergeable { - ui.Warning(fmt.Sprintf("Cannot merge PR: %v", err)) - ui.Info("You may need to wait for CI checks or resolve conflicts") + log.Warning("Cannot merge PR: %v", err) + log.Info("You may need to wait for CI checks or resolve conflicts") return } // 执行合并 - ui.Info(fmt.Sprintf("Merging PR #%d...", prNumber)) + log.Info("Merging PR #%d...", prNumber) if err := ghClient.MergePullRequest(owner, repo, prNumber, pr.Title); err != nil { - ui.Error(fmt.Sprintf("Failed to merge PR: %v", err)) + log.Error("Failed to merge PR: %v", err) return } - ui.Success("🎉 PR merged successfully!") + log.Success("🎉 PR merged successfully!") // 删除远程分支 - ui.Info(fmt.Sprintf("Deleting remote branch %s...", pr.Head)) + log.Info("Deleting remote branch %s...", pr.Head) if err := git.DeleteRemoteBranch(pr.Head); err != nil { - ui.Warning(fmt.Sprintf("Failed to delete remote branch: %v (may already be deleted)", err)) + log.Warning("Failed to delete remote branch: %v (may already be deleted)", err) } else { - ui.Success("Remote branch deleted") + log.Success("Remote branch deleted") } // 如果在同一个分支,切换到主分支 @@ -293,27 +293,26 @@ func runPRApprove(cmd *cobra.Command, args []string) { defaultBranch = "master" } - ui.Info(fmt.Sprintf("Switching to %s branch...", defaultBranch)) + log.Info("Switching to %s branch...", defaultBranch) if err := git.CheckoutBranch(defaultBranch); err != nil { - ui.Warning(fmt.Sprintf("Could not switch to %s, you may need to do this manually", defaultBranch)) + log.Warning("Could not switch to %s, you may need to do this manually", defaultBranch) } else { - ui.Success("Switched to default branch") + log.Success("Switched to default branch") // 删除本地分支 - ui.Info(fmt.Sprintf("Deleting local branch %s...", pr.Head)) + log.Info("Deleting local branch %s...", pr.Head) if err := git.DeleteBranch(pr.Head); err != nil { - ui.Warning(fmt.Sprintf("Failed to delete local branch: %v", err)) + log.Warning("Failed to delete local branch: %v", err) } else { - ui.Success("Local branch deleted") + log.Success("Local branch deleted") } } } - fmt.Println() - ui.Success("All done! 🎉") + log.Info("") + log.Success("All done! 🎉") } else { - fmt.Println() - ui.Info("PR approved. Use 'qkg pr merge' to merge it later, or run with --merge flag to auto-merge.") + log.Info("") + log.Info("PR approved. Use 'qkg pr merge' to merge it later, or run with --merge flag to auto-merge.") } } - diff --git a/go-version/cmd/qkflow/commands/pr_create.go b/go-version/cmd/qkflow/commands/pr_create.go index 16ca728..e0d7878 100644 --- a/go-version/cmd/qkflow/commands/pr_create.go +++ b/go-version/cmd/qkflow/commands/pr_create.go @@ -11,9 +11,9 @@ import ( "github.com/Wangggym/quick-workflow/internal/git" "github.com/Wangggym/quick-workflow/internal/github" "github.com/Wangggym/quick-workflow/internal/jira" - "github.com/Wangggym/quick-workflow/internal/ui" "github.com/Wangggym/quick-workflow/internal/watcher" - "github.com/Wangggym/quick-workflow/pkg/config" + "github.com/Wangggym/quick-workflow/internal/config" + "github.com/Wangggym/quick-workflow/internal/ui" "github.com/spf13/cobra" ) @@ -34,18 +34,18 @@ var prCreateCmd = &cobra.Command{ func runPRCreate(cmd *cobra.Command, args []string) { // 检查是否在 Git 仓库中 if !git.IsGitRepository() { - ui.Error("Not a git repository") + log.Error("Not a git repository") return } // 检查是否有未提交的更改 hasChanges, err := git.HasUncommittedChanges() if err != nil { - ui.Error(fmt.Sprintf("Failed to check git status: %v", err)) + log.Error("Failed to check git status: %v", err) return } if !hasChanges { - ui.Error("No changes to commit. Please stage your changes first with 'git add'") + log.Error("No changes to commit. Please stage your changes first with 'git add'") return } @@ -58,10 +58,10 @@ func runPRCreate(cmd *cobra.Command, args []string) { if err != nil { // 用户取消操作 if err.Error() == "interrupt" { - ui.Warning("Operation cancelled by user") + log.Warning("Operation cancelled by user") os.Exit(0) } - ui.Error(fmt.Sprintf("Failed to get input: %v", err)) + log.Error("Failed to get input: %v", err) return } } @@ -71,20 +71,20 @@ func runPRCreate(cmd *cobra.Command, args []string) { if jiraTicket != "" && jira.ValidateIssueKey(jiraTicket) { jiraClient, err := jira.NewClient() if err != nil { - ui.Warning(fmt.Sprintf("Failed to create Jira client: %v", err)) + log.Warning("Failed to create Jira client: %v", err) } else { jiraIssue, err = jiraClient.GetIssue(jiraTicket) if err != nil { - ui.Warning(fmt.Sprintf("Failed to get Jira issue: %v", err)) + log.Warning("Failed to get Jira issue: %v", err) } else { - ui.Info(fmt.Sprintf("Found Jira issue: %s", jiraIssue.Summary)) + log.Info("Found Jira issue: %s", jiraIssue.Summary) } } } // 显示 Jira 信息 if jiraIssue != nil { - ui.Info(fmt.Sprintf("Jira issue: %s", jiraIssue.Summary)) + log.Info("Jira issue: %s", jiraIssue.Summary) } // 选择变更类型 @@ -92,10 +92,10 @@ func runPRCreate(cmd *cobra.Command, args []string) { selectedTypes, err := ui.PromptMultiSelect("Select type(s) of changes:", prTypes) if err != nil { if err.Error() == "interrupt" { - ui.Warning("Operation cancelled by user") + log.Warning("Operation cancelled by user") os.Exit(0) } - ui.Warning("No types selected, continuing...") + log.Warning("No types selected, continuing...") selectedTypes = []string{} } @@ -103,16 +103,16 @@ func runPRCreate(cmd *cobra.Command, args []string) { var editorResult *editor.EditorResult addDescription, err := ui.PromptOptional("Add detailed description with images/videos?") if err == nil && addDescription { - ui.Info("Opening web editor...") + log.Info("Opening web editor...") editorResult, err = editor.StartEditor() if err != nil { - ui.Warning(fmt.Sprintf("Failed to start editor: %v", err)) + log.Warning("Failed to start editor: %v", err) editorResult = nil } else if editorResult.Content == "" && len(editorResult.Files) == 0 { - ui.Info("No content added, skipping...") + log.Info("No content added, skipping...") editorResult = nil } else { - ui.Success(fmt.Sprintf("Content saved! (%d characters, %d files)", len(editorResult.Content), len(editorResult.Files))) + log.Success("Content saved! (%d characters, %d files)", len(editorResult.Content), len(editorResult.Files)) } } @@ -124,33 +124,33 @@ func runPRCreate(cmd *cobra.Command, args []string) { if len(selectedTypes) > 0 { prType = ui.ExtractPRType(selectedTypes[0]) } - + // 使用 AI 生成简洁的 PR 标题 aiClient, err := ai.NewClient() if err == nil && prType != "" { - ui.Info("Generating PR title with AI...") + log.Info("Generating PR title with AI...") title, err = aiClient.GeneratePRTitle(jiraIssue.Summary, prType, "") if err != nil { - ui.Warning(fmt.Sprintf("AI generation failed: %v", err)) + log.Warning("AI generation failed: %v", err) // 回退到简单格式 title = generateSimpleTitle(jiraIssue.Summary, prType, "") } else { - ui.Success(fmt.Sprintf("Generated title: %s", title)) + log.Success("Generated title: %s", title) } } else { // 没有 AI 或没有类型,使用简单生成 title = generateSimpleTitle(jiraIssue.Summary, prType, "") - ui.Success(fmt.Sprintf("Generated title: %s", title)) + log.Success("Generated title: %s", title) } } else { // 没有 Jira,手动输入 title, err = ui.PromptInput("Enter PR title:", true) if err != nil { if err.Error() == "interrupt" { - ui.Warning("Operation cancelled by user") + log.Warning("Operation cancelled by user") os.Exit(0) } - ui.Error(fmt.Sprintf("Failed to get title: %v", err)) + log.Error("Failed to get title: %v", err) return } } @@ -165,18 +165,18 @@ func runPRCreate(cmd *cobra.Command, args []string) { branchName = cfg.BranchPrefix + "/" + branchName } - ui.Info(fmt.Sprintf("Creating branch: %s", branchName)) + log.Info("Creating branch: %s", branchName) // 创建分支 if err := git.CreateBranch(branchName); err != nil { - ui.Error(fmt.Sprintf("Failed to create branch: %v", err)) + log.Error("Failed to create branch: %v", err) return } // Stage 所有更改 - ui.Info("Staging changes...") + log.Info("Staging changes...") if err := git.AddAll(); err != nil { - ui.Error(fmt.Sprintf("Failed to stage changes: %v", err)) + log.Error("Failed to stage changes: %v", err) return } @@ -188,46 +188,46 @@ func runPRCreate(cmd *cobra.Command, args []string) { // 无 Jira ticket 时,添加 # 前缀 commitMessage = fmt.Sprintf("# %s", title) } - - ui.Info("Committing changes...") + + log.Info("Committing changes...") if err := git.Commit(commitMessage); err != nil { - ui.Error(fmt.Sprintf("Failed to commit: %v", err)) + log.Error("Failed to commit: %v", err) return } // 推送分支 - ui.Info("Pushing branch to remote...") + log.Info("Pushing branch to remote...") if err := git.Push(branchName); err != nil { - ui.Error(fmt.Sprintf("Failed to push: %v", err)) + log.Error("Failed to push: %v", err) return } // 获取仓库信息 remoteURL, err := git.GetRemoteURL() if err != nil { - ui.Error(fmt.Sprintf("Failed to get remote URL: %v", err)) + log.Error("Failed to get remote URL: %v", err) return } owner, repo, err := github.ParseRepositoryFromURL(remoteURL) if err != nil { - ui.Error(fmt.Sprintf("Failed to parse repository: %v", err)) + log.Error("Failed to parse repository: %v", err) return } // 获取默认分支 defaultBranch, err := git.GetDefaultBranch() if err != nil { - ui.Warning(fmt.Sprintf("Failed to detect default branch, using 'main': %v", err)) + log.Warning("Failed to detect default branch, using 'main': %v", err) defaultBranch = "main" } - ui.Info(fmt.Sprintf("Using base branch: %s", defaultBranch)) + log.Info("Using base branch: %s", defaultBranch) // 创建 PR - ui.Info("Creating pull request...") + log.Info("Creating pull request...") ghClient, err := github.NewClient() if err != nil { - ui.Error(fmt.Sprintf("Failed to create GitHub client: %v", err)) + log.Error("Failed to create GitHub client: %v", err) return } @@ -240,22 +240,22 @@ func runPRCreate(cmd *cobra.Command, args []string) { Base: defaultBranch, }) if err != nil { - ui.Error(fmt.Sprintf("Failed to create PR: %v", err)) + log.Error("Failed to create PR: %v", err) return } - ui.Success(fmt.Sprintf("Pull request created: %s", pr.HTMLURL)) + log.Success("Pull request created: %s", pr.HTMLURL) // 处理编辑器内容(上传文件并添加评论) if editorResult != nil && (editorResult.Content != "" || len(editorResult.Files) > 0) { - ui.Info("Processing description and files...") - + log.Info("Processing description and files...") + // 创建 Jira 客户端(如果需要) var jiraClient *jira.Client if jiraTicket != "" && jira.ValidateIssueKey(jiraTicket) { jiraClient, err = jira.NewClient() if err != nil { - ui.Warning(fmt.Sprintf("Failed to create Jira client for file upload: %v", err)) + log.Warning("Failed to create Jira client for file upload: %v", err) jiraClient = nil } } @@ -263,7 +263,7 @@ func runPRCreate(cmd *cobra.Command, args []string) { // 上传文件 var uploadResults []editor.UploadResult if len(editorResult.Files) > 0 { - ui.Info(fmt.Sprintf("Uploading %d file(s)...", len(editorResult.Files))) + log.Info("Uploading %d file(s)...", len(editorResult.Files)) uploadResults, err = editor.UploadFiles( editorResult.Files, ghClient, @@ -274,9 +274,9 @@ func runPRCreate(cmd *cobra.Command, args []string) { jiraTicket, ) if err != nil { - ui.Warning(fmt.Sprintf("Failed to upload files: %v", err)) + log.Warning("Failed to upload files: %v", err) } else { - ui.Success(fmt.Sprintf("Uploaded %d file(s)", len(uploadResults))) + log.Success("Uploaded %d file(s)", len(uploadResults)) } } @@ -288,22 +288,22 @@ func runPRCreate(cmd *cobra.Command, args []string) { // 添加评论到 GitHub PR if content != "" { - ui.Info("Adding description to GitHub PR...") + log.Info("Adding description to GitHub PR...") if err := ghClient.AddPRComment(owner, repo, pr.Number, content); err != nil { - ui.Warning(fmt.Sprintf("Failed to add comment to GitHub: %v", err)) + log.Warning("Failed to add comment to GitHub: %v", err) } else { - ui.Success("Description added to GitHub PR") + log.Success("Description added to GitHub PR") } } // 添加评论到 Jira if jiraClient != nil && jiraTicket != "" && content != "" { - ui.Info("Adding description to Jira...") + log.Info("Adding description to Jira...") jiraComment := fmt.Sprintf("*PR Description:*\n\n%s\n\n[View PR|%s]", content, pr.HTMLURL) if err := jiraClient.AddComment(jiraTicket, jiraComment); err != nil { - ui.Warning(fmt.Sprintf("Failed to add comment to Jira: %v", err)) + log.Warning("Failed to add comment to Jira: %v", err) } else { - ui.Success("Description added to Jira") + log.Success("Description added to Jira") } } @@ -319,58 +319,58 @@ func runPRCreate(cmd *cobra.Command, args []string) { if jiraTicket != "" && jira.ValidateIssueKey(jiraTicket) { jiraClient, err := jira.NewClient() if err != nil { - ui.Warning(fmt.Sprintf("Failed to create Jira client: %v", err)) + log.Warning("Failed to create Jira client: %v", err) } else { // 分配给当前用户 - ui.Info("Assigning Jira ticket to you...") + log.Info("Assigning Jira ticket to you...") if err := jiraClient.AssignToMe(jiraTicket); err != nil { - ui.Warning(fmt.Sprintf("Failed to assign ticket: %v", err)) + log.Warning("Failed to assign ticket: %v", err) } else { - ui.Success("Assigned Jira ticket to you") + log.Success("Assigned Jira ticket to you") } // 添加 PR 链接 - ui.Info("Adding PR link to Jira...") + log.Info("Adding PR link to Jira...") if err := jiraClient.AddPRLink(jiraTicket, pr.HTMLURL); err != nil { - ui.Warning(fmt.Sprintf("Failed to add PR link to Jira: %v", err)) + log.Warning("Failed to add PR link to Jira: %v", err) } else { - ui.Success("Added PR link to Jira") + log.Success("Added PR link to Jira") } // 更新状态 projectKey := jira.ExtractProjectKey(jiraTicket) - + // 检查状态缓存 statusCache, err := jira.NewStatusCache() if err != nil { - ui.Warning(fmt.Sprintf("Failed to create status cache: %v", err)) + log.Warning("Failed to create status cache: %v", err) } else { mapping, err := statusCache.GetProjectStatus(projectKey) if err != nil { - ui.Warning(fmt.Sprintf("Failed to get cached status: %v", err)) + log.Warning("Failed to get cached status: %v", err) } else if mapping == nil { // 第一次使用,配置状态映射 - ui.Info(fmt.Sprintf("First time using project %s, please configure status mappings", projectKey)) + log.Info("First time using project %s, please configure status mappings", projectKey) mapping, err = setupProjectStatusMapping(jiraClient, projectKey) if err != nil { - ui.Warning(fmt.Sprintf("Failed to setup status mapping: %v", err)) + log.Warning("Failed to setup status mapping: %v", err) } else if mapping != nil { // 保存配置 if err := statusCache.SaveProjectStatus(mapping); err != nil { - ui.Warning(fmt.Sprintf("Failed to save status mapping: %v", err)) + log.Warning("Failed to save status mapping: %v", err) } else { - ui.Success("Status mapping saved!") + log.Success("Status mapping saved!") } } } - + // 使用缓存的状态更新 if mapping != nil && mapping.PRCreatedStatus != "" { - ui.Info(fmt.Sprintf("Updating Jira status to: %s", mapping.PRCreatedStatus)) + log.Info("Updating Jira status to: %s", mapping.PRCreatedStatus) if err := jiraClient.UpdateStatus(jiraTicket, mapping.PRCreatedStatus); err != nil { - ui.Warning(fmt.Sprintf("Failed to update status: %v", err)) + log.Warning("Failed to update status: %v", err) } else { - ui.Success(fmt.Sprintf("Updated Jira status to: %s", mapping.PRCreatedStatus)) + log.Success("Updated Jira status to: %s", mapping.PRCreatedStatus) } } } @@ -380,7 +380,7 @@ func runPRCreate(cmd *cobra.Command, args []string) { // 添加到 watching list watchingList, err := watcher.NewWatchingList() if err != nil { - ui.Warning(fmt.Sprintf("Failed to load watching list: %v", err)) + log.Warning("Failed to load watching list: %v", err) } else { // Extract Jira tickets jiraTickets := make([]string, 0) @@ -399,20 +399,20 @@ func runPRCreate(cmd *cobra.Command, args []string) { } if err := watchingList.Add(watchingPR); err != nil { - ui.Warning(fmt.Sprintf("Failed to add PR to watching list: %v", err)) + log.Warning("Failed to add PR to watching list: %v", err) } else { - ui.Info("✅ Added PR to watching list for auto Jira updates") + log.Info("✅ Added PR to watching list for auto Jira updates") } } // 复制 URL 到剪贴板 copyToClipboard(pr.HTMLURL) - + // 打开浏览器 openBrowser(pr.HTMLURL) - - fmt.Println() - ui.Success("All done! 🎉") + + log.Info("") + log.Success("All done! 🎉") } func buildBranchName(jiraTicket, title string) string { @@ -451,7 +451,7 @@ func setupProjectStatusMapping(client *jira.Client, projectKey string) (*jira.St return nil, fmt.Errorf("failed to get project statuses: %w", err) } - ui.Info("Select status when PR is created/in progress:") + log.Info("Select status when PR is created/in progress:") createdStatus, err := ui.PromptSelect("Status for PR created:", statuses) if err != nil { if err.Error() == "interrupt" { @@ -460,7 +460,7 @@ func setupProjectStatusMapping(client *jira.Client, projectKey string) (*jira.St return nil, fmt.Errorf("failed to select created status: %w", err) } - ui.Info("Select status when PR is merged/done:") + log.Info("Select status when PR is merged/done:") mergedStatus, err := ui.PromptSelect("Status for PR merged:", statuses) if err != nil { if err.Error() == "interrupt" { @@ -484,13 +484,13 @@ func generateSimpleTitle(jiraSummary, prType, description string) string { } return description } - + // 否则使用 Jira 标题的前 50 个字符 summary := jiraSummary if len(summary) > 50 { summary = summary[:50] + "..." } - + if prType != "" { return fmt.Sprintf("%s: %s", prType, summary) } @@ -502,9 +502,9 @@ func copyToClipboard(text string) { cmd := exec.Command("pbcopy") cmd.Stdin = strings.NewReader(text) if err := cmd.Run(); err != nil { - ui.Warning("Failed to copy to clipboard") + log.Warning("Failed to copy to clipboard") } else { - ui.Success(fmt.Sprintf("Successfully copied %s to clipboard", text)) + log.Success("Successfully copied %s to clipboard", text) } } @@ -512,7 +512,7 @@ func openBrowser(url string) { // macOS cmd := exec.Command("open", url) if err := cmd.Run(); err != nil { - ui.Warning(fmt.Sprintf("Failed to open browser: %v", err)) + log.Warning("Failed to open browser: %v", err) } } diff --git a/go-version/cmd/qkflow/commands/pr_merge.go b/go-version/cmd/qkflow/commands/pr_merge.go index 4a5ed72..510c7cf 100644 --- a/go-version/cmd/qkflow/commands/pr_merge.go +++ b/go-version/cmd/qkflow/commands/pr_merge.go @@ -47,38 +47,38 @@ func runPRMerge(cmd *cobra.Command, args []string) { // 检查是否是 GitHub PR URL if github.IsPRURL(arg) { - ui.Info("Detected GitHub PR URL, parsing...") + log.Info("Detected GitHub PR URL, parsing...") owner, repo, prNumber, err = github.ParsePRFromURL(arg) if err != nil { - ui.Error(fmt.Sprintf("Failed to parse PR URL: %v", err)) + log.Error("Failed to parse PR URL: %v", err) return } - ui.Success(fmt.Sprintf("Parsed: %s/%s PR #%d", owner, repo, prNumber)) + log.Success("Parsed: %s/%s PR #%d", owner, repo, prNumber) } else { // 尝试作为 PR 号解析 prNumber, err = strconv.Atoi(arg) if err != nil { - ui.Error(fmt.Sprintf("Invalid PR number or URL: %s", arg)) - ui.Info("Expected: PR number (e.g., '123') or GitHub URL (e.g., 'https://github.com/owner/repo/pull/123')") + log.Error("Invalid PR number or URL: %s", arg) + log.Info("Expected: PR number (e.g., '123') or GitHub URL (e.g., 'https://github.com/owner/repo/pull/123')") return } // PR 号格式,需要从本地仓库获取 owner/repo if !git.IsGitRepository() { - ui.Error("Not a git repository. When using PR number, you must be in a git repository.") - ui.Info("Alternatively, use the full GitHub PR URL: https://github.com/owner/repo/pull/NUMBER") + log.Error("Not a git repository. When using PR number, you must be in a git repository.") + log.Info("Alternatively, use the full GitHub PR URL: https://github.com/owner/repo/pull/NUMBER") return } remoteURL, err := git.GetRemoteURL() if err != nil { - ui.Error(fmt.Sprintf("Failed to get remote URL: %v", err)) + log.Error("Failed to get remote URL: %v", err) return } owner, repo, err = github.ParseRepositoryFromURL(remoteURL) if err != nil { - ui.Error(fmt.Sprintf("Failed to parse repository: %v", err)) + log.Error("Failed to parse repository: %v", err) return } } @@ -86,20 +86,20 @@ func runPRMerge(cmd *cobra.Command, args []string) { // 没有提供参数,使用原有的自动检测逻辑 // 检查是否在 Git 仓库中 if !git.IsGitRepository() { - ui.Error("Not a git repository") + log.Error("Not a git repository") return } // 获取仓库信息 remoteURL, err := git.GetRemoteURL() if err != nil { - ui.Error(fmt.Sprintf("Failed to get remote URL: %v", err)) + log.Error("Failed to get remote URL: %v", err) return } owner, repo, err = github.ParseRepositoryFromURL(remoteURL) if err != nil { - ui.Error(fmt.Sprintf("Failed to parse repository: %v", err)) + log.Error("Failed to parse repository: %v", err) return } } @@ -107,7 +107,7 @@ func runPRMerge(cmd *cobra.Command, args []string) { // 创建 GitHub 客户端 ghClient, err := github.NewClient() if err != nil { - ui.Error(fmt.Sprintf("Failed to create GitHub client: %v", err)) + log.Error("Failed to create GitHub client: %v", err) return } @@ -116,7 +116,7 @@ func runPRMerge(cmd *cobra.Command, args []string) { // 尝试获取当前分支的 PR currentBranch, err := git.GetCurrentBranch() if err == nil && currentBranch != "" { - ui.Info(fmt.Sprintf("Checking for PR from current branch: %s", currentBranch)) + log.Info("Checking for PR from current branch: %s", currentBranch) // 先尝试 open 状态的 PR prs, err := ghClient.ListPullRequests(owner, repo, "open", "") @@ -124,7 +124,7 @@ func runPRMerge(cmd *cobra.Command, args []string) { for _, pr := range prs { if pr.Head == currentBranch { prNumber = pr.Number - ui.Success(fmt.Sprintf("Found PR #%d: %s", pr.Number, pr.Title)) + log.Success("Found PR #%d: %s", pr.Number, pr.Title) break } } @@ -137,7 +137,7 @@ func runPRMerge(cmd *cobra.Command, args []string) { for _, pr := range allPRs { if pr.Head == currentBranch { prNumber = pr.Number - ui.Success(fmt.Sprintf("Found PR #%d (%s): %s", pr.Number, pr.State, pr.Title)) + log.Success("Found PR #%d (%s): %s", pr.Number, pr.State, pr.Title) break } } @@ -146,15 +146,15 @@ func runPRMerge(cmd *cobra.Command, args []string) { // 如果还是没找到,提示用户 if prNumber == 0 { - ui.Warning(fmt.Sprintf("No PR found for branch: %s", currentBranch)) - ui.Info("This branch may not have a PR yet. Please create one first with:") - ui.Info(" qkg pr create") - fmt.Println() + log.Warning("No PR found for branch: %s", currentBranch) + log.Info("This branch may not have a PR yet. Please create one first with:") + log.Info(" qkg pr create") + log.Info("") // 询问用户是否手动输入 PR 号 manually, err := ui.PromptConfirm("Do you want to manually enter a PR number or select from list?", true) if err != nil || !manually { - ui.Info("Merge cancelled") + log.Info("Merge cancelled") return } } @@ -164,12 +164,12 @@ func runPRMerge(cmd *cobra.Command, args []string) { if prNumber == 0 { prs, err := ghClient.ListPullRequests(owner, repo, "open", "") if err != nil { - ui.Error(fmt.Sprintf("Failed to list PRs: %v", err)) + log.Error("Failed to list PRs: %v", err) return } if len(prs) == 0 { - ui.Error("No open pull requests found") + log.Error("No open pull requests found") return } @@ -182,10 +182,10 @@ func runPRMerge(cmd *cobra.Command, args []string) { selected, err := ui.PromptSelect("Select a PR to merge:", prOptions) if err != nil { if err.Error() == "interrupt" { - ui.Warning("Operation cancelled by user") + log.Warning("Operation cancelled by user") os.Exit(0) } - ui.Error(fmt.Sprintf("Failed to select PR: %v", err)) + log.Error("Failed to select PR: %v", err) return } @@ -201,50 +201,50 @@ func runPRMerge(cmd *cobra.Command, args []string) { if selectedPR != nil { prNumber = selectedPR.Number } else { - ui.Error("Failed to find selected PR") + log.Error("Failed to find selected PR") return } } } // 获取 PR 信息 - ui.Info(fmt.Sprintf("Fetching PR #%d...", prNumber)) + log.Info("Fetching PR #%d...", prNumber) pr, err := ghClient.GetPullRequest(owner, repo, prNumber) if err != nil { - ui.Error(fmt.Sprintf("Failed to get PR: %v", err)) + log.Error("Failed to get PR: %v", err) return } - ui.Info(fmt.Sprintf("PR: %s", pr.Title)) - ui.Info(fmt.Sprintf("Branch: %s -> %s", pr.Head, pr.Base)) - ui.Info(fmt.Sprintf("State: %s", pr.State)) + log.Info("PR: %s", pr.Title) + log.Info("Branch: %s -> %s", pr.Head, pr.Base) + log.Info("State: %s", pr.State) // 检查 PR 状态 alreadyMerged := false if pr.State == "closed" { - ui.Warning("This PR is already closed") + log.Warning("This PR is already closed") // 检查是否是已合并 alreadyMerged = true } else { // 合并 PR - ui.Info(fmt.Sprintf("Merging PR #%d...", prNumber)) + log.Info("Merging PR #%d...", prNumber) if err := ghClient.MergePullRequest(owner, repo, prNumber, pr.Title); err != nil { - ui.Error(fmt.Sprintf("Failed to merge PR: %v", err)) + log.Error("Failed to merge PR: %v", err) return } - ui.Success("Pull request merged!") + log.Success("Pull request merged!") } // 删除远程分支(如果还存在) if !alreadyMerged { - ui.Info(fmt.Sprintf("Deleting remote branch %s...", pr.Head)) + log.Info("Deleting remote branch %s...", pr.Head) if err := git.DeleteRemoteBranch(pr.Head); err != nil { - ui.Warning(fmt.Sprintf("Failed to delete remote branch: %v (may already be deleted)", err)) + log.Warning("Failed to delete remote branch: %v (may already be deleted)", err) } else { - ui.Success("Remote branch deleted") + log.Success("Remote branch deleted") } } else { - ui.Info("Skipping remote branch deletion (PR already merged)") + log.Info("Skipping remote branch deletion (PR already merged)") } // 切换到主分支并删除本地分支 @@ -256,71 +256,71 @@ func runPRMerge(cmd *cobra.Command, args []string) { defaultBranch = "master" } - ui.Info(fmt.Sprintf("Switching to %s branch...", defaultBranch)) + log.Info("Switching to %s branch...", defaultBranch) // 使用 checkout 而不是 create cmd := exec.Command("git", "checkout", defaultBranch) if err := cmd.Run(); err != nil { - ui.Warning(fmt.Sprintf("Could not switch to %s, you may need to do this manually", defaultBranch)) + log.Warning("Could not switch to %s, you may need to do this manually", defaultBranch) } else { // 切换成功后,拉取最新代码 - ui.Info(fmt.Sprintf("Pulling latest changes from %s...", defaultBranch)) + log.Info("Pulling latest changes from %s...", defaultBranch) pullCmd := exec.Command("git", "pull") if err := pullCmd.Run(); err != nil { - ui.Warning("Failed to pull latest changes, you may need to run 'git pull' manually") + log.Warning("Failed to pull latest changes, you may need to run 'git pull' manually") } else { - ui.Success("Updated to latest changes") + log.Success("Updated to latest changes") } } // 删除本地分支 - ui.Info(fmt.Sprintf("Deleting local branch %s...", pr.Head)) + log.Info("Deleting local branch %s...", pr.Head) if err := git.DeleteBranch(pr.Head); err != nil { - ui.Warning(fmt.Sprintf("Failed to delete local branch: %v", err)) + log.Warning("Failed to delete local branch: %v", err) } else { - ui.Success("Local branch deleted") + log.Success("Local branch deleted") } } // 从标题中提取 Jira ticket 并自动更新 jiraTicket := extractJiraTicket(pr.Title) if jiraTicket != "" && jira.ValidateIssueKey(jiraTicket) { - ui.Info(fmt.Sprintf("Found Jira ticket: %s", jiraTicket)) + log.Info("Found Jira ticket: %s", jiraTicket) jiraClient, err := jira.NewClient() if err != nil { - ui.Warning(fmt.Sprintf("Failed to create Jira client: %v", err)) + log.Warning("Failed to create Jira client: %v", err) } else { // 使用缓存的状态 projectKey := jira.ExtractProjectKey(jiraTicket) statusCache, err := jira.NewStatusCache() if err != nil { - ui.Warning(fmt.Sprintf("Failed to create status cache: %v", err)) + log.Warning("Failed to create status cache: %v", err) } else { mapping, err := statusCache.GetProjectStatus(projectKey) if err != nil { - ui.Warning(fmt.Sprintf("Failed to get cached status: %v", err)) + log.Warning("Failed to get cached status: %v", err) } else if mapping != nil && mapping.PRMergedStatus != "" { // 使用缓存的 merged 状态 - ui.Info(fmt.Sprintf("Updating Jira status to: %s", mapping.PRMergedStatus)) + log.Info("Updating Jira status to: %s", mapping.PRMergedStatus) if err := jiraClient.UpdateStatus(jiraTicket, mapping.PRMergedStatus); err != nil { - ui.Warning(fmt.Sprintf("Failed to update status: %v", err)) + log.Warning("Failed to update status: %v", err) } else { - ui.Success(fmt.Sprintf("Updated Jira status to: %s", mapping.PRMergedStatus)) + log.Success("Updated Jira status to: %s", mapping.PRMergedStatus) } } else { // 没有缓存,使用默认逻辑 statuses, err := jiraClient.GetProjectStatuses(projectKey) if err != nil { - ui.Warning(fmt.Sprintf("Failed to get statuses: %v", err)) + log.Warning("Failed to get statuses: %v", err) } else { defaultStatus := findDefaultMergedStatus(statuses) if defaultStatus != "" { - ui.Info(fmt.Sprintf("Updating Jira status to: %s", defaultStatus)) + log.Info("Updating Jira status to: %s", defaultStatus) if err := jiraClient.UpdateStatus(jiraTicket, defaultStatus); err != nil { - ui.Warning(fmt.Sprintf("Failed to update status: %v", err)) + log.Warning("Failed to update status: %v", err) } else { - ui.Success(fmt.Sprintf("Updated Jira status to: %s", defaultStatus)) + log.Success("Updated Jira status to: %s", defaultStatus) } } } @@ -329,8 +329,8 @@ func runPRMerge(cmd *cobra.Command, args []string) { } } - fmt.Println() - ui.Success("All done! 🎉") + log.Info("") + log.Success("All done! 🎉") } func extractJiraTicket(title string) string { diff --git a/go-version/cmd/qkflow/commands/root.go b/go-version/cmd/qkflow/commands/root.go index 43502c5..36e7e2e 100644 --- a/go-version/cmd/qkflow/commands/root.go +++ b/go-version/cmd/qkflow/commands/root.go @@ -1,15 +1,25 @@ package commands import ( - "fmt" - - "github.com/Wangggym/quick-workflow/internal/ui" + "github.com/Wangggym/quick-workflow/internal/config" + "github.com/Wangggym/quick-workflow/internal/logger" "github.com/Wangggym/quick-workflow/internal/updater" "github.com/Wangggym/quick-workflow/internal/utils" - "github.com/Wangggym/quick-workflow/pkg/config" "github.com/spf13/cobra" ) +// Global logger instance for all commands +var log *logger.Logger + +func init() { + // Initialize global logger for commands + // Level will be automatically loaded from environment variable or default value + log, _ = logger.NewLogger(&logger.LoggerOptions{ + Type: logger.LoggerTypeUI, + // Level omitted - will use QKFLOW_LOG_LEVEL env var or default LevelInfo + }) +} + var ( // Version is the application version Version = "dev" @@ -35,13 +45,13 @@ It automates common tasks like creating PRs, updating Jira status, and more.`, // 检查配置 cfg, err := config.Load() if err != nil { - ui.Error(fmt.Sprintf("Failed to load config: %v", err)) - ui.Warning("Please run 'qkflow init' to configure the tool") + log.Error("Failed to load config: %v", err) + log.Warning("Please run 'qkflow init' to configure the tool") return } if !config.IsConfigured() { - ui.Warning("Configuration incomplete. Please run 'qkflow init' to complete setup") + log.Warning("Configuration incomplete. Please run 'qkflow init' to complete setup") } // 检查更新(后台静默执行,不阻塞主流程) @@ -77,7 +87,7 @@ var versionCmd = &cobra.Command{ Use: "version", Short: "Print the version number", Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("qkflow version %s (built: %s)\n", Version, BuildTime) + log.Info("qkflow version %s (built: %s)", Version, BuildTime) }, } @@ -87,86 +97,83 @@ var configCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { cfg := config.Get() if cfg == nil { - ui.Error("No configuration found. Run 'qkflow init' first.") + log.Error("No configuration found. Run 'qkflow init' first.") return } - fmt.Println("Current configuration:") - fmt.Println() - + log.Info("Current configuration:") + log.Info("") + // Show storage location location := utils.GetConfigLocation() configDir, _ := utils.GetQuickWorkflowConfigDir() jiraDir, _ := utils.GetConfigDir() - fmt.Println("💾 Storage:") - fmt.Printf(" Location: %s\n", location) + log.Info("💾 Storage:") + log.Info(" Location: %s", location) if configDir != "" { - fmt.Printf(" Config: %s/config.yaml\n", configDir) + log.Info(" Config: %s/config.yaml", configDir) } if jiraDir != "" { - fmt.Printf(" Jira Status: %s/jira-status.json\n", jiraDir) + log.Info(" Jira Status: %s/jira-status.json", jiraDir) } - - fmt.Println() - fmt.Println("📧 Basic:") - fmt.Printf(" Email: %s\n", cfg.Email) + + log.Info("") + log.Info("📧 Basic:") + log.Info(" Email: %s", cfg.Email) if cfg.BranchPrefix != "" { - fmt.Printf(" Branch Prefix: %s\n", cfg.BranchPrefix) + log.Info(" Branch Prefix: %s", cfg.BranchPrefix) } - - fmt.Println() - fmt.Println("🐙 GitHub:") - fmt.Printf(" Token: %s\n", maskToken(cfg.GitHubToken)) + + log.Info("") + log.Info("🐙 GitHub:") + log.Info(" Token: %s", maskToken(cfg.GitHubToken)) if cfg.GitHubOwner != "" { - fmt.Printf(" Owner: %s\n", cfg.GitHubOwner) - } - if cfg.GitHubRepo != "" { - fmt.Printf(" Repo: %s\n", cfg.GitHubRepo) + log.Info(" Owner: %s", cfg.GitHubOwner) } - - fmt.Println() - fmt.Println("📋 Jira:") - fmt.Printf(" Service: %s\n", cfg.JiraServiceAddress) - fmt.Printf(" API Token: %s\n", maskToken(cfg.JiraAPIToken)) - - fmt.Println() - fmt.Println("🔄 Auto Update:") + + log.Info("") + log.Info("📋 Jira:") + log.Info(" Service: %s", cfg.JiraServiceAddress) + log.Info(" API Token: %s", maskToken(cfg.JiraAPIToken)) + + log.Info("") + log.Info("🔄 Auto Update:") if cfg.AutoUpdate { - fmt.Printf(" Status: ✅ Enabled (checks every 24h)\n") + log.Info(" Status: ✅ Enabled (checks every 24h)") } else { - fmt.Printf(" Status: ❌ Disabled (run 'qkflow update-cli' to update manually)\n") + log.Info(" Status: ❌ Disabled (run 'qkflow update-cli' to update manually)") } - - fmt.Println() - fmt.Println("🤖 AI (optional):") - + + log.Info("") + log.Info("🤖 AI (optional):") + // Determine which AI service is active hasDeepSeek := cfg.DeepSeekKey != "" hasOpenAI := cfg.OpenAIKey != "" - + if hasDeepSeek { - fmt.Printf(" DeepSeek Key: %s ✅ (Active)\n", maskToken(cfg.DeepSeekKey)) + log.Info(" DeepSeek Key: %s ✅ (Active)", maskToken(cfg.DeepSeekKey)) } else { - fmt.Printf(" DeepSeek Key: not configured\n") + log.Info(" DeepSeek Key: not configured") } - + if hasOpenAI { if hasDeepSeek { - fmt.Printf(" OpenAI Key: %s ✅ (Backup)\n", maskToken(cfg.OpenAIKey)) + log.Info(" OpenAI Key: %s ✅ (Backup)", maskToken(cfg.OpenAIKey)) } else { - fmt.Printf(" OpenAI Key: %s ✅ (Active)\n", maskToken(cfg.OpenAIKey)) + log.Info(" OpenAI Key: %s ✅ (Active)", maskToken(cfg.OpenAIKey)) } } else { - fmt.Printf(" OpenAI Key: not configured\n") + log.Info(" OpenAI Key: not configured") } - + if cfg.OpenAIProxyURL != "" { - fmt.Printf(" OpenAI Proxy URL: %s\n", cfg.OpenAIProxyURL) + log.Info(" OpenAI Proxy URL: %s", cfg.OpenAIProxyURL) } - + if !hasDeepSeek && !hasOpenAI { - fmt.Println() - fmt.Println(" 💡 Tip: Configure AI for automatic PR title/description generation") + log.Info("") + log.Info(" 💡 Tip: Configure AI for automatic PR title/description generation") } }, } @@ -177,4 +184,3 @@ func maskToken(token string) string { } return token[:4] + "****" + token[len(token)-4:] } - diff --git a/go-version/cmd/qkflow/commands/update.go b/go-version/cmd/qkflow/commands/update.go index 852a8d8..9d46ff5 100644 --- a/go-version/cmd/qkflow/commands/update.go +++ b/go-version/cmd/qkflow/commands/update.go @@ -1,11 +1,9 @@ package commands import ( - "fmt" "github.com/Wangggym/quick-workflow/internal/git" "github.com/Wangggym/quick-workflow/internal/github" - "github.com/Wangggym/quick-workflow/internal/ui" "github.com/spf13/cobra" ) @@ -26,30 +24,30 @@ If no PR is found, it will use "update" as the default commit message.`, func runUpdate(cmd *cobra.Command, args []string) { // 检查是否是 git 仓库 if !git.IsGitRepository() { - ui.Error("Not a git repository") + log.Error("Not a git repository") return } // 检查是否有未提交的更改 hasChanges, err := git.HasUncommittedChanges() if err != nil { - ui.Error(fmt.Sprintf("Failed to check git status: %v", err)) + log.Error("Failed to check git status: %v", err) return } if !hasChanges { - ui.Warning("No changes to commit") + log.Warning("No changes to commit") return } // 获取当前分支 branch, err := git.GetCurrentBranch() if err != nil { - ui.Error(fmt.Sprintf("Failed to get current branch: %v", err)) + log.Error("Failed to get current branch: %v", err) return } - ui.Info(fmt.Sprintf("Current branch: %s", branch)) + log.Info("Current branch: %s", branch) // 获取 PR 标题作为 commit message commitMessage := "update" // 默认 commit message @@ -66,37 +64,37 @@ func runUpdate(cmd *cobra.Command, args []string) { pr, err := ghClient.GetPRByBranch(owner, repo, branch) if err == nil && pr != nil { commitMessage = pr.Title - ui.Success(fmt.Sprintf("Got PR title: %s", commitMessage)) + log.Success("Got PR title: %s", commitMessage) } else { - ui.Warning(fmt.Sprintf("No open PR found for branch %s, using default message 'update'", branch)) + log.Warning("No open PR found for branch %s, using default message 'update'", branch) } } else { - ui.Warning(fmt.Sprintf("Failed to create GitHub client: %v, using default message", err)) + log.Warning("Failed to create GitHub client: %v, using default message", err) } } } // Stage all changes - ui.Info("Staging all changes...") + log.Info("Staging all changes...") if err := git.AddAll(); err != nil { - ui.Error(fmt.Sprintf("Failed to stage changes: %v", err)) + log.Error("Failed to stage changes: %v", err) return } // Commit - ui.Info(fmt.Sprintf("Committing with message: '%s'", commitMessage)) + log.Info("Committing with message: '%s'", commitMessage) if err := git.Commit(commitMessage); err != nil { - ui.Error(fmt.Sprintf("Failed to commit: %v", err)) + log.Error("Failed to commit: %v", err) return } // Push - ui.Info("Pushing to origin...") + log.Info("Pushing to origin...") if err := git.Push(branch); err != nil { - ui.Error(fmt.Sprintf("Failed to push: %v", err)) + log.Error("Failed to push: %v", err) return } - ui.Success("✅ Successfully committed and pushed changes!") + log.Success("✅ Successfully committed and pushed changes!") } diff --git a/go-version/cmd/qkflow/commands/watch.go b/go-version/cmd/qkflow/commands/watch.go index e7cb8e0..1c22ee0 100644 --- a/go-version/cmd/qkflow/commands/watch.go +++ b/go-version/cmd/qkflow/commands/watch.go @@ -4,15 +4,16 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "syscall" "time" + "github.com/Wangggym/quick-workflow/internal/config" "github.com/Wangggym/quick-workflow/internal/github" "github.com/Wangggym/quick-workflow/internal/jira" - "github.com/Wangggym/quick-workflow/internal/ui" + "github.com/Wangggym/quick-workflow/internal/logger" "github.com/Wangggym/quick-workflow/internal/utils" "github.com/Wangggym/quick-workflow/internal/watcher" - "github.com/Wangggym/quick-workflow/pkg/config" "github.com/spf13/cobra" ) @@ -110,7 +111,7 @@ func init() { watchLogCmd.Flags().BoolVar(&followLog, "follow", false, "Follow log output (like tail -f)") watchLogCmd.Flags().IntVar(&logLines, "last", 50, "Show last N lines") watchHistoryCmd.Flags().IntVar(&historyDays, "days", 7, "Show history for last N days") - + watchCmd.AddCommand(watchCheckCmd) watchCmd.AddCommand(watchStartCmd) watchCmd.AddCommand(watchStopCmd) @@ -127,102 +128,107 @@ func init() { func runWatchCheck(cmd *cobra.Command, args []string) { cfg := config.Get() if cfg == nil { - ui.Error("Configuration not found. Please run 'qkflow init' first") + log.Error("Configuration not found. Please run 'qkflow init' first") return } // Validate configuration if cfg.GitHubToken == "" { - ui.Error("GitHub token not configured") + log.Error("GitHub token not configured") return } if cfg.JiraServiceAddress == "" || cfg.Email == "" || cfg.JiraAPIToken == "" { - ui.Error("Jira not configured. Please run 'qkflow init' first") + log.Error("Jira not configured. Please run 'qkflow init' first") return } // Get GitHub client ghClient, err := github.NewClient() if err != nil { - ui.Error(fmt.Sprintf("Failed to create GitHub client: %v", err)) + log.Error("Failed to create GitHub client: %v", err) return } if dryRun { - ui.Info("🔍 Dry-run mode: No Jira updates will be made") - fmt.Println() + log.Info("🔍 Dry-run mode: No Jira updates will be made") + log.Info("") } // Initialize components - logger, err := watcher.NewLogger() + // Level will be automatically loaded from environment variable or default value + watcherLogger, err := logger.NewLogger(&logger.LoggerOptions{ + Type: logger.LoggerTypeFile, + FileName: "watch.log", + // Level omitted - will use QKFLOW_LOG_LEVEL env var or default LevelInfo + }) if err != nil { - ui.Error(fmt.Sprintf("Failed to create logger: %v", err)) + log.Error("Failed to create logger: %v", err) return } - defer logger.Close() + defer watcherLogger.Close() state, err := watcher.NewState() if err != nil { - ui.Error(fmt.Sprintf("Failed to load state: %v", err)) + log.Error("Failed to load state: %v", err) return } watchingList, err := watcher.NewWatchingList() if err != nil { - ui.Error(fmt.Sprintf("Failed to load watching list: %v", err)) + log.Error("Failed to load watching list: %v", err) return } - ui.Info(fmt.Sprintf("Checking %d watching PRs...", watchingList.Count())) - fmt.Println() + log.Info("Checking %d watching PRs...", watchingList.Count()) + log.Info("") if watchingList.Count() == 0 { - ui.Info("No PRs in watching list") - ui.Info("PRs will be added automatically when you create them with 'qkflow pr create'") + log.Info("No PRs in watching list") + log.Info("PRs will be added automatically when you create them with 'qkflow pr create'") return } jiraClient, err := jira.NewClient() if err != nil { - ui.Error(fmt.Sprintf("Failed to create Jira client: %v", err)) + log.Error("Failed to create Jira client: %v", err) return } statusCache, err := jira.NewStatusCache() if err != nil { - ui.Error(fmt.Sprintf("Failed to load Jira status cache: %v", err)) + log.Error("Failed to load Jira status cache: %v", err) return } // Create checker and processor - checker := watcher.NewChecker(ghClient, logger) - processor := watcher.NewProcessor(jiraClient, statusCache, logger) + checker := watcher.NewChecker(ghClient, watcherLogger) + processor := watcher.NewProcessor(jiraClient, statusCache, watcherLogger) // Check for merged PRs from watching list mergedPRs, err := checker.CheckMergedPRs(watchingList, state) if err != nil { - ui.Error(fmt.Sprintf("Failed to check PRs: %v", err)) - logger.Errorf("Failed to check PRs: %v", err) + log.Error("Failed to check PRs: %v", err) + watcherLogger.Error("Failed to check PRs: %v", err) return } if len(mergedPRs) == 0 { - ui.Success("✅ No newly merged PRs found") - logger.Info("No newly merged PRs found") + log.Success("✅ No newly merged PRs found") + watcherLogger.Info("No newly merged PRs found") return } - ui.Info(fmt.Sprintf("Found %d newly merged PR(s) with Jira tickets", len(mergedPRs))) - fmt.Println() + log.Info("Found %d newly merged PR(s) with Jira tickets", len(mergedPRs)) + log.Info("") // Process each PR for _, pr := range mergedPRs { - ui.Info(fmt.Sprintf("📋 PR #%d: %s", pr.Number, pr.Title)) - ui.Info(fmt.Sprintf(" Branch: %s", pr.Branch)) - ui.Info(fmt.Sprintf(" Jira: %v", pr.JiraTickets)) - ui.Info(fmt.Sprintf(" Merged: %s by %s", pr.MergedAt, pr.MergedBy)) - fmt.Println() + log.Info("📋 PR #%d: %s", pr.Number, pr.Title) + log.Info(" Branch: %s", pr.Branch) + log.Info(" Jira: %v", pr.JiraTickets) + log.Info(" Merged: %s by %s", pr.MergedAt, pr.MergedBy) + log.Info("") if dryRun { // Dry-run: just log what would happen @@ -230,13 +236,13 @@ func runWatchCheck(cmd *cobra.Command, args []string) { projectKey := watcher.GetProjectFromTicket(ticket) mapping, err := statusCache.GetProjectStatus(projectKey) if err != nil || mapping == nil { - ui.Warning(fmt.Sprintf(" ⚠️ %s: No status mapping configured for project %s", ticket, projectKey)) + log.Warning(" ⚠️ %s: No status mapping configured for project %s", ticket, projectKey) continue } - ui.Info(fmt.Sprintf(" Would update %s → %s", ticket, mapping.PRMergedStatus)) + log.Info(" Would update %s → %s", ticket, mapping.PRMergedStatus) } - fmt.Println() + log.Info("") continue } @@ -246,26 +252,26 @@ func runWatchCheck(cmd *cobra.Command, args []string) { // Display results for _, update := range processedPR.JiraUpdates { if update.Success { - ui.Success(fmt.Sprintf(" ✅ %s: %s → %s", update.Ticket, update.OldStatus, update.NewStatus)) + log.Success(" ✅ %s: %s → %s", update.Ticket, update.OldStatus, update.NewStatus) } else { - ui.Error(fmt.Sprintf(" ❌ %s: %s", update.Ticket, update.Error)) + log.Error(" ❌ %s: %s", update.Ticket, update.Error) } } - fmt.Println() + log.Info("") // Save to state if err := state.AddProcessedPR(processedPR); err != nil { - ui.Warning(fmt.Sprintf("Failed to save processed PR to state: %v", err)) + log.Warning("Failed to save processed PR to state: %v", err) } // Remove from watching list for _, watchingPR := range watchingList.GetAll() { if watchingPR.PRNumber == pr.Number { if err := watchingList.Remove(watchingPR.Owner, watchingPR.Repo, pr.Number); err != nil { - logger.Warningf("Failed to remove PR #%d from watching list: %v", pr.Number, err) + watcherLogger.Warning("Failed to remove PR #%d from watching list: %v", pr.Number, err) } else { - logger.Infof("Removed PR #%d from watching list", pr.Number) + watcherLogger.Info("Removed PR #%d from watching list", pr.Number) } break } @@ -274,26 +280,29 @@ func runWatchCheck(cmd *cobra.Command, args []string) { // Update last check time if err := state.UpdateLastCheckTime(); err != nil { - logger.Warningf("Failed to update last check time: %v", err) + watcherLogger.Warning("Failed to update last check time: %v", err) } // Clean old records retentionDays := 7 // Default from config if err := state.CleanOldRecords(retentionDays); err != nil { - logger.Warningf("Failed to clean old records: %v", err) + watcherLogger.Warning("Failed to clean old records: %v", err) } - if err := logger.CleanOldLogs(retentionDays); err != nil { - logger.Warningf("Failed to clean old logs: %v", err) - } + // Note: Log cleanup is handled by the logger itself, no manual cleanup needed if dryRun { - ui.Info("🔍 Dry-run completed. No changes were made.") + log.Info("🔍 Dry-run completed. No changes were made.") } else { - ui.Success(fmt.Sprintf("✅ Processed %d PR(s)", len(mergedPRs))) + log.Success("✅ Processed %d PR(s)", len(mergedPRs)) } - ui.Info(fmt.Sprintf("\n📝 Logs: %s", logger.GetFilePath())) + // Log file path is in the config directory + configDir, _ := utils.GetConfigDir() + if configDir != "" { + logPath := filepath.Join(configDir, "watch.log") + log.Info("\n📝 Logs: %s", logPath) + } } func runWatchDaemon(cmd *cobra.Command, args []string) { @@ -321,21 +330,21 @@ func runWatchStart(cmd *cobra.Command, args []string) { // Check if already running running, pid, err := watcher.IsRunning() if err != nil { - ui.Error(fmt.Sprintf("Failed to check daemon status: %v", err)) + log.Error("Failed to check daemon status: %v", err) return } if running { - ui.Warning(fmt.Sprintf("Watch daemon is already running (PID: %d)", pid)) + log.Warning("Watch daemon is already running (PID: %d)", pid) return } - ui.Info("🚀 Starting watch daemon...") + log.Info("🚀 Starting watch daemon...") // Fork daemon process execPath, err := os.Executable() if err != nil { - ui.Error(fmt.Sprintf("Failed to get executable path: %v", err)) + log.Error("Failed to get executable path: %v", err) return } @@ -346,7 +355,7 @@ func runWatchStart(cmd *cobra.Command, args []string) { process, err := os.StartProcess(execPath, []string{execPath, "watch", "daemon"}, procAttr) if err != nil { - ui.Error(fmt.Sprintf("Failed to start daemon: %v", err)) + log.Error("Failed to start daemon: %v", err) return } @@ -359,36 +368,36 @@ func runWatchStart(cmd *cobra.Command, args []string) { // Verify it's running running, pid, _ = watcher.IsRunning() if running { - ui.Success(fmt.Sprintf("✅ Watch daemon started successfully (PID: %d)", pid)) + log.Success("✅ Watch daemon started successfully (PID: %d)", pid) } else { - ui.Warning("Daemon may have failed to start. Check logs for details.") + log.Warning("Daemon may have failed to start. Check logs for details.") } } func runWatchStop(cmd *cobra.Command, args []string) { running, pid, err := watcher.IsRunning() if err != nil { - ui.Error(fmt.Sprintf("Failed to check daemon status: %v", err)) + log.Error("Failed to check daemon status: %v", err) return } if !running { - ui.Info("Watch daemon is not running") + log.Info("Watch daemon is not running") return } - ui.Info(fmt.Sprintf("🛑 Stopping watch daemon (PID: %d)...", pid)) + log.Info("🛑 Stopping watch daemon (PID: %d)...", pid) // Find process process, err := os.FindProcess(pid) if err != nil { - ui.Error(fmt.Sprintf("Failed to find process: %v", err)) + log.Error("Failed to find process: %v", err) return } // Send SIGTERM if err := process.Signal(syscall.SIGTERM); err != nil { - ui.Error(fmt.Sprintf("Failed to send stop signal: %v", err)) + log.Error("Failed to send stop signal: %v", err) return } @@ -397,16 +406,16 @@ func runWatchStop(cmd *cobra.Command, args []string) { time.Sleep(500 * time.Millisecond) running, _, _ := watcher.IsRunning() if !running { - ui.Success("✅ Watch daemon stopped") + log.Success("✅ Watch daemon stopped") return } } - ui.Warning("Daemon did not stop within timeout. It may still be shutting down.") + log.Warning("Daemon did not stop within timeout. It may still be shutting down.") } func runWatchRestart(cmd *cobra.Command, args []string) { - ui.Info("🔄 Restarting watch daemon...") + log.Info("🔄 Restarting watch daemon...") // Stop if running running, _, _ := watcher.IsRunning() @@ -422,58 +431,51 @@ func runWatchRestart(cmd *cobra.Command, args []string) { func runWatchStatus(cmd *cobra.Command, args []string) { state, err := watcher.NewState() if err != nil { - ui.Error(fmt.Sprintf("Failed to load state: %v", err)) + log.Error("Failed to load state: %v", err) return } - logger, err := watcher.NewLogger() - if err != nil { - ui.Error(fmt.Sprintf("Failed to create logger: %v", err)) - return - } - defer logger.Close() - cfg := config.Get() - fmt.Println() - fmt.Println("🔍 Watch Daemon Status") - fmt.Println() + log.Info("") + log.Info("🔍 Watch Daemon Status") + log.Info("") // Check if running running, pid, _ := watcher.IsRunning() - + if running { uptime := time.Since(state.DaemonStartTime) - fmt.Printf("Status: Running ✅\n") - fmt.Printf("PID: %d\n", pid) - fmt.Printf("Started: %s (uptime: %s)\n", + log.Info("Status: Running ✅") + log.Info("PID: %d", pid) + log.Info("Started: %s (uptime: %s)", state.DaemonStartTime.Format("2006-01-02 15:04:05"), formatDuration(uptime)) - + if !state.LastCheckTime.IsZero() { lastCheck := time.Since(state.LastCheckTime) - fmt.Printf("Last Check: %s (%s ago)\n", + log.Info("Last Check: %s (%s ago)", state.LastCheckTime.Format("2006-01-02 15:04:05"), formatDuration(lastCheck)) - + // Calculate next check scheduler := watcher.NewScheduler(nil) nextCheck := scheduler.CalculateNextCheckTime(time.Now()) - fmt.Printf("Next Check: %s (%s)\n", + log.Info("Next Check: %s (%s)", nextCheck.Format("2006-01-02 15:04:05"), scheduler.FormatNextCheckTime(nextCheck)) } } else { - fmt.Println("Status: Stopped ⏸️") - fmt.Println() - ui.Warning("Watch daemon is not running") + log.Info("Status: Stopped ⏸️") + log.Info("") + log.Warning("Watch daemon is not running") } - fmt.Println() - fmt.Println("📊 Statistics (last 7 days):") + log.Info("") + log.Info("📊 Statistics (last 7 days):") recentPRs := state.GetRecentPRs(7) - fmt.Printf(" PRs Processed: %d\n", len(recentPRs)) - + log.Info(" PRs Processed: %d", len(recentPRs)) + successCount := 0 for _, pr := range recentPRs { for _, update := range pr.JiraUpdates { @@ -482,32 +484,34 @@ func runWatchStatus(cmd *cobra.Command, args []string) { } } } - fmt.Printf(" Jira Updated: %d\n", successCount) - fmt.Printf(" Errors: %d\n", state.Stats.TotalErrors) + log.Info(" Jira Updated: %d", successCount) + log.Info(" Errors: %d", state.Stats.TotalErrors) if cfg != nil { - fmt.Println() - fmt.Println("📋 Configuration:") - fmt.Printf(" Repository: %s/%s\n", cfg.GitHubOwner, cfg.GitHubRepo) - fmt.Printf(" Author: %s (only your PRs)\n", cfg.GitHubOwner) - fmt.Printf(" Check Interval: 15 minutes (daytime)\n") - fmt.Printf(" Night Checks: 02:00, 06:00\n") - fmt.Printf(" Log Retention: 7 days\n") - } - - fmt.Println() - fmt.Println("📝 Files:") - fmt.Printf(" Log: %s\n", logger.GetFilePath()) - + log.Info("") + log.Info("📋 Configuration:") + log.Info(" Owner: %s (only your PRs)", cfg.GitHubOwner) + log.Info(" Check Interval: 15 minutes (daytime)") + log.Info(" Night Checks: 02:00, 06:00") + log.Info(" Log Retention: 7 days") + } + + log.Info("") + log.Info("📝 Files:") + if configDir, err := utils.GetConfigDir(); err == nil && configDir != "" { + logPath := filepath.Join(configDir, "watch.log") + log.Info(" Log: %s", logPath) + } + if cfg != nil { configDir, _ := utils.GetConfigDir() if configDir != "" { - fmt.Printf(" State: %s/watch-state.json\n", configDir) - fmt.Printf(" Config: %s/config.yaml\n", configDir) + log.Info(" State: %s/watch-state.json", configDir) + log.Info(" Config: %s/config.yaml", configDir) } } - fmt.Println() + log.Info("") } func formatDuration(d time.Duration) string { @@ -536,72 +540,72 @@ func formatDuration(d time.Duration) string { func runWatchInstall(cmd *cobra.Command, args []string) { cfg := config.Get() if cfg == nil { - ui.Error("Configuration not found. Please run 'qkflow init' first") + log.Error("Configuration not found. Please run 'qkflow init' first") return } - ui.Info("📦 Installing watch daemon...") - fmt.Println() + log.Info("📦 Installing watch daemon...") + log.Info("") // Check prerequisites - ui.Info("Checking prerequisites...") + log.Info("Checking prerequisites...") if cfg.GitHubToken == "" { - ui.Error("✗ GitHub token not configured") + log.Error("✗ GitHub token not configured") return } - ui.Info(" ✓ GitHub token configured") + log.Info(" ✓ GitHub token configured") if cfg.JiraServiceAddress == "" || cfg.Email == "" || cfg.JiraAPIToken == "" { - ui.Error("✗ Jira not configured") + log.Error("✗ Jira not configured") return } - ui.Info(" ✓ Jira configured") + log.Info(" ✓ Jira configured") // Check Jira status mappings statusCache, err := jira.NewStatusCache() if err != nil { - ui.Error(fmt.Sprintf("✗ Failed to load Jira status cache: %v", err)) + log.Error("✗ Failed to load Jira status cache: %v", err) return } mappings, err := statusCache.ListAllMappings() if err != nil || len(mappings) == 0 { - ui.Warning("✗ No Jira status mappings found") - ui.Info(" Please run 'qkflow jira setup' to configure status mappings") + log.Warning("✗ No Jira status mappings found") + log.Info(" Please run 'qkflow jira setup' to configure status mappings") return } - ui.Info(fmt.Sprintf(" ✓ Jira status mappings found (%d project(s))", len(mappings))) + log.Info(" ✓ Jira status mappings found (%d project(s))", len(mappings)) - fmt.Println() + log.Info("") // Check if already installed installed, err := watcher.IsLaunchAgentInstalled() if err != nil { - ui.Error(fmt.Sprintf("Failed to check installation status: %v", err)) + log.Error("Failed to check installation status: %v", err) return } if installed { - ui.Warning("Watch daemon is already installed") - ui.Info("To reinstall, run 'qkflow watch uninstall' first") + log.Warning("Watch daemon is already installed") + log.Info("To reinstall, run 'qkflow watch uninstall' first") return } // Get executable path execPath, err := os.Executable() if err != nil { - ui.Error(fmt.Sprintf("Failed to get executable path: %v", err)) + log.Error("Failed to get executable path: %v", err) return } // Install launch agent - ui.Info("Creating launch agent...") + log.Info("Creating launch agent...") if err := watcher.InstallLaunchAgent(execPath); err != nil { - ui.Error(fmt.Sprintf("✗ Failed to install launch agent: %v", err)) + log.Error("✗ Failed to install launch agent: %v", err) return } - ui.Info(" ✓ Generated plist file") - ui.Info(" ✓ Installed to ~/Library/LaunchAgents/") + log.Info(" ✓ Generated plist file") + log.Info(" ✓ Installed to ~/Library/LaunchAgents/") // Give it a moment to start time.Sleep(2 * time.Second) @@ -609,109 +613,107 @@ func runWatchInstall(cmd *cobra.Command, args []string) { // Check if running running, pid, _ := watcher.IsRunning() if running { - ui.Info(fmt.Sprintf(" ✓ Watch daemon started (PID: %d)", pid)) + log.Info(" ✓ Watch daemon started (PID: %d)", pid) } else { - ui.Warning(" ⚠️ Daemon may take a moment to start") - } - - fmt.Println() - fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - fmt.Println() - ui.Success("🎉 Installation complete!") - fmt.Println() - fmt.Println("The watch daemon is now running and will:") - fmt.Println(" • Check your PRs every 15 minutes (8:30-24:00)") - fmt.Println(" • Check twice during night (2:00, 6:00)") - fmt.Println(" • Update Jira when PRs merge") - fmt.Println(" • Start automatically on login") - fmt.Println() - fmt.Println("📊 Useful commands:") - fmt.Println(" • Check status: qkflow watch status") - fmt.Println(" • View logs: qkflow watch log --follow") - fmt.Println(" • View history: qkflow watch history") - fmt.Println(" • Stop daemon: qkflow watch stop") - fmt.Println(" • Uninstall: qkflow watch uninstall") - fmt.Println() - - logger, _ := watcher.NewLogger() - if logger != nil { - fmt.Printf("📝 Logs: %s\n", logger.GetFilePath()) - logger.Close() + log.Warning(" ⚠️ Daemon may take a moment to start") + } + + log.Info("") + log.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + log.Info("") + log.Success("🎉 Installation complete!") + log.Info("") + log.Info("The watch daemon is now running and will:") + log.Info(" • Check your PRs every 15 minutes (8:30-24:00)") + log.Info(" • Check twice during night (2:00, 6:00)") + log.Info(" • Update Jira when PRs merge") + log.Info(" • Start automatically on login") + log.Info("") + log.Info("📊 Useful commands:") + log.Info(" • Check status: qkflow watch status") + log.Info(" • View logs: qkflow watch log --follow") + log.Info(" • View history: qkflow watch history") + log.Info(" • Stop daemon: qkflow watch stop") + log.Info(" • Uninstall: qkflow watch uninstall") + log.Info("") + + configDir, _ := utils.GetConfigDir() + if configDir != "" { + logPath := filepath.Join(configDir, "watch.log") + log.Info("📝 Logs: %s", logPath) } } func runWatchUninstall(cmd *cobra.Command, args []string) { - ui.Info("🗑️ Uninstalling watch daemon...") - fmt.Println() + log.Info("🗑️ Uninstalling watch daemon...") + log.Info("") // Check if installed installed, err := watcher.IsLaunchAgentInstalled() if err != nil { - ui.Error(fmt.Sprintf("Failed to check installation status: %v", err)) + log.Error("Failed to check installation status: %v", err) return } if !installed { - ui.Info("Watch daemon is not installed") + log.Info("Watch daemon is not installed") return } // Stop daemon if running running, pid, _ := watcher.IsRunning() if running { - ui.Info(fmt.Sprintf("Stopping daemon (PID: %d)...", pid)) - + log.Info("Stopping daemon (PID: %d)...", pid) + process, err := os.FindProcess(pid) if err == nil { process.Signal(syscall.SIGTERM) time.Sleep(time.Second) } - - ui.Info(" ✓ Daemon stopped") + + log.Info(" ✓ Daemon stopped") } // Uninstall launch agent - ui.Info("Removing auto-start...") + log.Info("Removing auto-start...") if err := watcher.UninstallLaunchAgent(); err != nil { - ui.Error(fmt.Sprintf("✗ Failed to uninstall launch agent: %v", err)) + log.Error("✗ Failed to uninstall launch agent: %v", err) return } - + plistPath, _ := watcher.GetLaunchAgentPath() - ui.Info(" ✓ Removed launch agent") + log.Info(" ✓ Removed launch agent") if plistPath != "" { - ui.Info(fmt.Sprintf(" ✓ Deleted %s", plistPath)) + log.Info(" ✓ Deleted %s", plistPath) } - fmt.Println() - ui.Success("✅ Watch daemon completely uninstalled") - fmt.Println() - fmt.Println("The daemon will NOT start automatically anymore.") - + log.Info("") + log.Success("✅ Watch daemon completely uninstalled") + log.Info("") + log.Info("The daemon will NOT start automatically anymore.") + configDir, _ := utils.GetConfigDir() if configDir != "" { - fmt.Println("Logs and history are preserved at ~/.qkflow/") + log.Info("Logs and history are preserved at ~/.qkflow/") } - - fmt.Println() - fmt.Println("💡 To re-enable later, use 'qkflow watch install'") + + log.Info("") + log.Info("💡 To re-enable later, use 'qkflow watch install'") } func runWatchLog(cmd *cobra.Command, args []string) { - logger, err := watcher.NewLogger() + configDir, err := utils.GetConfigDir() if err != nil { - ui.Error(fmt.Sprintf("Failed to create logger: %v", err)) + log.Error("Failed to get config directory: %v", err) return } - defer logger.Close() - - logPath := logger.GetFilePath() + logPath := filepath.Join(configDir, "watch.log") if followLog { // Follow log (tail -f style) - ui.Info(fmt.Sprintf("Following logs: %s", logPath)) - ui.Info("Press Ctrl+C to stop") - fmt.Println() + log.Info("Following logs: %s", logPath) + log.Info("Press Ctrl+C to stop") + log.Info("") cmd := exec.Command("tail", "-f", logPath) cmd.Stdout = os.Stdout @@ -721,42 +723,42 @@ func runWatchLog(cmd *cobra.Command, args []string) { } // Show last N lines - lines, err := watcher.ReadLastLines(logPath, logLines) + lines, err := readLastLines(logPath, logLines) if err != nil { - ui.Error(fmt.Sprintf("Failed to read log file: %v", err)) + log.Error("Failed to read log file: %v", err) return } - fmt.Println() - fmt.Printf("📝 Last %d lines from %s:\n", logLines, logPath) - fmt.Println() + log.Info("") + log.Info("📝 Last %d lines from %s:", logLines, logPath) + log.Info("") for _, line := range lines { - fmt.Println(line) + log.Info(line) } - fmt.Println() + log.Info("") } func runWatchHistory(cmd *cobra.Command, args []string) { state, err := watcher.NewState() if err != nil { - ui.Error(fmt.Sprintf("Failed to load state: %v", err)) + log.Error("Failed to load state: %v", err) return } prs := state.GetRecentPRs(historyDays) if len(prs) == 0 { - ui.Info(fmt.Sprintf("No PRs processed in the last %d days", historyDays)) + log.Info("No PRs processed in the last %d days", historyDays) return } - fmt.Println() - fmt.Printf("📋 PR Processing History (Last %d days)\n", historyDays) - fmt.Println() + log.Info("") + log.Info("📋 PR Processing History (Last %d days)", historyDays) + log.Info("") for i, pr := range prs { if i > 0 { - fmt.Println() + log.Info("") } // Determine overall success @@ -779,88 +781,88 @@ func runWatchHistory(cmd *cobra.Command, args []string) { } } - fmt.Printf("%s PR #%d: %s\n", status, pr.PRNumber, pr.PRTitle) - fmt.Printf(" Branch: %s\n", pr.Branch) - fmt.Printf(" Merged: %s by %s\n", pr.MergedAt.Format("2006-01-02 15:04:05"), pr.MergedBy) + log.Info("%s PR #%d: %s", status, pr.PRNumber, pr.PRTitle) + log.Info(" Branch: %s", pr.Branch) + log.Info(" Merged: %s by %s", pr.MergedAt.Format("2006-01-02 15:04:05"), pr.MergedBy) if len(pr.JiraTickets) > 0 { - fmt.Printf(" Jira: ") + tickets := "" for j, ticket := range pr.JiraTickets { if j > 0 { - fmt.Printf(", ") + tickets += ", " } - fmt.Printf("%s", ticket) + tickets += ticket } - fmt.Println() + log.Info(" Jira: %s", tickets) } if len(pr.JiraUpdates) > 0 { for _, update := range pr.JiraUpdates { if update.Success { - fmt.Printf(" ✓ %s: %s → %s\n", update.Ticket, update.OldStatus, update.NewStatus) + log.Info(" ✓ %s: %s → %s", update.Ticket, update.OldStatus, update.NewStatus) } else { - fmt.Printf(" ✗ %s: %s\n", update.Ticket, update.Error) + log.Info(" ✗ %s: %s", update.Ticket, update.Error) } } } - fmt.Printf(" Processed: %s\n", pr.ProcessedAt.Format("2006-01-02 15:04:05")) + log.Info(" Processed: %s", pr.ProcessedAt.Format("2006-01-02 15:04:05")) } - fmt.Println() - fmt.Printf("Total: %d PRs processed, %d Jira updates\n", len(prs), state.Stats.TotalJiraUpdated) - fmt.Println() + log.Info("") + log.Info("Total: %d PRs processed, %d Jira updates", len(prs), state.Stats.TotalJiraUpdated) + log.Info("") } func runWatchConfig(cmd *cobra.Command, args []string) { cfg := config.Get() if cfg == nil { - ui.Error("Configuration not found") + log.Error("Configuration not found") return } - fmt.Println() - fmt.Println("⚙️ Watch Daemon Configuration") - fmt.Println() + log.Info("") + log.Info("⚙️ Watch Daemon Configuration") + log.Info("") // Show watching list status watchingList, err := watcher.NewWatchingList() if err == nil { - fmt.Println("📋 Watching List:") - fmt.Printf(" Total PRs: %d\n", watchingList.Count()) + log.Info("📋 Watching List:") + log.Info(" Total PRs: %d", watchingList.Count()) if watchingList.Count() > 0 { for _, pr := range watchingList.GetAll() { - fmt.Printf(" • PR #%d: %s/%s\n", pr.PRNumber, pr.Owner, pr.Repo) + log.Info(" • PR #%d: %s/%s", pr.PRNumber, pr.Owner, pr.Repo) } } } - fmt.Println() - fmt.Println("⏰ Schedule:") - fmt.Println(" Daytime (8:30-24:00): Every 15 minutes") - fmt.Println(" Night (0:00-8:30): 2:00, 6:00") - fmt.Println(" Log Retention: 7 days") + log.Info("") + log.Info("⏰ Schedule:") + log.Info(" Daytime (8:30-24:00): Every 15 minutes") + log.Info(" Night (0:00-8:30): 2:00, 6:00") + log.Info(" Log Retention: 7 days") - fmt.Println() - fmt.Println("🔔 Notifications:") - fmt.Println(" Desktop Notify: Enabled (macOS)") + log.Info("") + log.Info("🔔 Notifications:") + log.Info(" Desktop Notify: Enabled (macOS)") // Check installation status installed, _ := watcher.IsLaunchAgentInstalled() running, pid, _ := watcher.IsRunning() - fmt.Println() - fmt.Println("📊 Status:") + log.Info("") + log.Info("📊 Status:") if installed { - fmt.Println(" Auto-start: Enabled ✅") + log.Info(" Auto-start: Enabled ✅") } else { - fmt.Println(" Auto-start: Not installed ❌") + log.Info(" Auto-start: Not installed ❌") } if running { - fmt.Printf(" Daemon: Running (PID: %d) ✅\n", pid) + log.Info(" Daemon: Running (PID: %d) ✅", pid) } else { - fmt.Println(" Daemon: Stopped ⏸️") + log.Info(" Daemon: Stopped ⏸️") } // Show Jira mappings @@ -868,16 +870,48 @@ func runWatchConfig(cmd *cobra.Command, args []string) { if err == nil { mappings, err := statusCache.ListAllMappings() if err == nil && len(mappings) > 0 { - fmt.Println() - fmt.Println("🎫 Jira Status Mappings:") + log.Info("") + log.Info("🎫 Jira Status Mappings:") for _, mapping := range mappings { - fmt.Printf(" %s:\n", mapping.ProjectKey) - fmt.Printf(" PR Created → %s\n", mapping.PRCreatedStatus) - fmt.Printf(" PR Merged → %s\n", mapping.PRMergedStatus) + log.Info(" %s:", mapping.ProjectKey) + log.Info(" PR Created → %s", mapping.PRCreatedStatus) + log.Info(" PR Merged → %s", mapping.PRMergedStatus) } } } - fmt.Println() + log.Info("") } +// readLastLines reads the last N lines from a file +func readLastLines(filePath string, n int) ([]string, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read log file: %w", err) + } + + lines := make([]string, 0) + currentLine := "" + + for i := 0; i < len(data); i++ { + if data[i] == '\n' { + if currentLine != "" { + lines = append(lines, currentLine) + } + currentLine = "" + } else { + currentLine += string(data[i]) + } + } + + if currentLine != "" { + lines = append(lines, currentLine) + } + + // Return last n lines + if len(lines) <= n { + return lines, nil + } + + return lines[len(lines)-n:], nil +} diff --git a/go-version/docs/CHANGELOG.md b/go-version/docs/CHANGELOG.md new file mode 100644 index 0000000..73bf615 --- /dev/null +++ b/go-version/docs/CHANGELOG.md @@ -0,0 +1,158 @@ +# 变更日志 + +> 本文档记录 qkflow 的所有版本更新和功能变更。 + +--- + +## 📋 目录 + +- [v1.2.0](#v120---2024-11-18) - PR 审批和 Jira Reader 功能 +- [v1.1.0](#v110---2024-11-01) - PR 编辑器功能 +- [v1.0.0](#v100---2024-10-15) - 初始版本 + +--- + +## [v1.2.0] - 2024-11-18 + +### 🎉 新增 + +#### PR 审批功能 + +- **PR 审批命令**:`qkflow pr approve` 快速审批 Pull Request + - 支持 PR 编号和 GitHub URL + - 支持 `/files`、`/commits`、`/checks` 等 URL 路径 + - 默认使用 👍 作为审批评论 + - 支持自定义评论(`-c` 标志) + - 支持审批后自动合并(`-m` 标志) + - 自动检测当前分支的 PR + +**使用示例**: +```bash +# 基本审批 +qkflow pr approve 123 + +# 通过 URL 审批(跨仓库) +qkflow pr approve https://github.com/owner/repo/pull/456 -c "LGTM!" -m + +# 自动检测当前分支 +qkflow pr approve +``` + +#### Jira Issue 读取和导出功能 + +- **Jira 阅读器**:针对 Cursor AI 优化的 Issue 读取工具 + - `qkflow jira show` - 快速终端查看 + - `qkflow jira export` - 导出到文件(支持图片) + - `qkflow jira read` - 智能模式(推荐用于 Cursor AI) + - `qkflow jira clean` - 清理导出文件 + +**使用示例**: +```bash +# 智能读取(Cursor AI 优化) +qkflow jira read NA-9245 + +# 导出包含图片 +qkflow jira export NA-9245 --with-images +``` + +### ✨ 改进 + +- **URL 支持**:`pr approve` 和 `pr merge` 命令现在支持 GitHub PR URL + - 可以从任何地方使用,无需在 git 目录中 + - 支持跨仓库操作 + - 支持从浏览器直接复制粘贴 URL + +- **交互体验**:改进 PR 创建时的编辑器选择界面 + - 从简单的 Yes/No 改为更直观的选择界面 + - 默认选项更清晰 + +### 🐛 修复 + +- 修复配置加载问题 +- 改进错误提示信息 + +--- + +## [v1.1.0] - 2024-11-01 + +### 🎉 新增 + +#### PR 编辑器功能 + +- **基于 Web 的 PR 描述编辑器** + - GitHub 风格界面,带深色主题 + - Markdown 编辑器,带实时预览 + - 支持拖放图片和视频 + - 支持从剪贴板粘贴图片 + - 自动上传到 GitHub 和 Jira + - 支持的格式:PNG、JPG、GIF、WebP、SVG、MP4、MOV、WebM、AVI + +**使用示例**: +```bash +# 创建 PR 时选择添加详细描述 +qkflow pr create PROJ-123 +# 选择 "是,继续" 打开 Web 编辑器 +``` + +### ✨ 改进 + +- 优化 PR 创建流程 +- 改进文件上传错误处理 + +--- + +## [v1.0.0] - 2024-10-15 + +### 🎉 初始版本 + +qkflow Go 版本的首次发布,包含以下核心功能: + +#### 核心功能 + +- **PR 管理** + - `qkflow pr create` - 创建 Pull Request + - `qkflow pr merge` - 合并 Pull Request + - `qkflow update` - 快速更新(使用 PR 标题作为提交信息) + +- **Jira 集成** + - 自动更新 Jira 状态 + - 添加 PR 链接到 Jira + - Jira 状态配置管理 + +- **配置管理** + - `qkflow init` - 交互式配置向导 + - `qkflow config` - 查看配置 + - iCloud Drive 同步(macOS) + +- **监控守护进程** + - `qkflow watch install` - 安装监控守护进程 + - 自动监控 PR 并在合并时更新 Jira + +### ✨ 特性 + +- 📦 单一二进制文件,无需依赖 +- ⚡ 快速启动(<100ms) +- 🌍 跨平台支持(macOS、Linux、Windows) +- 🎨 交互式 CLI,美观的提示 +- ☁️ iCloud 同步配置(macOS) + +--- + +## 📚 相关文档 + +- [PR 使用指南](./guidelines/usage/PR_GUIDELINES.md) - PR 功能完整使用指南 +- [Jira 使用指南](./guidelines/usage/JIRA_GUIDELINES.md) - Jira 功能完整使用指南 +- [快速开始指南](./QUICKSTART.md) - 5 分钟快速上手 +- [迁移指南](./MIGRATION.md) - 从 Shell 版本迁移 + +--- + +## 🔗 版本导航 + +- [最新版本](#v120---2024-11-18) +- [v1.1.0](#v110---2024-11-01) +- [v1.0.0](#v100---2024-10-15) + +--- + +**最后更新**:2025-12-05 diff --git a/go-version/docs/MIGRATION.md b/go-version/docs/MIGRATION.md new file mode 100644 index 0000000..08fa344 --- /dev/null +++ b/go-version/docs/MIGRATION.md @@ -0,0 +1,328 @@ +# 迁移指南 + +> 本文档包含从 Shell 版本迁移到 Go 版本以及 iCloud 配置迁移的完整指南。 + +--- + +## 📋 目录 + +- [Shell 到 Go 版本迁移](#-shell-到-go-版本迁移) +- [iCloud 配置迁移](#-icloud-配置迁移) + +--- + +## 🔄 Shell 到 Go 版本迁移 + +### 🎯 为什么迁移? + +| 方面 | Shell 版本 | Go 版本 | 改进 | +|------|-----------|---------|------| +| **安装** | Clone repo + 安装 4+ 依赖 | 下载 1 个二进制文件 | ✅ 简化 90% | +| **配置** | 手动在 `.zshrc` 中设置环境变量 | 交互式 `qkflow init` | ✅ 更容易 | +| **启动时间** | ~1-2 秒 | <100ms | ✅ 快 10-20 倍 | +| **跨平台** | 仅 macOS/Linux | macOS/Linux/Windows | ✅ 通用 | +| **更新** | `git pull` + 重新安装 | 下载新二进制文件 | ✅ 更简单 | + +### 📋 前置条件 + +迁移前,请确保: +- ✅ 当前 Shell 版本正常工作 +- ✅ 可以访问 Jira 和 GitHub 凭证 +- ✅ 记录当前配置(特别是环境变量) + +### 🔄 迁移步骤 + +#### 步骤 1: 备份当前配置 + +```bash +# 保存当前环境变量 +cat ~/.zshrc | grep -E "(EMAIL|JIRA|GH_|OPENAI|DEEPSEEK)" > ~/qk-backup.txt +``` + +#### 步骤 2: 安装 Go 版本 + +```bash +# macOS (Apple Silicon) +curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-arm64 -o qkflow +chmod +x qkflow +sudo mv qkflow /usr/local/bin/ + +# macOS (Intel) +curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-amd64 -o qkflow +chmod +x qkflow +sudo mv qkflow /usr/local/bin/ + +# 验证安装 +qkflow version +``` + +#### 步骤 3: 运行初始设置 + +```bash +qkflow init +``` + +这将提示你输入: +- **Email**: 使用 `EMAIL` 环境变量的值 +- **GitHub Token**: 将从 `gh auth token` 自动检测 +- **Jira Service Address**: 使用 `JIRA_SERVICE_ADDRESS` 的值 +- **Jira API Token**: 使用 `JIRA_API_TOKEN` 的值 +- **Branch Prefix**(可选): 使用 `GH_BRANCH_PREFIX` 的值 + +#### 步骤 4: 测试新版本 + +```bash +# 测试 PR 创建 +cd your-project +git checkout -b test-qkflow-migration +echo "test" > test.txt +git add test.txt +qkflow pr create + +# 如果成功,你会看到: +# ✅ Branch created +# ✅ Changes committed +# ✅ Pushed to remote +# ✅ Pull request created: https://github.com/... +``` + +#### 步骤 5: 更新 Shell 别名(可选) + +如果你在 Shell 版本中有自定义别名,请更新它们: + +```bash +# 旧别名(Shell 版本) +alias prc='~/quick-workflow/pr-create.sh' +alias prm='~/quick-workflow/pr-merge.sh' + +# 新别名(Go 版本) +alias prc='qkflow pr create' +alias prm='qkflow pr merge' +``` + +#### 步骤 6: 清理旧安装(可选) + +一旦你验证了 Go 版本正常工作: + +```bash +# 从 .zshrc 中删除旧环境变量 +# 编辑 ~/.zshrc 并删除: +# export JIRA_API_TOKEN=... +# export JIRA_SERVICE_ADDRESS=... +# export GH_BRANCH_PREFIX=... + +# 重新加载 shell +source ~/.zshrc + +# 归档旧安装 +mv ~/quick-workflow ~/quick-workflow-shell-backup +``` + +### 🔍 命令映射 + +| Shell 版本 | Go 版本 | 说明 | +|-----------|---------|------| +| `pr-create.sh` | `qkflow pr create` | 相同功能,更快 | +| `pr-merge.sh` | `qkflow pr merge` | 相同功能,更快 | +| N/A | `qkflow init` | 新功能:设置向导 | +| N/A | `qkflow config` | 新功能:显示配置 | + +### 🐛 故障排除 + +#### 问题:找不到命令 "qkflow" + +```bash +# 检查二进制文件是否在 PATH 中 +which qkflow + +# 如果未找到,确保 /usr/local/bin 在 PATH 中 +export PATH="/usr/local/bin:$PATH" +``` + +#### 问题:"Config not found" + +```bash +# 运行设置向导 +qkflow init +``` + +#### 问题:"Failed to create GitHub client" + +```bash +# 确保 gh CLI 已认证 +gh auth status + +# 如果未认证,登录 +gh auth login + +# 重新运行 qkflow init +qkflow init +``` + +#### 问题:"Failed to get Jira issue" + +1. 验证 Jira API token 是否正确 +2. 检查 Jira service address 格式:`https://your-domain.atlassian.net` +3. 测试 Jira 凭证: + ```bash + curl -u your.email@example.com:your_jira_token \ + https://your-domain.atlassian.net/rest/api/2/myself + ``` + +### 🎉 迁移检查清单 + +- [ ] 已备份当前配置 +- [ ] 已安装 Go 版本 +- [ ] 已运行 `qkflow init` 并配置 +- [ ] 已测试 PR 创建 +- [ ] 已验证 Jira 集成工作正常 +- [ ] 已更新 shell 别名(如果有) +- [ ] 已删除旧环境变量 +- [ ] 已归档旧 Shell 安装 + +--- + +## ☁️ iCloud 配置迁移 + +### 🌟 新特性 + +在 macOS 上,`qkflow` 会优先将配置存储到 iCloud Drive,实现多设备自动同步! + +### 📍 存储位置 + +#### macOS with iCloud Drive(推荐) + +``` +~/Library/Mobile Documents/com~apple~CloudDocs/.qkflow/ +├── config.yaml # 主配置文件 +└── jira-status.json # Jira 状态映射 +``` + +配置会自动在你的所有 Mac 设备间同步 ☁️ + +#### 本地存储(回退方案) + +``` +~/.qkflow/ +├── config.yaml # 主配置文件 +└── jira-status.json # Jira 状态映射 +``` + +运行 `qkflow config` 查看实际存储位置。 + +### 🔄 迁移到 iCloud + +如果你之前使用的是本地配置,可以手动迁移到 iCloud: + +```bash +# 1. 确保 iCloud Drive 已启用 +# 打开"系统设置" → "Apple ID" → "iCloud" → 确保"iCloud Drive"已开启 + +# 2. 迁移配置文件 +if [ -f ~/.qkflow/config.yaml ]; then + cp ~/.qkflow/config.yaml \ + ~/Library/Mobile\ Documents/com~apple~CloudDocs/.qkflow/config.yaml +fi + +if [ -f ~/.qkflow/jira-status.json ]; then + cp ~/.qkflow/jira-status.json \ + ~/Library/Mobile\ Documents/com~apple~CloudDocs/.qkflow/jira-status.json +fi + +# 3. 验证迁移 +qkflow config +``` + +### 🎯 多设备使用 + +在新的 Mac 设备上: + +1. **安装 qkflow** + ```bash + curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-arm64 -o qkflow + chmod +x qkflow + sudo mv qkflow /usr/local/bin/ + ``` + +2. **等待 iCloud 同步** + - 打开 Finder → iCloud Drive + - 确保 `.qkflow` 文件夹已同步完成 + +3. **验证配置** + ```bash + qkflow config + ``` + +配置会自动从 iCloud Drive 读取,无需重新配置! + +### 🔒 安全说明 + +- iCloud Drive 存储是加密的 +- 配置文件权限设置为 `0600`(仅用户可读写) +- Token 和密钥安全地存储在你的 iCloud 账户中 +- 只有登录同一 Apple ID 的设备才能访问 + +### ⚠️ 注意事项 + +#### iCloud 同步延迟 + +iCloud Drive 同步可能需要几秒到几分钟,取决于网络连接速度和文件大小。 + +#### 离线工作 + +如果没有网络连接: +- 仍然可以读取和修改配置 +- 更改会在网络恢复后自动同步 + +#### 回退到本地存储 + +如果你不想使用 iCloud Drive: + +```bash +# 1. 禁用 iCloud Drive(系统设置) +# 2. qkflow 会自动回退到本地存储 + +# 或者手动移动配置回本地 +cp ~/Library/Mobile\ Documents/com~apple~CloudDocs/.qkflow/config.yaml \ + ~/.qkflow/config.yaml +``` + +### 🐛 故障排除 + +#### 问题 1: "No configuration found" 错误 + +```bash +# 检查 iCloud Drive 是否可用 +ls -la ~/Library/Mobile\ Documents/com~apple~CloudDocs/ + +# 如果目录不存在,启用 iCloud Drive +# 系统设置 → Apple ID → iCloud → iCloud Drive + +# 手动创建配置 +qkflow init +``` + +#### 问题 2: 配置不同步 + +```bash +# 1. 检查 iCloud 同步状态 +# 打开 Finder → iCloud Drive → 检查文件是否有云图标 + +# 2. 强制同步 +# 右键点击文件 → "从 iCloud 下载" + +# 3. 检查网络连接 +ping icloud.com +``` + +--- + +## 📚 相关文档 + +- [快速开始指南](../README.md#-快速开始) - 快速开始 +- [Jira 使用指南](./guidelines/usage/JIRA_GUIDELINES.md) - Jira 功能完整使用指南 +- [PR 使用指南](./guidelines/usage/PR_GUIDELINES.md) - PR 功能完整使用指南 + +--- + +**最后更新**:2025-12-05 diff --git a/go-version/docs/QUICKSTART.md b/go-version/docs/QUICKSTART.md new file mode 100644 index 0000000..5cca208 --- /dev/null +++ b/go-version/docs/QUICKSTART.md @@ -0,0 +1,229 @@ +# 快速开始指南 + +5 分钟快速上手 qkflow! + +> 💡 **提示**:这是快速入门指南。需要完整文档?请查看 [README.md](../README.md)。 + +## 📦 安装(30 秒) + +选择你的平台,复制粘贴即可: + +### macOS +```bash +# Apple Silicon (M1/M2/M3) - 推荐 +curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-arm64 -o qkflow && \ +chmod +x qkflow && \ +sudo mv qkflow /usr/local/bin/ + +# Intel Mac +curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-darwin-amd64 -o qkflow && \ +chmod +x qkflow && \ +sudo mv qkflow /usr/local/bin/ +``` + +### Linux +```bash +curl -L https://github.com/Wangggym/quick-workflow/releases/latest/download/qkflow-linux-amd64 -o qkflow && \ +chmod +x qkflow && \ +sudo mv qkflow /usr/local/bin/ +``` + +> 📖 **其他安装方式**:使用 Go 安装或从源码构建?查看 [README.md](../README.md#-安装)。 + +> ⚠️ **macOS 安全提示**:如果看到安全警告,运行 `xattr -d com.apple.quarantine qkflow` 移除隔离属性。 + +## ⚙️ 配置(2 分钟) + +### 前置条件 + +在运行 `qkflow init` 之前,确保: + +1. **GitHub CLI 已安装并认证** + ```bash + brew install gh # macOS + gh auth login + ``` + +2. **获取 Jira API 令牌**(如果使用 Jira) + - 访问:https://id.atlassian.com/manage-profile/security/api-tokens + - 创建新令牌并复制 + +### 运行设置向导 + +```bash +qkflow init +``` + +按照提示输入: +- **邮箱**:你的工作邮箱 +- **GitHub Token**:自动从 `gh` CLI 检测(无需手动输入) +- **Jira 地址**:`https://your-domain.atlassian.net`(可选) +- **Jira Token**:粘贴刚才获取的令牌(可选) +- **分支前缀**:可选(例如:`feature` 或你的用户名) + +> 📖 **配置详情**:关于配置存储位置(iCloud 同步等),查看 [README.md](../README.md#-配置)。 + +## 🎯 创建第一个 PR(2 分钟) + +### 步骤 1:进行更改 + +```bash +cd your-project +git checkout -b feature/test + +# 进行一些更改 +echo "# Test" >> README.md +git add README.md +``` + +### 步骤 2:创建 PR + +```bash +qkflow pr create PROJ-123 +``` + +按照提示操作: +1. **标题**:接受建议或输入自定义标题 +2. **描述**:可选的简短描述 +3. **变更类型**:选择适用的类型(feat、fix 等) +4. **Jira 状态**:选择新状态(可选) + +**完成!** 你的 PR 已创建,Jira 已更新!🎉 + +## 🔄 合并 PR(1 分钟) + +```bash +qkflow pr merge 123 +``` + +按照提示操作: +1. **确认合并**:查看 PR 详情 +2. **删除分支**:选择是否清理 +3. **更新 Jira**:设置最终状态 + +**完成!** PR 已合并并清理!🎉 + +## 💡 专业技巧 + +### 不使用 Jira + +```bash +# 跳过 Jira ticket(提示时按 Enter) +qkflow pr create +``` + +### 提示中的键盘快捷键 + +- **方向键**:导航选项 +- **空格**:选择/取消选择(多选) +- **Enter**:确认选择 +- **Ctrl+C**:取消操作 + +### 快速命令 + +```bash +# 显示配置 +qkflow config + +# 显示版本 +qkflow version + +# 获取帮助 +qkflow --help +qkflow pr --help +``` + +## 🎨 工作流示例 + +### 典型开发流程 + +```bash +# 1. 创建功能分支 +git checkout -b feature/awesome-feature + +# 2. 进行更改并暂存 +# ... 编写代码 ... +git add . + +# 3. 创建 PR(自动提交、推送、创建 PR、更新 Jira) +qkflow pr create PROJ-456 +# 按照提示输入标题、描述、变更类型等 + +# 4. 等待代码审查... + +# 5. 合并 PR(自动清理分支、更新 Jira) +qkflow pr merge 789 +``` + +### 快速更新现有 PR + +```bash +# 修改代码后,快速提交和推送 +qkflow update # 使用 PR 标题作为提交信息 +``` + +> 📖 **更多工作流示例**:查看 [README.md](../README.md#-工作流示例) 和 [PR 使用指南](guidelines/usage/PR_GUIDELINES.md)。 + +## 🐛 常见问题 + +### "Command not found: qkflow" + +```bash +# 检查二进制文件是否存在 +ls -l /usr/local/bin/qkflow + +# 检查 PATH +echo $PATH | grep -q "/usr/local/bin" && echo "OK" || echo "Add to PATH" + +# 如需要,添加到 PATH(添加到 ~/.zshrc) +export PATH="/usr/local/bin:$PATH" +``` + +### "Failed to create GitHub client" + +```bash +# 确保 gh 已认证 +gh auth status + +# 如果未认证 +gh auth login + +# 重新运行 qkflow init +qkflow init +``` + +### "Failed to get Jira issue" + +```bash +# 验证 Jira 凭据 +curl -u "your.email@example.com:your_jira_token" \ + https://your-domain.atlassian.net/rest/api/2/myself + +# 如果失败,获取新的 API token 并重新运行 qkflow init +``` + +## 📚 下一步 + +现在你已经掌握了基础操作!想要了解更多? + +- 📖 **完整功能文档**:[README.md](../README.md) - 所有命令和功能的详细说明 +- 📝 **PR 使用指南**:[PR_GUIDELINES.md](guidelines/usage/PR_GUIDELINES.md) - PR 功能的完整指南 +- 🎫 **Jira 使用指南**:[JIRA_GUIDELINES.md](guidelines/usage/JIRA_GUIDELINES.md) - Jira 集成详细说明 +- 🔄 **迁移指南**:[MIGRATION.md](MIGRATION.md) - 从 Shell 版本迁移 +- 🐛 **遇到问题?**:[GitHub Issues](https://github.com/Wangggym/quick-workflow/issues) + +## 🎉 准备就绪! + +恭喜!你现在已经设置好 qkflow。享受流畅的工作流吧! + +**常用命令备忘:** +```bash +qkflow pr create # 创建 PR +qkflow pr merge # 合并 PR +qkflow config # 显示配置 +qkflow --help # 获取帮助 +``` + +--- + +**祝编码愉快!🚀** diff --git a/go-version/docs/README.md b/go-version/docs/README.md new file mode 100644 index 0000000..cc36e28 --- /dev/null +++ b/go-version/docs/README.md @@ -0,0 +1,264 @@ +# qkflow 文档索引 + +## 📚 文档概览 + +本文档目录包含 qkflow CLI 工具的完整使用指南、架构文档和开发文档。 + +--- + +## 🚀 快速开始 + +### [README.md](../README.md#-快速开始) +**入门指南** + +- 安装方法(预编译二进制、源码构建) +- 初始化配置 +- 基本使用示例 +- 常见问题解答 + +### [QUICKSTART.md](./QUICKSTART.md) +**快速开始指南** + +- 快速上手指南 +- 常用命令速查 +- 典型工作流示例 + +--- + +## 📖 使用指南 + +> 所有使用指南位于 [`guidelines/`](./guidelines/) 目录下 + +### [README.md](./guidelines/README.md) +**指南目录说明** + +- 目录作用和功能说明 +- 用户指南和开发指南分类 +- 文档列表和使用场景 + +### 用户使用指南 (`usage/`) + +> 位于 [`guidelines/usage/`](./guidelines/usage/) 目录下 + +### [PR_GUIDELINES.md](./guidelines/usage/PR_GUIDELINES.md) +**PR 功能完整指南** + +- PR 审批功能(支持 PR 编号、URL、自动检测) +- PR URL 支持(通过 URL 操作 PR) +- PR 编辑器功能(添加详细描述和截图) +- 自定义审批评论和批量审批 + +### [JIRA_GUIDELINES.md](./guidelines/usage/JIRA_GUIDELINES.md) +**Jira 使用指南** + +- Jira Issue 读取和导出功能 +- Jira 状态配置管理 +- 与 Cursor AI 集成使用 +- 批量处理功能 + +### [AUTO_UPDATE_GUIDELINES.md](./guidelines/usage/AUTO_UPDATE_GUIDELINES.md) +**自动更新功能指南** + +- 自动更新机制说明 +- 配置自动更新 +- 手动更新方法 +- 更新频率和安全性 + +--- + +## 🏗️ 架构文档 + +> 所有架构文档位于 [`architecture/`](./architecture/) 目录下 + +### [README.md](./architecture/README.md) +**架构目录说明** + +- 目录作用和功能说明 +- 文档内容概述 +- 使用场景指南 + +### [ARCHITECTURE.md](./architecture/ARCHITECTURE.md) +**项目架构文档** + +- 项目概述和特性 +- 技术栈和依赖 +- 项目结构详解 +- 模块组织方式 +- 设计原则和模式 +- 代码组织规范 + +--- + +## 🔄 迁移文档 + +> 迁移文档位于 [`docs/`](./) 目录下 + +### [MIGRATION.md](./MIGRATION.md) +**迁移指南** + +- Shell 到 Go 版本迁移 +- iCloud 配置迁移 +- 功能更新说明 + +--- + +## 📋 需求文档 + +> 需求文档位于 [`requirements/`](./requirements/) 目录下 + +### [README.md](./requirements/README.md) +**需求文档目录说明** + +- 功能开发需求文档 +- 代码优化需求文档 +- 使用指南 + +### [TODO.md](./requirements/TODO.md) +**功能开发需求文档** + +- 从 workflow.go 和 workflow.rs 可以添加的功能 +- 按优先级分类的功能列表 +- 实现建议和开发顺序 + +### [OPTIMIZATION.md](./requirements/OPTIMIZATION.md) +**代码优化需求文档** + +- 架构优化需求 +- 性能优化需求 +- 用户体验优化需求 +- 代码质量优化需求 + +--- + +## 📝 变更日志 + +> 变更日志位于 [`docs/`](./) 目录下 + +### [CHANGELOG.md](./CHANGELOG.md) +**功能变更日志** + +- 所有功能的版本更新记录 +- PR 审批功能变更 +- Jira Reader 功能变更 +- 新功能添加 +- Bug 修复和改进 + +--- + +## 🚢 发布文档 + +> 发布文档位于 [`guidelines/development/`](./guidelines/development/) 目录下 + +### [RELEASE_GUIDELINES.md](./guidelines/development/RELEASE_GUIDELINES.md) +**发布操作指南** + +- 快速入门 +- 发布流程说明 +- 版本号规范 +- GitHub Actions 自动构建 +- 发布检查清单 +- 详细操作说明 + +--- + +## 📖 快速导航 + +### 新用户 +- 第一次使用? → [README.md](../README.md#-快速开始) +- 想快速上手? → [QUICKSTART.md](./QUICKSTART.md) + +### 功能使用 +- 想使用 PR 功能? → [PR_GUIDELINES.md](./guidelines/usage/PR_GUIDELINES.md) +- 想使用 Jira 功能? → [JIRA_GUIDELINES.md](./guidelines/usage/JIRA_GUIDELINES.md) +- 想了解自动更新? → [AUTO_UPDATE_GUIDELINES.md](./guidelines/usage/AUTO_UPDATE_GUIDELINES.md) + +### 架构和开发 +- 想了解项目架构? → [ARCHITECTURE.md](./architecture/ARCHITECTURE.md) +- 想了解开发规范? → [DEVELOPMENT_GUIDELINES.md](./guidelines/development/DEVELOPMENT_GUIDELINES.md) +- 想了解文档规范? → [DOCUMENT_GUIDELINES.md](./guidelines/development/DOCUMENT_GUIDELINES.md) +- 想贡献代码? → [CONTRIBUTING.md](./guidelines/development/CONTRIBUTING.md) + +### 迁移和更新 +- 需要迁移? → [MIGRATION.md](./MIGRATION.md) +- 查看变更日志? → [CHANGELOG.md](./CHANGELOG.md) + +### 需求和优化 +- 想了解功能需求? → [TODO.md](./requirements/TODO.md) +- 想了解优化需求? → [OPTIMIZATION.md](./requirements/OPTIMIZATION.md) + +### 发布 +- 想发布新版本? → [RELEASE_GUIDELINES.md](./guidelines/development/RELEASE_GUIDELINES.md) + +--- + +## 📝 文档结构说明 + +文档按照以下分类组织: + +1. **guidelines/** - 所有指南文档 + - **usage/** - 用户使用指南和功能文档 + - **development/** - 开发规范和文档编写指南(包括贡献指南) +2. **architecture/** - 项目架构和结构文档 +3. **requirements/** - 需求文档(`TODO.md`、`OPTIMIZATION.md`) +4. **docs/** - 迁移指南和变更日志(`MIGRATION.md`、`CHANGELOG.md`) + +--- + +## 🔄 文档更新记录 + +- **2025-12-05** - 文档优化和重构,统一文档风格,精简内容 +- **2025-11-18** - 添加 PR 审批和 Jira Reader 功能文档 +- **2025-10-15** - 创建文档索引和目录结构 + +--- + +## 📋 开发文档 + +> 开发相关文档位于 [`guidelines/development/`](./guidelines/development/) 目录下 + +> 详见 [指南目录说明](./guidelines/README.md) + +### [DOCUMENT_GUIDELINES.md](./guidelines/development/DOCUMENT_GUIDELINES.md) +**文档编写指南** + +- 文档类型和模板说明 +- 使用指南模板 +- 架构文档模板 +- 迁移文档模板 +- 变更日志模板 +- 发布文档模板 +- 文档格式规范 + +### [DEVELOPMENT_GUIDELINES.md](./guidelines/development/DEVELOPMENT_GUIDELINES.md) +**开发规范文档** + +- 代码风格规范(格式化、Lint、命名约定) +- 错误处理规范 +- 文档规范 +- 命名规范 +- 模块组织规范 +- Git 工作流 +- 提交规范 +- 测试规范 +- 代码审查 +- 依赖管理 +- 开发工具 + +### [CONTRIBUTING.md](./guidelines/development/CONTRIBUTING.md) +**贡献指南** + +- 如何开始贡献 +- 开发环境设置 +- 代码风格和提交规范 +- 测试指南 +- PR 审查流程 +- 错误报告和功能请求 +- 贡献领域 + +--- + +## 📚 相关资源 + +- [主 README](../README.md) - 项目主文档 +- [GitHub Repository](https://github.com/Wangggym/quick-workflow) - 源代码仓库 +- [Issues](https://github.com/Wangggym/quick-workflow/issues) - 问题反馈 diff --git a/go-version/docs/architecture/ARCHITECTURE.md b/go-version/docs/architecture/ARCHITECTURE.md new file mode 100644 index 0000000..19afbef --- /dev/null +++ b/go-version/docs/architecture/ARCHITECTURE.md @@ -0,0 +1,521 @@ +# 项目架构文档 + +> 本文档包含 qkflow 项目的完整架构说明,包括项目概述、技术栈、项目结构和设计原则。 + +--- + +## 📋 项目概述 + +### 📊 项目摘要 + +**状态**:✅ 已完成并可使用 +**语言**:Go 1.21+ +**类型**:CLI 工具 +**目的**:简化 GitHub 和 Jira 工作流 + +### 🎯 核心功能 + +#### 核心功能 +- ✅ 自动分支管理的 PR 创建 +- ✅ 带清理功能的 PR 合并 +- ✅ Jira 集成(状态更新、评论、链接) +- ✅ GitHub API 集成(PR CRUD 操作) +- ✅ Git 操作(分支、提交、推送、合并) +- ✅ 交互式 CLI 和美观的提示 +- ✅ 通过 `qk init` 进行配置管理 + +#### 用户体验 +- ✅ 单二进制分发(无依赖) +- ✅ 跨平台支持(macOS、Linux、Windows) +- ✅ 彩色输出和进度指示器 +- ✅ 清晰的错误消息 +- ✅ 交互式用户输入提示 +- ✅ 剪贴板集成(macOS) + +#### 开发者体验 +- ✅ 模块化架构 +- ✅ 类型安全的代码 +- ✅ 全面的错误处理 +- ✅ 易于测试和扩展 +- ✅ 文档完善的代码 +- ✅ 支持 GitHub Actions 的 CI/CD + +### 📈 与 Shell 版本对比 + +| 指标 | Shell 版本 | Go 版本 | 改进 | +|------|-----------|---------|------| +| **二进制大小** | N/A | ~15MB | 自包含 | +| **启动时间** | ~1.5s | <100ms | 15 倍更快 | +| **依赖项** | 4+ 工具 | 0 | 减少 100% | +| **安装** | 多步骤 | 一个命令 | 更简单 | +| **错误处理** | 基础 | 全面 | 更好 | +| **类型安全** | 无 | 完整 | 更安全 | +| **测试** | 有限 | 全面 | 更可靠 | +| **平台** | macOS/Linux | macOS/Linux/Windows | 更多平台 | +| **维护** | 手动 | 自动化 | 更容易更新 | + +--- + +## 🔧 技术栈 + +### 核心依赖 + +- **cobra**:CLI 框架,用于命令结构 +- **viper**:配置管理 +- **survey**:交互式提示和用户输入 +- **go-github**:官方 GitHub API 客户端 +- **go-jira**:Jira API 客户端 +- **oauth2**:OAuth2 认证 +- **fatih/color**:终端颜色 + +### 构建工具 + +- **go 1.21+**:语言和工具链 +- **make**:构建自动化 +- **golangci-lint**:代码检查 +- **GitHub Actions**:CI/CD + +--- + +## 📁 项目结构 + +``` +go-version/ +│ +├── 📝 配置文件 +│ ├── go.mod # Go 模块定义 +│ ├── go.sum # 依赖校验和 +│ ├── Makefile # 构建自动化 +│ ├── .gitignore # Git 忽略规则 +│ └── .golangci.yml # Linter 配置 +│ +├── 🎯 主应用程序 +│ └── cmd/ +│ └── qkflow/ +│ ├── main.go # 应用程序入口点 +│ └── commands/ +│ ├── root.go # 根命令和应用设置 +│ ├── init.go # 设置向导(qkflow init) +│ ├── pr.go # PR 命令组 +│ ├── pr_create.go # PR 创建逻辑 +│ └── pr_merge.go # PR 合并逻辑 +│ +├── 🔒 内部包 +│ └── internal/ +│ ├── github/ +│ │ └── client.go # GitHub API 客户端 +│ │ ├── NewClient() +│ │ ├── CreatePullRequest() +│ │ ├── GetPullRequest() +│ │ ├── MergePullRequest() +│ │ └── ParseRepositoryFromURL() +│ │ +│ ├── jira/ +│ │ └── client.go # Jira API 客户端 +│ │ ├── NewClient() +│ │ ├── GetIssue() +│ │ ├── UpdateStatus() +│ │ ├── AddComment() +│ │ ├── AddPRLink() +│ │ └── GetProjectStatuses() +│ │ +│ ├── git/ +│ │ └── operations.go # Git 命令包装器 +│ │ ├── CheckStatus() +│ │ ├── GetCurrentBranch() +│ │ ├── CreateBranch() +│ │ ├── Commit() +│ │ ├── Push() +│ │ ├── DeleteBranch() +│ │ ├── DeleteRemoteBranch() +│ │ ├── GetRemoteURL() +│ │ └── SanitizeBranchName() +│ │ +│ ├── config/ +│ │ └── config.go # 配置管理 +│ │ ├── Load() +│ │ ├── Get() +│ │ ├── Save() +│ │ ├── Validate() +│ │ └── IsConfigured() +│ │ +│ └── ui/ +│ └── prompt.go # 用户界面辅助函数 +│ ├── Success() +│ ├── Error() +│ ├── Warning() +│ ├── Info() +│ ├── PromptInput() +│ ├── PromptPassword() +│ ├── PromptConfirm() +│ ├── PromptSelect() +│ └── PromptMultiSelect() +│ +├── 📦 公共包 +│ └── (无 - 所有包都是内部的) +│ +├── 🛠️ 脚本 +│ └── scripts/ +│ ├── install.sh # 安装脚本 +│ ├── test.sh # 测试运行脚本 +│ └── release.sh # 发布自动化 +│ +├── 🤖 CI/CD +│ └── .github/ +│ └── workflows/ +│ └── build.yml # GitHub Actions 工作流 +│ +├── 📚 文档 +│ ├── README.md # 主文档 +│ ├── README.md # 主文档(包含快速开始) +│ └── docs/ # 详细文档 +│ ├── MIGRATION.md # 迁移指南(Shell → Go、iCloud、功能更新) +│ ├── CHANGELOG.md # 变更日志 +│ ├── guidelines/ # 使用和开发指南 +│ └── architecture/ # 架构文档 +│ +├── 📄 法律 +│ └── LICENSE # MIT 许可证 +│ +└── 🔨 构建输出(gitignored) + └── bin/ # 编译的二进制文件 + ├── qkflow # 当前平台 + ├── qkflow-darwin-amd64 # macOS Intel + ├── qkflow-darwin-arm64 # macOS Apple Silicon + ├── qkflow-linux-amd64 # Linux + └── qkflow-windows-amd64.exe # Windows +``` + +### 📊 文件统计 + +| 类别 | 文件数 | 代码行数(估算) | +|------|--------|-----------------| +| Go 源代码 | 10+ | ~2,000+ | +| 文档 | 10+ | ~3,000+ | +| 脚本 | 3 | ~300 | +| 配置 | 5 | ~200 | +| **总计** | **28+** | **~5,500+** | + +--- + +## 🏗️ 架构设计 + +### 设计原则 + +1. **模块化**:将关注点分离到不同的包中 +2. **可测试性**:易于模拟和测试 +3. **用户优先**:优先考虑用户体验 +4. **类型安全**:利用 Go 的类型系统 +5. **错误处理**:清晰、可操作的错误 +6. **性能**:快速启动和执行 + +### 包结构说明 + +#### `cmd/qkflow/commands` +- CLI 命令定义 +- 用户交互逻辑 +- 命令编排 + +#### `internal/github` +- GitHub API 客户端包装器 +- PR 操作(创建、获取、合并) +- 仓库解析 + +#### `internal/jira` +- Jira API 客户端包装器 +- Issue 操作(获取、更新) +- 状态管理 + +#### `internal/git` +- Git 命令执行 +- 分支管理 +- 提交和推送操作 + +#### `internal/ui` +- 用户提示和输入 +- 彩色输出 +- 进度指示器 + +#### `internal/config` +- 配置加载和保存 +- 环境变量支持 +- 验证 + +### 🔗 包依赖关系 + +``` +cmd/qkflow/commands + ├─→ internal/github + ├─→ internal/jira + ├─→ internal/git + ├─→ internal/ui + └─→ internal/config + +internal/github + └─→ internal/config + +internal/jira + └─→ internal/config + +internal/config + └─→ internal/utils + +internal/git + └─→ (无内部依赖) + +internal/ui + └─→ (无内部依赖) +``` + +### 📖 关键文件说明 + +#### 入口点 +- **`cmd/qkflow/main.go`**:应用程序入口点,调用命令执行 + +#### 命令 +- **`commands/root.go`**:根命令设置、版本、配置显示 +- **`commands/init.go`**:首次配置的交互式设置向导 +- **`commands/pr.go`**:PR 命令组(create/merge 的父命令) +- **`commands/pr_create.go`**:完整的 PR 创建工作流 +- **`commands/pr_merge.go`**:完整的 PR 合并工作流 + +#### 核心库 +- **`internal/github/client.go`**:带类型接口的 GitHub API 包装器 +- **`internal/jira/client.go`**:带状态管理的 Jira API 包装器 +- **`internal/git/operations.go`**:Git 命令执行和分支管理 +- **`internal/ui/prompt.go`**:用户交互和彩色输出 + +#### 基础设施 +- **`internal/config/config.go`**:配置加载、保存、验证 +- **`Makefile`**:构建命令(build、test、lint、install) +- **`.github/workflows/build.yml`**:多平台构建的 CI/CD 管道 + +--- + +## 🎨 设计模式 + +1. **工厂模式**:客户端创建(`NewClient()`) +2. **命令模式**:CLI 命令结构 +3. **仓库模式**:API 客户端抽象数据访问 +4. **外观模式**:简化复杂操作的接口 +5. **策略模式**:不同的 PR 类型和工作流 + +--- + +## 🔍 代码组织原则 + +### 1. 关注点分离 +- `cmd/` - CLI 接口和用户交互 +- `internal/` - 业务逻辑、API 客户端和配置 + +### 2. 依赖方向 +- 命令依赖内部包 +- 内部包可以依赖其他内部包 +- 无循环依赖 + +### 3. 可见性 +- `internal/` - 模块私有(外部项目无法导入) +- `cmd/` - 应用程序入口点 + +### 4. 测试 +- 每个包都有自己的测试 +- 模拟外部依赖 +- 表驱动测试模式 + +--- + +## 📦 外部依赖 + +``` +核心框架: +├── github.com/spf13/cobra # CLI 框架 +├── github.com/spf13/viper # 配置 +└── github.com/AlecAivazis/survey/v2 # 交互式提示 + +API 客户端: +├── github.com/google/go-github/v57 # GitHub API +├── github.com/andygrunwald/go-jira # Jira API +└── golang.org/x/oauth2 # OAuth2 认证 + +工具: +└── github.com/fatih/color # 终端颜色 +``` + +--- + +## 🚀 构建产物 + +运行 `make build-all` 后: + +``` +bin/ +├── qkflow-darwin-amd64 # macOS Intel (12-15MB) +├── qkflow-darwin-arm64 # macOS M1/M2 (12-15MB) +├── qkflow-linux-amd64 # Linux x86_64 (12-15MB) +└── qkflow-windows-amd64.exe # Windows 64-bit (12-15MB) +``` + +--- + +## 📈 项目指标 + +- **总代码行数**:~5,500+ +- **Go 文件**:10+ +- **包**:6+ +- **命令**:4+ +- **函数**:~80+ +- **结构体**:~15+ +- **接口**:~5+ + +--- + +## 🧪 测试策略 + +### 单元测试 +- 测试单个函数 +- 模拟外部依赖 +- 使用表驱动测试 + +### 集成测试 +- 测试 API 客户端(使用模拟) +- 测试命令执行 +- 测试配置管理 + +### 手动测试 +- 测试完整工作流 +- 测试错误场景 +- 在不同平台上测试 + +--- + +## 📊 构建和发布流程 + +### 开发构建 +```bash +make build # 为当前平台构建 +make test # 运行测试 +make lint # 运行 linter +``` + +### 多平台构建 +```bash +make build-all # 为 macOS、Linux、Windows 构建 +``` + +### 发布流程 +```bash +./scripts/release.sh v1.0.0 +# 创建 tag,触发 CI/CD +# GitHub Actions 构建并上传二进制文件 +``` + +--- + +## 🎯 导航指南 + +### 对于用户 +1. 开始 → `README.md`(概述) +2. 设置 → `README.md` 的[快速开始](#-快速开始)部分(5 分钟) +3. 迁移 → `docs/MIGRATION.md`(如果从 Shell 版本迁移) + +### 对于开发者 +1. 架构 → 本文档 +2. 贡献 → `docs/guidelines/development/CONTRIBUTING.md` +3. 代码 → 从 `cmd/qkflow/main.go` 开始 + +### 对于构建 +1. 依赖 → `go.mod` +2. 构建 → `Makefile` +3. CI/CD → `.github/workflows/build.yml` +4. 发布 → `scripts/release.sh` + +--- + +## 🔮 未来增强 + +### 高优先级 +- [ ] GitLab 支持 +- [ ] Bitbucket 支持 +- [ ] Draft PR 支持 +- [ ] PR 模板 +- [ ] 自定义工作流 + +### 中优先级 +- [ ] 更好的 Windows 集成 +- [ ] Shell 补全脚本 +- [ ] PR 审查自动化 +- [ ] 批量操作 +- [ ] Webhooks 集成 + +### 低优先级 +- [ ] GUI 版本 +- [ ] VS Code 扩展 +- [ ] 指标和分析 +- [ ] 团队仪表板 + +--- + +## 📞 支持和社区 + +### 获取帮助 +- 📖 首先阅读文档 +- 🐛 通过 GitHub Issues 报告 Bug +- 💡 通过 GitHub Issues 请求功能 +- 💬 在 GitHub Discussions 中提问 + +### 贡献 +- Fork、分支、编码、测试、PR +- 遵循编码标准 +- 为新功能编写测试 +- 更新文档 + +--- + +## 📄 许可证 + +MIT 许可证 - 详见 LICENSE 文件 + +--- + +## 🙏 致谢 + +### 原始项目 +- Shell 版本由 [Wangggym](https://github.com/Wangggym) 创建 + +### Go 版本 +- 架构和实现:AI 辅助开发 +- 测试和改进:社区贡献者 + +### 开源库 +- cobra、viper、survey(CLI 框架) +- go-github、go-jira(API 客户端) +- 以及更多优秀的 Go 包 + +--- + +## 📈 项目状态 + +**当前版本**:1.0.0(初始发布) +**状态**:✅ 可用于生产 +**稳定性**:稳定 +**维护**:活跃 + +--- + +## 🎉 总结 + +qkflow 的 Go 版本代表了原始 Shell 工具的全面现代化。它在以下方面带来了显著改进: + +- **可用性**:更简单的安装和设置 +- **性能**:更快的启动和执行 +- **可靠性**:类型安全、经过充分测试的代码 +- **可维护性**:清晰的架构、良好的文档 +- **可扩展性**:易于添加新功能 + +项目已准备好用于生产,并欢迎社区贡献! + +--- + +**最后更新**:2025-12-05 +**维护者**:Wangggym +**仓库**:https://github.com/Wangggym/quick-workflow diff --git a/go-version/docs/architecture/README.md b/go-version/docs/architecture/README.md new file mode 100644 index 0000000..f19bb87 --- /dev/null +++ b/go-version/docs/architecture/README.md @@ -0,0 +1,82 @@ +# Architecture 目录说明 + +## 📋 目录作用 + +`architecture/` 目录包含 qkflow 项目的技术架构文档,面向开发者提供项目的技术细节、设计原则和代码组织方式。 + +## 📂 目录结构 + +``` +architecture/ +└── ARCHITECTURE.md # 项目架构文档 +``` + +## 📖 文档内容 + +### ARCHITECTURE.md + +项目架构文档包含以下内容: + +#### 1. 项目概述 +- 项目摘要和状态 +- 技术栈和依赖 +- 核心特性介绍 + +#### 2. 项目结构 +- 完整的目录结构说明 +- 各模块的功能和职责 +- 代码组织方式 + +#### 3. 技术架构 +- 技术选型和原因 +- 依赖关系图 +- 模块间交互方式 + +#### 4. 设计原则 +- 代码组织原则 +- 设计模式和最佳实践 +- 扩展性考虑 + +#### 5. 包依赖关系 +- 内部包之间的依赖 +- 外部依赖说明 +- 依赖管理策略 + +## 🎯 使用场景 + +### 新加入的开发者 +- 快速了解项目整体架构 +- 理解代码组织方式 +- 找到相关代码位置 + +### 代码审查者 +- 理解设计意图 +- 评估代码是否符合架构原则 +- 检查模块边界 + +### 功能开发者 +- 了解现有模块结构 +- 确定新功能的位置 +- 理解模块间交互方式 + +### 架构师/维护者 +- 评估架构合理性 +- 规划架构演进 +- 文档化设计决策 + +## 📝 文档特点 + +- **全面性**:涵盖项目的各个方面 +- **结构化**:清晰的层次和组织 +- **实用性**:帮助开发者快速定位和理解代码 +- **可维护性**:随着项目演进持续更新 + +## 🔗 相关文档 + +- [文档索引](../README.md) - 查看所有文档 +- [开发指南](../guidelines/README.md) - 开发规范和指南 +- [开发规范](../guidelines/development/DEVELOPMENT_GUIDELINES.md) - 代码风格和规范 + +--- + +**最后更新**:2025-12-05 diff --git a/go-version/docs/guidelines/README.md b/go-version/docs/guidelines/README.md new file mode 100644 index 0000000..691b9b2 --- /dev/null +++ b/go-version/docs/guidelines/README.md @@ -0,0 +1,109 @@ + +# Guidelines 目录说明 + +## 📋 目录作用 + +`guidelines/` 目录包含 qkflow 项目的所有指南文档,分为用户使用指南和开发指南两大类。 + +## 📂 目录结构 + +``` +guidelines/ +├── usage/ # 用户使用指南 +│ ├── PR_GUIDELINES.md # PR 功能使用指南 +│ ├── JIRA_GUIDELINES.md # Jira 功能使用指南 +│ └── AUTO_UPDATE_GUIDELINES.md # 自动更新功能指南 +│ +└── development/ # 开发指南 + ├── DEVELOPMENT_GUIDELINES.md # 开发规范 + ├── DOCUMENT_GUIDELINES.md # 文档编写指南 + └── RELEASE_GUIDELINES.md # 发布操作指南 +``` + +> 💡 **注意**:快速开始指南已移至 [`../QUICKSTART.md`](../QUICKSTART.md) + +## 📖 文档分类 + +### 用户使用指南 (`usage/`) + +面向最终用户的功能使用文档,帮助用户快速上手和掌握 qkflow 的各项功能。 + +#### 文档列表 + +1. **PR_GUIDELINES.md** - PR 功能完整指南 + - PR 审批功能 + - PR URL 支持 + - PR 编辑器功能 + - 工作流示例和最佳实践 + +2. **JIRA_GUIDELINES.md** - Jira 功能完整指南 + - Jira Issue 读取和导出 + - Jira 状态配置管理 + - 与 Cursor AI 集成使用 + +3. **AUTO_UPDATE_GUIDELINES.md** - 自动更新功能指南 + - 自动更新机制说明 + - 配置和手动更新方法 + +> 💡 **快速开始指南**:已移至 [`../QUICKSTART.md`](../QUICKSTART.md) + +### 开发指南 (`development/`) + +面向开发者的规范和指南文档,帮助开发者理解项目规范、编写文档和发布版本。 + +#### 文档列表 + +1. **DEVELOPMENT_GUIDELINES.md** - 开发规范 + - 代码风格规范(格式化、Lint、命名约定) + - 错误处理规范 + - Git 工作流和提交规范 + - 测试和代码审查规范 + +2. **DOCUMENT_GUIDELINES.md** - 文档编写指南 + - 文档类型和模板说明 + - 各类文档的编写模板 + - 文档格式规范 + - 文档维护指南 + +3. **RELEASE_GUIDELINES.md** - 发布操作指南 + - 快速入门 + - 完整的发布流程 + - 版本管理规则 + - CI/CD 检查 + - 发布检查清单 + +## 🎯 使用场景 + +### 新用户 +- 查看 [`../QUICKSTART.md`](../QUICKSTART.md) 快速上手 +- 查看 `usage/PR_GUIDELINES.md` 了解 PR 功能 +- 查看 `usage/JIRA_GUIDELINES.md` 了解 Jira 集成 + +### 开发者 +- 查看 `development/DEVELOPMENT_GUIDELINES.md` 了解开发规范 +- 查看 `development/DOCUMENT_GUIDELINES.md` 了解文档编写规范 +- 查看 `development/RELEASE_GUIDELINES.md` 了解发布流程 + +### 维护者 +- 参考 `development/DOCUMENT_GUIDELINES.md` 维护文档 +- 参考 `development/RELEASE_GUIDELINES.md` 发布新版本 +- 参考 `development/DEVELOPMENT_GUIDELINES.md` 进行代码审查 + +## 📝 文档命名规范 + +所有指南文档遵循 `XX_GUIDELINES.md` 命名规范: +- `PR_GUIDELINES.md` - PR 相关功能指南 +- `JIRA_GUIDELINES.md` - Jira 相关功能指南 +- `DEVELOPMENT_GUIDELINES.md` - 开发规范指南 +- `DOCUMENT_GUIDELINES.md` - 文档编写指南 +- `RELEASE_GUIDELINES.md` - 发布操作指南 + +## 🔗 相关文档 + +- [文档索引](../README.md) - 查看所有文档 +- [架构文档](../architecture/README.md) - 项目架构说明 +- [迁移指南](../MIGRATION.md) - 迁移相关文档 + +--- + +**最后更新**:2025-12-05 diff --git a/go-version/docs/guidelines/development/DEVELOPMENT_GUIDELINES.md b/go-version/docs/guidelines/development/DEVELOPMENT_GUIDELINES.md new file mode 100644 index 0000000..0a0afe4 --- /dev/null +++ b/go-version/docs/guidelines/development/DEVELOPMENT_GUIDELINES.md @@ -0,0 +1,594 @@ +# 开发规范文档 + +> 本文档定义了 qkflow 项目的开发规范和最佳实践,所有贡献者都应遵循这些规范。 + +--- + +## 📋 目录 + +- [代码风格](#-代码风格) +- [错误处理](#-错误处理) +- [文档规范](#-文档规范) +- [命名规范](#-命名规范) +- [模块组织](#-模块组织) +- [Git 工作流](#-git-工作流) +- [提交规范](#-提交规范) +- [测试规范](#-测试规范) +- [代码审查](#-代码审查) +- [依赖管理](#-依赖管理) +- [开发工具](#-开发工具) + +--- + +## 🎨 代码风格 + +### 代码格式化 + +所有代码必须使用 `gofmt` 进行格式化: + +```bash +# 自动格式化代码 +go fmt ./... + +# 或使用 Makefile +make fmt +``` + +**规则**: +- 提交前必须运行 `go fmt` +- CI/CD 会检查代码格式,格式不正确会导致构建失败 +- 使用 Go 官方标准格式 + +### Lint 检查 + +使用 `golangci-lint` 进行代码质量检查: + +```bash +# 运行 lint 检查 +golangci-lint run + +# 或使用 Makefile +make lint +``` + +**规则**: +- 所有警告必须修复 +- 禁止使用 `//nolint` 除非有充分理由,并添加注释说明 +- 定期运行 `golangci-lint` 检查代码质量 + +### Go 命名约定 + +遵循 Go 官方命名约定: + +- **包名**:小写字母,简短(如 `github`、`jira`、`git`) +- **函数名**:驼峰命名,公开函数首字母大写(如 `CreatePullRequest`、`GetIssue`) +- **变量名**:驼峰命名,公开变量首字母大写(如 `Client`、`Config`) +- **常量名**:驼峰命名,公开常量首字母大写(如 `DefaultTimeout`、`MaxRetries`) +- **类型名**:驼峰命名,首字母大写(如 `HttpClient`、`JiraTicket`) +- **接口名**:驼峰命名,首字母大写,通常以 `er` 结尾(如 `Reader`、`Writer`)或描述性名称(如 `PlatformProvider`) + +### 代码组织 + +#### 导入顺序 + +1. 标准库导入 +2. 第三方库导入 +3. 项目内部导入 + +每组导入之间用空行分隔: + +```go +package github + +import ( + "context" + "fmt" + "strings" + + "github.com/google/go-github/v57/github" + "golang.org/x/oauth2" + + "github.com/Wangggym/quick-workflow/internal/config" +) +``` + +#### 方法组织模式 + +使用注释分隔符来清晰地区分公共方法和私有方法,提高代码可读性: + +```go +package http + +import ( + "fmt" + "sync" + "time" + + "github.com/go-resty/resty/v2" +) + +var ( + globalClient *HttpClient + once sync.Once +) + +// HttpClient HTTP 客户端封装 +type HttpClient struct { + client *resty.Client +} + +// ------------------- Public Methods ------------------- +// 外部访问 + +// Global 获取全局 HttpClient 单例 +// 外部应该统一使用此方法获取 HTTP 客户端实例 +func Global() *HttpClient { + once.Do(func() { + globalClient = newClient() + }) + return globalClient +} + +// SetTimeout 设置客户端超时时间 +func (c *HttpClient) SetTimeout(timeout time.Duration) *HttpClient { + c.client.SetTimeout(timeout) + return c +} + +// Get GET 请求 +func (c *HttpClient) Get(url string, config *RequestConfig) (*HttpResponse, error) { + // ... +} + +// ------------------- Private Methods ------------------- +// 内部方法 + +// newClient 创建新的 HttpClient(内部使用) +// 外部应该使用 Global() 方法获取单例实例 +func newClient() *HttpClient { + // ... +} + +// buildRequest 构建请求 +func (c *HttpClient) buildRequest(method string, url string, config *RequestConfig) *resty.Request { + // ... +} +``` + +**优势**: +- ✅ 清晰地区分公共 API 和内部实现 +- ✅ 提高代码可读性和可维护性 +- ✅ 便于快速定位公共方法 +- ✅ 符合大型项目的代码组织规范 +- ✅ 便于代码审查和维护 + +**注意事项**: +- 分隔符应该足够明显(使用 `-` 字符,建议 60-70 个字符) +- **左右两边的 `-` 字符数量应该一致** +- 保持分隔符长度一致,便于视觉识别 +- 在公共方法区域,按功能分组(如构造方法、配置方法、业务方法) +- 在私有方法区域,可以按调用关系或功能分组 +- 对于简单的文件(少于 10 个方法),可以省略分隔符 +- 对于复杂的文件(多个结构体、多个方法),强烈建议使用分隔符 + +**组织顺序建议**: +1. **包声明和导入** +2. **变量和常量声明** +3. **类型定义** +4. **公共方法区域**(使用分隔符) + - 构造方法 + - 配置方法 + - 业务方法 +5. **私有方法区域**(使用分隔符) + - 辅助方法 + - 内部实现 + +--- + +## ⚠️ 错误处理 + +### 错误包装 + +使用 `fmt.Errorf` 和 `%w` 动词包装错误,保留原始错误信息: + +```go +if err != nil { + return nil, fmt.Errorf("failed to create pull request: %w", err) +} +``` + +### 错误上下文 + +在错误消息中添加足够的上下文信息: + +```go +if err != nil { + return nil, fmt.Errorf("failed to create PR for %s/%s: %w", owner, repo, err) +} +``` + +### 错误检查 + +始终检查错误,不要忽略: + +```go +// ❌ 错误:忽略错误 +result, _ := someFunction() + +// ✅ 正确:检查错误 +result, err := someFunction() +if err != nil { + return fmt.Errorf("operation failed: %w", err) +} +``` + +### 自定义错误类型 + +对于需要区分处理的错误,定义自定义错误类型: + +```go +// ErrNotFound 表示资源未找到 +var ErrNotFound = errors.New("resource not found") + +// ErrUnauthorized 表示未授权 +var ErrUnauthorized = errors.New("unauthorized") +``` + +--- + +## 📝 文档规范 + +### 公共 API 文档 + +所有公开的函数、类型、常量都应该有文档注释: + +```go +// CreatePullRequest 创建新的 Pull Request +// 参数: +// - input: PR 创建输入参数,包含 owner、repo、title、body、head、base +// 返回: +// - *PullRequest: 创建的 PR 信息 +// - error: 如果创建失败,返回错误 +// 示例: +// input := CreatePullRequestInput{ +// Owner: "owner", +// Repo: "repo", +// Title: "title", +// Body: "body", +// Head: "feature-branch", +// Base: "main", +// } +// pr, err := client.CreatePullRequest(input) +func (c *Client) CreatePullRequest(input CreatePullRequestInput) (*PullRequest, error) { + // 实现 +} +``` + +### 文档注释格式 + +- 使用 `//` 为公共项添加文档 +- 第一行应该是简洁的摘要 +- 可以包含参数说明、返回值说明、错误说明、使用示例 + +### 内部文档 + +对于复杂的实现逻辑,添加内部注释: + +```go +// 使用指数退避策略进行重试 +// 初始延迟 1 秒,每次重试延迟翻倍,最大延迟 60 秒 +delay := time.Duration(1< 60*time.Second { + delay = 60 * time.Second +} +``` + +--- + +## 🏷️ 命名规范 + +### 文件命名 + +- **源文件**:`snake_case.go`(如 `jira_client.go`、`pr_helpers.go`) +- **测试文件**:`snake_case_test.go`(与源文件同名,添加 `_test` 后缀) +- **文档文件**:`SCREAMING_SNAKE_CASE.md`(如 `DEVELOPMENT_GUIDELINES.md`、`PR_ARCHITECTURE.md`) + - **架构文档**:`{MODULE}_ARCHITECTURE.md`(如 `PR_ARCHITECTURE.md`、`GIT_ARCHITECTURE.md`) + - **命令文档**:`{MODULE}_COMMAND_ARCHITECTURE.md`(如 `PR_COMMAND_ARCHITECTURE.md`) + - **指南文档**:`{TOPIC}_GUIDELINES.md`(如 `DEVELOPMENT_GUIDELINES.md`、`DOCUMENT_GUIDELINES.md`) + +### 函数命名 + +- **动作函数**:使用动词(如 `Create`、`Get`、`Update`、`Delete`) +- **查询函数**:使用 `Get` 前缀(如 `GetStatus`、`GetInfo`) +- **检查函数**:使用 `Is` 或 `Has` 前缀(如 `IsValid`、`HasPermission`) +- **转换函数**:使用 `To` 前缀(如 `ToString`、`ToJSON`) + +### 结构体命名 + +- 使用名词或名词短语(如 `HttpClient`、`JiraTicket`) +- 避免使用 `Data`、`Info`、`Manager` 等泛化名称,使用具体名称 + +### 常量命名 + +- 使用驼峰命名,首字母大写 +- 放在包顶层或专门的常量文件中 + +```go +// internal/jira/constants.go +const ( + MaxDownloadSize = 100 * 1024 * 1024 // 100MB + DefaultTimeout = 30 * time.Second +) +``` + +--- + +## 📁 模块组织 + +### 目录结构 + +遵循项目的三层架构: + +``` +go-version/ +├── cmd/ # CLI 入口 +│ └── qkflow/ +│ ├── main.go +│ └── commands/ +├── internal/ # 内部包 +│ ├── github/ # GitHub 客户端 +│ ├── jira/ # Jira 客户端 +│ ├── git/ # Git 操作 +│ ├── config/ # 配置管理 +│ └── ... +└── docs/ # 文档 + ├── guidelines/ # 所有指南 + │ ├── usage/ # 用户使用指南 + │ └── development/ # 开发指南 + ├── architecture/ # 架构文档 + └── ... +``` + +### 模块职责 + +- **`cmd/`**:CLI 命令入口,处理用户交互、参数解析 +- **`internal/`**:核心业务逻辑,可复用的功能模块 +- **`docs/`**:项目文档 + +### 模块依赖规则 + +- **命令层** → **内部包**:命令层可以依赖内部包,但不能反向依赖 +- **内部包之间**:可以相互依赖,但避免循环依赖 +- **基础模块**:`internal/config/` 等基础模块不依赖其他业务模块 + +--- + +## 🔀 Git 工作流 + +### 分支策略 + +- **`master`**:主分支,保持稳定,只接受合并请求 +- **`feature/*`**:功能分支,从 `master` 创建,完成后合并回 `master` +- **`fix/*`**:修复分支,从 `master` 创建,用于修复 bug +- **`hotfix/*`**:热修复分支,用于紧急修复生产问题 + +### 分支命名 + +- 功能分支:`feature/jira-attachments` +- 修复分支:`fix/pr-merge-error` +- 热修复分支:`hotfix/critical-bug` + +### 工作流程 + +1. **创建分支**:从 `master` 创建新分支 +2. **开发**:在分支上进行开发 +3. **提交**:遵循提交规范(见下方) +4. **推送**:推送到远程仓库 +5. **创建 PR**:创建 Pull Request 到 `master` +6. **代码审查**:等待代码审查 +7. **合并**:审查通过后合并到 `master` + +--- + +## 📋 提交规范 + +### Conventional Commits + +使用 [Conventional Commits](https://www.conventionalcommits.org/) 格式: + +``` +(): + + + +