bugsink-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +21 -0
- data/README.md +320 -0
- data/exe/bugsink +7 -0
- data/lib/bugsink/cli.rb +573 -0
- data/lib/bugsink/client.rb +260 -0
- data/lib/bugsink/config.rb +62 -0
- data/lib/bugsink/version.rb +5 -0
- data/lib/bugsink.rb +10 -0
- metadata +96 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bc4e718e40bae09f46946050bfd5a52133b1a7f1b4653bd5d74ac4b39eeeef44
|
|
4
|
+
data.tar.gz: 04bc34a4dabc823fd3fd262b1b28e7174407e0cb922d5d6bd8e34ca526f0dedb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a4af07cd5b996e3ded9cd7333a1f1f097f6c6720a3b41b4cea21e03b6631c53c08642122e1e78d5cfdd1706d4b462b8ec03e8b7a366942ff5456774b07178f2b
|
|
7
|
+
data.tar.gz: 5c1303e396df707697e9fa08331720031cc39630367b4fbb0672769da9017cb0f9add4b0a38dbb9466d5e09c1b818a5640f008fa04266095e045a4263d600db6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-02-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release of bugsink-cli gem
|
|
12
|
+
- Complete BugSink API wrapper with support for:
|
|
13
|
+
- Teams (list, get, create, update)
|
|
14
|
+
- Projects (list, get, create, update)
|
|
15
|
+
- Issues (list, get) - read-only
|
|
16
|
+
- Events (list, get, stacktrace) - read-only
|
|
17
|
+
- Releases (list, get, create)
|
|
18
|
+
- Configuration management via environment variables and .bugsink dotfile
|
|
19
|
+
- Multiple output formats: table, JSON, quiet
|
|
20
|
+
- Comprehensive CLI with help system
|
|
21
|
+
- HTTParty-based HTTP client with error handling
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tomáš Landovský
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# BugSink CLI
|
|
2
|
+
|
|
3
|
+
A command-line interface for the BugSink error tracking service. Provides full API access for teams, projects, issues, events, and releases.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the gem:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gem install bugsink-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add to your Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem 'bugsink-cli', '~> 0.1.0'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
### Environment Variables
|
|
22
|
+
|
|
23
|
+
**Required:**
|
|
24
|
+
- `BUGSINK_API_KEY` - Your BugSink API authentication token
|
|
25
|
+
|
|
26
|
+
**Optional:**
|
|
27
|
+
- `BUGSINK_HOST` - API host (default: `https://bugs.kopernici.cz`)
|
|
28
|
+
|
|
29
|
+
### Project Configuration
|
|
30
|
+
|
|
31
|
+
The CLI supports a `.bugsink` dotfile in your project root to store the default project ID:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Set default project
|
|
35
|
+
bugsink config set-project 8
|
|
36
|
+
|
|
37
|
+
# This creates .bugsink file with:
|
|
38
|
+
PROJECT_ID=8
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Set your API key
|
|
45
|
+
export BUGSINK_API_KEY="your-token-here"
|
|
46
|
+
|
|
47
|
+
# Test connection
|
|
48
|
+
bugsink config test
|
|
49
|
+
|
|
50
|
+
# Set default project
|
|
51
|
+
bugsink config set-project 8
|
|
52
|
+
|
|
53
|
+
# List latest issues
|
|
54
|
+
bugsink issues list --project=8 --sort=last_seen --order=desc
|
|
55
|
+
|
|
56
|
+
# Get formatted stacktrace
|
|
57
|
+
bugsink events stacktrace <event-uuid>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### General Syntax
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bugsink <resource> <action> [arguments] [options]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Global Options
|
|
69
|
+
|
|
70
|
+
- `--json` - Output as JSON (machine-readable)
|
|
71
|
+
- `--quiet` - Minimal output (IDs only)
|
|
72
|
+
|
|
73
|
+
### Resources
|
|
74
|
+
|
|
75
|
+
#### Config
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bugsink config show # Show current configuration
|
|
79
|
+
bugsink config set-project <id> # Set default project ID
|
|
80
|
+
bugsink config test # Test API connectivity
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Teams
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# List all teams
|
|
87
|
+
bugsink teams list [--json|--quiet]
|
|
88
|
+
|
|
89
|
+
# Get team details
|
|
90
|
+
bugsink teams get <uuid> [--json]
|
|
91
|
+
|
|
92
|
+
# Create team
|
|
93
|
+
bugsink teams create '{"name":"Team Name","visibility":"hidden"}'
|
|
94
|
+
|
|
95
|
+
# Update team
|
|
96
|
+
bugsink teams update <uuid> '{"name":"New Name"}'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Projects
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# List all projects (optionally filter by team)
|
|
103
|
+
bugsink projects list [--team=<uuid>] [--json|--quiet]
|
|
104
|
+
|
|
105
|
+
# Get project details
|
|
106
|
+
bugsink projects get <id> [--json]
|
|
107
|
+
|
|
108
|
+
# Create project
|
|
109
|
+
bugsink projects create '{
|
|
110
|
+
"team":"<team-uuid>",
|
|
111
|
+
"name":"Project Name",
|
|
112
|
+
"visibility":"team_members",
|
|
113
|
+
"alert_on_new_issue":true
|
|
114
|
+
}'
|
|
115
|
+
|
|
116
|
+
# Update project
|
|
117
|
+
bugsink projects update <id> '{"name":"New Name","alert_on_new_issue":false}'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Issues (Read-Only)
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# List issues for a project
|
|
124
|
+
bugsink issues list --project=<id> \
|
|
125
|
+
[--sort=last_seen|digest_order] \
|
|
126
|
+
[--order=asc|desc] \
|
|
127
|
+
[--json|--quiet]
|
|
128
|
+
|
|
129
|
+
# Get issue details
|
|
130
|
+
bugsink issues get <uuid> [--json]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Note:** Issues are **read-only** via the API. You cannot update status, resolve, or add comments through the CLI.
|
|
134
|
+
|
|
135
|
+
#### Events (Read-Only)
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# List events for an issue
|
|
139
|
+
bugsink events list --issue=<uuid> [--order=asc|desc] [--json|--quiet]
|
|
140
|
+
|
|
141
|
+
# Get event details
|
|
142
|
+
bugsink events get <uuid> [--json]
|
|
143
|
+
|
|
144
|
+
# Get formatted stacktrace (Markdown)
|
|
145
|
+
bugsink events stacktrace <uuid>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Note:** Events are **read-only** and created automatically via Sentry SDK. No manual creation/updates available.
|
|
149
|
+
|
|
150
|
+
#### Releases
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# List releases for a project
|
|
154
|
+
bugsink releases list --project=<id> [--json|--quiet]
|
|
155
|
+
|
|
156
|
+
# Get release details
|
|
157
|
+
bugsink releases get <uuid> [--json]
|
|
158
|
+
|
|
159
|
+
# Create release
|
|
160
|
+
bugsink releases create '{
|
|
161
|
+
"project":8,
|
|
162
|
+
"version":"v1.2.3",
|
|
163
|
+
"timestamp":"2026-02-03T12:00:00Z"
|
|
164
|
+
}'
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Note:** Releases can be created but not updated or deleted.
|
|
168
|
+
|
|
169
|
+
### Help System
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# General help
|
|
173
|
+
bugsink help
|
|
174
|
+
|
|
175
|
+
# Resource-specific help
|
|
176
|
+
bugsink help teams
|
|
177
|
+
bugsink help projects
|
|
178
|
+
bugsink help issues
|
|
179
|
+
bugsink help events
|
|
180
|
+
bugsink help releases
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Examples
|
|
184
|
+
|
|
185
|
+
### Daily Development Workflow
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Set up once
|
|
189
|
+
export BUGSINK_API_KEY="..."
|
|
190
|
+
cd /path/to/project
|
|
191
|
+
bugsink config set-project 8
|
|
192
|
+
|
|
193
|
+
# Check latest errors
|
|
194
|
+
bugsink issues list --project=8 --sort=last_seen --order=desc --json | jq '.[:5]'
|
|
195
|
+
|
|
196
|
+
# Get detailed stacktrace for an issue
|
|
197
|
+
ISSUE_UUID=$(bugsink issues list --project=8 --quiet | head -1)
|
|
198
|
+
EVENT_UUID=$(bugsink events list --issue=$ISSUE_UUID --quiet | head -1)
|
|
199
|
+
bugsink events stacktrace $EVENT_UUID
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Creating a Release
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# After deployment
|
|
206
|
+
VERSION="v$(date +%Y%m%d-%H%M%S)"
|
|
207
|
+
bugsink releases create "{\"project\":8,\"version\":\"$VERSION\"}"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Filtering and Searching
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Get all projects for a specific team
|
|
214
|
+
TEAM_UUID="ee4f4572-0957-4346-b433-3c605acbfa2a"
|
|
215
|
+
bugsink projects list --team=$TEAM_UUID --json | jq '.[].name'
|
|
216
|
+
|
|
217
|
+
# Get IDs of all teams
|
|
218
|
+
bugsink teams list --quiet
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Output Formats
|
|
222
|
+
|
|
223
|
+
### Table (Default)
|
|
224
|
+
|
|
225
|
+
Human-readable table format with tab-separated columns:
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
id name visibility
|
|
229
|
+
ee4f4572... Team Name team_members
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### JSON
|
|
233
|
+
|
|
234
|
+
Machine-readable JSON format (use `--json`):
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
[
|
|
238
|
+
{
|
|
239
|
+
"id": "ee4f4572-0957-4346-b433-3c605acbfa2a",
|
|
240
|
+
"name": "Team Name",
|
|
241
|
+
"visibility": "team_members"
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Quiet
|
|
247
|
+
|
|
248
|
+
Minimal output with just IDs (use `--quiet`):
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
ee4f4572-0957-4346-b433-3c605acbfa2a
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## API Capabilities & Limitations
|
|
255
|
+
|
|
256
|
+
### Supported Write Operations
|
|
257
|
+
|
|
258
|
+
- **Teams:** Create, Update
|
|
259
|
+
- **Projects:** Create, Update
|
|
260
|
+
- **Releases:** Create
|
|
261
|
+
|
|
262
|
+
### Read-Only Resources
|
|
263
|
+
|
|
264
|
+
- **Issues:** Cannot update status, resolve, or add comments
|
|
265
|
+
- **Events:** Created automatically via Sentry SDK ingestion only
|
|
266
|
+
|
|
267
|
+
### Not Supported
|
|
268
|
+
|
|
269
|
+
- **DELETE operations:** No resource can be deleted via API
|
|
270
|
+
- **Bulk operations:** Must process items one at a time
|
|
271
|
+
- **Filtering on list endpoints:** Limited filter support
|
|
272
|
+
|
|
273
|
+
## Library Usage
|
|
274
|
+
|
|
275
|
+
You can also use bugsink-cli as a Ruby library:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
require 'bugsink'
|
|
279
|
+
|
|
280
|
+
# Initialize client
|
|
281
|
+
config = Bugsink::Config.new
|
|
282
|
+
client = Bugsink::Client.new(config)
|
|
283
|
+
|
|
284
|
+
# List teams
|
|
285
|
+
response = client.teams_list
|
|
286
|
+
teams = response.data
|
|
287
|
+
|
|
288
|
+
# Get project
|
|
289
|
+
project = client.project_get(8)
|
|
290
|
+
|
|
291
|
+
# Create release
|
|
292
|
+
release = client.release_create(
|
|
293
|
+
project_id: 8,
|
|
294
|
+
version: 'v1.0.0'
|
|
295
|
+
)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Development
|
|
299
|
+
|
|
300
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
|
301
|
+
|
|
302
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
303
|
+
|
|
304
|
+
### Running Tests
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
# Run all tests
|
|
308
|
+
BUGSINK_API_KEY="your-key" bundle exec rspec
|
|
309
|
+
|
|
310
|
+
# Run specific test
|
|
311
|
+
BUGSINK_API_KEY="your-key" bundle exec rspec spec/bugsink/config_spec.rb
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Contributing
|
|
315
|
+
|
|
316
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/koperniki/bugsink-cli.
|
|
317
|
+
|
|
318
|
+
## License
|
|
319
|
+
|
|
320
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/exe/bugsink
ADDED
data/lib/bugsink/cli.rb
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'client'
|
|
6
|
+
require_relative 'config'
|
|
7
|
+
|
|
8
|
+
module Bugsink
|
|
9
|
+
# CLI command parser and router for BugSink API
|
|
10
|
+
class CLI
|
|
11
|
+
attr_reader :client, :config, :options
|
|
12
|
+
|
|
13
|
+
def initialize(args = ARGV)
|
|
14
|
+
@args = args
|
|
15
|
+
@options = {
|
|
16
|
+
format: 'table',
|
|
17
|
+
project_id: nil
|
|
18
|
+
}
|
|
19
|
+
@config = Config.new
|
|
20
|
+
@client = Client.new(@config)
|
|
21
|
+
rescue Config::ConfigError => e
|
|
22
|
+
error("Configuration error: #{e.message}")
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run
|
|
27
|
+
return show_help if @args.empty?
|
|
28
|
+
|
|
29
|
+
resource = @args[0]
|
|
30
|
+
action = @args[1]
|
|
31
|
+
|
|
32
|
+
case resource
|
|
33
|
+
when 'help', '--help', '-h'
|
|
34
|
+
show_help(action)
|
|
35
|
+
when '--version', '-v'
|
|
36
|
+
show_version
|
|
37
|
+
when 'config'
|
|
38
|
+
handle_config(action)
|
|
39
|
+
when 'teams'
|
|
40
|
+
handle_teams(action, @args[2..])
|
|
41
|
+
when 'projects'
|
|
42
|
+
handle_projects(action, @args[2..])
|
|
43
|
+
when 'issues'
|
|
44
|
+
handle_issues(action, @args[2..])
|
|
45
|
+
when 'events'
|
|
46
|
+
handle_events(action, @args[2..])
|
|
47
|
+
when 'releases'
|
|
48
|
+
handle_releases(action, @args[2..])
|
|
49
|
+
else
|
|
50
|
+
error("Unknown resource: #{resource}")
|
|
51
|
+
show_help
|
|
52
|
+
exit 1
|
|
53
|
+
end
|
|
54
|
+
rescue Client::ClientError => e
|
|
55
|
+
error("API error: #{e.message}")
|
|
56
|
+
exit 1
|
|
57
|
+
rescue ArgumentError => e
|
|
58
|
+
error("Argument error: #{e.message}")
|
|
59
|
+
exit 1
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
error("Unexpected error: #{e.class} - #{e.message}")
|
|
62
|
+
error(e.backtrace.join("\n")) if ENV['DEBUG']
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def handle_config(action)
|
|
69
|
+
case action
|
|
70
|
+
when 'show'
|
|
71
|
+
puts @config.to_s
|
|
72
|
+
when 'set-project'
|
|
73
|
+
project_id = @args[2]
|
|
74
|
+
error('Project ID required') && exit(1) unless project_id
|
|
75
|
+
@config.set_project_id(project_id.to_i)
|
|
76
|
+
success("Project ID set to #{project_id}")
|
|
77
|
+
when 'test'
|
|
78
|
+
@client.test_connection
|
|
79
|
+
success('API connection successful!')
|
|
80
|
+
else
|
|
81
|
+
error("Unknown config action: #{action}")
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def handle_teams(action, args)
|
|
87
|
+
case action
|
|
88
|
+
when 'list'
|
|
89
|
+
parse_format_options!(args)
|
|
90
|
+
response = @client.teams_list
|
|
91
|
+
output_list(response.data, format: @options[:format])
|
|
92
|
+
when 'get'
|
|
93
|
+
uuid = args[0]
|
|
94
|
+
error('Team UUID required') && exit(1) unless uuid
|
|
95
|
+
parse_format_options!(args[1..])
|
|
96
|
+
team = @client.team_get(uuid)
|
|
97
|
+
output_single(team, format: @options[:format])
|
|
98
|
+
when 'create'
|
|
99
|
+
data = parse_json_arg(args[0])
|
|
100
|
+
error('Name required in JSON') && exit(1) unless data['name']
|
|
101
|
+
team = @client.team_create(
|
|
102
|
+
name: data['name'],
|
|
103
|
+
visibility: data['visibility']
|
|
104
|
+
)
|
|
105
|
+
output_single(team, format: 'json')
|
|
106
|
+
when 'update'
|
|
107
|
+
uuid = args[0]
|
|
108
|
+
error('Team UUID required') && exit(1) unless uuid
|
|
109
|
+
data = parse_json_arg(args[1])
|
|
110
|
+
error('Update data required') && exit(1) if data.empty?
|
|
111
|
+
team = @client.team_update(
|
|
112
|
+
uuid,
|
|
113
|
+
name: data['name'],
|
|
114
|
+
visibility: data['visibility']
|
|
115
|
+
)
|
|
116
|
+
output_single(team, format: 'json')
|
|
117
|
+
else
|
|
118
|
+
error("Unknown teams action: #{action}")
|
|
119
|
+
exit 1
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def handle_projects(action, args)
|
|
124
|
+
case action
|
|
125
|
+
when 'list'
|
|
126
|
+
parse_project_options!(args)
|
|
127
|
+
response = @client.projects_list(team_uuid: @options[:team])
|
|
128
|
+
output_list(response.data, format: @options[:format])
|
|
129
|
+
when 'get'
|
|
130
|
+
id = args[0]
|
|
131
|
+
error('Project ID required') && exit(1) unless id
|
|
132
|
+
parse_format_options!(args[1..])
|
|
133
|
+
project = @client.project_get(id.to_i)
|
|
134
|
+
output_single(project, format: @options[:format])
|
|
135
|
+
when 'create'
|
|
136
|
+
data = parse_json_arg(args[0])
|
|
137
|
+
error('Team and name required in JSON') && exit(1) unless data['team'] && data['name']
|
|
138
|
+
project = @client.project_create(
|
|
139
|
+
team_uuid: data['team'],
|
|
140
|
+
name: data['name'],
|
|
141
|
+
visibility: data['visibility'],
|
|
142
|
+
alert_on_new_issue: data['alert_on_new_issue'],
|
|
143
|
+
alert_on_regression: data['alert_on_regression'],
|
|
144
|
+
alert_on_unmute: data['alert_on_unmute']
|
|
145
|
+
)
|
|
146
|
+
output_single(project, format: 'json')
|
|
147
|
+
when 'update'
|
|
148
|
+
id = args[0]
|
|
149
|
+
error('Project ID required') && exit(1) unless id
|
|
150
|
+
data = parse_json_arg(args[1])
|
|
151
|
+
error('Update data required') && exit(1) if data.empty?
|
|
152
|
+
project = @client.project_update(
|
|
153
|
+
id.to_i,
|
|
154
|
+
name: data['name'],
|
|
155
|
+
visibility: data['visibility'],
|
|
156
|
+
alert_on_new_issue: data['alert_on_new_issue'],
|
|
157
|
+
alert_on_regression: data['alert_on_regression'],
|
|
158
|
+
alert_on_unmute: data['alert_on_unmute']
|
|
159
|
+
)
|
|
160
|
+
output_single(project, format: 'json')
|
|
161
|
+
else
|
|
162
|
+
error("Unknown projects action: #{action}")
|
|
163
|
+
exit 1
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def handle_issues(action, args)
|
|
168
|
+
case action
|
|
169
|
+
when 'list'
|
|
170
|
+
parse_issue_options!(args)
|
|
171
|
+
project_id = @options[:project_id] || @config.project_id
|
|
172
|
+
error('Project ID required (use --project or set via config)') && exit(1) unless project_id
|
|
173
|
+
response = @client.issues_list(
|
|
174
|
+
project_id: project_id,
|
|
175
|
+
sort: @options[:sort] || 'last_seen',
|
|
176
|
+
order: @options[:order] || 'desc'
|
|
177
|
+
)
|
|
178
|
+
output_list(response.data, format: @options[:format])
|
|
179
|
+
when 'get'
|
|
180
|
+
uuid = args[0]
|
|
181
|
+
error('Issue UUID required') && exit(1) unless uuid
|
|
182
|
+
parse_format_options!(args[1..])
|
|
183
|
+
issue = @client.issue_get(uuid)
|
|
184
|
+
output_single(issue, format: @options[:format])
|
|
185
|
+
else
|
|
186
|
+
error("Unknown issues action: #{action}")
|
|
187
|
+
info('Note: Issues are read-only. Write operations not available in API.')
|
|
188
|
+
exit 1
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def handle_events(action, args)
|
|
193
|
+
case action
|
|
194
|
+
when 'list'
|
|
195
|
+
parse_event_options!(args)
|
|
196
|
+
error('Issue UUID required (use --issue)') && exit(1) unless @options[:issue]
|
|
197
|
+
response = @client.events_list(
|
|
198
|
+
issue_uuid: @options[:issue],
|
|
199
|
+
order: @options[:order] || 'desc'
|
|
200
|
+
)
|
|
201
|
+
output_list(response.data, format: @options[:format])
|
|
202
|
+
when 'get'
|
|
203
|
+
uuid = args[0]
|
|
204
|
+
error('Event UUID required') && exit(1) unless uuid
|
|
205
|
+
parse_format_options!(args[1..])
|
|
206
|
+
event = @client.event_get(uuid)
|
|
207
|
+
output_single(event, format: @options[:format])
|
|
208
|
+
when 'stacktrace'
|
|
209
|
+
uuid = args[0]
|
|
210
|
+
error('Event UUID required') && exit(1) unless uuid
|
|
211
|
+
stacktrace = @client.event_stacktrace(uuid)
|
|
212
|
+
puts stacktrace
|
|
213
|
+
else
|
|
214
|
+
error("Unknown events action: #{action}")
|
|
215
|
+
exit 1
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def handle_releases(action, args)
|
|
220
|
+
case action
|
|
221
|
+
when 'list'
|
|
222
|
+
parse_release_options!(args)
|
|
223
|
+
project_id = @options[:project_id] || @config.project_id
|
|
224
|
+
error('Project ID required (use --project or set via config)') && exit(1) unless project_id
|
|
225
|
+
response = @client.releases_list(project_id: project_id)
|
|
226
|
+
output_list(response.data, format: @options[:format])
|
|
227
|
+
when 'get'
|
|
228
|
+
uuid = args[0]
|
|
229
|
+
error('Release UUID required') && exit(1) unless uuid
|
|
230
|
+
parse_format_options!(args[1..])
|
|
231
|
+
release = @client.release_get(uuid)
|
|
232
|
+
output_single(release, format: @options[:format])
|
|
233
|
+
when 'create'
|
|
234
|
+
data = parse_json_arg(args[0])
|
|
235
|
+
error('Project and version required in JSON') && exit(1) unless data['project'] && data['version']
|
|
236
|
+
release = @client.release_create(
|
|
237
|
+
project_id: data['project'],
|
|
238
|
+
version: data['version'],
|
|
239
|
+
timestamp: data['timestamp']
|
|
240
|
+
)
|
|
241
|
+
output_single(release, format: 'json')
|
|
242
|
+
else
|
|
243
|
+
error("Unknown releases action: #{action}")
|
|
244
|
+
exit 1
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def parse_format_options!(args)
|
|
249
|
+
OptionParser.new do |opts|
|
|
250
|
+
opts.on('--json', 'Output as JSON') { @options[:format] = 'json' }
|
|
251
|
+
opts.on('--quiet', 'Minimal output') { @options[:format] = 'quiet' }
|
|
252
|
+
end.parse!(args)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def parse_project_options!(args)
|
|
256
|
+
OptionParser.new do |opts|
|
|
257
|
+
opts.on('--team=UUID', 'Filter by team UUID') { |v| @options[:team] = v }
|
|
258
|
+
opts.on('--json', 'Output as JSON') { @options[:format] = 'json' }
|
|
259
|
+
opts.on('--quiet', 'Minimal output') { @options[:format] = 'quiet' }
|
|
260
|
+
end.parse!(args)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def parse_issue_options!(args)
|
|
264
|
+
OptionParser.new do |opts|
|
|
265
|
+
opts.on('--project=ID', 'Project ID') { |v| @options[:project_id] = v.to_i }
|
|
266
|
+
opts.on('--sort=FIELD', 'Sort field') { |v| @options[:sort] = v }
|
|
267
|
+
opts.on('--order=ORDER', 'Sort order (asc|desc)') { |v| @options[:order] = v }
|
|
268
|
+
opts.on('--json', 'Output as JSON') { @options[:format] = 'json' }
|
|
269
|
+
opts.on('--quiet', 'Minimal output') { @options[:format] = 'quiet' }
|
|
270
|
+
end.parse!(args)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def parse_event_options!(args)
|
|
274
|
+
OptionParser.new do |opts|
|
|
275
|
+
opts.on('--issue=UUID', 'Issue UUID (required)') { |v| @options[:issue] = v }
|
|
276
|
+
opts.on('--order=ORDER', 'Sort order (asc|desc)') { |v| @options[:order] = v }
|
|
277
|
+
opts.on('--json', 'Output as JSON') { @options[:format] = 'json' }
|
|
278
|
+
opts.on('--quiet', 'Minimal output') { @options[:format] = 'quiet' }
|
|
279
|
+
end.parse!(args)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def parse_release_options!(args)
|
|
283
|
+
OptionParser.new do |opts|
|
|
284
|
+
opts.on('--project=ID', 'Project ID') { |v| @options[:project_id] = v.to_i }
|
|
285
|
+
opts.on('--json', 'Output as JSON') { @options[:format] = 'json' }
|
|
286
|
+
opts.on('--quiet', 'Minimal output') { @options[:format] = 'quiet' }
|
|
287
|
+
end.parse!(args)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def parse_json_arg(json_str)
|
|
291
|
+
return {} unless json_str
|
|
292
|
+
|
|
293
|
+
JSON.parse(json_str)
|
|
294
|
+
rescue JSON::ParserError => e
|
|
295
|
+
error("Invalid JSON: #{e.message}")
|
|
296
|
+
exit 1
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def output_list(data, format:)
|
|
300
|
+
case format
|
|
301
|
+
when 'json'
|
|
302
|
+
puts JSON.pretty_generate(data)
|
|
303
|
+
when 'quiet'
|
|
304
|
+
data.each { |item| puts item['id'] || item['uuid'] }
|
|
305
|
+
else
|
|
306
|
+
output_table(data)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def output_single(data, format:)
|
|
311
|
+
case format
|
|
312
|
+
when 'json'
|
|
313
|
+
puts JSON.pretty_generate(data)
|
|
314
|
+
when 'quiet'
|
|
315
|
+
puts data['id'] || data['uuid']
|
|
316
|
+
else
|
|
317
|
+
puts JSON.pretty_generate(data)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def output_table(data)
|
|
322
|
+
return puts 'No data' if data.empty?
|
|
323
|
+
|
|
324
|
+
# Extract common fields
|
|
325
|
+
keys = data.first.keys
|
|
326
|
+
headers = keys.join("\t")
|
|
327
|
+
puts headers
|
|
328
|
+
puts '-' * 80
|
|
329
|
+
|
|
330
|
+
data.each do |item|
|
|
331
|
+
values = keys.map { |k| format_value(item[k]) }
|
|
332
|
+
puts values.join("\t")
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def format_value(value)
|
|
337
|
+
case value
|
|
338
|
+
when Hash, Array
|
|
339
|
+
JSON.generate(value)
|
|
340
|
+
when nil
|
|
341
|
+
''
|
|
342
|
+
else
|
|
343
|
+
value.to_s
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def show_version
|
|
348
|
+
puts "BugSink CLI v#{Bugsink::VERSION}"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def show_help(resource = nil)
|
|
352
|
+
if resource
|
|
353
|
+
show_resource_help(resource)
|
|
354
|
+
else
|
|
355
|
+
show_general_help
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def show_general_help
|
|
360
|
+
puts <<~HELP
|
|
361
|
+
BugSink CLI - API wrapper for BugSink error tracking
|
|
362
|
+
|
|
363
|
+
Usage: bugsink <resource> <action> [options]
|
|
364
|
+
|
|
365
|
+
Resources:
|
|
366
|
+
config - Configuration management
|
|
367
|
+
teams - Team operations
|
|
368
|
+
projects - Project operations
|
|
369
|
+
issues - Issue operations (read-only)
|
|
370
|
+
events - Event operations (read-only)
|
|
371
|
+
releases - Release operations
|
|
372
|
+
|
|
373
|
+
Global Options:
|
|
374
|
+
--json Output as JSON
|
|
375
|
+
--quiet Minimal output (IDs only)
|
|
376
|
+
|
|
377
|
+
Common Commands:
|
|
378
|
+
bugsink config show Show current configuration
|
|
379
|
+
bugsink config set-project <id> Set default project ID
|
|
380
|
+
bugsink config test Test API connectivity
|
|
381
|
+
|
|
382
|
+
bugsink teams list List all teams
|
|
383
|
+
bugsink teams get <uuid> Get team details
|
|
384
|
+
bugsink teams create '{"name":"TeamName"}' Create team
|
|
385
|
+
|
|
386
|
+
bugsink projects list [--team=<uuid>] List projects
|
|
387
|
+
bugsink projects get <id> Get project details
|
|
388
|
+
|
|
389
|
+
bugsink issues list --project=<id> List issues for project
|
|
390
|
+
bugsink issues get <uuid> Get issue details
|
|
391
|
+
|
|
392
|
+
bugsink events list --issue=<uuid> List events for issue
|
|
393
|
+
bugsink events stacktrace <uuid> Get formatted stacktrace
|
|
394
|
+
|
|
395
|
+
bugsink releases list --project=<id> List releases
|
|
396
|
+
bugsink releases create '{"project":8,"version":"v1.0"}'
|
|
397
|
+
|
|
398
|
+
Environment Variables:
|
|
399
|
+
BUGSINK_API_KEY API authentication token (required)
|
|
400
|
+
BUGSINK_HOST API host (default: https://bugs.kopernici.cz)
|
|
401
|
+
|
|
402
|
+
Configuration File:
|
|
403
|
+
.bugsink Project ID for current directory
|
|
404
|
+
|
|
405
|
+
For resource-specific help:
|
|
406
|
+
bugsink help <resource>
|
|
407
|
+
|
|
408
|
+
Examples:
|
|
409
|
+
# Set up configuration
|
|
410
|
+
export BUGSINK_API_KEY="your-token-here"
|
|
411
|
+
bugsink config set-project 8
|
|
412
|
+
bugsink config test
|
|
413
|
+
|
|
414
|
+
# List latest issues
|
|
415
|
+
bugsink issues list --project=8 --sort=last_seen --order=desc --json
|
|
416
|
+
|
|
417
|
+
# Get stacktrace for an event
|
|
418
|
+
bugsink events stacktrace <event-uuid>
|
|
419
|
+
|
|
420
|
+
Note: Issues and Events are READ-ONLY via the API. Write operations are not supported.
|
|
421
|
+
HELP
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def show_resource_help(resource)
|
|
425
|
+
case resource
|
|
426
|
+
when 'config'
|
|
427
|
+
puts <<~HELP
|
|
428
|
+
bugsink config - Configuration management
|
|
429
|
+
|
|
430
|
+
Actions:
|
|
431
|
+
show Show current configuration
|
|
432
|
+
set-project <id> Set default project ID in .bugsink file
|
|
433
|
+
test Test API connectivity
|
|
434
|
+
|
|
435
|
+
Examples:
|
|
436
|
+
bugsink config show
|
|
437
|
+
bugsink config set-project 8
|
|
438
|
+
bugsink config test
|
|
439
|
+
HELP
|
|
440
|
+
when 'teams'
|
|
441
|
+
puts <<~HELP
|
|
442
|
+
bugsink teams - Team operations
|
|
443
|
+
|
|
444
|
+
Actions:
|
|
445
|
+
list List all teams
|
|
446
|
+
get <uuid> Get team details
|
|
447
|
+
create <json> Create new team
|
|
448
|
+
update <uuid> <json> Update team
|
|
449
|
+
|
|
450
|
+
JSON Format (create):
|
|
451
|
+
{"name":"Team Name","visibility":"hidden"}
|
|
452
|
+
Visibility options: joinable, discoverable, hidden
|
|
453
|
+
|
|
454
|
+
JSON Format (update):
|
|
455
|
+
{"name":"New Name"} // All fields optional
|
|
456
|
+
|
|
457
|
+
Examples:
|
|
458
|
+
bugsink teams list --json
|
|
459
|
+
bugsink teams get ee4f4572-0957-4346-b433-3c605acbfa2a
|
|
460
|
+
bugsink teams create '{"name":"My Team","visibility":"hidden"}'
|
|
461
|
+
bugsink teams update <uuid> '{"name":"Updated Name"}'
|
|
462
|
+
HELP
|
|
463
|
+
when 'projects'
|
|
464
|
+
puts <<~HELP
|
|
465
|
+
bugsink projects - Project operations
|
|
466
|
+
|
|
467
|
+
Actions:
|
|
468
|
+
list [--team=<uuid>] List projects (optionally filtered by team)
|
|
469
|
+
get <id> Get project details
|
|
470
|
+
create <json> Create new project
|
|
471
|
+
update <id> <json> Update project
|
|
472
|
+
|
|
473
|
+
JSON Format (create):
|
|
474
|
+
{
|
|
475
|
+
"team":"team-uuid",
|
|
476
|
+
"name":"Project Name",
|
|
477
|
+
"visibility":"team_members",
|
|
478
|
+
"alert_on_new_issue":true,
|
|
479
|
+
"alert_on_regression":true,
|
|
480
|
+
"alert_on_unmute":false
|
|
481
|
+
}
|
|
482
|
+
Visibility options: joinable, discoverable, team_members
|
|
483
|
+
|
|
484
|
+
JSON Format (update):
|
|
485
|
+
{"name":"New Name","alert_on_new_issue":false} // All fields optional
|
|
486
|
+
|
|
487
|
+
Examples:
|
|
488
|
+
bugsink projects list --team=<uuid> --json
|
|
489
|
+
bugsink projects get 8
|
|
490
|
+
bugsink projects create '{"team":"<uuid>","name":"Test Project"}'
|
|
491
|
+
bugsink projects update 8 '{"alert_on_new_issue":false}'
|
|
492
|
+
HELP
|
|
493
|
+
when 'issues'
|
|
494
|
+
puts <<~HELP
|
|
495
|
+
bugsink issues - Issue operations (READ-ONLY)
|
|
496
|
+
|
|
497
|
+
Actions:
|
|
498
|
+
list --project=<id> [--sort=<field>] [--order=<asc|desc>]
|
|
499
|
+
get <uuid>
|
|
500
|
+
|
|
501
|
+
Options:
|
|
502
|
+
--project=<id> Project ID (required for list)
|
|
503
|
+
--sort=<field> Sort by: last_seen, digest_order (default: last_seen)
|
|
504
|
+
--order=<order> Sort order: asc, desc (default: desc)
|
|
505
|
+
|
|
506
|
+
Examples:
|
|
507
|
+
bugsink issues list --project=8 --sort=last_seen --order=desc
|
|
508
|
+
bugsink issues get <uuid> --json
|
|
509
|
+
|
|
510
|
+
Note: Issues are READ-ONLY. No write operations available in API.
|
|
511
|
+
HELP
|
|
512
|
+
when 'events'
|
|
513
|
+
puts <<~HELP
|
|
514
|
+
bugsink events - Event operations (READ-ONLY)
|
|
515
|
+
|
|
516
|
+
Actions:
|
|
517
|
+
list --issue=<uuid> [--order=<asc|desc>]
|
|
518
|
+
get <uuid>
|
|
519
|
+
stacktrace <uuid>
|
|
520
|
+
|
|
521
|
+
Options:
|
|
522
|
+
--issue=<uuid> Issue UUID (required for list)
|
|
523
|
+
--order=<order> Sort order: asc, desc (default: desc)
|
|
524
|
+
|
|
525
|
+
Examples:
|
|
526
|
+
bugsink events list --issue=<uuid>
|
|
527
|
+
bugsink events get <uuid> --json
|
|
528
|
+
bugsink events stacktrace <uuid>
|
|
529
|
+
|
|
530
|
+
Note: Events are READ-ONLY. Created via Sentry SDK only.
|
|
531
|
+
HELP
|
|
532
|
+
when 'releases'
|
|
533
|
+
puts <<~HELP
|
|
534
|
+
bugsink releases - Release operations
|
|
535
|
+
|
|
536
|
+
Actions:
|
|
537
|
+
list --project=<id> List releases for project
|
|
538
|
+
get <uuid> Get release details
|
|
539
|
+
create <json> Create new release
|
|
540
|
+
|
|
541
|
+
JSON Format (create):
|
|
542
|
+
{
|
|
543
|
+
"project":8,
|
|
544
|
+
"version":"v1.2.3",
|
|
545
|
+
"timestamp":"2026-02-03T12:00:00Z" // Optional
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
Examples:
|
|
549
|
+
bugsink releases list --project=8
|
|
550
|
+
bugsink releases get <uuid>
|
|
551
|
+
bugsink releases create '{"project":8,"version":"v1.2.3"}'
|
|
552
|
+
|
|
553
|
+
Note: Releases can be created but not updated or deleted.
|
|
554
|
+
HELP
|
|
555
|
+
else
|
|
556
|
+
error("Unknown resource: #{resource}")
|
|
557
|
+
show_general_help
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def success(message)
|
|
562
|
+
puts "✓ #{message}"
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def info(message)
|
|
566
|
+
puts "ℹ #{message}"
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def error(message)
|
|
570
|
+
warn "✗ #{message}"
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
end
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'httparty'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'config'
|
|
6
|
+
|
|
7
|
+
module Bugsink
|
|
8
|
+
# HTTP client for BugSink API
|
|
9
|
+
class Client
|
|
10
|
+
include HTTParty
|
|
11
|
+
|
|
12
|
+
class ClientError < StandardError
|
|
13
|
+
attr_reader :code, :response
|
|
14
|
+
|
|
15
|
+
def initialize(message, code: nil, response: nil)
|
|
16
|
+
super(message)
|
|
17
|
+
@code = code
|
|
18
|
+
@response = response
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Response
|
|
23
|
+
attr_reader :data, :next_cursor
|
|
24
|
+
|
|
25
|
+
def initialize(response_body)
|
|
26
|
+
@data = response_body.is_a?(Hash) ? [response_body] : (response_body || [])
|
|
27
|
+
@next_cursor = extract_next_cursor(@data)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize_from_list(response_body)
|
|
31
|
+
@data = response_body['results'] || []
|
|
32
|
+
@next_cursor = response_body['next']
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def success?
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_data(new_data)
|
|
40
|
+
@data.concat(new_data)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def has_more?
|
|
44
|
+
!@next_cursor.nil?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def extract_next_cursor(data)
|
|
50
|
+
return nil unless data.is_a?(Hash)
|
|
51
|
+
data['next']
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attr_reader :config
|
|
56
|
+
|
|
57
|
+
def initialize(config = nil)
|
|
58
|
+
@config = config || Config.new
|
|
59
|
+
@config.validate!
|
|
60
|
+
|
|
61
|
+
self.class.base_uri @config.host
|
|
62
|
+
self.class.headers @config.authorization_header
|
|
63
|
+
self.class.headers 'Content-Type' => 'application/json'
|
|
64
|
+
self.class.headers 'Accept' => 'application/json'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Teams
|
|
68
|
+
def teams_list
|
|
69
|
+
response = self.class.get('/api/canonical/0/teams/')
|
|
70
|
+
check_response(response)
|
|
71
|
+
parse_list_response(response)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def team_get(uuid)
|
|
75
|
+
response = self.class.get("/api/canonical/0/teams/#{uuid}/")
|
|
76
|
+
check_response(response)
|
|
77
|
+
response.parsed_response
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def team_create(name:, visibility: nil)
|
|
81
|
+
body = { name: name }
|
|
82
|
+
body[:visibility] = visibility if visibility
|
|
83
|
+
|
|
84
|
+
response = self.class.post('/api/canonical/0/teams/', body: body.to_json)
|
|
85
|
+
check_response(response)
|
|
86
|
+
response.parsed_response
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def team_update(uuid, name: nil, visibility: nil)
|
|
90
|
+
body = {}
|
|
91
|
+
body[:name] = name if name
|
|
92
|
+
body[:visibility] = visibility if visibility
|
|
93
|
+
|
|
94
|
+
raise ArgumentError, 'At least one field must be provided for update' if body.empty?
|
|
95
|
+
|
|
96
|
+
response = self.class.patch("/api/canonical/0/teams/#{uuid}/", body: body.to_json)
|
|
97
|
+
check_response(response)
|
|
98
|
+
response.parsed_response
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Projects
|
|
102
|
+
def projects_list(team_uuid: nil)
|
|
103
|
+
query = {}
|
|
104
|
+
query[:team] = team_uuid if team_uuid
|
|
105
|
+
|
|
106
|
+
response = self.class.get('/api/canonical/0/projects/', query: query)
|
|
107
|
+
check_response(response)
|
|
108
|
+
parse_list_response(response)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def project_get(id)
|
|
112
|
+
response = self.class.get("/api/canonical/0/projects/#{id}/")
|
|
113
|
+
check_response(response)
|
|
114
|
+
response.parsed_response
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def project_create(team_uuid:, name:, visibility: nil, alert_on_new_issue: nil, alert_on_regression: nil, alert_on_unmute: nil)
|
|
118
|
+
body = {
|
|
119
|
+
team: team_uuid,
|
|
120
|
+
name: name
|
|
121
|
+
}
|
|
122
|
+
body[:visibility] = visibility if visibility
|
|
123
|
+
body[:alert_on_new_issue] = alert_on_new_issue unless alert_on_new_issue.nil?
|
|
124
|
+
body[:alert_on_regression] = alert_on_regression unless alert_on_regression.nil?
|
|
125
|
+
body[:alert_on_unmute] = alert_on_unmute unless alert_on_unmute.nil?
|
|
126
|
+
|
|
127
|
+
response = self.class.post('/api/canonical/0/projects/', body: body.to_json)
|
|
128
|
+
check_response(response)
|
|
129
|
+
response.parsed_response
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def project_update(id, name: nil, visibility: nil, alert_on_new_issue: nil, alert_on_regression: nil, alert_on_unmute: nil)
|
|
133
|
+
body = {}
|
|
134
|
+
body[:name] = name if name
|
|
135
|
+
body[:visibility] = visibility if visibility
|
|
136
|
+
body[:alert_on_new_issue] = alert_on_new_issue unless alert_on_new_issue.nil?
|
|
137
|
+
body[:alert_on_regression] = alert_on_regression unless alert_on_regression.nil?
|
|
138
|
+
body[:alert_on_unmute] = alert_on_unmute unless alert_on_unmute.nil?
|
|
139
|
+
|
|
140
|
+
raise ArgumentError, 'At least one field must be provided for update' if body.empty?
|
|
141
|
+
|
|
142
|
+
response = self.class.patch("/api/canonical/0/projects/#{id}/", body: body.to_json)
|
|
143
|
+
check_response(response)
|
|
144
|
+
response.parsed_response
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Issues
|
|
148
|
+
def issues_list(project_id:, sort: 'last_seen', order: 'desc', limit: 250, cursor: nil)
|
|
149
|
+
query = {
|
|
150
|
+
project: project_id,
|
|
151
|
+
sort: sort,
|
|
152
|
+
order: order,
|
|
153
|
+
limit: limit
|
|
154
|
+
}
|
|
155
|
+
query[:cursor] = cursor if cursor
|
|
156
|
+
|
|
157
|
+
response = self.class.get('/api/canonical/0/issues/', query: query)
|
|
158
|
+
check_response(response)
|
|
159
|
+
parse_list_response(response)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def issue_get(uuid)
|
|
163
|
+
response = self.class.get("/api/canonical/0/issues/#{uuid}/")
|
|
164
|
+
check_response(response)
|
|
165
|
+
response.parsed_response
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Events
|
|
169
|
+
def events_list(issue_uuid:, order: 'desc', limit: 250, cursor: nil)
|
|
170
|
+
query = {
|
|
171
|
+
issue: issue_uuid,
|
|
172
|
+
order: order,
|
|
173
|
+
limit: limit
|
|
174
|
+
}
|
|
175
|
+
query[:cursor] = cursor if cursor
|
|
176
|
+
|
|
177
|
+
response = self.class.get('/api/canonical/0/events/', query: query)
|
|
178
|
+
check_response(response)
|
|
179
|
+
parse_list_response(response)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def event_get(uuid)
|
|
183
|
+
response = self.class.get("/api/canonical/0/events/#{uuid}/")
|
|
184
|
+
check_response(response)
|
|
185
|
+
response.parsed_response
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def event_stacktrace(uuid)
|
|
189
|
+
response = self.class.get("/api/canonical/0/events/#{uuid}/stacktrace/")
|
|
190
|
+
check_response(response)
|
|
191
|
+
response.body
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Releases
|
|
195
|
+
def releases_list(project_id:)
|
|
196
|
+
query = { project: project_id }
|
|
197
|
+
|
|
198
|
+
response = self.class.get('/api/canonical/0/releases/', query: query)
|
|
199
|
+
check_response(response)
|
|
200
|
+
parse_list_response(response)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def release_get(uuid)
|
|
204
|
+
response = self.class.get("/api/canonical/0/releases/#{uuid}/")
|
|
205
|
+
check_response(response)
|
|
206
|
+
response.parsed_response
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def release_create(project_id:, version:, timestamp: nil)
|
|
210
|
+
body = {
|
|
211
|
+
project: project_id,
|
|
212
|
+
version: version
|
|
213
|
+
}
|
|
214
|
+
body[:timestamp] = timestamp if timestamp
|
|
215
|
+
|
|
216
|
+
response = self.class.post('/api/canonical/0/releases/', body: body.to_json)
|
|
217
|
+
check_response(response)
|
|
218
|
+
response.parsed_response
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Test connectivity
|
|
222
|
+
def test_connection
|
|
223
|
+
response = self.class.get('/api/canonical/0/teams/')
|
|
224
|
+
check_response(response)
|
|
225
|
+
true
|
|
226
|
+
rescue ClientError => e
|
|
227
|
+
raise ClientError.new("Connection test failed: #{e.message}", code: e.code, response: e.response)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
def check_response(response)
|
|
233
|
+
return response if response.code >= 200 && response.code < 300
|
|
234
|
+
|
|
235
|
+
error_message = "HTTP #{response.code}"
|
|
236
|
+
begin
|
|
237
|
+
error_body = response.parsed_response
|
|
238
|
+
error_message += ": #{error_body}" if error_body
|
|
239
|
+
rescue JSON::ParserError
|
|
240
|
+
error_message += ": #{response.body}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
raise ClientError.new(error_message, code: response.code, response: response.body)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def parse_list_response(response)
|
|
247
|
+
body = response.parsed_response
|
|
248
|
+
|
|
249
|
+
# Handle paginated list responses
|
|
250
|
+
if body.is_a?(Hash) && body.key?('results')
|
|
251
|
+
resp = Response.new(nil)
|
|
252
|
+
resp.initialize_from_list(body)
|
|
253
|
+
resp
|
|
254
|
+
else
|
|
255
|
+
# Handle simple array responses (like teams)
|
|
256
|
+
Response.new(body)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bugsink
|
|
4
|
+
# Configuration management for BugSink API client
|
|
5
|
+
class Config
|
|
6
|
+
class ConfigError < StandardError; end
|
|
7
|
+
|
|
8
|
+
attr_reader :api_key, :host, :project_id
|
|
9
|
+
|
|
10
|
+
DOTFILE = '.bugsink'.freeze
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@api_key = ENV['BUGSINK_API_KEY']
|
|
14
|
+
@host = ENV.fetch('BUGSINK_HOST', 'https://bugs.kopernici.cz')
|
|
15
|
+
@project_id = read_project_id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def valid?
|
|
19
|
+
!api_key.nil? && !api_key.empty?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate!
|
|
23
|
+
raise ConfigError, 'BUGSINK_API_KEY environment variable is required' unless valid?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def authorization_header
|
|
27
|
+
{ 'Authorization' => "Bearer #{api_key}" }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def set_project_id(id)
|
|
31
|
+
File.write(dotfile_path, "PROJECT_ID=#{id}\n")
|
|
32
|
+
@project_id = id
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def project_id_present?
|
|
36
|
+
!project_id.nil?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_s
|
|
40
|
+
<<~CONFIG
|
|
41
|
+
BugSink Configuration:
|
|
42
|
+
Host: #{host}
|
|
43
|
+
API Key: #{api_key ? "#{api_key[0..8]}...#{api_key[-8..]}" : 'not set'}
|
|
44
|
+
Project ID: #{project_id || 'not set'}
|
|
45
|
+
CONFIG
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def read_project_id
|
|
51
|
+
return nil unless File.exist?(dotfile_path)
|
|
52
|
+
|
|
53
|
+
content = File.read(dotfile_path).strip
|
|
54
|
+
match = content.match(/^PROJECT_ID=(\d+)$/)
|
|
55
|
+
match ? match[1].to_i : nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def dotfile_path
|
|
59
|
+
File.join(Dir.pwd, DOTFILE)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/bugsink.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: bugsink-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tomas Kopernik
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: httparty
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.22'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.22'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.13'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.13'
|
|
54
|
+
description: A command-line interface for the BugSink error tracking service, providing
|
|
55
|
+
full API access for teams, projects, issues, events, and releases.
|
|
56
|
+
email:
|
|
57
|
+
- tomas@kopernik.cz
|
|
58
|
+
executables:
|
|
59
|
+
- bugsink
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- CHANGELOG.md
|
|
64
|
+
- LICENSE
|
|
65
|
+
- README.md
|
|
66
|
+
- exe/bugsink
|
|
67
|
+
- lib/bugsink.rb
|
|
68
|
+
- lib/bugsink/cli.rb
|
|
69
|
+
- lib/bugsink/client.rb
|
|
70
|
+
- lib/bugsink/config.rb
|
|
71
|
+
- lib/bugsink/version.rb
|
|
72
|
+
homepage: https://github.com/koperniki/bugsink-cli
|
|
73
|
+
licenses:
|
|
74
|
+
- MIT
|
|
75
|
+
metadata:
|
|
76
|
+
homepage_uri: https://github.com/koperniki/bugsink-cli
|
|
77
|
+
source_code_uri: https://github.com/koperniki/bugsink-cli
|
|
78
|
+
changelog_uri: https://github.com/koperniki/bugsink-cli/blob/main/CHANGELOG.md
|
|
79
|
+
rdoc_options: []
|
|
80
|
+
require_paths:
|
|
81
|
+
- lib
|
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: 3.3.0
|
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
requirements: []
|
|
93
|
+
rubygems_version: 3.6.9
|
|
94
|
+
specification_version: 4
|
|
95
|
+
summary: CLI tool for interacting with BugSink error tracking API
|
|
96
|
+
test_files: []
|