monolynx-cli 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/PKG-INFO +41 -8
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/README.md +40 -7
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/commands/_common.py +56 -0
- monolynx_cli-0.3.0/src/monolynx_cli/commands/graph.py +143 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/commands/ticket.py +37 -8
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/main.py +2 -2
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/version.py +1 -1
- monolynx_cli-0.3.0/tests/test_graph_commands.py +563 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_member_commands.py +0 -1
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_oauth.py +1 -1
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_ticket_commands.py +490 -14
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_version_command.py +2 -2
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/.gitignore +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/LICENSE +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/packaging/homebrew/README.md +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/packaging/homebrew/monolynx.rb +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/pyproject.toml +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/scripts/bump_formula.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/__init__.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/__main__.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/client.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/commands/__init__.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/commands/auth.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/commands/completion.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/commands/config.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/commands/member.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/commands/project.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/commands/sprint.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/commands/version.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/commands/wiki.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/config.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/oauth.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/options.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/src/monolynx_cli/output.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/conftest.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_auth_commands.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_client.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_commands_common.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_completion.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_config.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_config_commands.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_error_handling.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_main.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_output.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_project_commands.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_smoke_e2e.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_sprint_commands.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_token_leak.py +0 -0
- {monolynx_cli-0.2.0 → monolynx_cli-0.3.0}/tests/test_wiki_commands.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: monolynx-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Interfejs wiersza poleceń dla platformy Monolynx
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/piotrkrych/monolynx
|
|
6
6
|
Project-URL: Documentation, https://gitlab.com/piotrkrych/monolynx/-/blob/main/cli/README.md
|
|
@@ -34,7 +34,7 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
|
|
35
35
|
[](https://pypi.org/project/monolynx-cli/)
|
|
36
36
|
|
|
37
|
-
Command-line client for the Monolynx platform: manage projects, tickets, sprints and wiki pages from your terminal.
|
|
37
|
+
Command-line client for the Monolynx platform: manage projects, tickets, sprints and wiki pages, and read the code dependency graph, from your terminal.
|
|
38
38
|
|
|
39
39
|
## Installation
|
|
40
40
|
|
|
@@ -157,7 +157,7 @@ monolynx auth login --endpoint https://monolynx.example.com --profile work
|
|
|
157
157
|
- `--token` stores an API token without a browser (`auth_type = "token"`).
|
|
158
158
|
- `--endpoint` and `--profile` work with both flows.
|
|
159
159
|
|
|
160
|
-
The OAuth access token is valid for 30 days. Every command that calls the API keeps the session alive. `project`, `member`, `ticket`, `sprint` and `
|
|
160
|
+
The OAuth access token is valid for 30 days. Every command that calls the API keeps the session alive. `project`, `member`, `ticket`, `sprint`, `wiki` and `graph` commands do it in two ways, `auth whoami` only reactively:
|
|
161
161
|
|
|
162
162
|
- Proactively (not in `auth whoami`): when the profile's `expires_at` is less than 24 hours away (or already past), the CLI refreshes the tokens once before the first request of the command and sends the request with the new access token. The server rotates the refresh token and both new tokens are saved in the profile, so regular use extends the session. If this refresh fails, the command goes on with the current token.
|
|
163
163
|
- Reactively: when the server answers 401, the CLI refreshes the tokens once and repeats the request. A failed refresh ends with a message asking you to run `monolynx auth login` and exit code 3.
|
|
@@ -239,11 +239,11 @@ All `ticket` commands work on the project resolved from `--project`, `MONOLYNX_P
|
|
|
239
239
|
|
|
240
240
|
| Command | Arguments and options | Description |
|
|
241
241
|
|---|---|---|
|
|
242
|
-
| `ticket list` | `--status`, `--priority`, `--search`, `--sprint`, `--due-before`, `--due-after`, `--overdue`, `--label`, `--page` | List project tickets with filters (one page). `--search` matches the title only; use `ticket search` to match the description too. |
|
|
242
|
+
| `ticket list` | `--status`, `--priority`, `--search`, `--sprint`, `--due-before`, `--due-after`, `--overdue`, `--label`, `--page`, `--compact` | List project tickets with filters (one page); every ticket carries its blockers (`blocked_by`). `--compact` leaves out the ticket descriptions. `--search` matches the title only; use `ticket search` to match the description too. |
|
|
243
243
|
| `ticket search` | `[query]`, `--status`, `--priority`, `--assignee`, `--sprint`, `--due-before`, `--due-after`, `--page` | Search tickets by phrase (title or description) and filters (one page). |
|
|
244
|
-
| `ticket get` | `<ticket>` | Show ticket details. |
|
|
245
|
-
| `ticket create` | `--title` (required), `--description`, `--description-file`, `--priority`, `--sp`/`--story-points`, `--sprint`, `--assignee`, `--due`, `--label`, `--ac`, `--spec-page`, `--blocked-by` | Create a ticket. `--label`, `--ac` and `--blocked-by` can be repeated; `--blocked-by` takes the UUID of the blocking ticket (not the key). |
|
|
246
|
-
| `ticket update` | `<ticket>`, `--title`, `--description`, `--description-file`, `--status`, `--priority`, `--sp`/`--story-points`, `--sprint`, `--assignee`, `--due`, `--label`, `--blocked-by`, `--no-blockers` | Update a ticket; only the given fields are sent. `--label` and `--blocked-by` replace the current values; `--blocked-by` takes the UUID of the blocking ticket (not the key). |
|
|
244
|
+
| `ticket get` | `<ticket>` | Show ticket details, including `spec_page_id` (`null` when the ticket has no spec page). |
|
|
245
|
+
| `ticket create` | `--title` (required), `--description`, `--description-file`, `--priority`, `--sp`/`--story-points`, `--sprint`, `--assignee`, `--due`, `--label`, `--ac`, `--spec-page`, `--blocked-by`, `--brief` | Create a ticket. `--label`, `--ac` and `--blocked-by` can be repeated; `--blocked-by` takes the UUID of the blocking ticket (not the key). `--brief` prints only the identifying fields, status and relations. |
|
|
246
|
+
| `ticket update` | `<ticket>`, `--title`, `--description`, `--description-file`, `--status`, `--priority`, `--sp`/`--story-points`, `--sprint`, `--assignee`, `--due`, `--label`, `--blocked-by`, `--no-blockers`, `--brief` | Update a ticket; only the given fields are sent. `--label` and `--blocked-by` replace the current values; `--blocked-by` takes the UUID of the blocking ticket (not the key). `--brief` prints only the identifying fields, status and relations. |
|
|
247
247
|
| `ticket delete` | `<ticket>`, `-y`/`--yes` | Delete a ticket (asks for confirmation unless `--yes`). |
|
|
248
248
|
| `ticket bulk-update` | `<tickets>...`, `--status`, `--priority`, `--assignee`, `--sprint`, `--due` | Update many tickets in one request. |
|
|
249
249
|
| `ticket comment list` | `<ticket>` | List ticket comments (all pages, flat list). |
|
|
@@ -261,6 +261,11 @@ Notes:
|
|
|
261
261
|
- Identifiers in the URL path (ticket, sprint, page, criterion, project slug) are URL-encoded. A bare `.` or `..` (or an empty value) is refused with exit code 2 before any request is sent.
|
|
262
262
|
- `--description -` and `--body -` read the text from stdin.
|
|
263
263
|
- In `ticket update` and `ticket bulk-update`, an empty string for `--sprint`, `--assignee` or `--due` clears the value.
|
|
264
|
+
- `ticket list` shows blockers per ticket. With `-o json` and `-o yaml`, `blocked_by` is the full list of objects (`id`, `key`, `title`, `status`), empty when the ticket has no blockers. With `-o table` and `-o csv` it is one `blocked_by` column holding the blocker keys joined with `, ` (empty when there are none). `ticket search` results carry no `blocked_by`.
|
|
265
|
+
- `ticket list` and `ticket get` check the server response before rendering. A list item without a `blocked_by` list, or ticket details without the `spec_page_id` key, end with the error `Nieprawidłowa odpowiedź serwera` and exit code 1. This happens against a server older than the CLI, which does not send these fields yet; the CLI refuses to render rather than show a ticket as having no blockers or no spec page.
|
|
266
|
+
- `ticket list --compact` sends `compact=true`, and the server returns the list items without the `description` field; every other field stays, including `blocked_by`. Without the flag the parameter is not sent and the items carry `description` as before.
|
|
267
|
+
- `ticket create --brief` and `ticket update --brief` print exactly seven fields, in this order: `id`, `key`, `title`, `status`, `sprint_id`, `blocked_by`, `blocks`. The request and the server response are unchanged; the CLI filters the output. With `-o json` and `-o yaml`, `blocked_by` and `blocks` are full lists of objects (`id`, `key`, `title`, `status`); with `-o table` and `-o csv` each is one column holding the ticket keys joined with `, `, as in `ticket list`.
|
|
268
|
+
- `--brief` checks the server response before rendering. A response that is not an object, has an empty or missing `id` or `key`, lacks the `title`, `status` or `sprint_id` key (`sprint_id` may be `null`), or whose `blocked_by` or `blocks` is not a list of objects with a non-empty `key` ends with the error `Nieprawidłowa odpowiedź serwera` and exit code 1. The write may already have reached the server, so check the ticket before repeating the command.
|
|
264
269
|
|
|
265
270
|
```bash
|
|
266
271
|
monolynx --project my-project ticket create --title "Fix login redirect" --priority high --sp 2 --ac "Redirect keeps the next parameter"
|
|
@@ -297,6 +302,34 @@ git log -1 --format=%B | monolynx --project my-project ticket comment add MP-11
|
|
|
297
302
|
|
|
298
303
|
`--public` publishes the page at `/blog/{slug}`, visible to anyone without signing in. `wiki delete` has no `-y` short form. `--content -` with an empty standard input, an unreadable or non-UTF-8 `--file`, and a missing project are usage errors (exit code 2, no request sent).
|
|
299
304
|
|
|
305
|
+
### `graph`
|
|
306
|
+
|
|
307
|
+
Read-only commands for the code dependency graph of the Connections module. All of them work on the project resolved from `--project`, `MONOLYNX_PROJECT` or the profile, and need the `connections:read` permission.
|
|
308
|
+
|
|
309
|
+
| Command | Arguments and options | Description |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| `graph nodes` | `--type`, `--search`, `--limit` (1 to 1000) | List graph nodes, optionally filtered by type and by a fragment of the name (case-insensitive). The server default for `--limit` is 100. |
|
|
312
|
+
| `graph node` | `<node_id>`, `--depth` (min. 1, the server caps it at 5), `--relation-type`, `--node-type` | Show a node with its neighborhood: the node itself, the neighboring nodes, the edges and the depth of every node. `--relation-type` and `--node-type` can be repeated. |
|
|
313
|
+
| `graph query` | `--node-type`, `--search`, `--limit` (1 to 1000) | Fetch the graph or a subgraph of the project (nodes and edges). `--search` matches a fragment of the name or file path and returns the matching nodes with their direct neighbors. The server default for `--limit` is 200. |
|
|
314
|
+
| `graph path` | `--source-id` (required), `--target-id` (required) | Find the shortest path between two nodes. |
|
|
315
|
+
| `graph stats` | | Show graph statistics: the number of nodes and edges, in total and per type. |
|
|
316
|
+
|
|
317
|
+
Notes:
|
|
318
|
+
|
|
319
|
+
- `<node_id>` is the `id` field from `graph nodes`. It may contain `/`; the CLI URL-encodes it.
|
|
320
|
+
- Node types are `File`, `Class`, `Method`, `Function`, `Const` and `Module`. Relation types are `CONTAINS`, `CALLS`, `IMPORTS`, `INHERITS`, `USES` and `IMPLEMENTS`.
|
|
321
|
+
- `--depth` is the neighborhood depth: at least 1, and the server caps larger values at 5 (the server default is 1). A `--depth` below 1 or a `--limit` outside 1 to 1000 (`graph nodes`, `graph query`) is a usage error: exit code 2, no request sent. Options you leave out are not sent, so the defaults stay on the server.
|
|
322
|
+
- `graph nodes` prints a flat list. `graph query` and `graph path` print an object with `nodes` and `edges`; `graph node` adds `node` and `depth_map` (node id to depth). `graph stats` prints `total_nodes`, `total_edges`, `nodes_by_type` and `edges_by_type`. When there is no path, `graph path` succeeds with empty `nodes` and `edges`.
|
|
323
|
+
- Every command checks the server response before rendering. A missing required field or a wrong type ends with the error `Nieprawidłowa odpowiedź serwera` and exit code 1, never with an empty result and exit code 0.
|
|
324
|
+
- A project with the graph database turned off, or an unreachable graph database, ends with a 503 error (exit code 1 after the retries). An unknown node in `graph node` is a 404 (exit code 1).
|
|
325
|
+
- There are no commands that write to the graph. The graph is filled by the graphify sync (`cicd/sync_graph.py`), see the Connections module in the dashboard.
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
monolynx --project my-project graph nodes --type Class --search Sprint
|
|
329
|
+
monolynx --project my-project graph node "<node_id>" --depth 2 --relation-type CALLS --relation-type IMPORTS
|
|
330
|
+
monolynx -o json --project my-project graph stats | jq '.nodes_by_type'
|
|
331
|
+
```
|
|
332
|
+
|
|
300
333
|
### `version`
|
|
301
334
|
|
|
302
335
|
| Command | Arguments and options | Description |
|
|
@@ -312,7 +345,7 @@ monolynx -o json version --check
|
|
|
312
345
|
|
|
313
346
|
### Reserved groups
|
|
314
347
|
|
|
315
|
-
`monitoring`, `issues`, `time`, `plan`, `rozliczenia`, `
|
|
348
|
+
`monitoring`, `issues`, `time`, `plan`, `rozliczenia`, `pipeline`, `heartbeat` and `role` appear in `monolynx --help` but are reserved: they have no commands yet (not yet available).
|
|
316
349
|
|
|
317
350
|
### Confirmations
|
|
318
351
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/monolynx-cli/)
|
|
4
4
|
|
|
5
|
-
Command-line client for the Monolynx platform: manage projects, tickets, sprints and wiki pages from your terminal.
|
|
5
|
+
Command-line client for the Monolynx platform: manage projects, tickets, sprints and wiki pages, and read the code dependency graph, from your terminal.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -125,7 +125,7 @@ monolynx auth login --endpoint https://monolynx.example.com --profile work
|
|
|
125
125
|
- `--token` stores an API token without a browser (`auth_type = "token"`).
|
|
126
126
|
- `--endpoint` and `--profile` work with both flows.
|
|
127
127
|
|
|
128
|
-
The OAuth access token is valid for 30 days. Every command that calls the API keeps the session alive. `project`, `member`, `ticket`, `sprint` and `
|
|
128
|
+
The OAuth access token is valid for 30 days. Every command that calls the API keeps the session alive. `project`, `member`, `ticket`, `sprint`, `wiki` and `graph` commands do it in two ways, `auth whoami` only reactively:
|
|
129
129
|
|
|
130
130
|
- Proactively (not in `auth whoami`): when the profile's `expires_at` is less than 24 hours away (or already past), the CLI refreshes the tokens once before the first request of the command and sends the request with the new access token. The server rotates the refresh token and both new tokens are saved in the profile, so regular use extends the session. If this refresh fails, the command goes on with the current token.
|
|
131
131
|
- Reactively: when the server answers 401, the CLI refreshes the tokens once and repeats the request. A failed refresh ends with a message asking you to run `monolynx auth login` and exit code 3.
|
|
@@ -207,11 +207,11 @@ All `ticket` commands work on the project resolved from `--project`, `MONOLYNX_P
|
|
|
207
207
|
|
|
208
208
|
| Command | Arguments and options | Description |
|
|
209
209
|
|---|---|---|
|
|
210
|
-
| `ticket list` | `--status`, `--priority`, `--search`, `--sprint`, `--due-before`, `--due-after`, `--overdue`, `--label`, `--page` | List project tickets with filters (one page). `--search` matches the title only; use `ticket search` to match the description too. |
|
|
210
|
+
| `ticket list` | `--status`, `--priority`, `--search`, `--sprint`, `--due-before`, `--due-after`, `--overdue`, `--label`, `--page`, `--compact` | List project tickets with filters (one page); every ticket carries its blockers (`blocked_by`). `--compact` leaves out the ticket descriptions. `--search` matches the title only; use `ticket search` to match the description too. |
|
|
211
211
|
| `ticket search` | `[query]`, `--status`, `--priority`, `--assignee`, `--sprint`, `--due-before`, `--due-after`, `--page` | Search tickets by phrase (title or description) and filters (one page). |
|
|
212
|
-
| `ticket get` | `<ticket>` | Show ticket details. |
|
|
213
|
-
| `ticket create` | `--title` (required), `--description`, `--description-file`, `--priority`, `--sp`/`--story-points`, `--sprint`, `--assignee`, `--due`, `--label`, `--ac`, `--spec-page`, `--blocked-by` | Create a ticket. `--label`, `--ac` and `--blocked-by` can be repeated; `--blocked-by` takes the UUID of the blocking ticket (not the key). |
|
|
214
|
-
| `ticket update` | `<ticket>`, `--title`, `--description`, `--description-file`, `--status`, `--priority`, `--sp`/`--story-points`, `--sprint`, `--assignee`, `--due`, `--label`, `--blocked-by`, `--no-blockers` | Update a ticket; only the given fields are sent. `--label` and `--blocked-by` replace the current values; `--blocked-by` takes the UUID of the blocking ticket (not the key). |
|
|
212
|
+
| `ticket get` | `<ticket>` | Show ticket details, including `spec_page_id` (`null` when the ticket has no spec page). |
|
|
213
|
+
| `ticket create` | `--title` (required), `--description`, `--description-file`, `--priority`, `--sp`/`--story-points`, `--sprint`, `--assignee`, `--due`, `--label`, `--ac`, `--spec-page`, `--blocked-by`, `--brief` | Create a ticket. `--label`, `--ac` and `--blocked-by` can be repeated; `--blocked-by` takes the UUID of the blocking ticket (not the key). `--brief` prints only the identifying fields, status and relations. |
|
|
214
|
+
| `ticket update` | `<ticket>`, `--title`, `--description`, `--description-file`, `--status`, `--priority`, `--sp`/`--story-points`, `--sprint`, `--assignee`, `--due`, `--label`, `--blocked-by`, `--no-blockers`, `--brief` | Update a ticket; only the given fields are sent. `--label` and `--blocked-by` replace the current values; `--blocked-by` takes the UUID of the blocking ticket (not the key). `--brief` prints only the identifying fields, status and relations. |
|
|
215
215
|
| `ticket delete` | `<ticket>`, `-y`/`--yes` | Delete a ticket (asks for confirmation unless `--yes`). |
|
|
216
216
|
| `ticket bulk-update` | `<tickets>...`, `--status`, `--priority`, `--assignee`, `--sprint`, `--due` | Update many tickets in one request. |
|
|
217
217
|
| `ticket comment list` | `<ticket>` | List ticket comments (all pages, flat list). |
|
|
@@ -229,6 +229,11 @@ Notes:
|
|
|
229
229
|
- Identifiers in the URL path (ticket, sprint, page, criterion, project slug) are URL-encoded. A bare `.` or `..` (or an empty value) is refused with exit code 2 before any request is sent.
|
|
230
230
|
- `--description -` and `--body -` read the text from stdin.
|
|
231
231
|
- In `ticket update` and `ticket bulk-update`, an empty string for `--sprint`, `--assignee` or `--due` clears the value.
|
|
232
|
+
- `ticket list` shows blockers per ticket. With `-o json` and `-o yaml`, `blocked_by` is the full list of objects (`id`, `key`, `title`, `status`), empty when the ticket has no blockers. With `-o table` and `-o csv` it is one `blocked_by` column holding the blocker keys joined with `, ` (empty when there are none). `ticket search` results carry no `blocked_by`.
|
|
233
|
+
- `ticket list` and `ticket get` check the server response before rendering. A list item without a `blocked_by` list, or ticket details without the `spec_page_id` key, end with the error `Nieprawidłowa odpowiedź serwera` and exit code 1. This happens against a server older than the CLI, which does not send these fields yet; the CLI refuses to render rather than show a ticket as having no blockers or no spec page.
|
|
234
|
+
- `ticket list --compact` sends `compact=true`, and the server returns the list items without the `description` field; every other field stays, including `blocked_by`. Without the flag the parameter is not sent and the items carry `description` as before.
|
|
235
|
+
- `ticket create --brief` and `ticket update --brief` print exactly seven fields, in this order: `id`, `key`, `title`, `status`, `sprint_id`, `blocked_by`, `blocks`. The request and the server response are unchanged; the CLI filters the output. With `-o json` and `-o yaml`, `blocked_by` and `blocks` are full lists of objects (`id`, `key`, `title`, `status`); with `-o table` and `-o csv` each is one column holding the ticket keys joined with `, `, as in `ticket list`.
|
|
236
|
+
- `--brief` checks the server response before rendering. A response that is not an object, has an empty or missing `id` or `key`, lacks the `title`, `status` or `sprint_id` key (`sprint_id` may be `null`), or whose `blocked_by` or `blocks` is not a list of objects with a non-empty `key` ends with the error `Nieprawidłowa odpowiedź serwera` and exit code 1. The write may already have reached the server, so check the ticket before repeating the command.
|
|
232
237
|
|
|
233
238
|
```bash
|
|
234
239
|
monolynx --project my-project ticket create --title "Fix login redirect" --priority high --sp 2 --ac "Redirect keeps the next parameter"
|
|
@@ -265,6 +270,34 @@ git log -1 --format=%B | monolynx --project my-project ticket comment add MP-11
|
|
|
265
270
|
|
|
266
271
|
`--public` publishes the page at `/blog/{slug}`, visible to anyone without signing in. `wiki delete` has no `-y` short form. `--content -` with an empty standard input, an unreadable or non-UTF-8 `--file`, and a missing project are usage errors (exit code 2, no request sent).
|
|
267
272
|
|
|
273
|
+
### `graph`
|
|
274
|
+
|
|
275
|
+
Read-only commands for the code dependency graph of the Connections module. All of them work on the project resolved from `--project`, `MONOLYNX_PROJECT` or the profile, and need the `connections:read` permission.
|
|
276
|
+
|
|
277
|
+
| Command | Arguments and options | Description |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| `graph nodes` | `--type`, `--search`, `--limit` (1 to 1000) | List graph nodes, optionally filtered by type and by a fragment of the name (case-insensitive). The server default for `--limit` is 100. |
|
|
280
|
+
| `graph node` | `<node_id>`, `--depth` (min. 1, the server caps it at 5), `--relation-type`, `--node-type` | Show a node with its neighborhood: the node itself, the neighboring nodes, the edges and the depth of every node. `--relation-type` and `--node-type` can be repeated. |
|
|
281
|
+
| `graph query` | `--node-type`, `--search`, `--limit` (1 to 1000) | Fetch the graph or a subgraph of the project (nodes and edges). `--search` matches a fragment of the name or file path and returns the matching nodes with their direct neighbors. The server default for `--limit` is 200. |
|
|
282
|
+
| `graph path` | `--source-id` (required), `--target-id` (required) | Find the shortest path between two nodes. |
|
|
283
|
+
| `graph stats` | | Show graph statistics: the number of nodes and edges, in total and per type. |
|
|
284
|
+
|
|
285
|
+
Notes:
|
|
286
|
+
|
|
287
|
+
- `<node_id>` is the `id` field from `graph nodes`. It may contain `/`; the CLI URL-encodes it.
|
|
288
|
+
- Node types are `File`, `Class`, `Method`, `Function`, `Const` and `Module`. Relation types are `CONTAINS`, `CALLS`, `IMPORTS`, `INHERITS`, `USES` and `IMPLEMENTS`.
|
|
289
|
+
- `--depth` is the neighborhood depth: at least 1, and the server caps larger values at 5 (the server default is 1). A `--depth` below 1 or a `--limit` outside 1 to 1000 (`graph nodes`, `graph query`) is a usage error: exit code 2, no request sent. Options you leave out are not sent, so the defaults stay on the server.
|
|
290
|
+
- `graph nodes` prints a flat list. `graph query` and `graph path` print an object with `nodes` and `edges`; `graph node` adds `node` and `depth_map` (node id to depth). `graph stats` prints `total_nodes`, `total_edges`, `nodes_by_type` and `edges_by_type`. When there is no path, `graph path` succeeds with empty `nodes` and `edges`.
|
|
291
|
+
- Every command checks the server response before rendering. A missing required field or a wrong type ends with the error `Nieprawidłowa odpowiedź serwera` and exit code 1, never with an empty result and exit code 0.
|
|
292
|
+
- A project with the graph database turned off, or an unreachable graph database, ends with a 503 error (exit code 1 after the retries). An unknown node in `graph node` is a 404 (exit code 1).
|
|
293
|
+
- There are no commands that write to the graph. The graph is filled by the graphify sync (`cicd/sync_graph.py`), see the Connections module in the dashboard.
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
monolynx --project my-project graph nodes --type Class --search Sprint
|
|
297
|
+
monolynx --project my-project graph node "<node_id>" --depth 2 --relation-type CALLS --relation-type IMPORTS
|
|
298
|
+
monolynx -o json --project my-project graph stats | jq '.nodes_by_type'
|
|
299
|
+
```
|
|
300
|
+
|
|
268
301
|
### `version`
|
|
269
302
|
|
|
270
303
|
| Command | Arguments and options | Description |
|
|
@@ -280,7 +313,7 @@ monolynx -o json version --check
|
|
|
280
313
|
|
|
281
314
|
### Reserved groups
|
|
282
315
|
|
|
283
|
-
`monitoring`, `issues`, `time`, `plan`, `rozliczenia`, `
|
|
316
|
+
`monitoring`, `issues`, `time`, `plan`, `rozliczenia`, `pipeline`, `heartbeat` and `role` appear in `monolynx --help` but are reserved: they have no commands yet (not yet available).
|
|
284
317
|
|
|
285
318
|
### Confirmations
|
|
286
319
|
|
|
@@ -32,6 +32,7 @@ from monolynx_cli.options import get_options
|
|
|
32
32
|
|
|
33
33
|
PER_PAGE = 100
|
|
34
34
|
STDIN_MARKER = "-"
|
|
35
|
+
BRIEF_TICKET_KEYS = ("id", "key", "title", "status", "sprint_id", "blocked_by", "blocks")
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def _stdin_is_tty() -> bool:
|
|
@@ -136,6 +137,61 @@ def _invalid_page(detail: str) -> MonolynxAPIError:
|
|
|
136
137
|
return MonolynxAPIError(200, "Nieprawidłowa odpowiedź serwera", detail)
|
|
137
138
|
|
|
138
139
|
|
|
140
|
+
def require_list_with_blockers(payload: Any) -> dict[str, Any]:
|
|
141
|
+
"""Sprawdź kontrakt odpowiedzi `ticket list`: koperta z `items`, każdy element z listą `blocked_by`.
|
|
142
|
+
|
|
143
|
+
Serwer sprzed MON-246 nie niesie `blocked_by`; kolumna z kluczami blokerów pokazałaby wtedy
|
|
144
|
+
puste pole, nieodróżnialne od ticketu bez blokerów, więc brak pola to błąd kontraktu.
|
|
145
|
+
"""
|
|
146
|
+
if not isinstance(payload, dict):
|
|
147
|
+
raise _invalid_page("Odpowiedź nie jest obiektem JSON.")
|
|
148
|
+
items = payload.get("items")
|
|
149
|
+
if not isinstance(items, list) or not all(isinstance(item, dict) for item in items):
|
|
150
|
+
raise _invalid_page("Pole items nie jest listą obiektów.")
|
|
151
|
+
for index, item in enumerate(items):
|
|
152
|
+
blockers = item.get("blocked_by")
|
|
153
|
+
if not isinstance(blockers, list):
|
|
154
|
+
raise _invalid_page(f"Element items[{index}] nie ma listy blocked_by (serwer bez obsługi blokerów w liście ticketów).")
|
|
155
|
+
for position, blocker in enumerate(blockers):
|
|
156
|
+
if not isinstance(blocker, dict) or not isinstance(blocker.get("key"), str) or not blocker["key"]:
|
|
157
|
+
raise _invalid_page(f"Element items[{index}].blocked_by[{position}] nie jest obiektem z niepustym kluczem key.")
|
|
158
|
+
return payload
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def require_spec_page_id(payload: Any) -> dict[str, Any]:
|
|
162
|
+
"""Sprawdź kontrakt odpowiedzi `ticket get`: klucz `spec_page_id` musi istnieć (wartość `null` jest poprawna)."""
|
|
163
|
+
if not isinstance(payload, dict):
|
|
164
|
+
raise _invalid_page("Odpowiedź nie jest obiektem JSON.")
|
|
165
|
+
if "spec_page_id" not in payload:
|
|
166
|
+
raise _invalid_page("Brak pola spec_page_id (serwer bez obsługi spec_page_id w szczegółach ticketu).")
|
|
167
|
+
return payload
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def require_brief_ticket(payload: Any) -> dict[str, Any]:
|
|
171
|
+
"""Sprawdź kontrakt odpowiedzi `ticket create|update` pod `--brief` i zwróć tylko 7 pól w stałej kolejności.
|
|
172
|
+
|
|
173
|
+
Brak klucza albo zły typ to błąd kontraktu, nigdy puste pole z kodem 0: agent czytający
|
|
174
|
+
potwierdzenie zapisu nie odróżniłby `""` od ticketu bez statusu. `sprint_id` może być `null`.
|
|
175
|
+
"""
|
|
176
|
+
if not isinstance(payload, dict):
|
|
177
|
+
raise _invalid_page("Odpowiedź nie jest obiektem JSON.")
|
|
178
|
+
for name in ("id", "key"):
|
|
179
|
+
value = payload.get(name)
|
|
180
|
+
if not isinstance(value, str) or not value:
|
|
181
|
+
raise _invalid_page(f"Pole {name} nie jest niepustym tekstem.")
|
|
182
|
+
for name in ("title", "status", "sprint_id"):
|
|
183
|
+
if name not in payload:
|
|
184
|
+
raise _invalid_page(f"Brak pola {name} w odpowiedzi ticketu.")
|
|
185
|
+
for name in ("blocked_by", "blocks"):
|
|
186
|
+
relations = payload.get(name)
|
|
187
|
+
if not isinstance(relations, list):
|
|
188
|
+
raise _invalid_page(f"Pole {name} nie jest listą (brak lub zły typ w odpowiedzi ticketu).")
|
|
189
|
+
for position, relation in enumerate(relations):
|
|
190
|
+
if not isinstance(relation, dict) or not isinstance(relation.get("key"), str) or not relation["key"]:
|
|
191
|
+
raise _invalid_page(f"Element {name}[{position}] nie jest obiektem z niepustym kluczem key.")
|
|
192
|
+
return {name: payload[name] for name in BRIEF_TICKET_KEYS}
|
|
193
|
+
|
|
194
|
+
|
|
139
195
|
def fetch_all(client: MonolynxClient, path: str, params: dict[str, Any] | None = None) -> list[dict[str, Any]]:
|
|
140
196
|
"""Pobierz elementy ze wszystkich stron; `params` (np. filtr statusu) idą na każdą stronę."""
|
|
141
197
|
items: list[dict[str, Any]] = []
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Komendy monolynx_cli.commands.graph: nodes, node, query, path, stats (tylko odczyt grafu Połączeń).
|
|
2
|
+
|
|
3
|
+
Komendy biją w /api/v2/projects/{slug}/graph/* przez klienta z `_common.open_project`. Zapis
|
|
4
|
+
grafu (tworzenie, usuwanie, replace_graph) jest wyłącznie w MCP i synchronizacji CI. Kształt
|
|
5
|
+
każdej odpowiedzi jest sprawdzany przed użyciem: brak wymaganego pola albo zły typ to
|
|
6
|
+
MonolynxAPIError (kod wyjścia 1), nigdy pusty wynik z kodem 0. Filtry nieustawione przez
|
|
7
|
+
użytkownika nie trafiają do żądania, więc domyślne wartości zostają po stronie serwera.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
from monolynx_cli.client import MonolynxAPIError
|
|
17
|
+
from monolynx_cli.commands import _common
|
|
18
|
+
from monolynx_cli.output import emit
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(no_args_is_help=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _invalid(detail: str) -> MonolynxAPIError:
|
|
24
|
+
return MonolynxAPIError(200, "Nieprawidłowa odpowiedź serwera", detail)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _require_objects(value: Any, name: str) -> list[dict[str, Any]]:
|
|
28
|
+
if not isinstance(value, list):
|
|
29
|
+
raise _invalid(f"Pole {name} nie jest listą.")
|
|
30
|
+
for index, item in enumerate(value):
|
|
31
|
+
if not isinstance(item, dict):
|
|
32
|
+
raise _invalid(f"Element {name}[{index}] nie jest obiektem JSON.")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _require_object(payload: Any) -> dict[str, Any]:
|
|
37
|
+
if not isinstance(payload, dict):
|
|
38
|
+
raise _invalid("Odpowiedź nie jest obiektem JSON.")
|
|
39
|
+
return payload
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _require_subgraph(payload: Any) -> dict[str, Any]:
|
|
43
|
+
data = _require_object(payload)
|
|
44
|
+
_require_objects(data.get("nodes"), "nodes")
|
|
45
|
+
_require_objects(data.get("edges"), "edges")
|
|
46
|
+
return data
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _require_node_detail(payload: Any) -> dict[str, Any]:
|
|
50
|
+
data = _require_subgraph(payload)
|
|
51
|
+
if not isinstance(data.get("node"), dict):
|
|
52
|
+
raise _invalid("Pole node nie jest obiektem JSON.")
|
|
53
|
+
if not isinstance(data.get("depth_map"), dict):
|
|
54
|
+
raise _invalid("Pole depth_map nie jest obiektem JSON.")
|
|
55
|
+
return data
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _require_stats(payload: Any) -> dict[str, Any]:
|
|
59
|
+
data = _require_object(payload)
|
|
60
|
+
for name in ("total_nodes", "total_edges"):
|
|
61
|
+
value = data.get(name)
|
|
62
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
63
|
+
raise _invalid(f"Pole {name} nie jest liczbą całkowitą.")
|
|
64
|
+
for name in ("nodes_by_type", "edges_by_type"):
|
|
65
|
+
if not isinstance(data.get(name), dict):
|
|
66
|
+
raise _invalid(f"Pole {name} nie jest obiektem JSON.")
|
|
67
|
+
return data
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("nodes")
|
|
71
|
+
def list_nodes(
|
|
72
|
+
ctx: typer.Context,
|
|
73
|
+
node_type: str | None = typer.Option(None, "--type", help="Typ node'a: File, Class, Method, Function, Const lub Module."),
|
|
74
|
+
search: str | None = typer.Option(None, "--search", help="Fragment nazwy node'a (bez rozróżniania wielkości liter)."),
|
|
75
|
+
limit: int | None = typer.Option(None, "--limit", min=1, max=1000, help="Maksymalna liczba node'ów (domyślnie 100 po stronie serwera)."),
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Wyświetl node'y grafu Połączeń z opcjonalnym filtrem po typie i nazwie."""
|
|
78
|
+
client, slug = _common.open_project(ctx)
|
|
79
|
+
params = _common.without_none({"type": node_type, "search": search, "limit": limit})
|
|
80
|
+
with client:
|
|
81
|
+
payload = client.get(_common.project_path(slug, "/graph/nodes"), params=params)
|
|
82
|
+
emit(ctx, _require_objects(payload, "odpowiedź"))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command("node")
|
|
86
|
+
def get_node(
|
|
87
|
+
ctx: typer.Context,
|
|
88
|
+
node_id: str = typer.Argument(..., help="Identyfikator node'a (pole id z `graph nodes`)."),
|
|
89
|
+
depth: int | None = typer.Option(
|
|
90
|
+
None, "--depth", min=1, help="Głębokość sąsiedztwa, min. 1, serwer obcina do 5 (domyślnie 1 po stronie serwera)."
|
|
91
|
+
),
|
|
92
|
+
relation_type: list[str] | None = typer.Option(
|
|
93
|
+
None, "--relation-type", help="Typ relacji: CONTAINS, CALLS, IMPORTS, INHERITS, USES lub IMPLEMENTS. Można powtarzać."
|
|
94
|
+
),
|
|
95
|
+
node_type: list[str] | None = typer.Option(
|
|
96
|
+
None, "--node-type", help="Typ sąsiada: File, Class, Method, Function, Const lub Module. Można powtarzać."
|
|
97
|
+
),
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Pokaż node wraz z sąsiedztwem (node'y, krawędzie i głębokość każdego node'a)."""
|
|
100
|
+
client, slug = _common.open_project(ctx)
|
|
101
|
+
params = _common.without_none({"depth": depth, "relation_types": relation_type or None, "node_types": node_type or None})
|
|
102
|
+
with client:
|
|
103
|
+
payload = client.get(_common.project_path(slug, f"/graph/nodes/{_common.path_segment(node_id)}"), params=params)
|
|
104
|
+
emit(ctx, _require_node_detail(payload))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command("query")
|
|
108
|
+
def query_graph(
|
|
109
|
+
ctx: typer.Context,
|
|
110
|
+
node_type: str | None = typer.Option(None, "--node-type", help="Typ node'a: File, Class, Method, Function, Const lub Module."),
|
|
111
|
+
search: str | None = typer.Option(
|
|
112
|
+
None, "--search", help="Fragment nazwy lub ścieżki pliku: zwraca dopasowane node'y i ich bezpośrednich sąsiadów."
|
|
113
|
+
),
|
|
114
|
+
limit: int | None = typer.Option(None, "--limit", min=1, max=1000, help="Maksymalna liczba node'ów, 1-1000 (domyślnie 200 po stronie serwera)."),
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Pobierz graf albo podgraf projektu (node'y i krawędzie)."""
|
|
117
|
+
client, slug = _common.open_project(ctx)
|
|
118
|
+
params = _common.without_none({"node_type": node_type, "search": search, "limit": limit})
|
|
119
|
+
with client:
|
|
120
|
+
payload = client.get(_common.project_path(slug, "/graph/query"), params=params)
|
|
121
|
+
emit(ctx, _require_subgraph(payload))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@app.command("path")
|
|
125
|
+
def find_path(
|
|
126
|
+
ctx: typer.Context,
|
|
127
|
+
source_id: str = typer.Option(..., "--source-id", help="Identyfikator node'a początkowego."),
|
|
128
|
+
target_id: str = typer.Option(..., "--target-id", help="Identyfikator node'a docelowego."),
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Znajdź najkrótszą ścieżkę między dwoma node'ami (brak ścieżki daje puste listy)."""
|
|
131
|
+
client, slug = _common.open_project(ctx)
|
|
132
|
+
with client:
|
|
133
|
+
payload = client.get(_common.project_path(slug, "/graph/path"), params={"source_id": source_id, "target_id": target_id})
|
|
134
|
+
emit(ctx, _require_subgraph(payload))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.command("stats")
|
|
138
|
+
def graph_stats(ctx: typer.Context) -> None:
|
|
139
|
+
"""Pokaż statystyki grafu: liczba node'ów i krawędzi, także per typ."""
|
|
140
|
+
client, slug = _common.open_project(ctx)
|
|
141
|
+
with client:
|
|
142
|
+
payload = client.get(_common.project_path(slug, "/graph/stats"))
|
|
143
|
+
emit(ctx, _require_stats(payload))
|
|
@@ -8,7 +8,12 @@ i każda inna wartość użytkownika w ścieżce przechodzą przez `_common.path
|
|
|
8
8
|
priorytetów i dat idą do API bez walidacji po stronie CLI: `create`, `update` i `bulk-update`
|
|
9
9
|
odrzuca serwer (422 -> kod wyjścia 1), ale nieznany status lub priorytet w filtrze (`list`,
|
|
10
10
|
`search`) serwer po cichu ignoruje - filtr nie działa, a odpowiedź to sukces.
|
|
11
|
-
`list`, `search` zwracają jedną stronę w kopercie (`--page`); `
|
|
11
|
+
`list`, `search` zwracają jedną stronę w kopercie (`--page`); `list` waliduje, że każdy element
|
|
12
|
+
niesie listę `blocked_by` (table i csv pokazują ją jako klucze po przecinku, json i yaml w pełni),
|
|
13
|
+
a `get` waliduje obecność `spec_page_id`; brak pola to błąd kontraktu (kod 1) przed renderem;
|
|
14
|
+
`list --compact` pomija opisy (parametr `compact` API, bez zmiany reszty odpowiedzi), a `create --brief` i
|
|
15
|
+
`update --brief` wypisują tylko `id`, `key`, `title`, `status`, `sprint_id`, `blocked_by` i `blocks` (filtr po
|
|
16
|
+
stronie klienta, table i csv pokazują relacje jako klucze po przecinku); odpowiedź bez tych pól to błąd kontraktu; `comment list`, `label list`
|
|
12
17
|
i `ac list` pobierają wszystkie strony (`_common.fetch_all`) i wypisują płaską listę.
|
|
13
18
|
|
|
14
19
|
Tekst opisu i komentarza można podać flagą, przez `-` (stdin) albo z pliku
|
|
@@ -25,7 +30,8 @@ from typing import Annotated, Any
|
|
|
25
30
|
import typer
|
|
26
31
|
|
|
27
32
|
from monolynx_cli.commands import _common
|
|
28
|
-
from monolynx_cli.
|
|
33
|
+
from monolynx_cli.options import OutputFormat, get_options
|
|
34
|
+
from monolynx_cli.output import emit, resolve_format
|
|
29
35
|
|
|
30
36
|
app = typer.Typer(no_args_is_help=True)
|
|
31
37
|
comment_app = typer.Typer(no_args_is_help=True)
|
|
@@ -44,6 +50,17 @@ def _ticket_path(slug: str, ticket: str, suffix: str = "") -> str:
|
|
|
44
50
|
return _common.project_path(slug, f"/tickets/{_common.path_segment(ticket)}{suffix}")
|
|
45
51
|
|
|
46
52
|
|
|
53
|
+
def _blocker_keys(blockers: list[Any]) -> str:
|
|
54
|
+
return ", ".join(b["key"] for b in blockers)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _emit_brief(ctx: typer.Context, payload: Any) -> None:
|
|
58
|
+
brief = _common.require_brief_ticket(payload)
|
|
59
|
+
if resolve_format(get_options(ctx).output) in (OutputFormat.TABLE, OutputFormat.CSV):
|
|
60
|
+
brief = {**brief, "blocked_by": _blocker_keys(brief["blocked_by"]), "blocks": _blocker_keys(brief["blocks"])}
|
|
61
|
+
emit(ctx, brief)
|
|
62
|
+
|
|
63
|
+
|
|
47
64
|
@app.command("list")
|
|
48
65
|
def list_tickets(
|
|
49
66
|
ctx: typer.Context,
|
|
@@ -56,8 +73,9 @@ def list_tickets(
|
|
|
56
73
|
overdue: Annotated[bool, typer.Option("--overdue", help="Tylko przeterminowane.")] = False,
|
|
57
74
|
label: Annotated[str | None, typer.Option("--label", help="ID etykiety.")] = None,
|
|
58
75
|
page: _PAGE_OPT = None,
|
|
76
|
+
compact: Annotated[bool, typer.Option("--compact", help="Lista bez opisów ticketów (pole description pominięte).")] = False,
|
|
59
77
|
) -> None:
|
|
60
|
-
"""Wypisz tickety projektu z filtrami."""
|
|
78
|
+
"""Wypisz tickety projektu z filtrami; każdy ticket niesie blokery (blocked_by), z --compact bez opisów."""
|
|
61
79
|
client, slug = _common.open_project(ctx)
|
|
62
80
|
params = _common.without_none(
|
|
63
81
|
{
|
|
@@ -69,11 +87,14 @@ def list_tickets(
|
|
|
69
87
|
"due_date_after": due_after,
|
|
70
88
|
"overdue": "true" if overdue else None,
|
|
71
89
|
"label_id": label,
|
|
90
|
+
"compact": "true" if compact else None,
|
|
72
91
|
"page": page,
|
|
73
92
|
}
|
|
74
93
|
)
|
|
75
94
|
with client:
|
|
76
|
-
payload = client.get(_common.project_path(slug, "/tickets"), params=params)
|
|
95
|
+
payload = _common.require_list_with_blockers(client.get(_common.project_path(slug, "/tickets"), params=params))
|
|
96
|
+
if resolve_format(get_options(ctx).output) in (OutputFormat.TABLE, OutputFormat.CSV):
|
|
97
|
+
payload = {**payload, "items": [{**item, "blocked_by": _blocker_keys(item["blocked_by"])} for item in payload["items"]]}
|
|
77
98
|
emit(ctx, payload)
|
|
78
99
|
|
|
79
100
|
|
|
@@ -110,10 +131,10 @@ def search_tickets(
|
|
|
110
131
|
|
|
111
132
|
@app.command("get")
|
|
112
133
|
def get_ticket(ctx: typer.Context, ticket: _TICKET_ARG) -> None:
|
|
113
|
-
"""Pokaż szczegóły ticketu."""
|
|
134
|
+
"""Pokaż szczegóły ticketu, w tym spec_page_id (null, gdy ticket nie ma strony specyfikacji)."""
|
|
114
135
|
client, slug = _common.open_project(ctx)
|
|
115
136
|
with client:
|
|
116
|
-
payload = client.get(_ticket_path(slug, ticket))
|
|
137
|
+
payload = _common.require_spec_page_id(client.get(_ticket_path(slug, ticket)))
|
|
117
138
|
emit(ctx, payload)
|
|
118
139
|
|
|
119
140
|
|
|
@@ -132,8 +153,9 @@ def create_ticket(
|
|
|
132
153
|
ac: Annotated[list[str] | None, typer.Option("--ac", help="Kryterium akceptacji; można powtarzać.")] = None,
|
|
133
154
|
spec_page: Annotated[str | None, typer.Option("--spec-page", help="ID strony wiki ze specyfikacją.")] = None,
|
|
134
155
|
blocked_by: Annotated[list[str] | None, typer.Option("--blocked-by", help="UUID ticketu blokującego; można powtarzać.")] = None,
|
|
156
|
+
brief: Annotated[bool, typer.Option("--brief", help="Wypisz tylko id, key, title, status, sprint_id, blocked_by i blocks.")] = False,
|
|
135
157
|
) -> None:
|
|
136
|
-
"""Utwórz ticket."""
|
|
158
|
+
"""Utwórz ticket; z --brief wypisuje tylko id, key, title, status, sprint_id i relacje."""
|
|
137
159
|
text = _common.read_text_option(description, description_file, option="--description", file_option="--description-file")
|
|
138
160
|
client, slug = _common.open_project(ctx)
|
|
139
161
|
body = _common.without_none(
|
|
@@ -153,6 +175,9 @@ def create_ticket(
|
|
|
153
175
|
)
|
|
154
176
|
with client:
|
|
155
177
|
payload = client.post(_common.project_path(slug, "/tickets"), json=body)
|
|
178
|
+
if brief:
|
|
179
|
+
_emit_brief(ctx, payload)
|
|
180
|
+
return
|
|
156
181
|
emit(ctx, payload)
|
|
157
182
|
|
|
158
183
|
|
|
@@ -172,8 +197,9 @@ def update_ticket(
|
|
|
172
197
|
label: Annotated[list[str] | None, typer.Option("--label", help="ID etykiety (zastępuje dotychczasowe); można powtarzać.")] = None,
|
|
173
198
|
blocked_by: Annotated[list[str] | None, typer.Option("--blocked-by", help="UUID ticketu blokującego (nadpisuje); można powtarzać.")] = None,
|
|
174
199
|
no_blockers: Annotated[bool, typer.Option("--no-blockers", help="Usuń wszystkie blokery.")] = False,
|
|
200
|
+
brief: Annotated[bool, typer.Option("--brief", help="Wypisz tylko id, key, title, status, sprint_id, blocked_by i blocks.")] = False,
|
|
175
201
|
) -> None:
|
|
176
|
-
"""Zaktualizuj ticket; do API trafiają tylko podane pola."""
|
|
202
|
+
"""Zaktualizuj ticket; do API trafiają tylko podane pola, z --brief wynik skrócony do pól identyfikujących i relacji."""
|
|
177
203
|
if blocked_by and no_blockers:
|
|
178
204
|
raise _common.usage_error("Podaj --blocked-by albo --no-blockers, nie oba naraz.")
|
|
179
205
|
text = _common.read_text_option(description, description_file, option="--description", file_option="--description-file")
|
|
@@ -196,6 +222,9 @@ def update_ticket(
|
|
|
196
222
|
client, slug = _common.open_project(ctx)
|
|
197
223
|
with client:
|
|
198
224
|
payload = client.patch(_ticket_path(slug, ticket), json=body)
|
|
225
|
+
if brief:
|
|
226
|
+
_emit_brief(ctx, payload)
|
|
227
|
+
return
|
|
199
228
|
emit(ctx, payload)
|
|
200
229
|
|
|
201
230
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Punkt wejścia CLI monolynx-cli: opcja --version, globalne flagi i szkielet 16 grup podkomend.
|
|
2
2
|
|
|
3
|
-
`auth`, `config`, `project`, `member`, `ticket`, `sprint` i `
|
|
3
|
+
`auth`, `config`, `project`, `member`, `ticket`, `sprint`, `wiki` i `graph` (tylko odczyt) mają realne komendy (moduły
|
|
4
4
|
monolynx_cli.commands.*). Pozostałe grupy są nadal puste -- kolejne tickety dodadzą do
|
|
5
5
|
nich podkomendy, ewentualnie wynosząc rozrośniętą grupę do własnego modułu. Poza grupami root
|
|
6
6
|
ma samodzielne komendy `completion <shell>` (skrypt uzupełniania na stdout, patrz
|
|
@@ -36,6 +36,7 @@ from monolynx_cli.client import DEFAULT_TIMEOUT, MonolynxError
|
|
|
36
36
|
from monolynx_cli.commands.auth import app as auth_app
|
|
37
37
|
from monolynx_cli.commands.completion import completion
|
|
38
38
|
from monolynx_cli.commands.config import app as config_app
|
|
39
|
+
from monolynx_cli.commands.graph import app as graph_app
|
|
39
40
|
from monolynx_cli.commands.member import app as member_app
|
|
40
41
|
from monolynx_cli.commands.project import app as project_app
|
|
41
42
|
from monolynx_cli.commands.sprint import app as sprint_app
|
|
@@ -127,7 +128,6 @@ issues_app = typer.Typer(no_args_is_help=True)
|
|
|
127
128
|
time_app = typer.Typer(no_args_is_help=True)
|
|
128
129
|
plan_app = typer.Typer(no_args_is_help=True)
|
|
129
130
|
rozliczenia_app = typer.Typer(no_args_is_help=True)
|
|
130
|
-
graph_app = typer.Typer(no_args_is_help=True)
|
|
131
131
|
pipeline_app = typer.Typer(no_args_is_help=True)
|
|
132
132
|
heartbeat_app = typer.Typer(no_args_is_help=True)
|
|
133
133
|
role_app = typer.Typer(no_args_is_help=True)
|