@kud/jira-ink 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +203 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Erwann Mest
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @kud/jira-ink
|
|
2
|
+
|
|
3
|
+
Controlled [Ink](https://github.com/vadimdemedes/ink) components for viewing
|
|
4
|
+
Jira issues in the terminal — the issue detail screen, extracted from
|
|
5
|
+
[`@kud/jira-cli`](https://github.com/kud/jira-cli)'s TUI so it can be shared
|
|
6
|
+
with ambre's cockpit.
|
|
7
|
+
|
|
8
|
+
Every component is **presentation-only**: props in, no data fetching, no
|
|
9
|
+
app-level input. The consuming surface owns selection, navigation and loading,
|
|
10
|
+
so the same components drop into a full-screen CLI or a single pane inside a
|
|
11
|
+
larger dashboard. Data comes from [`@kud/jira`](https://github.com/kud/jira);
|
|
12
|
+
markdown rendering comes from [`@kud/ink-markdown`](https://github.com/kud/ink-markdown);
|
|
13
|
+
primitives and colour tokens come from [`@kud/ink-ui`](https://github.com/kud/ink-ui).
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
@kud/jira headless core — client, types (framework-agnostic)
|
|
17
|
+
└─ @kud/jira-ink this package — Ink rendering
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install @kud/jira-ink @kud/jira @kud/ink-ui ink react
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`ink` and `react` are peer dependencies.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
A host with a client and a key mounts the self-fetching body — the shape a cockpit wants:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { createJiraClient, loadConfig } from "@kud/jira"
|
|
34
|
+
import { IssueBody } from "@kud/jira-ink"
|
|
35
|
+
|
|
36
|
+
const loaded = loadConfig()
|
|
37
|
+
if ("missing" in loaded) throw new Error(loaded.missing.join(", "))
|
|
38
|
+
const client = createJiraClient(loaded.config)
|
|
39
|
+
|
|
40
|
+
<IssueBody
|
|
41
|
+
client={client}
|
|
42
|
+
baseUrl={loaded.config.baseUrl}
|
|
43
|
+
issueKey="SHOP-1234"
|
|
44
|
+
onExit={() => setScreen("list")}
|
|
45
|
+
/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A host that already holds an `IssueDetail` and owns the prompts behind the write verbs — jira-cli's own TUI — mounts the view directly. A verb whose handler is absent is neither hinted nor bound, so a read-only host gets the same screen minus the keys it cannot honour:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { IssueDetailView, issueDetailOf } from "@kud/jira-ink"
|
|
52
|
+
|
|
53
|
+
const issue = await issueDetailOf(client, baseUrl, "SHOP-1234")
|
|
54
|
+
|
|
55
|
+
<IssueDetailView
|
|
56
|
+
issue={issue}
|
|
57
|
+
width={80}
|
|
58
|
+
height={30}
|
|
59
|
+
onBack={back}
|
|
60
|
+
onTransition={transition}
|
|
61
|
+
onComment={comment}
|
|
62
|
+
onAssign={assign}
|
|
63
|
+
onOpenBrowser={open}
|
|
64
|
+
/>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Export | What it is |
|
|
68
|
+
| ----------------- | ------------------------------------------------------------------------ |
|
|
69
|
+
| `IssueBody` | Self-fetching issue screen: hand it a client and a key, read-only |
|
|
70
|
+
| `IssueDetailView` | The issue screen over an `IssueDetail`, with optional write verbs |
|
|
71
|
+
| `issueDetailOf` | Fetches one issue through a `@kud/jira` client into the view-ready shape |
|
|
72
|
+
| `transitionsOf` | The issue's available status transitions, `{ id, name, to }` |
|
|
73
|
+
| `IssueDetail` | The shape the view reads: strings throughout, ADF already Markdown |
|
|
74
|
+
## Development
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npm install
|
|
78
|
+
npm run typecheck
|
|
79
|
+
npm test # ink-testing-library
|
|
80
|
+
npm run build # tsup → dist/
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Licence
|
|
84
|
+
|
|
85
|
+
MIT © Erwann Mest
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { LocatedAttachment, JiraClient } from '@kud/jira';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One issue, as the detail screen reads it: every field already a string, ADF
|
|
6
|
+
* already Markdown, attachments already located. The view never touches a raw
|
|
7
|
+
* `JiraIssue`, so a host that fakes a `JiraClient` for a test or a screenshot
|
|
8
|
+
* gets a screen it can predict, and one that fetched the issue by some other
|
|
9
|
+
* route can build this shape by hand.
|
|
10
|
+
*/
|
|
11
|
+
type IssueDetail = {
|
|
12
|
+
key: string;
|
|
13
|
+
summary: string;
|
|
14
|
+
status: string;
|
|
15
|
+
type: string;
|
|
16
|
+
assignee: string;
|
|
17
|
+
reporter: string;
|
|
18
|
+
labels: string[];
|
|
19
|
+
parent?: {
|
|
20
|
+
key: string;
|
|
21
|
+
summary: string;
|
|
22
|
+
};
|
|
23
|
+
url: string;
|
|
24
|
+
description: string;
|
|
25
|
+
comments: {
|
|
26
|
+
id: string;
|
|
27
|
+
author: string;
|
|
28
|
+
created: string;
|
|
29
|
+
body: string;
|
|
30
|
+
}[];
|
|
31
|
+
attachments: LocatedAttachment[];
|
|
32
|
+
};
|
|
33
|
+
type Transition = {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
to: string;
|
|
37
|
+
};
|
|
38
|
+
declare const issueDetailOf: (client: JiraClient, baseUrl: string, key: string) => Promise<IssueDetail>;
|
|
39
|
+
declare const transitionsOf: (client: JiraClient, key: string) => Promise<Transition[]>;
|
|
40
|
+
|
|
41
|
+
type IssueDetailViewProps = {
|
|
42
|
+
issue: IssueDetail;
|
|
43
|
+
height: number;
|
|
44
|
+
width: number;
|
|
45
|
+
onBack: () => void;
|
|
46
|
+
/**
|
|
47
|
+
* The write actions are optional, and a key whose handler is absent is
|
|
48
|
+
* neither bound nor hinted. A host that only READS — cockpit's ticket drill —
|
|
49
|
+
* mounts the same screen jira-cli's TUI does, minus the verbs it has no way
|
|
50
|
+
* to honour, rather than a hint row promising `t` and `c` that do nothing.
|
|
51
|
+
*/
|
|
52
|
+
onTransition?: () => void;
|
|
53
|
+
onComment?: () => void;
|
|
54
|
+
onAssign?: () => void;
|
|
55
|
+
onOpenBrowser?: () => void;
|
|
56
|
+
};
|
|
57
|
+
declare const IssueDetailView: ({ issue, height, width, onBack, onTransition, onComment, onAssign, onOpenBrowser, }: IssueDetailViewProps) => react.JSX.Element;
|
|
58
|
+
|
|
59
|
+
type IssueBodyProps = {
|
|
60
|
+
client: JiraClient;
|
|
61
|
+
/** Instance URL, as `loadConfig` resolves it; only used to build the browser link. */
|
|
62
|
+
baseUrl: string;
|
|
63
|
+
issueKey: string;
|
|
64
|
+
onExit: () => void;
|
|
65
|
+
/** Override the terminal-derived size when the host draws inside a frame. */
|
|
66
|
+
width?: number;
|
|
67
|
+
height?: number;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* The issue screen as a host mounts it: hand it a client and a key, and it
|
|
71
|
+
* fetches, draws and reads its own keys until `onExit`. `JenkinsBody`'s shape,
|
|
72
|
+
* for the same reason — a cockpit mounting a ticket over its list should not
|
|
73
|
+
* have to know how an issue is fetched or what the loading states look like.
|
|
74
|
+
*
|
|
75
|
+
* Read-only by construction. The write verbs (`t`, `c`, `a`) exist on
|
|
76
|
+
* `IssueDetailView` for jira-cli's own TUI, which owns the prompts they need;
|
|
77
|
+
* here they are simply not wired, so the hint row does not offer them.
|
|
78
|
+
*/
|
|
79
|
+
declare const IssueBody: ({ client, baseUrl, issueKey, onExit, width, height, }: IssueBodyProps) => react.JSX.Element;
|
|
80
|
+
|
|
81
|
+
export { IssueBody, type IssueBodyProps, type IssueDetail, IssueDetailView, type IssueDetailViewProps, type Transition, issueDetailOf, transitionsOf };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { MarkdownViewport } from '@kud/ink-markdown';
|
|
2
|
+
import { useTabs, KeyValue, Badge, Tabs, FooterHints, LoadingScreen, StatusMessage } from '@kud/ink-ui';
|
|
3
|
+
import { useInput, Box, Text, useStdout } from 'ink';
|
|
4
|
+
import { useState, useEffect } from 'react';
|
|
5
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import { locateAttachments, adfToMarkdown } from '@kud/jira';
|
|
8
|
+
|
|
9
|
+
// src/components/issue-detail-view.tsx
|
|
10
|
+
var truncate = (s, max) => s.length <= max ? s : `${s.slice(0, Math.max(0, max - 1))}\u2026`;
|
|
11
|
+
var originOf = (a) => a.origins.map(
|
|
12
|
+
(o) => o.kind === "comment" ? `comment by ${o.author ?? "unknown"}` : o.kind === "description" ? "description" : "issue"
|
|
13
|
+
).join(", ");
|
|
14
|
+
var humanSize = (bytes) => {
|
|
15
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
16
|
+
const e = Math.min(
|
|
17
|
+
units.length - 1,
|
|
18
|
+
Math.floor(Math.log(Math.max(bytes, 1)) / Math.log(1024))
|
|
19
|
+
);
|
|
20
|
+
const value = bytes / 1024 ** e;
|
|
21
|
+
return `${e === 0 ? value : value.toFixed(1)}${units[e]}`;
|
|
22
|
+
};
|
|
23
|
+
var commentsAsMarkdown = (issue) => issue.comments.length === 0 ? "_No comments._" : issue.comments.map((c) => `**${c.author}** \xB7 ${c.created}
|
|
24
|
+
|
|
25
|
+
${c.body}`).join("\n\n---\n\n");
|
|
26
|
+
var attachmentsAsMarkdown = (issue) => issue.attachments.length === 0 ? "_No attachments._" : issue.attachments.map(
|
|
27
|
+
(a) => `- \`${a.filename}\` \u2014 ${a.mimeType}, ${humanSize(a.size)}
|
|
28
|
+
from ${originOf(a)}
|
|
29
|
+
\`jira attachment read ${issue.key} ${a.filename}\``
|
|
30
|
+
).join("\n");
|
|
31
|
+
var IssueDetailView = ({
|
|
32
|
+
issue,
|
|
33
|
+
height,
|
|
34
|
+
width,
|
|
35
|
+
onBack,
|
|
36
|
+
onTransition,
|
|
37
|
+
onComment,
|
|
38
|
+
onAssign,
|
|
39
|
+
onOpenBrowser
|
|
40
|
+
}) => {
|
|
41
|
+
const tabs = [
|
|
42
|
+
{ value: "description", label: "Description" },
|
|
43
|
+
{ value: "comments", label: "Comments", count: issue.comments.length },
|
|
44
|
+
{
|
|
45
|
+
value: "attachments",
|
|
46
|
+
label: "Attachments",
|
|
47
|
+
count: issue.attachments.length
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
const { active } = useTabs(tabs, { initial: "description" });
|
|
51
|
+
const [scroll, setScroll] = useState(0);
|
|
52
|
+
useInput((input, key) => {
|
|
53
|
+
if (key.escape || key.leftArrow) return onBack();
|
|
54
|
+
if (input === "t" && onTransition) return onTransition();
|
|
55
|
+
if (input === "c" && onComment) return onComment();
|
|
56
|
+
if (input === "a" && onAssign) return onAssign();
|
|
57
|
+
if (input === "o" && onOpenBrowser) return onOpenBrowser();
|
|
58
|
+
if (key.downArrow || input === "j") setScroll((s) => s + 1);
|
|
59
|
+
if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
|
|
60
|
+
});
|
|
61
|
+
const source = active === "comments" ? commentsAsMarkdown(issue) : active === "attachments" ? attachmentsAsMarkdown(issue) : issue.description || "_No description._";
|
|
62
|
+
const bodyHeight = Math.max(3, height - 12);
|
|
63
|
+
const hints = [
|
|
64
|
+
["\u2190\u2192", "section"],
|
|
65
|
+
["\u2191\u2193", "scroll"],
|
|
66
|
+
...onTransition ? [["t", "transition"]] : [],
|
|
67
|
+
...onComment ? [["c", "comment"]] : [],
|
|
68
|
+
...onAssign ? [["a", "assign to me"]] : [],
|
|
69
|
+
...onOpenBrowser ? [["o", "browser"]] : [],
|
|
70
|
+
["esc", "back"]
|
|
71
|
+
];
|
|
72
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
73
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
74
|
+
issue.key,
|
|
75
|
+
" ",
|
|
76
|
+
issue.summary
|
|
77
|
+
] }),
|
|
78
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
79
|
+
issue.parent && /* @__PURE__ */ jsx(
|
|
80
|
+
KeyValue,
|
|
81
|
+
{
|
|
82
|
+
label: "parent",
|
|
83
|
+
value: `${issue.parent.key} ${truncate(issue.parent.summary, 60)}`
|
|
84
|
+
}
|
|
85
|
+
),
|
|
86
|
+
/* @__PURE__ */ jsx(KeyValue, { label: "status", value: /* @__PURE__ */ jsx(Badge, { children: issue.status }) }),
|
|
87
|
+
/* @__PURE__ */ jsx(KeyValue, { label: "type", value: issue.type }),
|
|
88
|
+
/* @__PURE__ */ jsx(KeyValue, { label: "assignee", value: issue.assignee }),
|
|
89
|
+
/* @__PURE__ */ jsx(KeyValue, { label: "reporter", value: issue.reporter }),
|
|
90
|
+
issue.labels.length > 0 && /* @__PURE__ */ jsx(KeyValue, { label: "labels", value: issue.labels.join(", ") })
|
|
91
|
+
] }),
|
|
92
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Tabs, { items: tabs, active: active ?? "description" }) }),
|
|
93
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
|
|
94
|
+
MarkdownViewport,
|
|
95
|
+
{
|
|
96
|
+
source,
|
|
97
|
+
width: Math.max(20, width - 2),
|
|
98
|
+
height: bodyHeight,
|
|
99
|
+
scrollOffset: scroll
|
|
100
|
+
}
|
|
101
|
+
) }),
|
|
102
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(FooterHints, { hints }) })
|
|
103
|
+
] });
|
|
104
|
+
};
|
|
105
|
+
var issueDetailOf = async (client, baseUrl, key) => {
|
|
106
|
+
const issue = await client.getIssue(key);
|
|
107
|
+
const attachments = locateAttachments(issue);
|
|
108
|
+
const filenameOf = (id) => attachments.find((a) => a.id === id)?.filename;
|
|
109
|
+
const f = issue.fields;
|
|
110
|
+
return {
|
|
111
|
+
key: issue.key,
|
|
112
|
+
summary: f.summary ?? "",
|
|
113
|
+
status: f.status?.name ?? "\u2014",
|
|
114
|
+
type: f.issuetype?.name ?? "\u2014",
|
|
115
|
+
assignee: f.assignee?.displayName ?? "unassigned",
|
|
116
|
+
reporter: f.reporter?.displayName ?? "\u2014",
|
|
117
|
+
labels: f.labels ?? [],
|
|
118
|
+
parent: f.parent ? { key: f.parent.key, summary: f.parent.fields?.summary ?? "" } : void 0,
|
|
119
|
+
url: `${baseUrl}/browse/${issue.key}`,
|
|
120
|
+
description: adfToMarkdown(f.description, filenameOf),
|
|
121
|
+
comments: (f.comment?.comments ?? []).map((c) => ({
|
|
122
|
+
id: c.id,
|
|
123
|
+
author: c.author?.displayName ?? "unknown",
|
|
124
|
+
created: c.created?.slice(0, 10) ?? "",
|
|
125
|
+
body: adfToMarkdown(c.body, filenameOf)
|
|
126
|
+
})),
|
|
127
|
+
attachments
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
var transitionsOf = async (client, key) => (await client.getTransitions(key)).transitions.map((t) => ({
|
|
131
|
+
id: t.id,
|
|
132
|
+
name: t.name,
|
|
133
|
+
to: t.to?.name ?? t.name
|
|
134
|
+
}));
|
|
135
|
+
var useTermSize = () => {
|
|
136
|
+
const { stdout } = useStdout();
|
|
137
|
+
const [size, setSize] = useState({
|
|
138
|
+
cols: stdout.columns || 80,
|
|
139
|
+
rows: stdout.rows || 24
|
|
140
|
+
});
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const onResize = () => setSize({ cols: stdout.columns || 80, rows: stdout.rows || 24 });
|
|
143
|
+
stdout.on("resize", onResize);
|
|
144
|
+
return () => {
|
|
145
|
+
stdout.off("resize", onResize);
|
|
146
|
+
};
|
|
147
|
+
}, [stdout]);
|
|
148
|
+
return size;
|
|
149
|
+
};
|
|
150
|
+
var openInBrowser = (url) => {
|
|
151
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
152
|
+
exec(`${opener} "${url}"`);
|
|
153
|
+
};
|
|
154
|
+
var IssueBody = ({
|
|
155
|
+
client,
|
|
156
|
+
baseUrl,
|
|
157
|
+
issueKey,
|
|
158
|
+
onExit,
|
|
159
|
+
width,
|
|
160
|
+
height
|
|
161
|
+
}) => {
|
|
162
|
+
const term = useTermSize();
|
|
163
|
+
const [state, setState] = useState({ phase: "loading" });
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
let live = true;
|
|
166
|
+
setState({ phase: "loading" });
|
|
167
|
+
issueDetailOf(client, baseUrl, issueKey).then((issue) => live && setState({ phase: "ready", issue })).catch(
|
|
168
|
+
(e) => live && setState({
|
|
169
|
+
phase: "error",
|
|
170
|
+
message: e instanceof Error ? e.message : String(e)
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
return () => {
|
|
174
|
+
live = false;
|
|
175
|
+
};
|
|
176
|
+
}, [client, baseUrl, issueKey]);
|
|
177
|
+
useInput(
|
|
178
|
+
(_input, key) => {
|
|
179
|
+
if (key.escape) onExit();
|
|
180
|
+
},
|
|
181
|
+
{ isActive: state.phase !== "ready" }
|
|
182
|
+
);
|
|
183
|
+
if (state.phase === "loading")
|
|
184
|
+
return /* @__PURE__ */ jsx(LoadingScreen, { label: `Loading ${issueKey}\u2026` });
|
|
185
|
+
if (state.phase === "error")
|
|
186
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(StatusMessage, { variant: "error", children: [
|
|
187
|
+
issueKey,
|
|
188
|
+
": ",
|
|
189
|
+
state.message
|
|
190
|
+
] }) });
|
|
191
|
+
return /* @__PURE__ */ jsx(
|
|
192
|
+
IssueDetailView,
|
|
193
|
+
{
|
|
194
|
+
issue: state.issue,
|
|
195
|
+
width: width ?? term.cols,
|
|
196
|
+
height: height ?? term.rows,
|
|
197
|
+
onBack: onExit,
|
|
198
|
+
onOpenBrowser: () => openInBrowser(state.issue.url)
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export { IssueBody, IssueDetailView, issueDetailOf, transitionsOf };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kud/jira-ink",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ink components for viewing Jira issues: the issue detail screen, shared by jira-cli's TUI and cockpit",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "tsup --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"ink": ">=7",
|
|
29
|
+
"react": ">=19"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@kud/ink-ui": "0.25.0",
|
|
33
|
+
"@kud/ink-markdown": "0.1.1",
|
|
34
|
+
"@kud/jira": "0.3.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "26.5.0",
|
|
38
|
+
"@types/react": "19.2.18",
|
|
39
|
+
"ink": "7.1.1",
|
|
40
|
+
"ink-testing-library": "4.0.0",
|
|
41
|
+
"react": "19.2.8",
|
|
42
|
+
"tsup": "8.5.1",
|
|
43
|
+
"typescript": "5.9.3",
|
|
44
|
+
"vitest": "5.0.0"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"jira",
|
|
48
|
+
"ink",
|
|
49
|
+
"react",
|
|
50
|
+
"terminal",
|
|
51
|
+
"tui",
|
|
52
|
+
"components",
|
|
53
|
+
"kud"
|
|
54
|
+
],
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "https://github.com/kud/jira-ink"
|
|
59
|
+
}
|
|
60
|
+
}
|