fizzy-cli 0.3.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a799ac974669e5becb4d5bd617a4158be7878e36a432014b4c9b1cba9823b92b
4
- data.tar.gz: 4d14b732076b82dd8e0d26102da3db311a6ff14d38707f055e17264012949e53
3
+ metadata.gz: ca34b7cb72db62ab1659d5210eb4463496870cf817c88fa58321e7443a630ad3
4
+ data.tar.gz: 8d653993a4bb9bc85079f2b16c1f0c84d496abaab50c15c6ed5e9443fd16b089
5
5
  SHA512:
6
- metadata.gz: bc9f814d55706dab9762776cccd10492cc0087c0c3152ab968701e98e87812714a52dfa0e9c79eccb3133e7288762f5a081a63c07b8d829f848a85f37606c4d9
7
- data.tar.gz: fff7b7a0f5435d848a8b0c87a82f7f2eaa92d586dc4359f5c8711fd890a7b3f3ed5dc597e326017612cd944f264a076d436f30116df669632b46399921c93532
6
+ metadata.gz: 1152c52e0574fc5b9cb4f499de24e3cceaf046d2cdddc4f6a20f8d2eac1def18ebec9adf36be0f7f0aa61acbdcc24ef6b7353b04e0ecc61a58faba907b0157ec
7
+ data.tar.gz: 0a64fd82e2b22f214cf73a1770f3a373cc461986f521485311d27c8914d4d0948c6be68ae91252f103d27655f4f59dccc6b35adf47491d024bdaafd9e57df2e2
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [0.4.0] - 2026-02-22
9
+
10
+ ### Added
11
+ - Per-project `.fizzy.yml` config file for account and board defaults
12
+ - `fizzy init` interactive command to create `.fizzy.yml`
13
+ - `ProjectConfig` class that walks up directories to find nearest `.fizzy.yml`
14
+ - Resolution priority: CLI flag > `.fizzy.yml` > global tokens.yml default
15
+ - `Auth.token_data` class method to centralize token file reading
16
+ - `--board` is now optional for columns and card create when set in `.fizzy.yml`
17
+
18
+ ### Fixed
19
+ - Client follows Location header on any 2xx with empty body, not just 201
20
+ - All update commands handle nil response body instead of crashing with NoMethodError
21
+ - Skill file incorrectly documented `fizzy identity` instead of `fizzy auth identity`
22
+ - Guard against empty string in `--board` and `--account` flag values
23
+ - `ProjectConfig` rescues `Psych::SyntaxError` with user-friendly error message
24
+ - Blank or non-hash `.fizzy.yml` files treated as empty config instead of crashing
25
+
26
+ ## [0.3.2] - 2026-02-22
27
+
28
+ ### Changed
29
+ - Moved AI Agent Skill section in README to immediately follow Install
30
+
8
31
  ## [0.3.1] - 2026-02-22
9
32
 
10
33
  ### Changed
data/README.md CHANGED
@@ -13,6 +13,18 @@ Requires Ruby >= 3.2.
13
13
  gem install fizzy
14
14
  ```
15
15
 
16
+ ### AI Agent Skill
17
+
18
+ Fizzy CLI ships with a [skill file](skills/fizzy-cli/SKILL.md) that teaches AI coding assistants (Claude Code, Codex) how to use the CLI. Install it so agents can manage your Fizzy boards and cards autonomously.
19
+
20
+ ```sh
21
+ fizzy skill install # Claude Code, user-level (default)
22
+ fizzy skill install --target codex # OpenAI Codex / OpenCode
23
+ fizzy skill install --target all # All supported agents
24
+ fizzy skill install --scope project # Project-level instead of user-level
25
+ fizzy skill uninstall # Remove skill file
26
+ ```
27
+
16
28
  ## Authentication
17
29
 
18
30
  ### Getting a Personal Access Token
@@ -39,6 +51,26 @@ fizzy auth accounts # List available accounts
39
51
  fizzy auth switch SLUG # Change default account
40
52
  ```
41
53
 
54
+ ## Project Configuration
55
+
56
+ Set project-level defaults so you don't need `--account` and `--board` on every command:
57
+
58
+ ```sh
59
+ fizzy init
60
+ ```
61
+
62
+ This creates a `.fizzy.yml` in the current directory:
63
+
64
+ ```yaml
65
+ account: acme
66
+ board: b1
67
+ ```
68
+
69
+ **Resolution priority** (highest wins):
70
+ 1. CLI flag (`--account` / `--board`)
71
+ 2. `.fizzy.yml` (nearest ancestor directory)
72
+ 3. Global `default_account` from `~/.config/fizzy-cli/tokens.yml`
73
+
42
74
  ## Usage
43
75
 
44
76
  All commands support `--json` for JSON output and `--account SLUG` to override the default account.
@@ -178,6 +210,7 @@ end
178
210
 
179
211
  | Source | Purpose |
180
212
  |--------|---------|
213
+ | `.fizzy.yml` | Per-project account and board defaults |
181
214
  | `~/.config/fizzy-cli/tokens.yml` | Stored auth tokens and default account |
182
215
  | `FIZZY_TOKEN` env var | Token override (requires `--account`) |
183
216
 
@@ -190,26 +223,6 @@ bundle exec rake test # tests only
190
223
  bundle exec rubocop # lint only
191
224
  ```
192
225
 
193
- ## AI Agent Integration
194
-
195
- Fizzy CLI ships with a [skill file](skills/fizzy-cli/SKILL.md) that teaches AI coding assistants how to use the CLI. Install it so agents can manage your Fizzy boards and cards autonomously.
196
-
197
- The `fizzy` gem must be installed and authenticated first:
198
-
199
- ```sh
200
- gem install fizzy-cli
201
- fizzy auth login --token YOUR_TOKEN
202
- ```
203
-
204
- ```sh
205
- fizzy skill install # Claude Code, user-level (default)
206
- fizzy skill install --target codex # OpenAI Codex / OpenCode
207
- fizzy skill install --target all # All supported agents
208
- fizzy skill install --scope project # Project-level instead of user-level
209
- fizzy skill uninstall # Remove skill file
210
- ```
211
-
212
-
213
226
  ## Contributing
214
227
 
215
228
  Bug reports and pull requests are welcome on GitHub at https://github.com/dpaluy/fizzy-cli.
data/lib/fizzy/auth.rb CHANGED
@@ -19,13 +19,17 @@ module Fizzy
19
19
  slug&.delete_prefix("/")
20
20
  end
21
21
 
22
- def self.resolve_from_file(account_slug)
22
+ def self.token_data
23
23
  unless File.exist?(TOKEN_FILE)
24
24
  raise AuthError,
25
25
  "No tokens file at #{TOKEN_FILE}. Run: fizzy auth login --token TOKEN"
26
26
  end
27
27
 
28
- data = YAML.safe_load_file(TOKEN_FILE, permitted_classes: [Time])
28
+ YAML.safe_load_file(TOKEN_FILE, permitted_classes: [Time])
29
+ end
30
+
31
+ def self.resolve_from_file(account_slug)
32
+ data = token_data
29
33
  slug = normalize_slug(account_slug || data["default_account"])
30
34
  account = data["accounts"]&.find { |a| normalize_slug(a["account_slug"]) == slug }
31
35
  raise AuthError, "No account found for #{slug}" unless account
@@ -17,8 +17,24 @@ module Fizzy
17
17
  parent_options || options
18
18
  end
19
19
 
20
+ def project_config
21
+ @project_config ||= ProjectConfig.new
22
+ end
23
+
20
24
  def account
21
- @account ||= Auth.resolve(global_options[:account])
25
+ slug = global_options[:account]
26
+ slug = nil if slug&.empty?
27
+ @account ||= Auth.resolve(slug || project_config.account)
28
+ end
29
+
30
+ def board
31
+ b = options[:board]
32
+ b && !b.empty? ? b : project_config.board
33
+ end
34
+
35
+ def require_board!
36
+ board || raise(Thor::Error,
37
+ "No value provided for option '--board'. Set via --board, .fizzy.yml, or: fizzy init")
22
38
  end
23
39
 
24
40
  def client
@@ -41,10 +41,14 @@ module Fizzy
41
41
  def update(board_id)
42
42
  resp = client.put("boards/#{board_id}", body: { name: options[:name] })
43
43
  b = resp.body
44
- output_detail(b, pairs: [
45
- ["ID", b["id"]],
46
- ["Name", b["name"]]
47
- ])
44
+ if b
45
+ output_detail(b, pairs: [
46
+ ["ID", b["id"]],
47
+ ["Name", b["name"]]
48
+ ])
49
+ else
50
+ puts "Board #{board_id} updated."
51
+ end
48
52
  end
49
53
 
50
54
  desc "delete BOARD_ID", "Delete a board"
@@ -51,14 +51,14 @@ module Fizzy
51
51
  end
52
52
 
53
53
  desc "create TITLE", "Create a card"
54
- option :board, required: true, desc: "Board ID"
54
+ option :board, desc: "Board ID"
55
55
  option :body, desc: "Card body (HTML)"
56
56
  option :column, desc: "Column ID"
57
57
  def create(title)
58
58
  body = { title: title }
59
59
  body[:body] = options[:body] if options[:body]
60
60
  body[:column_id] = options[:column] if options[:column]
61
- resp = client.post("boards/#{options[:board]}/cards", body: body)
61
+ resp = client.post("boards/#{require_board!}/cards", body: body)
62
62
  c = resp.body
63
63
  output_detail(c, pairs: [
64
64
  ["Number", "##{c["number"]}"],
@@ -73,10 +73,14 @@ module Fizzy
73
73
  def update(number)
74
74
  resp = client.put("cards/#{number}", body: build_body(:title, :body))
75
75
  c = resp.body
76
- output_detail(c, pairs: [
77
- ["Number", "##{c["number"]}"],
78
- ["Title", c["title"]]
79
- ])
76
+ if c
77
+ output_detail(c, pairs: [
78
+ ["Number", "##{c["number"]}"],
79
+ ["Title", c["title"]]
80
+ ])
81
+ else
82
+ puts "Card ##{number} updated."
83
+ end
80
84
  end
81
85
 
82
86
  desc "delete NUMBER", "Delete a card"
@@ -6,18 +6,18 @@ module Fizzy
6
6
  include Base
7
7
 
8
8
  desc "list", "List columns for a board"
9
- option :board, required: true, desc: "Board ID"
9
+ option :board, desc: "Board ID"
10
10
  def list
11
- data = paginator.all("boards/#{options[:board]}/columns")
11
+ data = paginator.all("boards/#{require_board!}/columns")
12
12
  output_list(data, headers: %w[ID Name Position]) do |c|
13
13
  [c["id"], c["name"], c["position"]]
14
14
  end
15
15
  end
16
16
 
17
17
  desc "get COLUMN_ID", "Show a column"
18
- option :board, required: true, desc: "Board ID"
18
+ option :board, desc: "Board ID"
19
19
  def get(column_id)
20
- resp = client.get("boards/#{options[:board]}/columns/#{column_id}")
20
+ resp = client.get("boards/#{require_board!}/columns/#{column_id}")
21
21
  c = resp.body
22
22
  output_detail(c, pairs: [
23
23
  ["ID", c["id"]],
@@ -28,9 +28,9 @@ module Fizzy
28
28
  end
29
29
 
30
30
  desc "create NAME", "Create a column"
31
- option :board, required: true, desc: "Board ID"
31
+ option :board, desc: "Board ID"
32
32
  def create(name)
33
- resp = client.post("boards/#{options[:board]}/columns", body: { name: name })
33
+ resp = client.post("boards/#{require_board!}/columns", body: { name: name })
34
34
  c = resp.body
35
35
  output_detail(c, pairs: [
36
36
  ["ID", c["id"]],
@@ -39,21 +39,25 @@ module Fizzy
39
39
  end
40
40
 
41
41
  desc "update COLUMN_ID", "Update a column"
42
- option :board, required: true, desc: "Board ID"
42
+ option :board, desc: "Board ID"
43
43
  option :name, required: true, desc: "New column name"
44
44
  def update(column_id)
45
- resp = client.put("boards/#{options[:board]}/columns/#{column_id}", body: { name: options[:name] })
45
+ resp = client.put("boards/#{require_board!}/columns/#{column_id}", body: { name: options[:name] })
46
46
  c = resp.body
47
- output_detail(c, pairs: [
48
- ["ID", c["id"]],
49
- ["Name", c["name"]]
50
- ])
47
+ if c
48
+ output_detail(c, pairs: [
49
+ ["ID", c["id"]],
50
+ ["Name", c["name"]]
51
+ ])
52
+ else
53
+ puts "Column #{column_id} updated."
54
+ end
51
55
  end
52
56
 
53
57
  desc "delete COLUMN_ID", "Delete a column"
54
- option :board, required: true, desc: "Board ID"
58
+ option :board, desc: "Board ID"
55
59
  def delete(column_id)
56
- client.delete("boards/#{options[:board]}/columns/#{column_id}")
60
+ client.delete("boards/#{require_board!}/columns/#{column_id}")
57
61
  puts "Column #{column_id} deleted."
58
62
  end
59
63
  end
@@ -44,10 +44,14 @@ module Fizzy
44
44
  def update(comment_id)
45
45
  resp = client.put("cards/#{options[:card]}/comments/#{comment_id}", body: build_body(:body))
46
46
  c = resp.body
47
- output_detail(c, pairs: [
48
- ["ID", c["id"]],
49
- ["Body", c["body"]]
50
- ])
47
+ if c
48
+ output_detail(c, pairs: [
49
+ ["ID", c["id"]],
50
+ ["Body", c["body"]]
51
+ ])
52
+ else
53
+ puts "Comment #{comment_id} updated."
54
+ end
51
55
  end
52
56
 
53
57
  desc "delete COMMENT_ID", "Delete a comment"
@@ -37,11 +37,15 @@ module Fizzy
37
37
  path = "cards/#{options[:card]}/steps/#{step_id}"
38
38
  resp = client.put(path, body: build_body(:description, :completed))
39
39
  s = resp.body
40
- output_detail(s, pairs: [
41
- ["ID", s["id"]],
42
- ["Description", s["description"]],
43
- ["Completed", s["completed"]]
44
- ])
40
+ if s
41
+ output_detail(s, pairs: [
42
+ ["ID", s["id"]],
43
+ ["Description", s["description"]],
44
+ ["Completed", s["completed"]]
45
+ ])
46
+ else
47
+ puts "Step #{step_id} updated."
48
+ end
45
49
  end
46
50
 
47
51
  desc "delete STEP_ID", "Delete a step"
@@ -33,11 +33,15 @@ module Fizzy
33
33
  def update(user_id)
34
34
  resp = client.put("users/#{user_id}", body: build_body(:name, :role))
35
35
  u = resp.body
36
- output_detail(u, pairs: [
37
- ["ID", u["id"]],
38
- ["Name", u["name"]],
39
- ["Role", u["role"]]
40
- ])
36
+ if u
37
+ output_detail(u, pairs: [
38
+ ["ID", u["id"]],
39
+ ["Name", u["name"]],
40
+ ["Role", u["role"]]
41
+ ])
42
+ else
43
+ puts "User #{user_id} updated."
44
+ end
41
45
  end
42
46
 
43
47
  desc "deactivate USER_ID", "Deactivate a user"
data/lib/fizzy/cli.rb CHANGED
@@ -25,6 +25,19 @@ module Fizzy
25
25
  puts "fizzy-cli #{VERSION}"
26
26
  end
27
27
 
28
+ desc "init", "Create .fizzy.yml in current directory"
29
+ def init
30
+ config_path = File.join(Dir.pwd, ProjectConfig::FILENAME)
31
+ return if File.exist?(config_path) && !yes?("#{config_path} already exists. Overwrite?")
32
+
33
+ selected = pick_account
34
+ config = { "account" => selected["account_slug"] }
35
+ config["board"] = pick_board(selected) if yes?("Set a default board?")
36
+
37
+ File.write(config_path, YAML.dump(config))
38
+ say "Wrote #{config_path}"
39
+ end
40
+
28
41
  desc "boards SUBCOMMAND ...ARGS", "Manage boards"
29
42
  subcommand "boards", CLI::Boards
30
43
 
@@ -62,5 +75,47 @@ module Fizzy
62
75
  subcommand "skill", CLI::Skill
63
76
 
64
77
  def self.exit_on_failure? = true
78
+
79
+ private
80
+
81
+ def pick_account
82
+ data = Auth.token_data
83
+ accounts = Array(data["accounts"])
84
+ raise AuthError, "No accounts found. Run: fizzy auth login --token TOKEN" if accounts.empty?
85
+
86
+ say "Available accounts:"
87
+ accounts.each_with_index do |a, i|
88
+ marker = a["account_slug"] == data["default_account"] ? " (default)" : ""
89
+ say " #{i + 1}. #{a["account_name"]} (#{a["account_slug"]})#{marker}"
90
+ end
91
+
92
+ choice = ask("Select account number [1]:").strip
93
+ choice = "1" if choice.empty?
94
+ idx = choice.to_i - 1
95
+ raise Thor::Error, "Invalid selection" unless idx >= 0 && idx < accounts.size
96
+
97
+ accounts[idx]
98
+ end
99
+
100
+ def pick_board(account)
101
+ c = Client.new(token: account["access_token"], account_slug: account["account_slug"])
102
+ boards = c.get("boards").body
103
+
104
+ if boards.empty?
105
+ say "No boards found."
106
+ return nil
107
+ end
108
+
109
+ say "Boards:"
110
+ boards.each_with_index do |b, i|
111
+ say " #{i + 1}. #{b["name"]} (#{b["id"]})"
112
+ end
113
+
114
+ board_idx = ask("Select board number:").strip.to_i - 1
115
+ return boards[board_idx]["id"] if board_idx >= 0 && board_idx < boards.size
116
+
117
+ say "Invalid selection, skipping board."
118
+ nil
119
+ end
65
120
  end
66
121
  end
data/lib/fizzy/client.rb CHANGED
@@ -81,8 +81,8 @@ module Fizzy
81
81
 
82
82
  case status
83
83
  when 200..299
84
- # Follow Location header on 201 to fetch the created resource
85
- if status == 201 && parsed_body.nil? && response["location"]
84
+ # Follow Location header to fetch the resource when body is empty
85
+ if parsed_body.nil? && response["location"]
86
86
  location = response["location"].sub(/\.json$/, "")
87
87
  return get(location)
88
88
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fizzy
4
+ class ProjectConfig
5
+ FILENAME = ".fizzy.yml"
6
+
7
+ attr_reader :path
8
+
9
+ def initialize(start_dir = Dir.pwd)
10
+ @path = find_config(start_dir)
11
+ data = @path ? YAML.safe_load_file(@path) : {}
12
+ @data = data.is_a?(Hash) ? data : {}
13
+ rescue Psych::SyntaxError => e
14
+ raise Thor::Error, "Bad .fizzy.yml at #{@path}: #{e.message}"
15
+ end
16
+
17
+ def found? = !@path.nil?
18
+
19
+ def account = @data["account"]
20
+
21
+ def board = @data["board"]
22
+
23
+ private
24
+
25
+ def find_config(dir)
26
+ candidate = File.join(dir, FILENAME)
27
+ return candidate if File.exist?(candidate)
28
+
29
+ parent = File.dirname(dir)
30
+ return nil if parent == dir
31
+
32
+ find_config(parent)
33
+ end
34
+ end
35
+ end
data/lib/fizzy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fizzy
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/fizzy.rb CHANGED
@@ -8,6 +8,7 @@ require "yaml"
8
8
  require_relative "fizzy/version"
9
9
  require_relative "fizzy/errors"
10
10
  require_relative "fizzy/auth"
11
+ require_relative "fizzy/project_config"
11
12
  require_relative "fizzy/client"
12
13
  require_relative "fizzy/paginator"
13
14
  require_relative "fizzy/formatter"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fizzy-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Paluy
@@ -59,6 +59,7 @@ files:
59
59
  - lib/fizzy/errors.rb
60
60
  - lib/fizzy/formatter.rb
61
61
  - lib/fizzy/paginator.rb
62
+ - lib/fizzy/project_config.rb
62
63
  - lib/fizzy/version.rb
63
64
  homepage: https://github.com/dpaluy/fizzy-cli
64
65
  licenses: