fizzy-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 +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +194 -0
- data/bin/fizzy +13 -0
- data/lib/fizzy/auth.rb +37 -0
- data/lib/fizzy/cli/auth.rb +110 -0
- data/lib/fizzy/cli/base.rb +68 -0
- data/lib/fizzy/cli/boards.rb +57 -0
- data/lib/fizzy/cli/cards.rb +178 -0
- data/lib/fizzy/cli/columns.rb +61 -0
- data/lib/fizzy/cli/comments.rb +61 -0
- data/lib/fizzy/cli/notifications.rb +42 -0
- data/lib/fizzy/cli/pins.rb +30 -0
- data/lib/fizzy/cli/reactions.rb +49 -0
- data/lib/fizzy/cli/steps.rb +55 -0
- data/lib/fizzy/cli/tags.rb +17 -0
- data/lib/fizzy/cli/users.rb +50 -0
- data/lib/fizzy/cli.rb +62 -0
- data/lib/fizzy/client.rb +124 -0
- data/lib/fizzy/errors.rb +21 -0
- data/lib/fizzy/formatter.rb +37 -0
- data/lib/fizzy/paginator.rb +43 -0
- data/lib/fizzy/version.rb +5 -0
- data/lib/fizzy.rb +13 -0
- metadata +88 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8f3929ca84a2457054103a3f63addd79d594e2a1b10c35335a0de3ec0255144b
|
|
4
|
+
data.tar.gz: ed75704d968deb083d4535ce85d6f68827877e65bc735b36c23ac22f0b7f01dc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5dd289c5a61f833fb5f90f6efa6a2bbf9d7532f5a6650157debaa918347e94bc9336490dd352a3d9985fa7a875b2730a74a3ee291444b09035220a48365d489a
|
|
7
|
+
data.tar.gz: 3bf5e77238da1f4f1d49dfb4d74874ab7305be00bf6478ad3e0b2090145d2259922ca928548d58af9fa5325db2295255f2dac3e073eb4590a19c624cba0ecc1c
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-02-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release
|
|
12
|
+
- Board, card, column, step, comment, reaction, tag, user, notification, and pin management
|
|
13
|
+
- Thor-based CLI with subcommands
|
|
14
|
+
- Token auth with multi-account support (`--account`)
|
|
15
|
+
- JSON output mode (`--json`) for all commands
|
|
16
|
+
- Link-header pagination support
|
|
17
|
+
- Table, detail, and JSON output formatters
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 David Paluy
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Fizzy CLI
|
|
2
|
+
|
|
3
|
+
A Ruby command-line client for [Fizzy](https://fizzy.do) project management.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/rb/fizzy-cli)
|
|
6
|
+
[](https://github.com/dpaluy/fizzy-cli/actions/workflows/ci.yml)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Requires Ruby >= 3.2.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
gem install fizzy
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Authentication
|
|
17
|
+
|
|
18
|
+
### Getting a Personal Access Token
|
|
19
|
+
|
|
20
|
+
1. Log in to [app.fizzy.do](https://app.fizzy.do)
|
|
21
|
+
2. Open your profile settings (click your avatar)
|
|
22
|
+
3. Scroll to the **Developer** section
|
|
23
|
+
4. Click **personal access tokens**
|
|
24
|
+
5. Create a new token and copy it
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
### Logging in
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
fizzy auth login --token YOUR_TOKEN
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Tokens are stored at `~/.config/fizzy-cli/tokens.json`. You can also set `FIZZY_TOKEN` as an environment variable with `--account` to skip the file.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
fizzy auth status # Show current auth
|
|
38
|
+
fizzy auth accounts # List available accounts
|
|
39
|
+
fizzy auth switch SLUG # Change default account
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
All commands support `--json` for JSON output and `--account SLUG` to override the default account.
|
|
45
|
+
|
|
46
|
+
### Boards
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
fizzy boards list
|
|
50
|
+
fizzy boards get BOARD_ID
|
|
51
|
+
fizzy boards create "My Board"
|
|
52
|
+
fizzy boards update BOARD_ID --name "New Name"
|
|
53
|
+
fizzy boards delete BOARD_ID
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Cards
|
|
57
|
+
|
|
58
|
+
Cards are addressed by number (not ID).
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
fizzy cards list
|
|
62
|
+
fizzy cards list --board BOARD_ID --status open
|
|
63
|
+
fizzy cards get 42
|
|
64
|
+
fizzy cards create "Card title" --board BOARD_ID
|
|
65
|
+
fizzy cards update 42 --title "New title"
|
|
66
|
+
fizzy cards delete 42
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Card actions:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
fizzy cards close 42
|
|
73
|
+
fizzy cards reopen 42
|
|
74
|
+
fizzy cards not-now 42
|
|
75
|
+
fizzy cards triage 42 --column COLUMN_ID
|
|
76
|
+
fizzy cards untriage 42
|
|
77
|
+
fizzy cards tag 42 "bug"
|
|
78
|
+
fizzy cards assign 42 USER_ID
|
|
79
|
+
fizzy cards watch 42
|
|
80
|
+
fizzy cards unwatch 42
|
|
81
|
+
fizzy cards golden 42
|
|
82
|
+
fizzy cards ungolden 42
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Columns
|
|
86
|
+
|
|
87
|
+
Columns are scoped to a board.
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
fizzy columns list --board BOARD_ID
|
|
91
|
+
fizzy columns get COLUMN_ID --board BOARD_ID
|
|
92
|
+
fizzy columns create "To Do" --board BOARD_ID
|
|
93
|
+
fizzy columns update COLUMN_ID --board BOARD_ID --name "Done"
|
|
94
|
+
fizzy columns delete COLUMN_ID --board BOARD_ID
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Steps
|
|
98
|
+
|
|
99
|
+
Steps are scoped to a card (no list endpoint; steps appear in `cards get`).
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
fizzy steps create "Write tests" --card 42
|
|
103
|
+
fizzy steps get STEP_ID --card 42
|
|
104
|
+
fizzy steps update STEP_ID --card 42 --completed
|
|
105
|
+
fizzy steps delete STEP_ID --card 42
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Comments
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
fizzy comments list --card 42
|
|
112
|
+
fizzy comments get COMMENT_ID --card 42
|
|
113
|
+
fizzy comments create "Looks good" --card 42
|
|
114
|
+
fizzy comments update COMMENT_ID --card 42 --body "Updated"
|
|
115
|
+
fizzy comments delete COMMENT_ID --card 42
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Reactions
|
|
119
|
+
|
|
120
|
+
Works on cards and comments.
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
fizzy reactions list --card 42
|
|
124
|
+
fizzy reactions list --card 42 --comment COMMENT_ID
|
|
125
|
+
fizzy reactions create "thumbsup" --card 42
|
|
126
|
+
fizzy reactions delete REACTION_ID --card 42
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Tags, Users, Notifications, Pins
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
fizzy tags list
|
|
133
|
+
fizzy users list
|
|
134
|
+
fizzy users get USER_ID
|
|
135
|
+
fizzy notifications list
|
|
136
|
+
fizzy notifications read NOTIFICATION_ID
|
|
137
|
+
fizzy notifications mark-all-read
|
|
138
|
+
fizzy pins pin 42
|
|
139
|
+
fizzy pins unpin 42
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Other
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
fizzy identity # Show accounts and user info
|
|
146
|
+
fizzy version # Print version
|
|
147
|
+
fizzy help # List all commands
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Error Handling
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
begin
|
|
154
|
+
# any fizzy command
|
|
155
|
+
rescue Fizzy::AuthError => e
|
|
156
|
+
# 401 - invalid or expired token
|
|
157
|
+
rescue Fizzy::ForbiddenError => e
|
|
158
|
+
# 403 - insufficient permissions
|
|
159
|
+
rescue Fizzy::NotFoundError => e
|
|
160
|
+
# 404 - resource not found
|
|
161
|
+
rescue Fizzy::ValidationError => e
|
|
162
|
+
# 422 - invalid input
|
|
163
|
+
rescue Fizzy::RateLimitError => e
|
|
164
|
+
# 429 - too many requests, retry with backoff
|
|
165
|
+
rescue Fizzy::ServerError => e
|
|
166
|
+
# 500+ - server error
|
|
167
|
+
rescue Fizzy::NetworkError => e
|
|
168
|
+
# Timeout or connection failure
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Configuration
|
|
173
|
+
|
|
174
|
+
| Source | Purpose |
|
|
175
|
+
|--------|---------|
|
|
176
|
+
| `~/.config/fizzy-cli/tokens.json` | Stored auth tokens and default account |
|
|
177
|
+
| `FIZZY_TOKEN` env var | Token override (requires `--account`) |
|
|
178
|
+
|
|
179
|
+
## Development
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
bundle install
|
|
183
|
+
bundle exec rake # runs tests + rubocop
|
|
184
|
+
bundle exec rake test # tests only
|
|
185
|
+
bundle exec rubocop # lint only
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Contributing
|
|
189
|
+
|
|
190
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/dpaluy/fizzy-cli.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/bin/fizzy
ADDED
data/lib/fizzy/auth.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fizzy
|
|
4
|
+
class Auth
|
|
5
|
+
CONFIG_DIR = File.expand_path("~/.config/fizzy-cli")
|
|
6
|
+
TOKEN_FILE = File.join(CONFIG_DIR, "tokens.json")
|
|
7
|
+
|
|
8
|
+
def self.resolve(account_slug = nil)
|
|
9
|
+
if ENV["FIZZY_TOKEN"]
|
|
10
|
+
raise AuthError, "--account required with FIZZY_TOKEN" unless account_slug
|
|
11
|
+
|
|
12
|
+
return { "access_token" => ENV["FIZZY_TOKEN"], "account_slug" => normalize_slug(account_slug) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
resolve_from_file(account_slug)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.normalize_slug(slug)
|
|
19
|
+
slug&.delete_prefix("/")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.resolve_from_file(account_slug)
|
|
23
|
+
unless File.exist?(TOKEN_FILE)
|
|
24
|
+
raise AuthError,
|
|
25
|
+
"No tokens file at #{TOKEN_FILE}. Run: fizzy auth login --token TOKEN"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
data = JSON.parse(File.read(TOKEN_FILE))
|
|
29
|
+
slug = normalize_slug(account_slug || data["default_account"])
|
|
30
|
+
account = data["accounts"]&.find { |a| normalize_slug(a["account_slug"]) == slug }
|
|
31
|
+
raise AuthError, "No account found for #{slug}" unless account
|
|
32
|
+
|
|
33
|
+
account
|
|
34
|
+
end
|
|
35
|
+
private_class_method :resolve_from_file
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Fizzy
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
class AuthCommands < Thor
|
|
8
|
+
include Base
|
|
9
|
+
|
|
10
|
+
namespace "auth"
|
|
11
|
+
|
|
12
|
+
desc "identity", "Show current identity"
|
|
13
|
+
def identity
|
|
14
|
+
resp = client.get("/my/identity")
|
|
15
|
+
if json?
|
|
16
|
+
Formatter.json(resp.body)
|
|
17
|
+
else
|
|
18
|
+
resp.body["accounts"].each do |a|
|
|
19
|
+
puts "#{a["name"]} (#{a["slug"]})"
|
|
20
|
+
puts " #{a["user"]["name"]} <#{a["user"]["email_address"]}> — #{a["user"]["role"]}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "login", "Authenticate with a Personal Access Token"
|
|
26
|
+
option :token, required: true, desc: "Personal Access Token"
|
|
27
|
+
def login
|
|
28
|
+
token = options[:token]
|
|
29
|
+
|
|
30
|
+
# Verify token by fetching identity
|
|
31
|
+
c = Client.new(token: token, account_slug: "")
|
|
32
|
+
resp = c.get("/my/identity")
|
|
33
|
+
accounts = resp.body["accounts"]
|
|
34
|
+
|
|
35
|
+
raise AuthError, "No accounts found for this token" if accounts.empty?
|
|
36
|
+
|
|
37
|
+
# Build tokens data
|
|
38
|
+
token_accounts = accounts.map do |a|
|
|
39
|
+
{
|
|
40
|
+
"account_slug" => a["slug"],
|
|
41
|
+
"account_name" => a["name"],
|
|
42
|
+
"account_id" => a["id"],
|
|
43
|
+
"access_token" => token,
|
|
44
|
+
"user" => {
|
|
45
|
+
"id" => a["user"]["id"],
|
|
46
|
+
"name" => a["user"]["name"],
|
|
47
|
+
"email_address" => a["user"]["email_address"],
|
|
48
|
+
"role" => a["user"]["role"]
|
|
49
|
+
},
|
|
50
|
+
"created_at" => Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
data = {
|
|
55
|
+
"accounts" => token_accounts,
|
|
56
|
+
"default_account" => accounts.first["slug"]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
FileUtils.mkdir_p(Auth::CONFIG_DIR)
|
|
60
|
+
File.write(Auth::TOKEN_FILE, JSON.pretty_generate(data))
|
|
61
|
+
File.chmod(0o600, Auth::TOKEN_FILE)
|
|
62
|
+
|
|
63
|
+
puts "Authenticated as #{accounts.first.dig("user", "name")}"
|
|
64
|
+
accounts.each do |a|
|
|
65
|
+
marker = a["slug"] == data["default_account"] ? " (default)" : ""
|
|
66
|
+
puts " #{a["name"]} (#{a["slug"]})#{marker}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
desc "status", "Show current auth status"
|
|
71
|
+
def status
|
|
72
|
+
acct = Auth.resolve
|
|
73
|
+
c = Client.new(token: acct["access_token"], account_slug: acct["account_slug"])
|
|
74
|
+
resp = c.get("/my/identity")
|
|
75
|
+
|
|
76
|
+
puts "Token: #{Auth::TOKEN_FILE}"
|
|
77
|
+
puts "Account: #{acct["account_name"]} (#{acct["account_slug"]})"
|
|
78
|
+
puts "User: #{acct.dig("user", "name")} <#{acct.dig("user", "email_address")}>"
|
|
79
|
+
|
|
80
|
+
accounts_count = resp.body["accounts"].size
|
|
81
|
+
puts "Accounts: #{accounts_count}" if accounts_count > 1
|
|
82
|
+
rescue Fizzy::AuthError => e
|
|
83
|
+
puts "Not authenticated: #{e.message}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
desc "accounts", "List available accounts"
|
|
87
|
+
def accounts
|
|
88
|
+
data = JSON.parse(File.read(Auth::TOKEN_FILE))
|
|
89
|
+
data["accounts"].each do |a|
|
|
90
|
+
marker = a["account_slug"] == data["default_account"] ? " *" : ""
|
|
91
|
+
puts "#{a["account_name"]} (#{a["account_slug"]})#{marker}"
|
|
92
|
+
puts " #{a.dig("user", "name")} <#{a.dig("user", "email_address")}>"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
desc "switch ACCOUNT_SLUG", "Set default account"
|
|
97
|
+
def switch(account_slug)
|
|
98
|
+
data = JSON.parse(File.read(Auth::TOKEN_FILE))
|
|
99
|
+
normalized = Auth.normalize_slug(account_slug)
|
|
100
|
+
account = data["accounts"].find { |a| Auth.normalize_slug(a["account_slug"]) == normalized }
|
|
101
|
+
raise AuthError, "No account found for #{account_slug}" unless account
|
|
102
|
+
|
|
103
|
+
data["default_account"] = normalized
|
|
104
|
+
File.write(Auth::TOKEN_FILE, JSON.pretty_generate(data))
|
|
105
|
+
|
|
106
|
+
puts "Switched to #{account["account_name"]} (#{normalized})"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Fizzy
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
module Base
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.class_eval do
|
|
10
|
+
def self.exit_on_failure? = true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def global_options
|
|
17
|
+
parent_options || options
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def account
|
|
21
|
+
@account ||= Auth.resolve(global_options[:account])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def client
|
|
25
|
+
@client ||= Client.new(
|
|
26
|
+
token: account["access_token"],
|
|
27
|
+
account_slug: account["account_slug"]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def slug
|
|
32
|
+
"/#{client.account_slug}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def paginator
|
|
36
|
+
@paginator ||= Paginator.new(client)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def json?
|
|
40
|
+
global_options[:json]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_body(*keys)
|
|
44
|
+
keys.each_with_object({}) do |key, body|
|
|
45
|
+
val = options[key]
|
|
46
|
+
body[key] = val unless val.nil?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def output_list(data, headers:, &row_mapper)
|
|
51
|
+
if json?
|
|
52
|
+
Formatter.json(data)
|
|
53
|
+
else
|
|
54
|
+
rows = Array(data).map(&row_mapper)
|
|
55
|
+
Formatter.table(rows, headers: headers)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def output_detail(data, pairs:)
|
|
60
|
+
if json?
|
|
61
|
+
Formatter.json(data)
|
|
62
|
+
else
|
|
63
|
+
Formatter.detail(pairs)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fizzy
|
|
4
|
+
class CLI < Thor
|
|
5
|
+
class Boards < Thor
|
|
6
|
+
include Base
|
|
7
|
+
|
|
8
|
+
desc "list", "List all boards"
|
|
9
|
+
def list
|
|
10
|
+
data = paginator.all("#{slug}/boards")
|
|
11
|
+
output_list(data, headers: %w[ID Name Cards Columns]) do |b|
|
|
12
|
+
[b["id"], b["name"], b["open_cards_count"], b["columns_count"]]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "get BOARD_ID", "Show a board"
|
|
17
|
+
def get(board_id)
|
|
18
|
+
resp = client.get("#{slug}/boards/#{board_id}")
|
|
19
|
+
b = resp.body
|
|
20
|
+
output_detail(b, pairs: [
|
|
21
|
+
["ID", b["id"]],
|
|
22
|
+
["Name", b["name"]],
|
|
23
|
+
["Open cards", b["open_cards_count"]],
|
|
24
|
+
["Columns", b["columns_count"]],
|
|
25
|
+
["Created", b["created_at"]]
|
|
26
|
+
])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "create NAME", "Create a board"
|
|
30
|
+
def create(name)
|
|
31
|
+
resp = client.post("#{slug}/boards", body: { name: name })
|
|
32
|
+
b = resp.body
|
|
33
|
+
output_detail(b, pairs: [
|
|
34
|
+
["ID", b["id"]],
|
|
35
|
+
["Name", b["name"]]
|
|
36
|
+
])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc "update BOARD_ID", "Update a board"
|
|
40
|
+
option :name, required: true, desc: "New board name"
|
|
41
|
+
def update(board_id)
|
|
42
|
+
resp = client.put("#{slug}/boards/#{board_id}", body: { name: options[:name] })
|
|
43
|
+
b = resp.body
|
|
44
|
+
output_detail(b, pairs: [
|
|
45
|
+
["ID", b["id"]],
|
|
46
|
+
["Name", b["name"]]
|
|
47
|
+
])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
desc "delete BOARD_ID", "Delete a board"
|
|
51
|
+
def delete(board_id)
|
|
52
|
+
client.delete("#{slug}/boards/#{board_id}")
|
|
53
|
+
puts "Board #{board_id} deleted."
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|