planka-cli 0.1.0 → 0.1.1

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: dabd1d040fd4dede722ec5c62b8e60d37f2a344b80e825862c8dd7c9843c0d79
4
- data.tar.gz: 7a900110053223770ce56dda103e5b47c99809b98ba682618d1c1dcb042cbb39
3
+ metadata.gz: b2ddf9bdb8529ed70987d7c273f935aab7c045a5dd3f6a5215ed7ffcd252da63
4
+ data.tar.gz: d4a59fb87d01349a42d6e229e64d204d47dd0573bd54a3bc64bf6e0b98c8d0ff
5
5
  SHA512:
6
- metadata.gz: a2042e943919152542795ddc968803f7274c17bb3e114d40b55b774ed7c7e6d024befffce847bf17cf8867b524b25d1764cae049240ecf2d9b8f7be15c6e923b
7
- data.tar.gz: f8491ffec2927b09ebda03abfe934e8eafdac147ca8ddf41497c8b3b7360976079cd401b0bf257b0d068d43fc9f5c07e9a7d38f338ccd8f486d1af112c986ad1
6
+ metadata.gz: b85b944f8308805e48a5756a649a02f379dbde6d6130d0548b758afa09d7494e08708e0b6439c1fc08640825edd81cd83ece4deb68db1ac9ec44d7ff276c3768
7
+ data.tar.gz: 3ed86441e6e7bee6aeab7df92b3955e7d96cd61d545a530031559f3e5a4b63a22d2d6ff125f69863591634bca77700af9e0365b8800973f9e5acc58a929c4800
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Extracted Planka Ruby workflows from Lucenta; added configurable instance, board and branch prefix, gem packaging, and the `planka` command.
data/README.md CHANGED
@@ -1,161 +1,86 @@
1
1
  # planka-cli
2
2
 
3
- Ruby command-line tools and workflow utilities for Planka's REST API. Extracted
4
- from Lucenta into this standalone local repository; no remote has been created
5
- and the gem has not been published. `work-next` and `pr_assets` remain in Lucenta.
3
+ Ruby CLI and library for Planka board workflows, extracted from Lucenta.
4
+ Requires Ruby 3.2 or newer. Board workflows use the conventions
5
+ `ready-for-agent`, `in-progress`, `done`, `Acceptance criteria`, `Blocked by`,
6
+ `feature:<slug>` and `effort:<slug>`. Linked tasks represent blocking edges.
6
7
 
7
- ## Install
8
-
9
- Use Ruby 3.4 or newer (development uses 3.4.9), Bundler, and Bash. From this
10
- repository:
8
+ ## Install locally
11
9
 
12
10
  ```sh
13
11
  bundle install
14
- gem build planka-cli.gemspec
15
- gem install ./planka-cli-0.1.0.gem
12
+ make ci
13
+ gem install ./pkg/planka-cli-0.1.0.gem
14
+ planka --help
16
15
  ```
17
16
 
18
- Put RubyGems' executable directory on `PATH`. Invoke `planka-*` commands from
19
- the checkout whose configuration you want to use, not from this gem's directory.
20
- Ruby launchers package the shell implementations of `planka-op` and `planka-mcp`
21
- so the commands also work through RubyGems' installed executable wrappers.
22
-
23
- For the current sibling-directory extraction, Lucenta's Gemfile uses:
17
+ The gem has not been published. Its executable names include `planka` and
18
+ all eight existing `planka-*` commands. `bin/planka-op` and the Node MCP
19
+ launcher remain in Lucenta; they are repository integrations rather than Ruby
20
+ gem commands.
24
21
 
25
- ```ruby
26
- source "https://rubygems.org"
27
- gem "planka-cli", path: "../planka-cli"
28
- ```
22
+ ## Configure
29
23
 
30
- Run `bundle install` in Lucenta, then use `bundle exec planka-next-card` and the
31
- other installed commands. Relocating the checkout requires updating that path.
32
- After deciding on a remote and publishing the gem to RubyGems, replace the path
33
- with a released version constraint and update the bundle. No remote or
34
- publication is assumed by the current setup.
24
+ Supply resolved credentials in the process environment:
35
25
 
36
- ## Configuration
26
+ | Variable | Used for |
27
+ | --- | --- |
28
+ | `PLANKA_BASE_URL` | Instance URL, e.g. `https://planka.example.com` |
29
+ | `PLANKA_AGENT_EMAIL` | Login email or username |
30
+ | `PLANKA_AGENT_PASSWORD` | Login password |
31
+ | `PLANKA_BOARD_ID` | Board used by `next-card` only |
32
+ | `PLANKA_BRANCH_PREFIX` | Optional preview/DNS prefix, e.g. `lucenta-`; default empty |
37
33
 
38
- The command's **current working directory** owns `.mcp.json` and `.env.op`.
39
- Nothing is loaded from the installed gem, Lucenta, or a homelab checkout.
34
+ Card commands derive the board from the card. `loop-lock` and `spec-sweep`
35
+ inspect all boards visible to the signed-in user. No command reads `.mcp.json`,
36
+ looks in another checkout for secrets, or invokes 1Password automatically.
37
+ For `op://` references, launch the command through your own `op run` setup.
40
38
 
41
- | Variable | Purpose |
42
- | --- | --- |
43
- | `PLANKA_BASE_URL` | Planka instance URL, for example `http://localhost:3000`. Also used to render card links. |
44
- | `PLANKA_AGENT_EMAIL` | User email or username for API sign-in. |
45
- | `PLANKA_AGENT_PASSWORD` | Password for API sign-in. |
46
- | `PLANKA_BOARD_ID` | Board selected by `planka-next-card` and `Planka.board_id`. No default board exists. |
47
- | `PLANKA_MCP_CONFIG` | Optional MCP config filename; relative paths resolve from the current working directory. Defaults to `.mcp.json` if present. An explicitly selected missing file is an error. |
48
- | `OP_SERVICE_ACCOUNT_TOKEN` | Exported 1Password service-account token; no env file is needed when already set. |
49
- | `OP_ENV_FILE` | Optional file to source when no token is exported; relative to the current working directory. Otherwise `.env.op` is used. |
50
- | `XDG_STATE_HOME` | Optional state root for 1Password's isolated home; defaults to `$HOME/.local/state`. |
51
-
52
- Explicit environment variables override values in the selected MCP config's
53
- `mcpServers.planka.env`. Plain credentials need neither an MCP config nor
54
- 1Password:
39
+ ## Commands
55
40
 
56
41
  ```sh
57
- export PLANKA_BASE_URL=http://localhost:3000
58
- export PLANKA_AGENT_EMAIL=agent@example.test
59
- export PLANKA_AGENT_PASSWORD='your-password'
60
- export PLANKA_BOARD_ID='your-board-id'
61
- planka-next-card
42
+ planka next-card # top unclaimed, unblocked ticket by position
43
+ planka next-card feature:search # earliest takeable ticket for a feature
44
+ planka next-card effort:search # wayfinder map and frontier
45
+ planka branch-name CARD # branch slug; fits 63 chars including prefix
46
+ planka unticked CARD # incomplete acceptance criteria
47
+ planka loop-lock # signed-in user's open claim without PR handoff
48
+ planka claim CARD # add membership and move to in-progress
49
+ planka comment CARD 'Branch: card/example'
50
+ planka link BLOCKED BLOCKER [BLOCKER...]
51
+ planka spec-sweep # comment and move finished specs to done
62
52
  ```
63
53
 
64
- Alternatively, create a caller-owned MCP config (substitute your URL, board,
65
- and secret references):
66
-
67
- ```json
68
- {
69
- "mcpServers": {
70
- "planka": {
71
- "command": "planka-mcp",
72
- "env": {
73
- "PLANKA_BASE_URL": "http://localhost:3000",
74
- "PLANKA_BOARD_ID": "your-board-id",
75
- "PLANKA_AGENT_EMAIL": "op://vault/planka-agent/username",
76
- "PLANKA_AGENT_PASSWORD": "op://vault/planka-agent/password"
77
- }
78
- }
79
- }
80
- }
81
- ```
54
+ Cards may be numeric ids or card URLs. `claim`, `comment`, `link` and
55
+ `spec-sweep` write to Planka. `claim` and `link` may be safely repeated.
56
+ `next-card` uses the authenticated `gh` CLI when a blocker has a GitHub PR
57
+ handoff. `Branch:` and `PR:` lines in comments determine parent branches.
82
58
 
83
- When `PLANKA_*` settings contain `op://` references, the command re-runs through
84
- its packaged `planka-op` wrapper. Install the 1Password `op` CLI and export a
85
- service-account token or create `.env.op` containing
86
- `OP_SERVICE_ACCOUNT_TOKEN='your-token'`. Treat the file as a shell file; it is
87
- sourced, not parsed as dotenv. Keep credential files out of version control.
88
- The wrapper isolates 1Password state under `planka-cli/op-home`, restores the
89
- real `HOME` for the child command, and uses `op run --no-masking` to preserve JSON
90
- and MCP streams. Do not print secret values from commands run through it.
59
+ ## Library
91
60
 
92
- `planka-mcp` uses Node.js/npm's `npx` to launch
93
- `@navyatec/planka-v2-mcp@1.3.4`. It applies the same config and credential rules;
94
- plain values do not invoke `op`. In an MCP client using the local path bundle,
95
- use `"command": "bundle", "args": ["exec", "planka-mcp"]` instead.
61
+ ```ruby
62
+ require "planka-cli"
96
63
 
97
- ## Commands
64
+ Planka::Client.session do |client|
65
+ board = Planka::Board.new(client.board(ENV.fetch("PLANKA_BOARD_ID")),
66
+ base_url: ENV.fetch("PLANKA_BASE_URL"))
67
+ puts board.cards.first
68
+ end
69
+ ```
98
70
 
99
- A `<card>` argument accepts a numeric ID or a card URL. Commands preserve their
100
- line-oriented workflow output; there is no implicit board or instance.
71
+ `Board` also accepts a captured API `included` payload. Supply `base_url:` when
72
+ using multiple instances in one process. `BranchName.for(card, prefix: "app-")`
73
+ accepts a per-call prefix. The library preserves the `Planka` namespace.
101
74
 
102
- | Command | Behavior |
103
- | --- | --- |
104
- | `planka-next-card [feature-label\|effort-label]` | Read-only selection on `PLANKA_BOARD_ID`. With no label, chooses the first takeable ticket by list position. Feature selection uses creation order and reports spec, ticket number, blockers and parent branch. Effort selection reports a wayfinder map and frontier. |
105
- | `planka-branch-name <card>` | Prints the workflow branch name; read-only. |
106
- | `planka-claim <card>` | Adds the signed-in user as a member if needed and moves the card to its board's `in-progress` list. Safe to repeat. |
107
- | `planka-comment <card> <text>` | Adds a comment as the signed-in user. Quote multiline text as one argument. |
108
- | `planka-link <blocked-card> <blocker-card>...` | Adds linked blocking tasks in a `Blocked by` task list. Existing edges are skipped; self-blocking is rejected. |
109
- | `planka-unticked <card>` | Prints incomplete tasks in `Acceptance criteria`, one per line; read-only. |
110
- | `planka-loop-lock` | Searches all visible boards for an open claimed card without a PR handoff, printing `free` or `held`, claim time and age; read-only. |
111
- | `planka-spec-sweep` | Searches all visible boards, comments on completed feature specs in `in-progress`, and moves them to `done`. |
112
- | `planka-op <command> [args...]` | Resolves secret references in a command's environment using `op run`; requires a service-account token. |
113
- | `planka-mcp` | Starts the pinned Planka MCP server using `npx`. |
114
-
115
- The Ruby utilities are available with `require "planka"` and
116
- `require "planka/client"`. `Planka::Client.configure!` loads caller-owned MCP
117
- settings into unset environment variables; `resolve_credentials!(script, argv)`
118
- additionally validates API credentials and resolves secret references by
119
- re-executing the calling script. `Client.session` signs in and signs out around
120
- the supplied block. `Planka.board_id` fetches `PLANKA_BOARD_ID` and
121
- `Planka.card_url(id)` joins the configured base URL with `/cards/<id>`.
122
-
123
- ## Workflow conventions and dependencies
124
-
125
- These tools retain the extracted workflow conventions rather than imposing a
126
- general-purpose Planka schema:
127
-
128
- - Tickets have an `Acceptance criteria` task list; specs do not. Selection
129
- requires an unclaimed card in `ready-for-agent` with no incomplete linked
130
- blocking tasks. Lists have Planka `active` or `closed` types.
131
- - Claiming uses `in-progress`; completed specs move to `done`.
132
- - `feature:<slug>` associates specs and tickets. `effort:<slug>` groups wayfinder
133
- cards; `wayfinder:map` identifies their map.
134
- - Blocking edges are linked tasks. Planka completes those tasks when their
135
- linked blocker cards close.
136
- - Handoff comments contain `Branch: <branch>` and optionally `PR: <url>` on
137
- separate lines. PR inspection uses an authenticated GitHub `gh` CLI when a
138
- handoff references a PR; run it in the relevant project checkout.
139
- - Branches are `feature/<feature>-<title-slug>` or `card/<title-slug>`. The
140
- historical 55-character limit is preserved, reserving eight characters for
141
- the Lucenta workflow's `lucenta-` DNS/Compose prefix. Names cut at whole words;
142
- this extraction does not redesign that convention.
143
-
144
- Only secret-reference resolution requires `op`. MCP additionally needs `npx`
145
- and network access for its pinned package. API commands need network access to
146
- the configured Planka instance; PR-dependent selection needs `gh`.
147
-
148
- ## Development
75
+ ## Development and extraction boundary
149
76
 
150
- ```sh
151
- bundle install
152
- make test
153
- make ci
154
- ```
77
+ `make test` runs the captured-board tests and local HTTP command tests.
78
+ `make build` builds the gem; `make ci` runs both. CI covers Ruby 3.2, 3.4 and 4.0.
79
+ The tests do not contact a live Planka instance. API endpoints and payload shapes
80
+ are inherited from Lucenta, including its captured Community board fixture;
81
+ compatibility with other Planka releases has not been established.
155
82
 
156
- Both targets run the standalone Minitest suite. Moved fixtures retain their
157
- `test/fixtures/files/planka` layout. Tests and their helper are included in the
158
- gem so Lucenta's remaining workflow tests can share the extracted board fixture.
159
- Gem artifacts, Bundler state, coverage, and local credential/config files are
160
- ignored. This README records the standalone extraction and configuration
161
- cutover; the former hardcoded instance and board constants have been removed.
83
+ Lucenta continues using its original library and 1Password integrations.
84
+ Switching it to this gem is a separate consumer change once a repository or
85
+ release is available to CI. No release license has been selected; choose one
86
+ before publishing the gem.
data/exe/planka ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ require "planka-cli"
3
+
4
+ commands = %w[link next-card branch-name claim comment unticked spec-sweep loop-lock]
5
+ if ARGV == ["--version"]
6
+ puts Planka::VERSION
7
+ elsif ARGV.empty? || ARGV == ["--help"]
8
+ puts "usage: planka <command> [arguments]\ncommands: #{commands.join(', ')}"
9
+ else
10
+ command = ARGV.shift
11
+ abort "planka: unknown command #{command.inspect}; see planka --help" unless commands.include?(command)
12
+ load File.expand_path("planka-#{command}", __dir__)
13
+ end
@@ -2,17 +2,16 @@
2
2
  # Prints the branch a card is worked on, e.g.
3
3
  # feature/tracer-1.2-an-account-signs-in-the-first-admin. Read-only.
4
4
  #
5
- # bin/planka-branch-name <card> an id or card URL
5
+ # planka-branch-name <card> an id or card URL
6
6
  #
7
7
  # The logic lives in lib/planka/branch_name.rb.
8
- require_relative "../lib/planka"
9
- require_relative "../lib/planka/client"
8
+ require "planka"
9
+ require "planka/client"
10
10
 
11
11
  abort "usage: planka-branch-name <card>" unless ARGV.size == 1
12
12
 
13
13
  begin
14
14
  card_id = Planka.card_id(ARGV.first)
15
- Planka::Client.resolve_credentials!(__FILE__, ARGV)
16
15
 
17
16
  Planka::Client.session do |client|
18
17
  board = Planka::Board.new(client.board(client.card(card_id).dig("item", "boardId")))
@@ -2,18 +2,16 @@
2
2
  # Claims a card for the signed-in user: makes it a card member, then moves the
3
3
  # card to its board's in-progress list. Safe to re-run.
4
4
  #
5
- # bin/planka-claim <card> an id or card URL
5
+ # planka-claim <card> an id or card URL
6
6
  #
7
- # The work-next loop runs it as its own user
8
- # (PLANKA_MCP_CONFIG=workflows/work-next/mcp.json).
9
- require_relative "../lib/planka"
10
- require_relative "../lib/planka/client"
7
+ # Runs as the user configured in PLANKA_AGENT_EMAIL.
8
+ require "planka"
9
+ require "planka/client"
11
10
 
12
11
  abort "usage: planka-claim <card>" unless ARGV.size == 1
13
12
 
14
13
  begin
15
14
  card_id = Planka.card_id(ARGV.first)
16
- Planka::Client.resolve_credentials!(__FILE__, ARGV)
17
15
 
18
16
  Planka::Client.session do |client|
19
17
  me = client.me.fetch("id")
@@ -1,18 +1,16 @@
1
1
  #!/usr/bin/env ruby
2
2
  # Comments on a card as the signed-in user.
3
3
  #
4
- # bin/planka-comment <card> <text> the card as an id or card URL
4
+ # planka-comment <card> <text> the card as an id or card URL
5
5
  #
6
- # The work-next loop runs it as its own user
7
- # (PLANKA_MCP_CONFIG=workflows/work-next/mcp.json).
8
- require_relative "../lib/planka"
9
- require_relative "../lib/planka/client"
6
+ # Runs as the user configured in PLANKA_AGENT_EMAIL.
7
+ require "planka"
8
+ require "planka/client"
10
9
 
11
10
  abort "usage: planka-comment <card> <text>" unless ARGV.size == 2
12
11
 
13
12
  begin
14
13
  card_id = Planka.card_id(ARGV.first)
15
- Planka::Client.resolve_credentials!(__FILE__, ARGV)
16
14
 
17
15
  Planka::Client.session { |client| client.comment(card_id, ARGV.last) }
18
16
  rescue Planka::Error, KeyError => e
@@ -2,19 +2,18 @@
2
2
  # Records blocking edges between Planka cards as linked tasks, which the
3
3
  # planka MCP server cannot create.
4
4
  #
5
- # bin/planka-link <blocked-card> <blocker-card> [<blocker-card>...]
5
+ # planka-link <blocked-card> <blocker-card> [<blocker-card>...]
6
6
  #
7
7
  # Cards are ids or card URLs. Each blocker becomes a task linked to that card
8
8
  # in a "Blocked by" task list on the blocked card. Re-running is safe: blockers
9
9
  # already linked are skipped. The logic lives in lib/planka/blocking.rb.
10
- require_relative "../lib/planka"
11
- require_relative "../lib/planka/client"
10
+ require "planka"
11
+ require "planka/client"
12
12
 
13
13
  abort "usage: planka-link <blocked-card> <blocker-card> [<blocker-card>...]" if ARGV.size < 2
14
14
 
15
15
  begin
16
16
  blocked, *blockers = ARGV.map { |arg| Planka.card_id(arg) }
17
- Planka::Client.resolve_credentials!(__FILE__, ARGV)
18
17
 
19
18
  Planka::Client.session do |client|
20
19
  puts Planka::Blocking.new(client).link(blocked, blockers)
@@ -3,15 +3,14 @@
3
3
  # Planka: a card on any board it can see that it has claimed, that is still
4
4
  # open, and that has no PR handed off yet. Read-only.
5
5
  #
6
- # bin/planka-loop-lock prints "free", or "held: <card>" and then
6
+ # planka-loop-lock prints "free", or "held: <card>" and then
7
7
  # "claimed: <ISO 8601 time>" and "age: <seconds>"
8
8
  #
9
- # Run it as the loop's user (PLANKA_MCP_CONFIG=workflows/work-next/mcp.json).
9
+ # Run it with the loop user's resolved credentials.
10
10
  # The logic lives in lib/planka/loop_lock.rb.
11
- require_relative "../lib/planka"
12
- require_relative "../lib/planka/client"
11
+ require "planka"
12
+ require "planka/client"
13
13
 
14
- Planka::Client.resolve_credentials!(__FILE__, ARGV)
15
14
 
16
15
  begin
17
16
  Planka::Client.session do |client|
@@ -15,15 +15,14 @@
15
15
  #
16
16
  # The planka MCP server drops card labels and card memberships, so this reads
17
17
  # the board from Planka's API instead. The logic lives in lib/planka.
18
- require_relative "../lib/planka"
19
- require_relative "../lib/planka/client"
18
+ require "planka"
19
+ require "planka/client"
20
20
 
21
21
  abort "usage: planka-next-card [feature-label|effort-label]" if ARGV.size > 1
22
- Planka::Client.resolve_credentials!(__FILE__, ARGV)
23
22
 
24
23
  begin
25
24
  Planka::Client.session do |client|
26
- board = Planka::Board.new(client.board(Planka.board_id))
25
+ board = Planka::Board.new(client.board(ENV.fetch("PLANKA_BOARD_ID")))
27
26
  puts Planka::NextCard.for(ARGV.first, board:, comments: client, pull_requests: Planka::PullRequest)
28
27
  end
29
28
  rescue Planka::Error, KeyError => e
@@ -4,22 +4,19 @@
4
4
  # criteria) are all in closed-type lists. Runs on every board the signed-in
5
5
  # user can see, and prints each spec it moves.
6
6
  #
7
- # bin/planka-spec-sweep
7
+ # planka-spec-sweep
8
8
  #
9
- # The work-next loop runs it as its own user on every tick
10
- # (PLANKA_MCP_CONFIG=workflows/work-next/mcp.json). The logic lives in
11
- # lib/planka/spec_sweep.rb.
12
- require_relative "../lib/planka"
13
- require_relative "../lib/planka/client"
9
+ # The logic lives in lib/planka/spec_sweep.rb.
10
+ require "planka"
11
+ require "planka/client"
14
12
 
15
- Planka::Client.resolve_credentials!(__FILE__, ARGV)
16
13
 
17
14
  begin
18
15
  Planka::Client.session do |client|
19
16
  client.board_ids.each do |board_id|
20
17
  board = Planka::Board.new(client.board(board_id))
21
18
  Planka::SpecSweep.finished(board).each do |spec|
22
- client.comment(spec.id, "Every ticket for this spec is closed, so the work-next loop's housekeeping moves it to done.")
19
+ client.comment(spec.id, "Every ticket for this spec is closed, so planka-cli moves it to done.")
23
20
  client.move_card(spec.id, board.list_id("done"))
24
21
  puts "done: #{spec}"
25
22
  end
@@ -1,15 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
  # Prints a card's unticked acceptance criteria, one per line. Read-only.
3
3
  #
4
- # bin/planka-unticked <card> an id or card URL
5
- require_relative "../lib/planka"
6
- require_relative "../lib/planka/client"
4
+ # planka-unticked <card> an id or card URL
5
+ require "planka"
6
+ require "planka/client"
7
7
 
8
8
  abort "usage: planka-unticked <card>" unless ARGV.size == 1
9
9
 
10
10
  begin
11
11
  card_id = Planka.card_id(ARGV.first)
12
- Planka::Client.resolve_credentials!(__FILE__, ARGV)
13
12
 
14
13
  Planka::Client.session do |client|
15
14
  board = Planka::Board.new(client.board(client.card(card_id).dig("item", "boardId")))
data/lib/planka/board.rb CHANGED
@@ -3,7 +3,10 @@ module Planka
3
3
  # as a flat list under "included", joined by id; this indexes them once so
4
4
  # a Card can answer questions about itself.
5
5
  class Board
6
- def initialize(included)
6
+ attr_reader :base_url
7
+
8
+ def initialize(included, base_url: ENV.fetch("PLANKA_BASE_URL"))
9
+ @base_url = base_url.sub(%r{/+\z}, "")
7
10
  @included = included
8
11
  @cards = included.fetch("cards").to_h { |attrs| [ attrs["id"], Card.new(self, attrs) ] }
9
12
  @lists = included.fetch("lists").to_h { |list| [ list["id"], list["name"] ] }
@@ -1,24 +1,26 @@
1
1
  module Planka
2
2
  # The branch a card is worked on: feature/<slug>-<ticket#>-<title> for a
3
3
  # card with a feature label, card/<title> without one. It also names the
4
- # checkout's Compose project and preview host as lucenta-<branch-label>, a
4
+ # checkout's Compose project and preview host with an optional prefix, a
5
5
  # DNS label of at most 63 characters, so it is cut on a word to fit.
6
6
  module BranchName
7
- MAX = 63 - "lucenta-".length
7
+ def self.for(card, prefix: ENV.fetch("PLANKA_BRANCH_PREFIX", ""))
8
+ max = 63 - prefix.length
9
+ raise Error, "branch prefix leaves too little space" if max < 8
8
10
 
9
- def self.for(card)
10
11
  feature = card.features.first&.delete_prefix("feature:")
11
12
  name = feature ? "feature/#{feature}-#{slug(card.name)}" : "card/#{slug(card.name)}"
12
- fit(name)
13
+ fit(name, max:)
13
14
  end
14
15
 
15
16
  def self.slug(text) = text.downcase.gsub(/[^a-z0-9.]+/, "-").gsub(/\A-|-\z/, "")
16
17
 
17
- def self.fit(name)
18
- return name if name.length <= MAX
18
+ def self.fit(name, max:)
19
+ return name if name.length <= max
19
20
 
20
- cut = name[0, MAX]
21
- cut = cut[0, cut.rindex("-")] unless name[MAX] == "-" || cut.end_with?("-")
21
+ cut = name[0, max]
22
+ boundary = cut.rindex("-")
23
+ cut = cut[0, boundary] if boundary && name[max] != "-" && !cut.end_with?("-")
22
24
  cut.chomp("-")
23
25
  end
24
26
  end
data/lib/planka/card.rb CHANGED
@@ -38,7 +38,7 @@ module Planka
38
38
  def blockers = blocking_tasks.map { |task| @board.card(task["linkedCardId"]) }
39
39
  def open_blockers = blocking_tasks.reject { |task| task["isCompleted"] }.map { |task| @board.card(task["linkedCardId"]) }
40
40
 
41
- def url = Planka.card_url(id)
41
+ def url = "#{@board.base_url}/cards/#{id}"
42
42
  def to_s = "#{name} (#{url})"
43
43
 
44
44
  private
data/lib/planka/client.rb CHANGED
@@ -1,5 +1,3 @@
1
- require_relative "../planka"
2
- require "rbconfig"
3
1
  require "json"
4
2
  require "net/http"
5
3
  require "uri"
@@ -7,31 +5,6 @@ require "uri"
7
5
  module Planka
8
6
  # A signed-in session against Planka's REST API as the board's bot user.
9
7
  class Client
10
- # Configuration belongs to the calling checkout, not the installed gem.
11
- # Explicit environment values take precedence over its MCP settings.
12
- def self.configure!
13
- path = File.expand_path(ENV.fetch("PLANKA_MCP_CONFIG", ".mcp.json"))
14
- if ENV.key?("PLANKA_MCP_CONFIG") || File.file?(path)
15
- config = JSON.parse(File.read(path))
16
- config.fetch("mcpServers").fetch("planka").fetch("env").each do |key, value|
17
- ENV[key] = value unless ENV.key?(key)
18
- end
19
- end
20
- end
21
-
22
- # Plain credentials need neither 1Password nor an MCP config. Secret
23
- # references re-run the command through the gem's packaged shell wrapper.
24
- def self.resolve_credentials!(script, argv)
25
- configure!
26
- %w[PLANKA_BASE_URL PLANKA_AGENT_EMAIL PLANKA_AGENT_PASSWORD].each { |key| ENV.fetch(key) }
27
- return if ENV["PLANKA_CREDENTIALS_RESOLVED"]
28
- return unless ENV.any? { |key, value| key.start_with?("PLANKA_") && value.start_with?("op://") }
29
-
30
- wrapper = File.expand_path("../../bin/planka-op", __dir__)
31
- exec({ "PLANKA_CREDENTIALS_RESOLVED" => "1" }, RbConfig.ruby, wrapper,
32
- RbConfig.ruby, File.expand_path(script), *argv)
33
- end
34
-
35
8
  def self.session
36
9
  client = new(ENV.fetch("PLANKA_BASE_URL"))
37
10
  client.sign_in(ENV.fetch("PLANKA_AGENT_EMAIL"), ENV.fetch("PLANKA_AGENT_PASSWORD"))
@@ -1,3 +1,3 @@
1
1
  module Planka
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
data/lib/planka-cli.rb ADDED
@@ -0,0 +1,2 @@
1
+ require_relative "planka"
2
+ require_relative "planka/client"
data/lib/planka.rb CHANGED
@@ -1,11 +1,9 @@
1
1
  require "time"
2
2
 
3
- # Planka board tooling shared by the planka-* commands and workflow callers.
3
+ # Planka board workflows, shared by the library and CLI.
4
4
  module Planka
5
5
  Error = Class.new(StandardError)
6
6
 
7
- def self.board_id = ENV.fetch("PLANKA_BOARD_ID")
8
- def self.card_url(id) = "#{ENV.fetch("PLANKA_BASE_URL").delete_suffix("/")}/cards/#{id}"
9
7
 
10
8
  # A card id, given as an id or a card URL.
11
9
  def self.card_id(arg)
@@ -23,3 +21,5 @@ require_relative "planka/branch_name"
23
21
  require_relative "planka/loop_lock"
24
22
  require_relative "planka/spec_sweep"
25
23
  require_relative "planka/blocking"
24
+
25
+ require_relative "planka/version"