bjira 0.0.23 → 0.0.25
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/README.md +8 -64
- package/docs/advanced.md +53 -0
- package/docs/commands/attachment.md +19 -0
- package/docs/commands/comment.md +12 -0
- package/docs/commands/create.md +22 -0
- package/docs/commands/field.md +14 -0
- package/docs/commands/init.md +14 -0
- package/docs/commands/issue.md +17 -0
- package/docs/commands/label.md +12 -0
- package/docs/commands/open.md +13 -0
- package/docs/commands/preset.md +22 -0
- package/docs/commands/project.md +10 -0
- package/docs/commands/query.md +24 -0
- package/docs/commands/run.md +22 -0
- package/docs/commands/set.md +14 -0
- package/docs/commands/sprint.md +13 -0
- package/docs/commands.md +19 -0
- package/docs/concepts.md +29 -0
- package/docs/configuration.md +38 -0
- package/docs/globals.md +9 -0
- package/docs/index.md +18 -0
- package/docs/installation.md +22 -0
- package/package.json +3 -1
- package/src/comment.js +1 -1
- package/src/create.js +43 -13
- package/src/errorhandler.js +2 -2
- package/src/index.js +2 -0
- package/src/init.js +1 -1
- package/src/issue.js +43 -6
- package/src/jira.js +2 -2
- package/src/open.js +58 -0
- package/src/query.js +12 -7
- package/src/run.js +1 -1
- package/src/set.js +21 -2
- package/src/sprint.js +2 -2
- package/src/table.js +66 -15
package/README.md
CHANGED
|
@@ -1,73 +1,17 @@
|
|
|
1
|
-
# Jira
|
|
1
|
+
# Jira CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A simple Jira CLI tool. License: MIT.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
|
|
8
|
+
npm install -g bjira
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Documentation
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
jira settings.
|
|
13
|
+
Full documentation is available in the [docs](./docs/index.md) folder.
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
$ bjira init
|
|
18
|
-
? Provide your jira host: your-domain.atlassian.net
|
|
19
|
-
? Please provide your jira username: username
|
|
20
|
-
? API token: [hidden]
|
|
21
|
-
? Enable HTTPS Protocol? Yes
|
|
22
|
-
Config file succesfully created in: /home/<username>/.bjira.json
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## How to use it
|
|
26
|
-
|
|
27
|
-
Run `bjira help` to see the main help menu. Each command is well documented.
|
|
28
|
-
|
|
29
|
-
There are 2 main concepts to know:
|
|
30
|
-
- presets
|
|
31
|
-
- custom fields.
|
|
32
|
-
|
|
33
|
-
### Presets
|
|
34
|
-
|
|
35
|
-
Let's say you want to retrieve all the open issues assigned to you for project
|
|
36
|
-
FOO. The query is something like this:
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
bjira query 'project = "FOO" AND status != "Done" AND status != "Cancelled" AND assignee = currentUser()'
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
You can save this query as a preset:
|
|
43
|
-
```
|
|
44
|
-
bjira create mine 'project = "FOO" AND status != "Done" AND status != "Cancelled" AND assignee = currentUser()'
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Then, you can run it using its query name:
|
|
48
|
-
```
|
|
49
|
-
bjira run mine
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
If you want to have parameters in your query, use `$$$` as placeholder. For instance:
|
|
53
|
-
```
|
|
54
|
-
bjira preset create search 'project = "FOO" AND text ~ "$$$" ORDER BY created DESC'
|
|
55
|
-
bjira run search -q "hello world"
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
### Custom fields
|
|
60
|
-
Jira is strongly configurable via custom fields. You can retrieve the list of custom fields using:
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
bjira field listall
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
If you want to see some of them in the issue report, add them for the project (FOO) and the issue type (Story):
|
|
67
|
-
|
|
68
|
-
```
|
|
69
|
-
bjira field add FOO Story "Story Points"
|
|
70
|
-
```
|
|
15
|
+
## License
|
|
71
16
|
|
|
72
|
-
|
|
73
|
-
You can also set custom fields using `bira set custom `Story Points' ISSUE-ID`.
|
|
17
|
+
MIT — see [LICENSE.md](./LICENSE.md).
|
package/docs/advanced.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
## Advanced Examples
|
|
2
|
+
|
|
3
|
+
### 1) Grouping results by parent (Epics/Subtasks)
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
# See epics with their child stories/tasks in a tree
|
|
7
|
+
bjira query -g "project = \"FOO\" ORDER BY updated DESC"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
### 2) Parameterized presets for quick search
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
bjira preset create text-search "project = \"FOO\" AND text ~ \"$$$\" ORDER BY created DESC"
|
|
14
|
+
bjira run text-search -q "payment gateway timeout"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 3) Managing custom fields
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
# Discover fields for a project and type
|
|
21
|
+
bjira field listall
|
|
22
|
+
|
|
23
|
+
# Track a field so it appears in show and is editable
|
|
24
|
+
bjira field add FOO Story "Story Points"
|
|
25
|
+
|
|
26
|
+
# Set it on an issue
|
|
27
|
+
bjira set custom 'Story Points' FOO-123
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 4) Linking work to Epics during creation
|
|
31
|
+
|
|
32
|
+
In `bjira create`, answer "yes" to set a parent epic, then pick an epic from the same project or search-as-you-type.
|
|
33
|
+
|
|
34
|
+
### 5) Sprints overview for your issues
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
# Select board and active/future sprint, then see your issues by status
|
|
38
|
+
bjira sprint show
|
|
39
|
+
|
|
40
|
+
# Show all users
|
|
41
|
+
bjira sprint show --all
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 6) Quick actions
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
# Assign to yourself
|
|
48
|
+
bjira set assignee --me FOO-123
|
|
49
|
+
|
|
50
|
+
# Open in browser
|
|
51
|
+
bjira open FOO-123
|
|
52
|
+
```
|
|
53
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## attachment
|
|
2
|
+
|
|
3
|
+
Work with attachments.
|
|
4
|
+
|
|
5
|
+
Subcommands:
|
|
6
|
+
|
|
7
|
+
- `attachment get <ISSUE-ID> <ATTACHMENT-ID>`: download the attachment to stdout.
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
# List attachments via show, then download by id
|
|
13
|
+
bjira show -a ISSUE-123 | less
|
|
14
|
+
bjira attachment get ISSUE-123 10001 > file.bin
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Notes:
|
|
18
|
+
- Upload and delete are not implemented yet.
|
|
19
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## create
|
|
2
|
+
|
|
3
|
+
Create a new issue via interactive prompts.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
bjira create
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Flow:
|
|
12
|
+
- Pick a project.
|
|
13
|
+
- Pick an issue type.
|
|
14
|
+
- Enter summary and optionally description (opens `$EDITOR`).
|
|
15
|
+
- Provide required custom fields (if any).
|
|
16
|
+
- Optionally set parent Epic (search-as-you-type, defaults to epics in project).
|
|
17
|
+
- After creation, optionally assign, set status, set configured custom fields, and add to a sprint.
|
|
18
|
+
|
|
19
|
+
Notes:
|
|
20
|
+
- Custom field prompts are limited to supported types (string, number, select/multiselect via `allowedValues`).
|
|
21
|
+
- Use `field add` to make custom fields visible and manageable in `show`/`set`.
|
|
22
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## field
|
|
2
|
+
|
|
3
|
+
Manage custom fields visibility and discovery.
|
|
4
|
+
|
|
5
|
+
Subcommands:
|
|
6
|
+
|
|
7
|
+
- `field listall`: show all projects, issue types, and available fields, marking supported ones.
|
|
8
|
+
- `field add <PROJECT> <ISSUE-TYPE> <FIELD-NAME>`: track a field so it appears in `show` and can be set with `set custom`.
|
|
9
|
+
- `field remove <PROJECT> <ISSUE-TYPE> <FIELD-NAME>`: stop tracking the field.
|
|
10
|
+
- `field list`: list tracked fields from your config.
|
|
11
|
+
|
|
12
|
+
Notes:
|
|
13
|
+
- Supported field types: `string`, `number`, and select-like fields exposing `allowedValues`.
|
|
14
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## init
|
|
2
|
+
|
|
3
|
+
Create the initial configuration file interactively.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
bjira init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Prompts: Jira host, protocol (HTTPS recommended), username, and API token.
|
|
12
|
+
|
|
13
|
+
The file is saved to the path specified by `-c, --config` (defaults to `~/.bjira.json`).
|
|
14
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## issue (show)
|
|
2
|
+
|
|
3
|
+
Display details for a single issue.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
bjira show [options] <ISSUE-ID>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
- `-a, --attachments`: include attachments details.
|
|
13
|
+
- `-C, --comments`: include comments.
|
|
14
|
+
- `-s, --subissues`: list sub-issues; for Epics, also lists child issues.
|
|
15
|
+
|
|
16
|
+
The output includes key metadata, custom fields you have added via `field add`, timestamps, and counts for attachments and comments.
|
|
17
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
## label
|
|
2
|
+
|
|
3
|
+
Add or remove labels on an issue.
|
|
4
|
+
|
|
5
|
+
Subcommands:
|
|
6
|
+
|
|
7
|
+
- `label add <ISSUE-ID>`: adds a label after interactive autocomplete. Prevents duplicates.
|
|
8
|
+
- `label remove <ISSUE-ID>`: removes one of the existing labels.
|
|
9
|
+
|
|
10
|
+
Notes:
|
|
11
|
+
- If the issue type does not support labels, the command exits with a message.
|
|
12
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## open
|
|
2
|
+
|
|
3
|
+
Open an issue in the default browser and print its URL.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
bjira open <ISSUE-ID>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Notes:
|
|
12
|
+
- Uses `open` (macOS), `xdg-open` (Linux), or `cmd /c start` (Windows). If automatic open fails, the URL is printed so you can copy/paste.
|
|
13
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## preset
|
|
2
|
+
|
|
3
|
+
Manage named JQL presets.
|
|
4
|
+
|
|
5
|
+
Subcommands:
|
|
6
|
+
|
|
7
|
+
- `preset create <NAME> <QUERY>`: create a new preset. Use `$$$` to mark positional parameters.
|
|
8
|
+
- `preset remove <NAME>`: delete a preset.
|
|
9
|
+
- `preset list`: list presets with their JQL.
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
# Save a reusable search for your work
|
|
15
|
+
bjira preset create mine "project = \"FOO\" AND statusCategory != Done AND assignee = currentUser()"
|
|
16
|
+
|
|
17
|
+
# Parameterized search (one placeholder)
|
|
18
|
+
bjira preset create search "project = \"FOO\" AND text ~ \"$$$\" ORDER BY created DESC"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Run presets using the `run` command.
|
|
22
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## query
|
|
2
|
+
|
|
3
|
+
Run an ad-hoc JQL query.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
bjira query [options] <JQL>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
- `-l, --limit <N>`: maximum number of issues to fetch (default 20; pagination continues until limit or last page).
|
|
13
|
+
- `-g, --grouped`: group results by parent (Epic or Parent) and render a tree.
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
# My open issues in project FOO
|
|
19
|
+
bjira query "project = \"FOO\" AND statusCategory != Done AND assignee = currentUser()"
|
|
20
|
+
|
|
21
|
+
# Group by parent
|
|
22
|
+
bjira query -g "project = \"FOO\" ORDER BY updated DESC"
|
|
23
|
+
```
|
|
24
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## run
|
|
2
|
+
|
|
3
|
+
Run a saved preset.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
bjira run [options] <NAME>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
- `-q, --query <arg...>`: supply positional arguments for `$$$` placeholders in the preset JQL. Repeat for multiple placeholders in order.
|
|
13
|
+
- `-l, --limit <N>`: maximum number of issues to fetch (default 20).
|
|
14
|
+
- `-g, --grouped`: group by parent and render a tree.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
bjira run mine
|
|
20
|
+
bjira run search -q "login bug"
|
|
21
|
+
```
|
|
22
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## set
|
|
2
|
+
|
|
3
|
+
Update fields and transitions of issues.
|
|
4
|
+
|
|
5
|
+
Subcommands:
|
|
6
|
+
|
|
7
|
+
- `set assignee [--me] <ISSUE-ID>`: assign to a user. With `--me`, assign to the current user without a prompt.
|
|
8
|
+
- `set unassign <ISSUE-ID>`: clear the assignee.
|
|
9
|
+
- `set status <ISSUE-ID>`: change status via available transitions; required transition fields are prompted if supported.
|
|
10
|
+
- `set custom <FIELD-NAME> <ISSUE-ID>`: set a tracked custom field (see `field add`).
|
|
11
|
+
|
|
12
|
+
Notes:
|
|
13
|
+
- Supported field types for `set custom`: string, number, select/multi-select fields exposing `allowedValues`.
|
|
14
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## sprint
|
|
2
|
+
|
|
3
|
+
Work with Agile sprints (boards).
|
|
4
|
+
|
|
5
|
+
Subcommands:
|
|
6
|
+
|
|
7
|
+
- `sprint add <ISSUE-ID>`: add the issue to an active or future sprint (you pick board and sprint if multiple).
|
|
8
|
+
- `sprint remove <ISSUE-ID>`: move the issue back to the backlog.
|
|
9
|
+
- `sprint show [-a|--all]`: show sprint issues grouped by assignee and status columns. By default only your issues are shown; with `--all` show all users.
|
|
10
|
+
|
|
11
|
+
Notes:
|
|
12
|
+
- This uses Jira Agile (board/sprint) APIs; ensure your user has permissions.
|
|
13
|
+
|
package/docs/commands.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## Commands Overview
|
|
2
|
+
|
|
3
|
+
Per-command guides:
|
|
4
|
+
|
|
5
|
+
- [attachment](commands/attachment.md)
|
|
6
|
+
- [comment](commands/comment.md)
|
|
7
|
+
- [create](commands/create.md)
|
|
8
|
+
- [field](commands/field.md)
|
|
9
|
+
- [init](commands/init.md)
|
|
10
|
+
- [issue (show)](commands/issue.md)
|
|
11
|
+
- [label](commands/label.md)
|
|
12
|
+
- [open](commands/open.md)
|
|
13
|
+
- [preset](commands/preset.md)
|
|
14
|
+
- [project](commands/project.md)
|
|
15
|
+
- [query](commands/query.md)
|
|
16
|
+
- [run](commands/run.md)
|
|
17
|
+
- [set](commands/set.md)
|
|
18
|
+
- [sprint](commands/sprint.md)
|
|
19
|
+
|
package/docs/concepts.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
## Concepts
|
|
2
|
+
|
|
3
|
+
### Presets
|
|
4
|
+
|
|
5
|
+
Presets are named JQL queries saved in your config file and executed via `bjira run`.
|
|
6
|
+
|
|
7
|
+
- Use `$$$` as a positional placeholder; supply values with `-q` in the same order.
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
bjira preset create mine "project = \"FOO\" AND statusCategory != Done AND assignee = currentUser()"
|
|
13
|
+
bjira preset create search "project = \"FOO\" AND text ~ \"$$$\" ORDER BY created DESC"
|
|
14
|
+
bjira run mine
|
|
15
|
+
bjira run search -q "security vulnerability"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Custom Fields
|
|
19
|
+
|
|
20
|
+
Jira projects often define custom fields (e.g., Story Points). To surface and manage them in bjira:
|
|
21
|
+
|
|
22
|
+
1. Discover supported fields using `bjira field listall`.
|
|
23
|
+
2. Add fields to your config using `bjira field add <PROJECT> <ISSUE-TYPE> <FIELD-NAME>`.
|
|
24
|
+
3. View these fields in `bjira show <ISSUE-ID>`.
|
|
25
|
+
4. Update them using `bjira set custom '<FIELD-NAME>' <ISSUE-ID>`.
|
|
26
|
+
|
|
27
|
+
Supported field types:
|
|
28
|
+
- `string`, `number`, and select/multi-select fields exposing `allowedValues`.
|
|
29
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## Configuration
|
|
2
|
+
|
|
3
|
+
Run the interactive setup to create your config file:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
bjira init
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
You will be prompted for Jira host, username, and API token. By default the configuration is stored in `~/.bjira.json`. You can change the location using the global `-c, --config <file>` option in any command.
|
|
10
|
+
|
|
11
|
+
Example config file:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"jira": {
|
|
16
|
+
"host": "your-domain.atlassian.net",
|
|
17
|
+
"protocol": "https",
|
|
18
|
+
"username": "you@example.com",
|
|
19
|
+
"password": "<api token>",
|
|
20
|
+
"apiVersion": 3,
|
|
21
|
+
"strictSSL": true
|
|
22
|
+
},
|
|
23
|
+
"presets": {
|
|
24
|
+
"mine": "project = \"FOO\" AND statusCategory != Done AND assignee = currentUser()",
|
|
25
|
+
"search": "project = \"FOO\" AND text ~ \"$$$\" ORDER BY created DESC"
|
|
26
|
+
},
|
|
27
|
+
"fields": [
|
|
28
|
+
{ "projectName": "FOO", "issueTypeName": "Story", "fieldName": "Story Points" }
|
|
29
|
+
],
|
|
30
|
+
"latestProject": "FOO"
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Notes:
|
|
35
|
+
- `presets` hold reusable JQL, with `$$$` placeholders for positional parameters.
|
|
36
|
+
- `fields` config controls which custom fields are shown in `show` output and usable via `set custom`.
|
|
37
|
+
- `latestProject` is managed automatically to prioritize project picks.
|
|
38
|
+
|
package/docs/globals.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## Global Options
|
|
2
|
+
|
|
3
|
+
- `-c, --config <file>`: path to the configuration file (defaults to `~/.bjira.json`).
|
|
4
|
+
- `--version`: print version.
|
|
5
|
+
- `help`: per-command help.
|
|
6
|
+
|
|
7
|
+
Environment variables:
|
|
8
|
+
- `EDITOR`: editor binary used to write descriptions and comments in interactive flows.
|
|
9
|
+
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# bjira — Documentation
|
|
2
|
+
|
|
3
|
+
This documentation covers installation, configuration, and usage of the `bjira` CLI to interact with Jira.
|
|
4
|
+
|
|
5
|
+
- Quick start: see `installation.md` and `configuration.md`.
|
|
6
|
+
- Concepts: `concepts.md` (presets and custom fields).
|
|
7
|
+
- Global options: `globals.md`.
|
|
8
|
+
- Commands: index in `commands.md` with links to per-command guides.
|
|
9
|
+
- Advanced usage: `advanced.md`.
|
|
10
|
+
|
|
11
|
+
Index:
|
|
12
|
+
|
|
13
|
+
- [Installation](installation.md)
|
|
14
|
+
- [Configuration](configuration.md)
|
|
15
|
+
- [Global Options](globals.md)
|
|
16
|
+
- [Concepts (Presets, Custom Fields)](concepts.md)
|
|
17
|
+
- [Commands](commands.md)
|
|
18
|
+
- [Advanced Examples](advanced.md)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## Installation
|
|
2
|
+
|
|
3
|
+
- Requirements: Node.js >= 13.2.0, Jira Cloud account with API token.
|
|
4
|
+
- Install globally:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
npm install -g bjira
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- Verify:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
bjira --version
|
|
14
|
+
bjira help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If you use commands that open an editor (e.g., `comment`, description during `create`), set the `EDITOR` environment variable, for example:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
export EDITOR=vim
|
|
21
|
+
```
|
|
22
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bjira",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "A simple jira CLI tool",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"author": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"engineStrict": true,
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"chalk": "^5.3.0",
|
|
27
28
|
"commander": "^8.2.0",
|
|
28
29
|
"execa": "^5.1.1",
|
|
29
30
|
"fuzzy": "^0.1.3",
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
"inquirer-checkbox-plus-prompt": "^1.0.1",
|
|
33
34
|
"jira-client": "^6.23.0",
|
|
34
35
|
"ora": "^6.0.1",
|
|
36
|
+
"string-width": "^5.1.2",
|
|
35
37
|
"temp": "^0.9.4"
|
|
36
38
|
}
|
|
37
39
|
}
|
package/src/comment.js
CHANGED
package/src/create.js
CHANGED
|
@@ -58,8 +58,7 @@ class Create extends Command {
|
|
|
58
58
|
.issuetypes.find(i => i.name === issueType.name).fields;
|
|
59
59
|
const requiredFields = Object.keys(issueFields).filter(
|
|
60
60
|
key => issueFields[key].required &&
|
|
61
|
-
Field.isSupported(issueFields[key]) &&
|
|
62
|
-
!["issuetype", "summary", "description", "project"].includes(key)).map(key => issueFields[key]);
|
|
61
|
+
Field.isSupported(issueFields[key]) && ["issuetype", "summary", "description", "project"].includes(key) === false).map(key => issueFields[key]);
|
|
63
62
|
|
|
64
63
|
for (const field of requiredFields) {
|
|
65
64
|
const fieldData = await Field.askFieldIfSupported(field);
|
|
@@ -68,18 +67,49 @@ class Create extends Command {
|
|
|
68
67
|
|
|
69
68
|
if (issueType.name !== 'Epic' &&
|
|
70
69
|
await Ask.askBoolean('Do you want to set a parent epic?')) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
const parentKey = await Ask.askCallback('Choose the epic:', async (input) => {
|
|
71
|
+
// Default suggestions: epics in the same project, most recently updated, not done.
|
|
72
|
+
if (!input) {
|
|
73
|
+
const jql = `project = \"${project.key}\" AND issuetype = \"Epic\" AND statusCategory != Done ORDER BY updated DESC`;
|
|
74
|
+
const result = await jira.apiRequest('/search/jql', {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
followAllRedirects: true,
|
|
77
|
+
body: {
|
|
78
|
+
jql,
|
|
79
|
+
fields: ['summary', 'key'],
|
|
80
|
+
maxResults: 20
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!result.issues) return [];
|
|
85
|
+
return result.issues.map(epic => ({
|
|
86
|
+
name: `${epic.key} - ${epic.fields.summary}`,
|
|
81
87
|
value: epic.key
|
|
82
|
-
}))
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// As-you-type search: allow partial matches via wildcard.
|
|
92
|
+
const inputWithWildcard = `${input}*`;
|
|
93
|
+
// Search epics by key or summary across projects, matching prefixes.
|
|
94
|
+
const jql = `issuetype = \"Epic\" AND (summary ~ \"${inputWithWildcard}\" OR key ~ \"${inputWithWildcard}\") ORDER BY updated DESC`;
|
|
95
|
+
const result = await jira.apiRequest('/search/jql', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
followAllRedirects: true,
|
|
98
|
+
body: {
|
|
99
|
+
jql,
|
|
100
|
+
fields: ['summary', 'key'],
|
|
101
|
+
maxResults: 20
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!result.issues) return [];
|
|
106
|
+
return result.issues.map(epic => ({
|
|
107
|
+
name: `${epic.key} - ${epic.fields.summary}`,
|
|
108
|
+
value: epic.key
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (parentKey) {
|
|
83
113
|
newIssue.fields.parent = {
|
|
84
114
|
key: parentKey
|
|
85
115
|
};
|
package/src/errorhandler.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import Table from './table.js';
|
|
6
6
|
|
|
7
7
|
class ErrorHandler {
|
|
8
|
-
static showError(
|
|
8
|
+
static showError(e) {
|
|
9
9
|
const table = new Table({
|
|
10
10
|
head: ['Errors']
|
|
11
11
|
});
|
|
@@ -35,7 +35,7 @@ class ErrorHandler {
|
|
|
35
35
|
console.log(table.toString());
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
static showWarningMessages(
|
|
38
|
+
static showWarningMessages(messages) {
|
|
39
39
|
const table = new Table({
|
|
40
40
|
head: ['Warnings']
|
|
41
41
|
});
|
package/src/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import Query from './query.js';
|
|
|
22
22
|
import Run from './run.js';
|
|
23
23
|
import Set from './set.js';
|
|
24
24
|
import Sprint from './sprint.js';
|
|
25
|
+
import Open from './open.js';
|
|
25
26
|
|
|
26
27
|
const DEFAULT_CONFIG_FILE = path.join(os.homedir(), ".bjira.json")
|
|
27
28
|
|
|
@@ -39,6 +40,7 @@ const commands = [
|
|
|
39
40
|
new Run(),
|
|
40
41
|
new Set(),
|
|
41
42
|
new Sprint(),
|
|
43
|
+
new Open(),
|
|
42
44
|
];
|
|
43
45
|
|
|
44
46
|
const pjson = JSON.parse(fs.readFileSync(new URL("../package.json",
|
package/src/init.js
CHANGED
|
@@ -21,7 +21,7 @@ class Init extends Command {
|
|
|
21
21
|
protocol: await Ask.askBoolean('Enable HTTPS Protocol?') ? 'https' : 'http',
|
|
22
22
|
username: (await Ask.askString('Please provide your jira username:')).trim(),
|
|
23
23
|
password: (await Ask.askPassword('API token:')).trim(),
|
|
24
|
-
apiVersion:
|
|
24
|
+
apiVersion: 3,
|
|
25
25
|
strictSSL: true,
|
|
26
26
|
},
|
|
27
27
|
presets: {},
|
package/src/issue.js
CHANGED
|
@@ -21,7 +21,7 @@ class Issue extends Command {
|
|
|
21
21
|
.description('Show an issue')
|
|
22
22
|
.option('-a, --attachments', 'Show the attachments too')
|
|
23
23
|
.option('-C, --comments', 'Show the comments too')
|
|
24
|
-
.option('-s, --subissues', 'Show the
|
|
24
|
+
.option('-s, --subissues', 'Show the subissues too')
|
|
25
25
|
.argument('<id>', 'The issue ID')
|
|
26
26
|
.action(async id => {
|
|
27
27
|
const jira = new Jira(program);
|
|
@@ -77,6 +77,12 @@ class Issue extends Command {
|
|
|
77
77
|
[
|
|
78
78
|
'Labels', issue.fields['Labels'].join(', ')
|
|
79
79
|
],
|
|
80
|
+
[
|
|
81
|
+
'Parent', {
|
|
82
|
+
color: "yellow",
|
|
83
|
+
text: issue.fields['Parent']?.key
|
|
84
|
+
}
|
|
85
|
+
],
|
|
80
86
|
[
|
|
81
87
|
'Sprint', {
|
|
82
88
|
color: "yellow",
|
|
@@ -182,7 +188,7 @@ class Issue extends Command {
|
|
|
182
188
|
'Updated on', comment['Updated']
|
|
183
189
|
],
|
|
184
190
|
[
|
|
185
|
-
'Body', comment.body
|
|
191
|
+
'Body', Issue.showComment(comment.body)
|
|
186
192
|
],
|
|
187
193
|
]);
|
|
188
194
|
});
|
|
@@ -192,13 +198,13 @@ class Issue extends Command {
|
|
|
192
198
|
|
|
193
199
|
if (cmd.opts().subissues) {
|
|
194
200
|
console.log("\nSub-issues:");
|
|
195
|
-
const children = await Query.runQuery(jira, `parent = "${id}"
|
|
196
|
-
await Query.showIssues(jira, children.issues, children.
|
|
201
|
+
const children = await Query.runQuery(jira, `parent = "${id}"`);
|
|
202
|
+
await Query.showIssues(jira, children.issues, children.isLast, resultFields, false);
|
|
197
203
|
|
|
198
204
|
if (issue.fields['Issue Type'].name === 'Epic') {
|
|
199
205
|
console.log("\nEpic issues:");
|
|
200
206
|
const children = await jira.spin('Fetching child issues...', jira.api.getIssuesForEpic(id));
|
|
201
|
-
await Query.showIssues(jira, children.issues, children.
|
|
207
|
+
await Query.showIssues(jira, children.issues, children.isLast, resultFields, false);
|
|
202
208
|
}
|
|
203
209
|
}
|
|
204
210
|
|
|
@@ -208,7 +214,7 @@ class Issue extends Command {
|
|
|
208
214
|
static replaceFields(obj, fields) {
|
|
209
215
|
if (Array.isArray(obj)) {
|
|
210
216
|
obj.forEach((o, pos) => {
|
|
211
|
-
obj[
|
|
217
|
+
obj[pos] = Issue.replaceFields(o, fields);
|
|
212
218
|
});
|
|
213
219
|
} else if (obj && typeof obj === "object") {
|
|
214
220
|
Object.keys(obj).forEach(key => {
|
|
@@ -237,6 +243,37 @@ class Issue extends Command {
|
|
|
237
243
|
return str;
|
|
238
244
|
}
|
|
239
245
|
|
|
246
|
+
static showComment(obj) {
|
|
247
|
+
if (typeof obj === "string") return obj;
|
|
248
|
+
|
|
249
|
+
switch (obj.type) {
|
|
250
|
+
case 'doc':
|
|
251
|
+
case 'paragraph':
|
|
252
|
+
return obj.content.map(a => Issue.showComment(a)).join("");
|
|
253
|
+
|
|
254
|
+
case 'text':
|
|
255
|
+
return obj.text;
|
|
256
|
+
|
|
257
|
+
case 'inlineCard':
|
|
258
|
+
return obj.attrs.url;
|
|
259
|
+
|
|
260
|
+
case 'status':
|
|
261
|
+
case 'mention':
|
|
262
|
+
case 'emoji':
|
|
263
|
+
return obj.attrs.text;
|
|
264
|
+
|
|
265
|
+
case 'hardBreak':
|
|
266
|
+
return '\n';
|
|
267
|
+
|
|
268
|
+
case 'date':
|
|
269
|
+
const date = new Date(obj.attrs.timestamp * 1000);
|
|
270
|
+
return date.toLocaleString();
|
|
271
|
+
|
|
272
|
+
default:
|
|
273
|
+
return '';
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
240
277
|
showEpicIssue(issue) {
|
|
241
278
|
if (!issue) return "";
|
|
242
279
|
return `${issue.key} (${issue.fields['Summary'].trim()})`;
|
package/src/jira.js
CHANGED
|
@@ -15,7 +15,7 @@ class Jira {
|
|
|
15
15
|
|
|
16
16
|
if (!fs.existsSync(this.configFile)) {
|
|
17
17
|
console.log(`Config file ${this.configFile} does not exist.`);
|
|
18
|
-
process.exit();
|
|
18
|
+
process.exit(1);
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -100,7 +100,7 @@ class Jira {
|
|
|
100
100
|
return result;
|
|
101
101
|
} catch (e) {
|
|
102
102
|
spinner.stop();
|
|
103
|
-
ErrorHandler.showError(
|
|
103
|
+
ErrorHandler.showError(e);
|
|
104
104
|
process.exit(1);
|
|
105
105
|
}
|
|
106
106
|
}
|
package/src/open.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import execa from 'execa';
|
|
6
|
+
import color from 'chalk';
|
|
7
|
+
|
|
8
|
+
import Command from './command.js';
|
|
9
|
+
import Jira from './jira.js';
|
|
10
|
+
import Issue from './issue.js';
|
|
11
|
+
|
|
12
|
+
class Open extends Command {
|
|
13
|
+
addOptions(program) {
|
|
14
|
+
program.command('open')
|
|
15
|
+
.description('Open an issue in the default browser')
|
|
16
|
+
.argument('<id>', 'The issue ID')
|
|
17
|
+
.action(async id => {
|
|
18
|
+
const jira = new Jira(program);
|
|
19
|
+
const url = Issue.url(jira, id);
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
cmd,
|
|
23
|
+
args
|
|
24
|
+
} = this._browserOpener(url);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const subprocess = execa(cmd, args, {
|
|
28
|
+
detached: true,
|
|
29
|
+
stdio: 'ignore'
|
|
30
|
+
});
|
|
31
|
+
subprocess.unref?.();
|
|
32
|
+
console.log(color.blue(url));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.log('Unable to open the browser automatically. Open this URL:');
|
|
35
|
+
console.log(color.blue(url));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_browserOpener(url) {
|
|
41
|
+
switch (process.platform) {
|
|
42
|
+
case 'darwin':
|
|
43
|
+
return {
|
|
44
|
+
cmd: 'open', args: [url]
|
|
45
|
+
};
|
|
46
|
+
case 'win32':
|
|
47
|
+
return {
|
|
48
|
+
cmd: 'cmd', args: ['/c', 'start', '', url]
|
|
49
|
+
};
|
|
50
|
+
default:
|
|
51
|
+
return {
|
|
52
|
+
cmd: 'xdg-open', args: [url]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default Open;
|
package/src/query.js
CHANGED
|
@@ -29,12 +29,12 @@ class Query extends Command {
|
|
|
29
29
|
const resultFields = await Field.listFields(jira);
|
|
30
30
|
const result = await Query.runQuery(jira, query, opts.limit);
|
|
31
31
|
|
|
32
|
-
await Query.showIssues(jira, result.issues, result.
|
|
32
|
+
await Query.showIssues(jira, result.issues, result.isLast, resultFields, opts.grouped);
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
static async showIssues(jira, issues,
|
|
37
|
-
console.log(`Showing ${color.bold(issues.length)} issues of ${color.bold(
|
|
36
|
+
static async showIssues(jira, issues, isLast, fields, grouped) {
|
|
37
|
+
console.log(`Showing ${color.bold(issues.length)} issues of ${isLast ? color.bold(issues.length) : "more"}`);
|
|
38
38
|
|
|
39
39
|
issues = issues.map(issue => Issue.replaceFields(issue, fields)).map(issue => ({
|
|
40
40
|
children: [],
|
|
@@ -150,6 +150,7 @@ class Query extends Command {
|
|
|
150
150
|
static async runQuery(jira, query, expectedResult) {
|
|
151
151
|
let issues = [];
|
|
152
152
|
let nextPageToken = null;
|
|
153
|
+
let isLast = true;
|
|
153
154
|
while (issues.length < (expectedResult === undefined ? issues.length + 1 : expectedResult)) {
|
|
154
155
|
const result = await jira.spin('Running query...',
|
|
155
156
|
jira.apiRequest('/search/jql', {
|
|
@@ -159,23 +160,27 @@ class Query extends Command {
|
|
|
159
160
|
jql: query,
|
|
160
161
|
nextPageToken,
|
|
161
162
|
fields: ['*all'],
|
|
162
|
-
maxResults:
|
|
163
|
+
maxResults: 20,
|
|
163
164
|
}
|
|
164
165
|
}));
|
|
165
166
|
|
|
166
167
|
if (result.warningMessages) {
|
|
167
168
|
ErrorHandler.showWarningMessages(result.warningMessages);
|
|
168
|
-
return
|
|
169
|
+
return {
|
|
170
|
+
isLast: true,
|
|
171
|
+
issues: []
|
|
172
|
+
};
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
issues = issues.concat(result.issues);
|
|
172
176
|
nextPageToken = result.nextPageToken;
|
|
177
|
+
isLast = result.isLast;
|
|
173
178
|
|
|
174
|
-
if (
|
|
179
|
+
if (isLast) break;
|
|
175
180
|
}
|
|
176
181
|
|
|
177
182
|
return {
|
|
178
|
-
|
|
183
|
+
isLast,
|
|
179
184
|
issues
|
|
180
185
|
};
|
|
181
186
|
}
|
package/src/run.js
CHANGED
|
@@ -47,7 +47,7 @@ class Run extends Command {
|
|
|
47
47
|
const resultFields = await Field.listFields(jira);
|
|
48
48
|
const result = await Query.runQuery(jira, query, opts.limit);
|
|
49
49
|
|
|
50
|
-
await Query.showIssues(jira, result.issues, result.
|
|
50
|
+
await Query.showIssues(jira, result.issues, result.isLast, resultFields, opts.grouped);
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
|
package/src/set.js
CHANGED
|
@@ -13,12 +13,18 @@ class Set extends Command {
|
|
|
13
13
|
addOptions(program) {
|
|
14
14
|
const setCmd = program.command('set')
|
|
15
15
|
.description('Update fields in an issue');
|
|
16
|
-
setCmd.command('assignee')
|
|
16
|
+
const assigneeCmd = setCmd.command('assignee')
|
|
17
17
|
.description('Assign the issue to somebody')
|
|
18
|
+
.option('--me', 'Assign the issue to the current user')
|
|
18
19
|
.argument('<ID>', 'The issue ID')
|
|
19
20
|
.action(async id => {
|
|
20
21
|
const jira = new Jira(program);
|
|
21
|
-
|
|
22
|
+
const opts = assigneeCmd.opts();
|
|
23
|
+
if (opts.me) {
|
|
24
|
+
await Set.assignToMe(jira, id);
|
|
25
|
+
} else {
|
|
26
|
+
await Set.assignIssue(jira, id);
|
|
27
|
+
}
|
|
22
28
|
});
|
|
23
29
|
|
|
24
30
|
setCmd.command('unassign')
|
|
@@ -90,6 +96,19 @@ class Set extends Command {
|
|
|
90
96
|
await jira.spin(`Updating issue ${id}...`, jira.api.updateIssue(id, issue));
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
static async assignToMe(jira, id) {
|
|
100
|
+
const me = await jira.spin('Retrieving current user...', jira.api.getCurrentUser());
|
|
101
|
+
const issue = {
|
|
102
|
+
fields: {
|
|
103
|
+
assignee: {
|
|
104
|
+
accountId: me.accountId
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
await jira.spin(`Updating issue ${id}...`, jira.api.updateIssue(id, issue));
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
static async setCustomField(jira, customField, id, defaultValue = null) {
|
|
94
113
|
const field = await Field.fetchAndAskFieldIfSupported(jira, customField, defaultValue);
|
|
95
114
|
if (field === null) {
|
package/src/sprint.js
CHANGED
|
@@ -110,7 +110,7 @@ class Sprint extends Command {
|
|
|
110
110
|
if (showCmd.opts().all) {
|
|
111
111
|
filteredUsers = User.sortUsers(currentUser, users);
|
|
112
112
|
} else {
|
|
113
|
-
filteredUsers =
|
|
113
|
+
filteredUsers = users.filter(user => user.accountId === currentUser.accountId);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
filteredUsers.forEach(user => {
|
|
@@ -131,7 +131,7 @@ class Sprint extends Command {
|
|
|
131
131
|
for (let i = 0; i < maxIssues; ++i) {
|
|
132
132
|
const line = statuses.map(status => {
|
|
133
133
|
if (status.issues.length > i) {
|
|
134
|
-
return `${status.issues[i].key} ${status.issues[i].fields['Summary'].trim()}`
|
|
134
|
+
return `${color.yellow(status.issues[i].key)} ${status.issues[i].fields['Summary'].trim()}`
|
|
135
135
|
}
|
|
136
136
|
return ""
|
|
137
137
|
});
|
package/src/table.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MIT
|
|
4
4
|
|
|
5
5
|
import color from 'chalk';
|
|
6
|
+
import stringWidth from 'string-width';
|
|
6
7
|
|
|
7
8
|
class Table {
|
|
8
9
|
constructor(options) {
|
|
@@ -25,11 +26,15 @@ class Table {
|
|
|
25
26
|
if (typeof field !== "object") field = {
|
|
26
27
|
text: field
|
|
27
28
|
};
|
|
28
|
-
field.text = ("" + field.text).split("\n");
|
|
29
|
+
field.text = ("" + (field.text === undefined ? "" : field.text)).split("\n");
|
|
29
30
|
this._columns[pos].rows.push(field);
|
|
30
31
|
});
|
|
31
32
|
|
|
32
|
-
for (let i = row.length; i < this._columns.length; ++i)
|
|
33
|
+
for (let i = row.length; i < this._columns.length; ++i) {
|
|
34
|
+
this._columns[i].rows.push({
|
|
35
|
+
text: [""]
|
|
36
|
+
});
|
|
37
|
+
}
|
|
33
38
|
|
|
34
39
|
++this._rows;
|
|
35
40
|
}
|
|
@@ -46,8 +51,9 @@ class Table {
|
|
|
46
51
|
this._columns.forEach(column => this._computeColumnWidth(column));
|
|
47
52
|
|
|
48
53
|
const totalWidth = this._columns.reduce((x, column) => x + column.width, this._columns.length - 1);
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
const availableColumns = (process.stdout && process.stdout.columns) ? process.stdout.columns : 120;
|
|
55
|
+
if (totalWidth > availableColumns) {
|
|
56
|
+
this._resizeWidthOf(totalWidth - availableColumns + (this._columns.length - 1));
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
const lines = [];
|
|
@@ -77,10 +83,10 @@ class Table {
|
|
|
77
83
|
|
|
78
84
|
_computeColumnWidth(column) {
|
|
79
85
|
column.width = Math.max(...column.rows.map(row =>
|
|
80
|
-
Math.max(...row.text.map(line => line
|
|
86
|
+
Math.max(...row.text.map(line => this._visibleLength(line)))
|
|
81
87
|
));
|
|
82
88
|
|
|
83
|
-
if (column.head) column.width = Math.max(column.width, column.head
|
|
89
|
+
if (column.head) column.width = Math.max(column.width, this._visibleLength(column.head));
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
_computeRowHeight(column, row) {
|
|
@@ -90,13 +96,13 @@ class Table {
|
|
|
90
96
|
_toWidth(str, width, spaces) {
|
|
91
97
|
str = str || "";
|
|
92
98
|
|
|
93
|
-
let strLength = str
|
|
99
|
+
let strLength = this._visibleLength(str);
|
|
94
100
|
if (strLength > width) {
|
|
95
|
-
return this.
|
|
101
|
+
return this._truncateVisible(str, width - 1) + "…";
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
if (spaces) {
|
|
99
|
-
|
|
105
|
+
while (this._visibleLength(str) < width) str += " ";
|
|
100
106
|
}
|
|
101
107
|
return str;
|
|
102
108
|
}
|
|
@@ -122,14 +128,14 @@ class Table {
|
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
_maybeSplitRow(row, width) {
|
|
125
|
-
if (row
|
|
131
|
+
if (this._visibleLength(row) <= width) return [row];
|
|
126
132
|
|
|
127
133
|
const rows = [];
|
|
128
134
|
let currentRow = "";
|
|
129
135
|
for (let part of row.split(" ")) {
|
|
130
136
|
if (currentRow.length === 0) {
|
|
131
137
|
currentRow = part;
|
|
132
|
-
} else if ((currentRow
|
|
138
|
+
} else if ((this._visibleLength(currentRow) + this._visibleLength(part) + 1) < width) {
|
|
133
139
|
currentRow += " " + part;
|
|
134
140
|
} else {
|
|
135
141
|
rows.push(currentRow);
|
|
@@ -146,9 +152,9 @@ class Table {
|
|
|
146
152
|
|
|
147
153
|
_resizeWidthOfOne() {
|
|
148
154
|
const max = Math.max(...this._columns.map(column => column.width));
|
|
149
|
-
for (let
|
|
150
|
-
const column = this._columns[
|
|
151
|
-
if (!this._unresizableColumns.includes(
|
|
155
|
+
for (let i = 0; i < this._columns.length; ++i) {
|
|
156
|
+
const column = this._columns[i];
|
|
157
|
+
if (!this._unresizableColumns.includes(i) && column.width === max) {
|
|
152
158
|
--column.width;
|
|
153
159
|
break;
|
|
154
160
|
}
|
|
@@ -156,7 +162,7 @@ class Table {
|
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
_truncate(str, width) {
|
|
159
|
-
return
|
|
165
|
+
return this._truncateVisible(str, width);
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
_stylize(row, text) {
|
|
@@ -174,6 +180,51 @@ class Table {
|
|
|
174
180
|
|
|
175
181
|
return color[row.color].apply(null, [text]);
|
|
176
182
|
}
|
|
183
|
+
|
|
184
|
+
// Returns the display width of a string (ANSI-aware, CJK/emoji aware).
|
|
185
|
+
_visibleLength(str) {
|
|
186
|
+
if (!str) return 0;
|
|
187
|
+
return stringWidth("" + str);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Truncate a string to a target visible width, preserving ANSI codes and
|
|
191
|
+
// ensuring styles are reset if we cut before a reset sequence.
|
|
192
|
+
_truncateVisible(str, width) {
|
|
193
|
+
if (width <= 0) return "";
|
|
194
|
+
const input = "" + (str || "");
|
|
195
|
+
const ansi = this._ansiRegex();
|
|
196
|
+
let out = "";
|
|
197
|
+
let i = 0;
|
|
198
|
+
while (i < input.length) {
|
|
199
|
+
const ch = input[i];
|
|
200
|
+
if (ch === "\u001b") {
|
|
201
|
+
const match = input.slice(i).match(ansi);
|
|
202
|
+
if (match && match.index === 0) {
|
|
203
|
+
out += match[0];
|
|
204
|
+
i += match[0].length;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
out += ch;
|
|
208
|
+
i++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const candidate = out + ch;
|
|
212
|
+
if (stringWidth(candidate) > width) break;
|
|
213
|
+
out = candidate;
|
|
214
|
+
i++;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// If we truncated before natural end, ensure we reset styles.
|
|
218
|
+
if (i < input.length) {
|
|
219
|
+
out += "\u001b[0m";
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
_ansiRegex() {
|
|
225
|
+
// eslint-disable-next-line no-control-regex
|
|
226
|
+
return /\u001B\[[0-?]*[ -\/]*[@-~]/g;
|
|
227
|
+
}
|
|
177
228
|
};
|
|
178
229
|
|
|
179
230
|
export default Table;
|