planka-cli 0.1.0 → 0.1.2
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 +4 -4
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +62 -137
- data/exe/planka +13 -0
- data/{bin → exe}/planka-branch-name +3 -4
- data/{bin → exe}/planka-claim +4 -6
- data/{bin → exe}/planka-comment +4 -6
- data/{bin → exe}/planka-link +3 -4
- data/{bin → exe}/planka-loop-lock +4 -5
- data/{bin → exe}/planka-next-card +3 -4
- data/{bin → exe}/planka-spec-sweep +5 -8
- data/{bin → exe}/planka-unticked +3 -4
- data/lib/planka/board.rb +4 -1
- data/lib/planka/branch_name.rb +10 -8
- data/lib/planka/card.rb +1 -1
- data/lib/planka/client.rb +0 -27
- data/lib/planka/version.rb +1 -1
- data/lib/planka-cli.rb +2 -0
- data/lib/planka.rb +3 -3
- metadata +29 -73
- data/.ruby-version +0 -1
- data/Makefile +0 -6
- data/bin/planka-mcp +0 -6
- data/bin/planka-op +0 -3
- data/libexec/planka-mcp +0 -5
- data/libexec/planka-op +0 -46
- data/test/fixtures/files/planka/board.json +0 -693
- data/test/lib/planka/blocker_test.rb +0 -51
- data/test/lib/planka/blocking_test.rb +0 -76
- data/test/lib/planka/board_test.rb +0 -77
- data/test/lib/planka/branch_name_test.rb +0 -36
- data/test/lib/planka/configuration_test.rb +0 -83
- data/test/lib/planka/handoff_test.rb +0 -45
- data/test/lib/planka/loop_lock_test.rb +0 -49
- data/test/lib/planka/next_card_test.rb +0 -158
- data/test/lib/planka/planka_test_helper.rb +0 -63
- data/test/lib/planka/spec_sweep_test.rb +0 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4e6d83c7e7ebe3b731656d8418719c02a90eb013d3f80a0c73b1948844452f1
|
|
4
|
+
data.tar.gz: 7df47140846f26a0d7b7c2c0d39681733cf1035f9ff5d48f182653f24706c7ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28ac1b28c5d966991f3d4d930bf219f5b3354ad99b85e902c352efce71cb77665370b53f490a1c53e997610a94a8c0250bb25a98c1cd4bc28915a8d2f2f6b6ac
|
|
7
|
+
data.tar.gz: cb2da29dc478d8d90d2dacdd4e97febf5715f227723e57f49bf670d41380df46511bdf06087ef93e6602515d8fbbd85c538127084bc25b6887e87abee984db40
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marshall Yount
|
|
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
CHANGED
|
@@ -1,161 +1,86 @@
|
|
|
1
1
|
# planka-cli
|
|
2
2
|
|
|
3
|
-
Ruby
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
source "https://rubygems.org"
|
|
27
|
-
gem "planka-cli", path: "../planka-cli"
|
|
28
|
-
```
|
|
22
|
+
## Configure
|
|
29
23
|
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
planka
|
|
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
|
-
|
|
65
|
-
and
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
#
|
|
5
|
+
# planka-branch-name <card> an id or card URL
|
|
6
6
|
#
|
|
7
7
|
# The logic lives in lib/planka/branch_name.rb.
|
|
8
|
-
|
|
9
|
-
|
|
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")))
|
data/{bin → exe}/planka-claim
RENAMED
|
@@ -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
|
-
#
|
|
5
|
+
# planka-claim <card> an id or card URL
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
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")
|
data/{bin → exe}/planka-comment
RENAMED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# Comments on a card as the signed-in user.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
4
|
+
# planka-comment <card> <text> the card as an id or card URL
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
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
|
data/{bin → exe}/planka-link
RENAMED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
#
|
|
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
|
|
9
|
+
# Run it with the loop user's resolved credentials.
|
|
10
10
|
# The logic lives in lib/planka/loop_lock.rb.
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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(
|
|
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
|
-
#
|
|
7
|
+
# planka-spec-sweep
|
|
8
8
|
#
|
|
9
|
-
# The
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
data/{bin → exe}/planka-unticked
RENAMED
|
@@ -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
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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"] ] }
|
data/lib/planka/branch_name.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 <=
|
|
18
|
+
def self.fit(name, max:)
|
|
19
|
+
return name if name.length <= max
|
|
19
20
|
|
|
20
|
-
cut = name[0,
|
|
21
|
-
|
|
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 =
|
|
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"))
|
data/lib/planka/version.rb
CHANGED
data/lib/planka-cli.rb
ADDED
data/lib/planka.rb
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
require "time"
|
|
2
2
|
|
|
3
|
-
# Planka board
|
|
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"
|