jira2cli 0.1.0__py3-none-any.whl

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.
jira2cli/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """CLI package for Jira AI integrations."""
2
+
3
+ from .cli import app, main
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["__version__", "app", "main"]
jira2cli/cli.py ADDED
@@ -0,0 +1,26 @@
1
+ """Typer application for jira2cli."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from .commands import register_commands
8
+
9
+ app = typer.Typer(
10
+ name="jira2cli",
11
+ help="Jira CLI powered by jira2ai-core.",
12
+ no_args_is_help=True,
13
+ add_completion=False,
14
+ )
15
+
16
+ register_commands(app)
17
+
18
+
19
+ @app.callback()
20
+ def callback() -> None:
21
+ """Jira CLI powered by jira2ai-core."""
22
+
23
+
24
+ def main() -> None:
25
+ """Run the jira2cli application."""
26
+ app()
@@ -0,0 +1,25 @@
1
+ """Command registration for jira2cli."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from .attachments import register_attachment_commands
8
+ from .links import register_link_commands
9
+ from .metadata import register_metadata_commands
10
+ from .read import register_read_commands
11
+ from .search import register_search_commands
12
+ from .write import register_write_commands
13
+
14
+
15
+ def register_commands(app: typer.Typer) -> None:
16
+ """Register jira2cli commands on the root Typer app."""
17
+ register_read_commands(app)
18
+ register_search_commands(app)
19
+ register_metadata_commands(app)
20
+ register_write_commands(app)
21
+ register_link_commands(app)
22
+ register_attachment_commands(app)
23
+
24
+
25
+ __all__ = ["register_commands"]
@@ -0,0 +1,45 @@
1
+ """Attachment-download jira2cli commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from jira2ai_core import client
7
+ from jira2ai_core.operations import attachments as attachment_operations
8
+
9
+ from jira2cli.output import raise_cli_exception
10
+
11
+
12
+ def attachment_command(
13
+ attachment_id: str = typer.Argument(..., help="Attachment ID (e.g. 63899)"),
14
+ output_path: str | None = typer.Option(
15
+ None,
16
+ "--output-path",
17
+ help=(
18
+ "Path to save the attachment. Can be a directory or a full file path. "
19
+ "Defaults to the current directory."
20
+ ),
21
+ ),
22
+ ) -> None:
23
+ """Download a Jira attachment by its ID."""
24
+ try:
25
+ attachment_operations.validate_attachment_id(attachment_id)
26
+ api = client.get_api()
27
+ plan = attachment_operations.plan_attachment_download(
28
+ attachment_id,
29
+ output_path=output_path,
30
+ api=api,
31
+ )
32
+ attachment_operations.download_attachment_content(plan, api=api)
33
+ output = attachment_operations.format_attachment_download_result(plan)
34
+ except Exception as exc:
35
+ raise_cli_exception(exc)
36
+
37
+ typer.echo(output)
38
+
39
+
40
+ def register_attachment_commands(app: typer.Typer) -> None:
41
+ """Register attachment-download commands."""
42
+ app.command("attachment")(attachment_command)
43
+
44
+
45
+ __all__ = ["attachment_command", "register_attachment_commands"]
@@ -0,0 +1,95 @@
1
+ """Link-management jira2cli commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from jira2ai_core import client
7
+ from jira2ai_core.operations import links
8
+
9
+ from jira2cli.output import (
10
+ raise_cli_exception,
11
+ render_operation_result,
12
+ validate_output_options,
13
+ )
14
+
15
+
16
+ def add_link_command(
17
+ link_type: str = typer.Argument(..., help="Link type name (e.g. Blocks, Clones)"),
18
+ outward_key: str = typer.Argument(..., help="Issue key on the outward side."),
19
+ inward_key: str = typer.Argument(..., help="Issue key on the inward side."),
20
+ raw_output: bool = typer.Option(
21
+ False,
22
+ "--raw",
23
+ help="Render the raw API payload as JSON.",
24
+ ),
25
+ json_output: bool = typer.Option(
26
+ False,
27
+ "--json",
28
+ help="Render structured output as JSON.",
29
+ ),
30
+ ) -> None:
31
+ """Create a link between two Jira issues."""
32
+ validate_output_options(json_output=json_output, raw_output=raw_output)
33
+
34
+ try:
35
+ api = client.get_api()
36
+ result = links.create_issue_link(
37
+ link_type,
38
+ outward_key,
39
+ inward_key,
40
+ api=api,
41
+ )
42
+ except Exception as exc:
43
+ raise_cli_exception(exc)
44
+
45
+ typer.echo(
46
+ render_operation_result(
47
+ result,
48
+ json_output=json_output,
49
+ raw_output=raw_output,
50
+ )
51
+ )
52
+
53
+
54
+ def delete_link_command(
55
+ link_id: str = typer.Argument(..., help="Issue link ID to delete."),
56
+ raw_output: bool = typer.Option(
57
+ False,
58
+ "--raw",
59
+ help="Render the raw API payload as JSON.",
60
+ ),
61
+ json_output: bool = typer.Option(
62
+ False,
63
+ "--json",
64
+ help="Render structured output as JSON.",
65
+ ),
66
+ ) -> None:
67
+ """Delete a Jira issue link by ID."""
68
+ validate_output_options(json_output=json_output, raw_output=raw_output)
69
+
70
+ try:
71
+ api = client.get_api()
72
+ result = links.delete_issue_link(link_id, api=api)
73
+ except Exception as exc:
74
+ raise_cli_exception(exc)
75
+
76
+ typer.echo(
77
+ render_operation_result(
78
+ result,
79
+ json_output=json_output,
80
+ raw_output=raw_output,
81
+ )
82
+ )
83
+
84
+
85
+ def register_link_commands(app: typer.Typer) -> None:
86
+ """Register link-management commands."""
87
+ app.command("add-link")(add_link_command)
88
+ app.command("delete-link")(delete_link_command)
89
+
90
+
91
+ __all__ = [
92
+ "add_link_command",
93
+ "delete_link_command",
94
+ "register_link_commands",
95
+ ]
@@ -0,0 +1,205 @@
1
+ """Metadata and reference jira2cli commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from jira2ai_core import client
7
+ from jira2ai_core.errors import Jira2AIValidationError
8
+ from jira2ai_core.jql import JQL_REFERENCE
9
+ from jira2ai_core.operations import fields as field_operations
10
+ from jira2ai_core.operations import links, projects, users
11
+
12
+ from jira2cli.output import (
13
+ raise_cli_exception,
14
+ render_operation_result,
15
+ validate_output_options,
16
+ )
17
+
18
+
19
+ def fields_command(
20
+ project_key: str | None = typer.Option(
21
+ None,
22
+ "--project-key",
23
+ help="Project key for issue type or create-field metadata.",
24
+ ),
25
+ issue_type: str | None = typer.Option(
26
+ None,
27
+ "--issue-type",
28
+ help="Issue type name used with --project-key for create fields.",
29
+ ),
30
+ issue_key: str | None = typer.Option(
31
+ None,
32
+ "--issue-key",
33
+ help="Existing issue key for edit-field metadata.",
34
+ ),
35
+ raw_output: bool = typer.Option(
36
+ False,
37
+ "--raw",
38
+ help="Render the raw API payload as JSON.",
39
+ ),
40
+ json_output: bool = typer.Option(
41
+ False,
42
+ "--json",
43
+ help="Render structured output as JSON.",
44
+ ),
45
+ ) -> None:
46
+ """Get field metadata for creating or editing Jira issues."""
47
+ validate_output_options(json_output=json_output, raw_output=raw_output)
48
+
49
+ try:
50
+ if issue_key:
51
+ api = client.get_api()
52
+ result = field_operations.get_edit_fields(issue_key, api=api)
53
+ else:
54
+ if not project_key:
55
+ raise Jira2AIValidationError(
56
+ "Provide either --project-key (to list issue types / create fields) "
57
+ "or --issue-key (to list edit fields)."
58
+ )
59
+
60
+ api = client.get_api()
61
+ if issue_type:
62
+ result = field_operations.get_create_fields(
63
+ project_key,
64
+ issue_type,
65
+ api=api,
66
+ )
67
+ else:
68
+ result = field_operations.list_issue_types(project_key, api=api)
69
+ except Exception as exc:
70
+ raise_cli_exception(exc)
71
+
72
+ typer.echo(
73
+ render_operation_result(
74
+ result,
75
+ json_output=json_output,
76
+ raw_output=raw_output,
77
+ )
78
+ )
79
+
80
+
81
+ def projects_command(
82
+ query: str | None = typer.Option(
83
+ None,
84
+ "--query",
85
+ help="Filter by project key or name.",
86
+ ),
87
+ raw_output: bool = typer.Option(
88
+ False,
89
+ "--raw",
90
+ help="Render the raw API payload as JSON.",
91
+ ),
92
+ json_output: bool = typer.Option(
93
+ False,
94
+ "--json",
95
+ help="Render structured output as JSON.",
96
+ ),
97
+ ) -> None:
98
+ """List Jira projects accessible to the current user."""
99
+ validate_output_options(json_output=json_output, raw_output=raw_output)
100
+
101
+ try:
102
+ api = client.get_api()
103
+ result = projects.list_projects(query, api=api)
104
+ except Exception as exc:
105
+ raise_cli_exception(exc)
106
+
107
+ typer.echo(
108
+ render_operation_result(
109
+ result,
110
+ json_output=json_output,
111
+ raw_output=raw_output,
112
+ )
113
+ )
114
+
115
+
116
+ def users_command(
117
+ query: str = typer.Argument(..., help="Search string for user name or email"),
118
+ max_results: int = typer.Option(
119
+ 10,
120
+ "--max-results",
121
+ min=1,
122
+ max=50,
123
+ help="Maximum users to return.",
124
+ ),
125
+ raw_output: bool = typer.Option(
126
+ False,
127
+ "--raw",
128
+ help="Render the raw API payload as JSON.",
129
+ ),
130
+ json_output: bool = typer.Option(
131
+ False,
132
+ "--json",
133
+ help="Render structured output as JSON.",
134
+ ),
135
+ ) -> None:
136
+ """Search Jira users by name or email."""
137
+ validate_output_options(json_output=json_output, raw_output=raw_output)
138
+
139
+ try:
140
+ api = client.get_api()
141
+ result = users.search_users(query, max_results=max_results, api=api)
142
+ except Exception as exc:
143
+ raise_cli_exception(exc)
144
+
145
+ typer.echo(
146
+ render_operation_result(
147
+ result,
148
+ json_output=json_output,
149
+ raw_output=raw_output,
150
+ )
151
+ )
152
+
153
+
154
+ def link_types_command(
155
+ raw_output: bool = typer.Option(
156
+ False,
157
+ "--raw",
158
+ help="Render the raw API payload as JSON.",
159
+ ),
160
+ json_output: bool = typer.Option(
161
+ False,
162
+ "--json",
163
+ help="Render structured output as JSON.",
164
+ ),
165
+ ) -> None:
166
+ """List available Jira issue link types."""
167
+ validate_output_options(json_output=json_output, raw_output=raw_output)
168
+
169
+ try:
170
+ api = client.get_api()
171
+ result = links.list_link_types(api=api)
172
+ except Exception as exc:
173
+ raise_cli_exception(exc)
174
+
175
+ typer.echo(
176
+ render_operation_result(
177
+ result,
178
+ json_output=json_output,
179
+ raw_output=raw_output,
180
+ )
181
+ )
182
+
183
+
184
+ def jql_syntax_command() -> None:
185
+ """Print the shared JQL syntax reference."""
186
+ typer.echo(JQL_REFERENCE)
187
+
188
+
189
+ def register_metadata_commands(app: typer.Typer) -> None:
190
+ """Register metadata and reference commands."""
191
+ app.command("fields")(fields_command)
192
+ app.command("projects")(projects_command)
193
+ app.command("users")(users_command)
194
+ app.command("link-types")(link_types_command)
195
+ app.command("jql-syntax")(jql_syntax_command)
196
+
197
+
198
+ __all__ = [
199
+ "fields_command",
200
+ "jql_syntax_command",
201
+ "link_types_command",
202
+ "projects_command",
203
+ "register_metadata_commands",
204
+ "users_command",
205
+ ]
@@ -0,0 +1,118 @@
1
+ """Read-oriented jira2cli commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ import typer
8
+ from jira2ai_core import client
9
+ from jira2ai_core.operations import comments, issues
10
+
11
+ from jira2cli.output import (
12
+ raise_cli_exception,
13
+ render_operation_result,
14
+ validate_output_options,
15
+ )
16
+
17
+
18
+ def read_command(
19
+ issue_key: str = typer.Argument(..., help="Issue key (e.g. PROJ-123)"),
20
+ extra_fields: list[str] | None = typer.Option(
21
+ None,
22
+ "--extra-field",
23
+ help=(
24
+ "Additional fields to retrieve beyond the standard read fields. "
25
+ "May be repeated."
26
+ ),
27
+ ),
28
+ raw_output: bool = typer.Option(
29
+ False,
30
+ "--raw",
31
+ help="Render the raw API payload as JSON.",
32
+ ),
33
+ json_output: bool = typer.Option(
34
+ False,
35
+ "--json",
36
+ help="Render structured output as JSON.",
37
+ ),
38
+ ) -> None:
39
+ """Read a Jira issue by key with full details."""
40
+ validate_output_options(json_output=json_output, raw_output=raw_output)
41
+
42
+ try:
43
+ api = client.get_api()
44
+ result = issues.read_issue(issue_key, extra_fields=extra_fields, api=api)
45
+ except Exception as exc:
46
+ raise_cli_exception(exc)
47
+
48
+ typer.echo(
49
+ render_operation_result(
50
+ result,
51
+ json_output=json_output,
52
+ raw_output=raw_output,
53
+ )
54
+ )
55
+
56
+
57
+ def comments_command(
58
+ issue_key: str = typer.Argument(..., help="Issue key (e.g. PROJ-123)"),
59
+ start_at: int = typer.Option(
60
+ 0,
61
+ "--start-at",
62
+ min=0,
63
+ help="Index of the first comment to return.",
64
+ ),
65
+ max_results: int = typer.Option(
66
+ 50,
67
+ "--max-results",
68
+ min=1,
69
+ max=100,
70
+ help="Maximum comments to return.",
71
+ ),
72
+ order_by: Literal["created", "-created"] = typer.Option(
73
+ "created",
74
+ "--order-by",
75
+ help="Use created for oldest first or -created for newest first.",
76
+ ),
77
+ raw_output: bool = typer.Option(
78
+ False,
79
+ "--raw",
80
+ help="Render the raw API payload as JSON.",
81
+ ),
82
+ json_output: bool = typer.Option(
83
+ False,
84
+ "--json",
85
+ help="Render structured output as JSON.",
86
+ ),
87
+ ) -> None:
88
+ """List comments on a Jira issue."""
89
+ validate_output_options(json_output=json_output, raw_output=raw_output)
90
+
91
+ try:
92
+ api = client.get_api()
93
+ result = comments.list_comments(
94
+ issue_key,
95
+ start_at=start_at,
96
+ max_results=max_results,
97
+ order_by=order_by,
98
+ api=api,
99
+ )
100
+ except Exception as exc:
101
+ raise_cli_exception(exc)
102
+
103
+ typer.echo(
104
+ render_operation_result(
105
+ result,
106
+ json_output=json_output,
107
+ raw_output=raw_output,
108
+ )
109
+ )
110
+
111
+
112
+ def register_read_commands(app: typer.Typer) -> None:
113
+ """Register read-oriented commands."""
114
+ app.command("read")(read_command)
115
+ app.command("comments")(comments_command)
116
+
117
+
118
+ __all__ = ["comments_command", "read_command", "register_read_commands"]
@@ -0,0 +1,69 @@
1
+ """Search jira2cli commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from jira2ai_core import client
7
+ from jira2ai_core.operations import search as search_operations
8
+
9
+ from jira2cli.output import (
10
+ raise_cli_exception,
11
+ render_operation_result,
12
+ validate_output_options,
13
+ )
14
+
15
+
16
+ def search_command(
17
+ jql: str = typer.Argument(..., help="JQL query string"),
18
+ max_results: int = typer.Option(
19
+ 20,
20
+ "--max-results",
21
+ min=1,
22
+ max=50,
23
+ help="Maximum issues to return.",
24
+ ),
25
+ fields: list[str] | None = typer.Option(
26
+ None,
27
+ "--field",
28
+ help="Field to include in the search response. May be repeated.",
29
+ ),
30
+ raw_output: bool = typer.Option(
31
+ False,
32
+ "--raw",
33
+ help="Render the raw API payload as JSON.",
34
+ ),
35
+ json_output: bool = typer.Option(
36
+ False,
37
+ "--json",
38
+ help="Render structured output as JSON.",
39
+ ),
40
+ ) -> None:
41
+ """Search Jira issues using JQL."""
42
+ validate_output_options(json_output=json_output, raw_output=raw_output)
43
+
44
+ try:
45
+ api = client.get_api()
46
+ result = search_operations.search_issues(
47
+ jql,
48
+ max_results=max_results,
49
+ fields=fields,
50
+ api=api,
51
+ )
52
+ except Exception as exc:
53
+ raise_cli_exception(exc)
54
+
55
+ typer.echo(
56
+ render_operation_result(
57
+ result,
58
+ json_output=json_output,
59
+ raw_output=raw_output,
60
+ )
61
+ )
62
+
63
+
64
+ def register_search_commands(app: typer.Typer) -> None:
65
+ """Register search commands."""
66
+ app.command("search")(search_command)
67
+
68
+
69
+ __all__ = ["register_search_commands", "search_command"]
@@ -0,0 +1,168 @@
1
+ """Write-oriented jira2cli commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from jira2ai_core import client
7
+ from jira2ai_core.operations import comments as comment_operations
8
+ from jira2ai_core.operations import issues
9
+
10
+ from jira2cli.output import (
11
+ raise_cli_exception,
12
+ render_operation_result,
13
+ validate_output_options,
14
+ )
15
+ from jira2cli.parsing import parse_fields_json
16
+
17
+
18
+ def create_command(
19
+ project_key: str = typer.Argument(..., help="Project key (e.g. PROJ)"),
20
+ issue_type: str = typer.Argument(..., help="Issue type name (e.g. Bug, Task)"),
21
+ summary: str = typer.Argument(..., help="Issue title / summary"),
22
+ description: str | None = typer.Option(
23
+ None,
24
+ "--description",
25
+ help="Issue description in markdown.",
26
+ ),
27
+ fields_json: str | None = typer.Option(
28
+ None,
29
+ "--fields-json",
30
+ help="Additional issue fields as a JSON object.",
31
+ ),
32
+ raw_output: bool = typer.Option(
33
+ False,
34
+ "--raw",
35
+ help="Render the raw API payload as JSON.",
36
+ ),
37
+ json_output: bool = typer.Option(
38
+ False,
39
+ "--json",
40
+ help="Render structured output as JSON.",
41
+ ),
42
+ ) -> None:
43
+ """Create a Jira issue."""
44
+ fields = parse_fields_json(fields_json)
45
+ validate_output_options(json_output=json_output, raw_output=raw_output)
46
+
47
+ try:
48
+ api = client.get_api()
49
+ result = issues.create_issue(
50
+ project_key,
51
+ issue_type,
52
+ summary,
53
+ description=description,
54
+ fields=fields,
55
+ api=api,
56
+ )
57
+ except Exception as exc:
58
+ raise_cli_exception(exc)
59
+
60
+ typer.echo(
61
+ render_operation_result(
62
+ result,
63
+ json_output=json_output,
64
+ raw_output=raw_output,
65
+ )
66
+ )
67
+
68
+
69
+ def edit_command(
70
+ issue_key: str = typer.Argument(..., help="Issue key (e.g. PROJ-123)"),
71
+ summary: str | None = typer.Option(
72
+ None,
73
+ "--summary",
74
+ help="New issue title / summary.",
75
+ ),
76
+ description: str | None = typer.Option(
77
+ None,
78
+ "--description",
79
+ help="New issue description in markdown.",
80
+ ),
81
+ fields_json: str | None = typer.Option(
82
+ None,
83
+ "--fields-json",
84
+ help="Additional fields to update as a JSON object.",
85
+ ),
86
+ raw_output: bool = typer.Option(
87
+ False,
88
+ "--raw",
89
+ help="Render the raw API payload as JSON.",
90
+ ),
91
+ json_output: bool = typer.Option(
92
+ False,
93
+ "--json",
94
+ help="Render structured output as JSON.",
95
+ ),
96
+ ) -> None:
97
+ """Update a Jira issue."""
98
+ fields = parse_fields_json(fields_json)
99
+ validate_output_options(json_output=json_output, raw_output=raw_output)
100
+ raw = raw_output or json_output
101
+
102
+ try:
103
+ api = client.get_api()
104
+ result = issues.edit_issue(
105
+ issue_key,
106
+ summary=summary,
107
+ description=description,
108
+ fields=fields,
109
+ raw=raw,
110
+ api=api,
111
+ )
112
+ except Exception as exc:
113
+ raise_cli_exception(exc)
114
+
115
+ typer.echo(
116
+ render_operation_result(
117
+ result,
118
+ json_output=json_output,
119
+ raw_output=raw_output,
120
+ )
121
+ )
122
+
123
+
124
+ def comment_command(
125
+ issue_key: str = typer.Argument(..., help="Issue key (e.g. PROJ-123)"),
126
+ body: str = typer.Argument(..., help="Comment text in markdown"),
127
+ raw_output: bool = typer.Option(
128
+ False,
129
+ "--raw",
130
+ help="Render the raw API payload as JSON.",
131
+ ),
132
+ json_output: bool = typer.Option(
133
+ False,
134
+ "--json",
135
+ help="Render structured output as JSON.",
136
+ ),
137
+ ) -> None:
138
+ """Add a comment to a Jira issue."""
139
+ validate_output_options(json_output=json_output, raw_output=raw_output)
140
+
141
+ try:
142
+ api = client.get_api()
143
+ result = comment_operations.add_comment(issue_key, body, api=api)
144
+ except Exception as exc:
145
+ raise_cli_exception(exc)
146
+
147
+ typer.echo(
148
+ render_operation_result(
149
+ result,
150
+ json_output=json_output,
151
+ raw_output=raw_output,
152
+ )
153
+ )
154
+
155
+
156
+ def register_write_commands(app: typer.Typer) -> None:
157
+ """Register write-oriented commands."""
158
+ app.command("create")(create_command)
159
+ app.command("edit")(edit_command)
160
+ app.command("comment")(comment_command)
161
+
162
+
163
+ __all__ = [
164
+ "comment_command",
165
+ "create_command",
166
+ "edit_command",
167
+ "register_write_commands",
168
+ ]
jira2cli/output.py ADDED
@@ -0,0 +1,107 @@
1
+ """CLI rendering and error helpers for jira2cli."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, NoReturn
7
+
8
+ import typer
9
+ from jira2ai_core.errors import Jira2AIError, Jira2AIValidationError
10
+ from jira2ai_core.results import OperationResult
11
+
12
+
13
+ def render_operation_result(
14
+ result: OperationResult,
15
+ *,
16
+ json_output: bool = False,
17
+ raw_output: bool = False,
18
+ ) -> str:
19
+ """Render an operation result for CLI stdout."""
20
+ if json_output or raw_output:
21
+ return json.dumps(
22
+ _json_payload(result),
23
+ indent=2,
24
+ sort_keys=True,
25
+ default=str,
26
+ )
27
+ return result.text
28
+
29
+
30
+ def _json_payload(result: OperationResult) -> Any:
31
+ """Select the most structured payload available for JSON output."""
32
+ if result.data is not None:
33
+ return result.data
34
+
35
+ if result.raw_content is None:
36
+ return result.text
37
+
38
+ try:
39
+ return json.loads(result.raw_content)
40
+ except json.JSONDecodeError:
41
+ return result.raw_content
42
+
43
+
44
+ def format_cli_error(error: Jira2AIError) -> str:
45
+ """Format a core error for CLI stderr."""
46
+ if not error.details:
47
+ return error.message
48
+
49
+ return (
50
+ f"{error.message}\n"
51
+ f"Details:\n{json.dumps(error.details, indent=2, sort_keys=True, default=str)}"
52
+ )
53
+
54
+
55
+ def error_exit_code(error: Jira2AIError) -> int:
56
+ """Return the CLI exit code for a core error."""
57
+ if isinstance(error, Jira2AIValidationError):
58
+ return 2
59
+ return 1
60
+
61
+
62
+ def raise_cli_usage_error(
63
+ message: str,
64
+ *,
65
+ param_hint: str | None = None,
66
+ ) -> NoReturn:
67
+ """Write a CLI usage error to stderr and exit with code 2."""
68
+ if param_hint:
69
+ typer.echo(f"{param_hint}: {message}", err=True)
70
+ else:
71
+ typer.echo(message, err=True)
72
+ raise typer.Exit(code=2)
73
+
74
+
75
+ def validate_output_options(*, json_output: bool, raw_output: bool) -> None:
76
+ """Reject conflicting structured output flags."""
77
+ if json_output and raw_output:
78
+ raise_cli_usage_error(
79
+ "Use only one of --json or --raw.",
80
+ param_hint="--json / --raw",
81
+ )
82
+
83
+
84
+ def raise_cli_error(error: Jira2AIError) -> NoReturn:
85
+ """Write a CLI-friendly core error message to stderr and exit."""
86
+ typer.echo(format_cli_error(error), err=True)
87
+ raise typer.Exit(code=error_exit_code(error))
88
+
89
+
90
+ def raise_cli_exception(error: Exception) -> NoReturn:
91
+ """Write a CLI-friendly error message to stderr and exit."""
92
+ if isinstance(error, Jira2AIError):
93
+ raise_cli_error(error)
94
+
95
+ typer.echo(str(error), err=True)
96
+ raise typer.Exit(code=1)
97
+
98
+
99
+ __all__ = [
100
+ "error_exit_code",
101
+ "format_cli_error",
102
+ "raise_cli_error",
103
+ "raise_cli_exception",
104
+ "raise_cli_usage_error",
105
+ "render_operation_result",
106
+ "validate_output_options",
107
+ ]
jira2cli/parsing.py ADDED
@@ -0,0 +1,39 @@
1
+ """Parsing helpers for jira2cli options and arguments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from jira2cli.output import raise_cli_usage_error
9
+
10
+
11
+ def parse_json_object(
12
+ value: str | None,
13
+ *,
14
+ option_name: str,
15
+ ) -> dict[str, Any] | None:
16
+ """Parse an optional JSON object string into a dictionary."""
17
+ if value is None:
18
+ return None
19
+
20
+ try:
21
+ parsed = json.loads(value)
22
+ except json.JSONDecodeError as exc:
23
+ raise_cli_usage_error(
24
+ f"must be valid JSON ({exc.msg} at line {exc.lineno}, column {exc.colno})",
25
+ param_hint=option_name,
26
+ )
27
+
28
+ if not isinstance(parsed, dict):
29
+ raise_cli_usage_error("must be a JSON object", param_hint=option_name)
30
+
31
+ return parsed
32
+
33
+
34
+ def parse_fields_json(value: str | None) -> dict[str, Any] | None:
35
+ """Parse the ``--fields-json`` option into a dictionary."""
36
+ return parse_json_object(value, option_name="--fields-json")
37
+
38
+
39
+ __all__ = ["parse_fields_json", "parse_json_object"]
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.3
2
+ Name: jira2cli
3
+ Version: 0.1.0
4
+ Summary: CLI for Jira AI integrations powered by jira2ai-core
5
+ Author: en-ver
6
+ Requires-Dist: jira2ai-core==0.1.0
7
+ Requires-Dist: typer>=0.16.0
8
+ Requires-Python: >=3.13
9
+ Description-Content-Type: text/markdown
10
+
11
+ # jira2cli
12
+
13
+ CLI adapter for Jira Cloud, powered by `jira2ai-core`.
14
+
15
+ `jira2cli` is currently intended for local and development use from this workspace. Do not assume a published `uvx` or PyPI install path yet.
16
+
17
+ ## Environment
18
+
19
+ `jira2cli` uses the same Jira credentials as `jira2mcp`:
20
+
21
+ | Variable | Description |
22
+ |---|---|
23
+ | `JIRA_URL` | Your Jira instance URL |
24
+ | `JIRA_USER` | Your Jira account email |
25
+ | `JIRA_API_TOKEN` | Your Jira API token |
26
+
27
+ Example:
28
+
29
+ ```bash
30
+ export JIRA_URL="https://yourcompany.atlassian.net"
31
+ export JIRA_USER="you@company.com"
32
+ export JIRA_API_TOKEN="your-api-token"
33
+ ```
34
+
35
+ ## Local usage
36
+
37
+ From the workspace root:
38
+
39
+ ```bash
40
+ uv sync --all-packages --group dev
41
+ uv run --package jira2cli jira2cli --help
42
+ uv run --package jira2cli jira2cli read PROJ-123
43
+ uv run --package jira2cli jira2cli search 'project = PROJ ORDER BY updated DESC'
44
+ uv run --package jira2cli jira2cli fields --project-key PROJ
45
+ ```
46
+
47
+ ## Workspace layout
48
+
49
+ - `packages/jira2ai-core` — shared operations.
50
+ - `packages/jira2mcp` — MCP adapter published as `jira2mcp`.
51
+ - `packages/jira2cli` — CLI adapter package.
52
+
53
+ For MCP installs and Claude setup, use `uvx jira2mcp` and `claude mcp add jira -- uvx jira2mcp` as documented in the repository root README.
54
+
55
+ ## Maintainers
56
+
57
+ Do not assume `uvx jira2cli` or `pip install jira2cli` is available yet. Keep using local workspace commands until the release gates in the maintainer docs are completed.
58
+
59
+ Release sequencing, package tags, and Trusted Publishing boundaries:
60
+
61
+ - <https://github.com/en-ver/jira2ai/blob/main/docs/releasing.md>
62
+ - <https://github.com/en-ver/jira2ai/blob/main/CONTRIBUTING.md>
@@ -0,0 +1,15 @@
1
+ jira2cli/__init__.py,sha256=z4AAAQgd_09bwWxrgWye5OajJdWuObZoXp4GDWL55Ko,137
2
+ jira2cli/cli.py,sha256=54r56Sp4Qi2jF9__AA6QwfLiEeMM5NTO8rYQ7KILbSQ,452
3
+ jira2cli/commands/__init__.py,sha256=WZGtWkmLffL9lfYM2HwHNkHwPHs2I3HnJx9Bw3Wrbfs,716
4
+ jira2cli/commands/attachments.py,sha256=kWRLr3BP58G4BW1GZFencOibHDkIdCxO_vIJTewZWW0,1396
5
+ jira2cli/commands/links.py,sha256=SMBDJxRVsoFrBfe6SKq5kHfHRddNNwble70poP6Oxis,2455
6
+ jira2cli/commands/metadata.py,sha256=ZBBH1o3oudf7XdVqomhqj8iT96-mdnVS1cQFztD5Zrw,5525
7
+ jira2cli/commands/read.py,sha256=YETS6IY910CCAisn5FmXAuO_6HfwlNh7HBpvvtfX2U4,3067
8
+ jira2cli/commands/search.py,sha256=R-dYCvMvv1AbBSpb8nvpnGsE9ZYvg8dqJo0IOceKquE,1691
9
+ jira2cli/commands/write.py,sha256=VF2D0V-9dmRIW2iMQ0m9cMQ_1TdmQbxYGeTLp09MsAc,4472
10
+ jira2cli/output.py,sha256=7eWAhqaaSHnp8iS8Em4Yu_yCDbvYQcaVjnNuDdeAGUs,2816
11
+ jira2cli/parsing.py,sha256=RmaDAHWIZuRW-Ftv1ZEKlQxieFO90fzZwZz_p_GcRwE,1033
12
+ jira2cli-0.1.0.dist-info/WHEEL,sha256=s49dN1sxqzkgPplo4QuUaKomil-_cbDzeLK4-pZKD-A,81
13
+ jira2cli-0.1.0.dist-info/entry_points.txt,sha256=aOTzz0QUAQo_IX15GItsq2BElryWL-CmRHav7mJuYBc,44
14
+ jira2cli-0.1.0.dist-info/METADATA,sha256=e1nALyGN3uNOp8z7U1SDbk5SO0bjsfT93occxIRUDi4,1889
15
+ jira2cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ jira2cli = jira2cli:main
3
+