plc-cli 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruno Mariano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,386 @@
1
+ # Plane Cockpit — CLI + TUI for Plane (Cloud and self-hosted)
2
+
3
+ <p align="center">
4
+ <img src="docs/assets/img/plc-logo.png" alt="Plane Cockpit logo" width="340" />
5
+ </p>
6
+
7
+ Plane Cockpit is a terminal client for [Plane](https://github.com/makeplane/plane), inspired by `gh` and `gh dash`,
8
+ distributed as a `plc` binary.
9
+ It provides a fast CLI for daily operations on projects, issues (work items), and dashboards,
10
+ plus a TUI (`plc dash`) for visual exploration. It supports both Plane Cloud and Plane
11
+ self-hosted deployments behind reverse proxies, custom TLS, and custom headers.
12
+
13
+ ## Install
14
+
15
+ The published package exposes a `plc` binary. Pick whichever fits:
16
+
17
+ **From npm (global install):**
18
+
19
+ ```bash
20
+ npm install -g plc-cli
21
+ plc --help
22
+ ```
23
+
24
+ **One-off with npx (no install):**
25
+
26
+ ```bash
27
+ npx plc-cli --help
28
+ ```
29
+
30
+ **From a clone (Makefile):**
31
+
32
+ ```bash
33
+ git clone https://github.com/brunoomariano/PlaneCockpit.git
34
+ cd PlaneCockpit
35
+ make bootstrap # installs the dev toolchain (mise) + dependencies
36
+ make install # builds, installs `plc` globally, and seeds an example config
37
+ ```
38
+
39
+ End users installing from npm/npx do not need `mise` or a toolchain: the
40
+ published artifact is a plain Node binary and `better-sqlite3` ships a prebuilt
41
+ native binding. The `make install` path is for working from a checkout
42
+ (dogfooding / development); `make uninstall` removes it. Requires Node `>=24`.
43
+
44
+ ## Quick start
45
+
46
+ 1. Drop a config file at `~/.config/plane-cli/config.yaml`.
47
+ See [`examples/config.yaml`](examples/config.yaml). This file is safe to commit.
48
+
49
+ 2. Authenticate:
50
+
51
+ ```bash
52
+ plc auth login
53
+ ```
54
+
55
+ The API key is prompted (masked) and stored at `~/.config/plane-cli/hosts.yaml`
56
+ with `0600` permissions — separate from `config.yaml` so the latter can live in
57
+ version control.
58
+
59
+ For a config file that carries the key directly, set `auth.api_key` under the
60
+ profile instead.
61
+
62
+ 3. Run:
63
+
64
+ ```bash
65
+ plc auth status
66
+ plc project list
67
+ plc issue list --view "Current sprint"
68
+ plc dash
69
+ ```
70
+
71
+ ## Commands
72
+
73
+ | Command | What it does |
74
+ | -------------------------------------- | ------------------------------------------------------ |
75
+ | `plc auth login` / `logout` / `status` | manage the stored API key for the active profile |
76
+ | `plc project list` / `view <id>` | list projects, or show one by identifier |
77
+ | `plc issue list` | list issues, optionally via a configured `--view` |
78
+ | `plc issue view <key>` | show a single issue (e.g. `ENG-123`) |
79
+ | `plc issue open <key>` | open the issue in the default browser |
80
+ | `plc issue create` | create an issue (interactive, or headless via flags) |
81
+ | `plc issue edit <key>` | edit title, description, or priority |
82
+ | `plc issue assign <key> <user>` | assign an issue (use `me` for yourself) |
83
+ | `plc issue transition <key> <state>` | move an issue to a state (by name or id) |
84
+ | `plc issue label <key> [labels...]` | set an issue's labels (no labels clears them) |
85
+ | `plc issue comment <key>` | add a comment (inline, from a file, or interactive) |
86
+ | `plc issue delete <key>` | delete an issue (confirms unless `--yes`) |
87
+ | `plc config show` / `validate` | print the resolved config (keys masked) or validate it |
88
+ | `plc profile list` / `use <name>` | list profiles, or switch the active one |
89
+ | `plc cache status` / `warm` / `clear` | inspect, prime, or clear the cache |
90
+ | `plc log path` / `tail` / `clear` | locate, read, or remove the TUI log |
91
+ | `plc dash` | open the interactive TUI dashboard |
92
+
93
+ Run `plc <command> --help` for the full flag list of any command.
94
+
95
+ ## Configuration
96
+
97
+ Plane Cockpit keeps two files apart, modeled after `gh`:
98
+
99
+ | File | Purpose | Safe to commit? |
100
+ | ------------- | -------------------------------------------- | --------------- |
101
+ | `config.yaml` | profiles, server URLs, views, cache settings | yes |
102
+ | `hosts.yaml` | API keys per host + profile (`chmod 0600`) | **no** |
103
+
104
+ The config file is YAML-first and validated with `zod`; invalid configs fail at
105
+ startup with the offending path.
106
+
107
+ For the complete list of options — server, auth, defaults, cache, and every view
108
+ filter and its accepted values — see [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md).
109
+
110
+ `config.yaml` is read from a single location (after the `--config <path>` flag,
111
+ which overrides it):
112
+
113
+ `~/.config/plane-cli/config.yaml`
114
+
115
+ `hosts.yaml` is always read from `~/.config/plane-cli/hosts.yaml`.
116
+
117
+ All configuration lives in these two files — there are no environment variable
118
+ overrides. The active profile can be selected per invocation with `--profile`.
119
+
120
+ ### API key resolution
121
+
122
+ In priority order:
123
+
124
+ 1. `auth.api_key` inline in `config.yaml`.
125
+ 2. Entry in `~/.config/plane-cli/hosts.yaml` (written by `plc auth login`).
126
+
127
+ `plc auth logout` removes the stored entry for the active profile.
128
+
129
+ ### Profiles
130
+
131
+ A single config can declare multiple environments (e.g. `production`, `staging`).
132
+ Switch per invocation with `--profile`:
133
+
134
+ ```bash
135
+ plc --profile staging issue list
136
+ ```
137
+
138
+ Or persist a new active profile:
139
+
140
+ ```bash
141
+ plc profile use staging
142
+ ```
143
+
144
+ ### Self-hosted
145
+
146
+ Plane Cockpit normalizes trailing slashes, supports custom headers, configurable timeouts, and
147
+ relaxed TLS for self-hosted clusters behind a reverse proxy:
148
+
149
+ ```yaml
150
+ server:
151
+ base_url: https://plane.internal.company.com
152
+ workspace_slug: acme
153
+ timeout_ms: 10000
154
+ headers:
155
+ X-Forwarded-Proto: https
156
+ tls:
157
+ reject_unauthorized: false # only for internal CAs
158
+ ```
159
+
160
+ ## Cache
161
+
162
+ The cache is optional and pluggable. Providers:
163
+
164
+ - `memory` — in-process, the default.
165
+ - `sqlite` — local persistent cache, file at `~/.cache/plane-cli/cache.sqlite` by default.
166
+ - `redis` — shared cache (declare `cache.redis.url`).
167
+ - `noop` — disables caching entirely.
168
+
169
+ The CLI works fully without Redis. To bypass cache for a single invocation, pass
170
+ `--no-cache`.
171
+
172
+ Common subcommands:
173
+
174
+ ```bash
175
+ plc cache status
176
+ plc cache warm
177
+ plc cache clear --prefix plc:acme:project
178
+ ```
179
+
180
+ ## Views
181
+
182
+ Declare the universe of projects once under `defaults.projects`, then declare
183
+ views in YAML and reference them from the CLI or TUI:
184
+
185
+ ```yaml
186
+ defaults:
187
+ # The universe of projects this profile can reach. The TUI scans all of them
188
+ # by default; the CLI (`plc issue list` without `--project`) uses the first.
189
+ projects: ["ENG", "OPS", "DESIGN"]
190
+ # Profile-wide default sort, inherited by any view that omits its own `sort`.
191
+ sort: [{ priority: desc }, { updated_at: desc }]
192
+
193
+ views:
194
+ - name: "My open" # no `projects` => scans every project above
195
+ filters:
196
+ assignee: me
197
+ state_group: [unstarted, started]
198
+ sort: priority # scalar shorthand; uses the field's natural direction
199
+
200
+ - name: "Eng sprint"
201
+ projects: ["ENG"] # restricts to a subset of defaults.projects
202
+ filters:
203
+ cycle: current # cycle/module are only allowed on single-project views
204
+ state_group: [started]
205
+ sort: # multi-level: each key breaks ties of the one above
206
+ - priority: desc
207
+ - state: asc
208
+ - updated_at: desc
209
+ ```
210
+
211
+ ```bash
212
+ plc issue list --view "My open"
213
+ ```
214
+
215
+ A view without `projects` inherits the full `defaults.projects` set and
216
+ aggregates issues across all of them, reordered by the view's `sort`. A view
217
+ with `projects` restricts to that subset, which must be contained in
218
+ `defaults.projects`. Because `cycle` and `module` identify a single project,
219
+ they may only be used on views that resolve to exactly one project.
220
+
221
+ `sort` accepts either a scalar (`sort: priority`, applied in the field's natural
222
+ direction) or an ordered list of single-key maps (`- priority: desc`) for
223
+ multi-level sorting, where each entry breaks ties of the ones above it. Valid
224
+ fields are `priority`, `state`, `project`, `assign`, `created_at`, and
225
+ `updated_at`. A view inherits `defaults.sort` when it omits its own.
226
+
227
+ The full list of filters (`assignee`, `state_group`, `labels`, `priority`,
228
+ `cycle`, `module`, plus client-side `state_search` / `project_state_search`) and
229
+ their accepted values is documented in
230
+ [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md#filters).
231
+
232
+ ## TUI usage
233
+
234
+ `plc dash` opens a multi-panel dashboard. Views auto-refresh on a configurable
235
+ interval — set `defaults.auto_refresh_seconds` (default `15`; `0` disables it,
236
+ leaving manual `r` refresh intact).
237
+
238
+ ### Keybindings
239
+
240
+ Press `?` inside the TUI to open the help modal — it lists every binding grouped
241
+ by context (Global, Issue list, Views, Filter, Help) and supports incremental
242
+ search by description, action id, or key.
243
+
244
+ Default bindings:
245
+
246
+ | Key | Action |
247
+ | ------------------ | -------------------------------------- |
248
+ | `?` | toggle help modal |
249
+ | `j` / `k` / arrows | navigate the issue list |
250
+ | `g` / `G` | jump to top / bottom |
251
+ | `PgUp` / `PgDn` | scroll one page |
252
+ | `[` / `]` | switch view |
253
+ | `Enter` | open issue detail modal (Markdown) |
254
+ | `Esc` (in modal) | close current modal back to list |
255
+ | `o` | open the selected issue in the browser |
256
+ | `e` | edit the selected issue |
257
+ | `n` | create a new issue |
258
+ | `>` / `<` | advance / retreat the issue's state |
259
+ | `r` | refresh |
260
+ | `/` | filter the loaded rows (see below) |
261
+ | `q` | quit |
262
+
263
+ Inside the detail modal, the same `j`/`k`/arrows/`PgUp`/`PgDn`/`g`/`G`
264
+ bindings scroll the Markdown description; `o` opens the issue in the
265
+ browser; `Esc` closes back to the list.
266
+
267
+ `e` (from the list or the detail modal) opens an edit modal for the selected
268
+ issue. `j`/`k`/arrows move focus across the editable fields (title, description,
269
+ state, assignee, priority, labels); `Enter` acts on the focused field. Title and
270
+ description open an inline text editor (`ctrl+s` applies, `Esc` cancels;
271
+ description is multiline with `Enter` for newline). State, assignee, priority and
272
+ labels open a picker — the assignee and label pickers are multi-select (`Enter`
273
+ toggles an entry, `ctrl+s` confirms the set, an empty set clears it), state and
274
+ priority confirm on `Enter`. Inside a picker, `Esc` returns to the form. `ctrl+s`
275
+ in the form saves every change in a single request; `Esc` closes, asking to
276
+ confirm first if there are unsaved changes.
277
+
278
+ `>` / `<` move the selected issue one step forward / back along its project's
279
+ workflow states, after a confirmation that names the move (`ENG-1: Todo → In
280
+ Progress?`); `y` applies it, `n`/`Esc` cancels. At the first/last state it is a
281
+ no-op with a hint.
282
+
283
+ `n` opens a create modal for a new issue, reusing the same form and pickers. When
284
+ the active view spans several projects it first asks which project to create in
285
+ (a single project is inferred). Fill the fields the same way as editing; a title
286
+ is required. `ctrl+s` creates the issue, `Esc` cancels.
287
+
288
+ `/` filters the loaded rows with a small `key:value` query (no refetch). Bare
289
+ words match the title/key; typed tokens narrow by field:
290
+
291
+ | Token | Matches | Example |
292
+ | -------- | ---------------------------- | --------------- |
293
+ | `ass:` | assignee display name / `me` | `ass:joe` |
294
+ | `state:` | state name (substring) | `state:prog` |
295
+ | `group:` | state group | `group:started` |
296
+ | `prio:` | priority | `prio:high` |
297
+ | `label:` | label name (substring) | `label:bug` |
298
+ | `proj:` | project identifier | `proj:ENG` |
299
+
300
+ Tokens of different keys combine with AND; repeating a key is an OR within it
301
+ (`label:bug label:ui`). An unknown `key:` falls back to a title/key substring.
302
+ The status bar shows how many of the loaded rows match, so an empty result reads
303
+ as "filtered", not "no data".
304
+
305
+ Issue descriptions are stored as HTML on Plane and rendered inline as
306
+ Markdown by a small custom renderer (headings, lists, code, links,
307
+ blockquotes, strikethrough).
308
+
309
+ ### Customizing keybindings
310
+
311
+ Drop a `~/.config/plane-cli/keybindings.yaml` file. Each entry maps an action id
312
+ to a key spec. See [`examples/keybindings.yaml`](examples/keybindings.yaml).
313
+
314
+ ```yaml
315
+ list.next: down
316
+ list.prev: up
317
+ list.refresh: ctrl+r
318
+ global.help: "?"
319
+ ```
320
+
321
+ The `?` modal flags overridden bindings with a green `*`.
322
+
323
+ ## Logs
324
+
325
+ The TUI cannot print to stderr without corrupting the canvas, so `plc dash` writes
326
+ JSON Lines to `$XDG_STATE_HOME/plane-cli/log.jsonl` (default
327
+ `~/.local/state/plane-cli/log.jsonl`). Render errors caught by the React error boundary
328
+ go to the same file. Rotated to `log.jsonl.1` at ~1 MB.
329
+
330
+ ```bash
331
+ plc log path # print the log file path
332
+ plc log tail -n 100 # last 100 entries
333
+ plc log clear # remove the file
334
+ plc --debug dash # raise log level to debug for the next run
335
+ ```
336
+
337
+ CLI commands (everything other than `dash`) continue to log to stderr via `pino`.
338
+
339
+ ## Output formats
340
+
341
+ Every list / view command supports `--json`, `--yaml`, and `--limit`. `--debug` enables
342
+ verbose logging and full stack traces.
343
+
344
+ ## Development
345
+
346
+ The dev toolchain is managed by [`mise`](https://mise.jdx.dev):
347
+
348
+ ```bash
349
+ mise install # node + pnpm versions pinned in mise.toml
350
+ make bootstrap # install dev toolchain + project deps
351
+ make ci # full pipeline: fmt-check + lint + typecheck + test-cov + build
352
+ ```
353
+
354
+ Common targets:
355
+
356
+ | Target | Description |
357
+ | ------------------------ | --------------------------------------- |
358
+ | `make bootstrap` | install dev toolchain and dependencies |
359
+ | `make dev ARGS="..."` | run the CLI from source |
360
+ | `make build` | build the production bundle |
361
+ | `make test` | run unit tests |
362
+ | `make test-cov` | run tests with coverage (95% threshold) |
363
+ | `make fmt` / `make lint` | format / lint |
364
+ | `make ci` | full pipeline |
365
+ | `make clean` | remove build artifacts |
366
+
367
+ Run `make help` for the full list.
368
+
369
+ ## Troubleshooting
370
+
371
+ - **`api key not found`** — run `plc auth login` for the active profile, or set
372
+ `auth.api_key` inline under the profile.
373
+ - **`config validation failed`** — run `plc config validate` to see the offending path
374
+ reported by `zod`.
375
+ - **TLS errors against self-hosted** — set `server.tls.reject_unauthorized: false` only
376
+ if you intentionally use a private CA.
377
+ - **Stale data** — re-run with `--no-cache` to bypass the cache, or `plc cache clear`.
378
+
379
+ ## Contributing
380
+
381
+ See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for commit, tag, and PR guidelines.
382
+
383
+ - [`ARCHITECTURE.md`](ARCHITECTURE.md) — how the codebase is organized and why.
384
+ - [`SECURITY.md`](SECURITY.md) — how to report a vulnerability.
385
+ - [`CHANGELOG.md`](CHANGELOG.md) — notable changes per release.
386
+ - [`AGENTS.md`](AGENTS.md) — conventions for humans and LLM agents.