@niiiiiiile/iw-jira-cli 0.5.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/.env.example ADDED
@@ -0,0 +1,14 @@
1
+ # Jira サイトのホスト名(https:// は不要)
2
+ JIRA_HOST=your-company.atlassian.net
3
+
4
+ # Atlassian アカウントのメールアドレス
5
+ JIRA_EMAIL=your-email@example.com
6
+
7
+ # Atlassian API トークン
8
+ # 発行: https://id.atlassian.com/manage-profile/security/api-tokens
9
+ JIRA_API_TOKEN=your-api-token-here
10
+
11
+ # 出力モード(省略可)
12
+ # 1/true=常にコンパクト出力、0/false=常にフル出力
13
+ # 未設定=パイプ/非TTY のときだけコンパクト(エージェント向けトークン削減)
14
+ # JIRA_CLI_COMPACT=1
package/README.md ADDED
@@ -0,0 +1,277 @@
1
+ # iw-jira-cli
2
+
3
+ `iw-jira-cli` は Jira Cloud REST API v3 を操作する CLI ツールです。`setup` / `profile` による接続情報管理、課題操作、メンション対応コメント、`whoami` による接続確認を一貫した CLI フローで提供します。AI エージェント連携向けのコンパクト出力にも対応しています。
4
+
5
+ ![Node.js](https://img.shields.io/badge/Node.js-20%2B-339933?logo=node.js&logoColor=white)
6
+ ![CLI](https://img.shields.io/badge/interface-CLI-4D4D4D)
7
+ ![License](https://img.shields.io/badge/license-MIT-blue)
8
+
9
+ ## Table of contents
10
+
11
+ * [Overview](#overview)
12
+ * [Features](#features)
13
+ * [Installation](#installation)
14
+ + [Run with npx](#run-with-npx)
15
+ + [Install globally](#install-globally)
16
+ + [Run from source](#run-from-source)
17
+ * [Quick start](#quick-start)
18
+ * [Profiles](#profiles)
19
+ * [Environment variables](#environment-variables)
20
+ * [Authentication check](#authentication-check)
21
+ * [Credential precedence](#credential-precedence)
22
+ * [Usage](#usage)
23
+ + [Show an issue](#show-an-issue)
24
+ + [Search issues](#search-issues)
25
+ + [Create an issue](#create-an-issue)
26
+ + [Update an issue](#update-an-issue)
27
+ + [Transition an issue](#transition-an-issue)
28
+ + [Comments](#comments)
29
+ + [User search](#user-search)
30
+ + [Project list](#project-list)
31
+ * [Output format](#output-format)
32
+ * [Mention syntax](#mention-syntax)
33
+ * [Development](#development)
34
+ * [License](#license)
35
+
36
+ ## Overview
37
+
38
+ `iw-jira-cli` は Jira 課題の取得、検索、更新、コメント、ステータス遷移を行うための CLI です。初回は `iw-jira-cli setup` で資格情報を保存し、その後は `issue` / `project` / `user` / `whoami` をそのまま使えます。
39
+
40
+ ## Features
41
+
42
+ - 課題の取得・検索(JQL 対応)・作成・更新
43
+ - ステータス遷移(`issue transitions` / `issue transition`)
44
+ - コメントの取得・追加
45
+ - ユーザー検索(メンション用 accountId の確認)
46
+ - プロジェクト一覧
47
+ - `setup` / `profile` による接続情報管理
48
+ - 認証ユーザー情報の取得(`whoami`, `myself`)
49
+ - メンション記法: `@[accountId]` または `@[email:user@example.com]`
50
+ - パイプ・非 TTY 時の自動コンパクト出力(`JIRA_CLI_COMPACT` で制御可能)
51
+
52
+ ## Installation
53
+
54
+ ### Run with npx
55
+
56
+ ```bash
57
+ npx @niiiiiiile/iw-jira-cli@latest
58
+ ```
59
+
60
+ ### Install globally
61
+
62
+ ```bash
63
+ npm install -g @niiiiiiile/iw-jira-cli
64
+ ```
65
+
66
+ ### Run from source
67
+
68
+ ```bash
69
+ git clone https://github.com/Niiiiile/jira-cli.git
70
+ cd iw-jira-cli
71
+ npm install
72
+ npm run build
73
+ ```
74
+
75
+ ## Quick start
76
+
77
+ ```bash
78
+ iw-jira-cli setup \
79
+ --host your-company.atlassian.net \
80
+ --email your-email@example.com \
81
+ --api-token your-api-token-here
82
+ ```
83
+
84
+ 初回登録時は `default` プロファイルに保存され、自動でデフォルトになります。
85
+
86
+ API トークンは [Atlassian アカウント設定](https://id.atlassian.com/manage-profile/security/api-tokens) から発行できます。
87
+
88
+ ## Profiles
89
+
90
+ ```bash
91
+ # work プロファイルを追加
92
+ iw-jira-cli profile add work \
93
+ --host your-company.atlassian.net \
94
+ --email your-email@example.com \
95
+ --api-token your-api-token-here
96
+
97
+ # プロファイル一覧
98
+ iw-jira-cli profile list
99
+
100
+ # デフォルト切り替え
101
+ iw-jira-cli profile use work
102
+ ```
103
+
104
+ ## Environment variables
105
+
106
+ `.env.example` をコピーして `.env` を作成し、各値を設定してください。
107
+
108
+ ```bash
109
+ cp .env.example .env
110
+ ```
111
+
112
+ ```env
113
+ JIRA_HOST=your-company.atlassian.net
114
+ JIRA_EMAIL=your-email@example.com
115
+ JIRA_API_TOKEN=your-api-token-here
116
+ ```
117
+
118
+ 環境変数はシェルに直接設定することも可能です:
119
+
120
+ ```bash
121
+ export JIRA_HOST=your-company.atlassian.net
122
+ export JIRA_EMAIL=your-email@example.com
123
+ export JIRA_API_TOKEN=your-api-token-here
124
+ ```
125
+
126
+ ## Authentication check
127
+
128
+ ```bash
129
+ iw-jira-cli whoami
130
+ ```
131
+
132
+ 既存の `iw-jira-cli myself` も引き続き利用できます。
133
+
134
+ ## Credential precedence
135
+
136
+ 1. コマンドフラグ(`--host`, `--email`, `--api-token`)
137
+ 2. 設定ファイルのプロファイル(`--profile` または default)
138
+ 3. 環境変数 / `.env`(`JIRA_HOST`, `JIRA_EMAIL`, `JIRA_API_TOKEN`)
139
+
140
+ ## Usage
141
+
142
+ ```bash
143
+ npx @niiiiiiile/iw-jira-cli@latest --help
144
+ ```
145
+
146
+ ### Show an issue
147
+
148
+ ```bash
149
+ # キーで取得(show は issue get の短縮)
150
+ npx @niiiiiiile/iw-jira-cli@latest show PROJECT-123
151
+
152
+ # URL でも指定可
153
+ npx @niiiiiiile/iw-jira-cli@latest show https://your-company.atlassian.net/browse/PROJECT-123
154
+ ```
155
+
156
+ グローバルインストール済みの場合は `iw-jira-cli` コマンドとして呼び出せます。
157
+
158
+ ### Search issues
159
+
160
+ ```bash
161
+ # 自分にアサインされた未解決の課題(デフォルト)
162
+ iw-jira-cli issue search
163
+
164
+ # プロジェクトを指定
165
+ iw-jira-cli issue search PROJECT
166
+
167
+ # JQL で検索
168
+ iw-jira-cli issue search --jql "project = PROJECT AND status = 'In Progress'"
169
+
170
+ # 件数を指定
171
+ iw-jira-cli issue search PROJECT --limit 50
172
+ ```
173
+
174
+ ### Create an issue
175
+
176
+ ```bash
177
+ iw-jira-cli issue create \
178
+ --project PROJECT \
179
+ --summary "バグ修正: ログイン画面のエラー" \
180
+ --type Bug \
181
+ --description "再現手順: ..." \
182
+ --assignee email:user@example.com
183
+ ```
184
+
185
+ ### Update an issue
186
+
187
+ ```bash
188
+ iw-jira-cli issue update PROJECT-123 \
189
+ --summary "新しいタイトル" \
190
+ --assignee email:user@example.com \
191
+ --due-date 2026-04-30
192
+ ```
193
+
194
+ ### Transition an issue
195
+
196
+ ```bash
197
+ # 利用可能な遷移を確認
198
+ iw-jira-cli issue transitions PROJECT-123
199
+
200
+ # 遷移名で変更(部分一致)
201
+ iw-jira-cli issue transition PROJECT-123 --name "In Progress"
202
+
203
+ # 遷移 ID で変更
204
+ iw-jira-cli issue transition PROJECT-123 --id 21
205
+ ```
206
+
207
+ ### Comments
208
+
209
+ ```bash
210
+ # コメント一覧
211
+ iw-jira-cli issue comments PROJECT-123
212
+
213
+ # コメントを追加(メンション可)
214
+ iw-jira-cli issue comment PROJECT-123 --body "確認しました @[email:user@example.com]"
215
+ ```
216
+
217
+ ### User search
218
+
219
+ ```bash
220
+ # メンション用 accountId を確認
221
+ iw-jira-cli user search "田中"
222
+ ```
223
+
224
+ ### Project list
225
+
226
+ ```bash
227
+ iw-jira-cli project list
228
+ iw-jira-cli project list --query "my project"
229
+ ```
230
+
231
+ ## Output format
232
+
233
+ デフォルトは TOON 形式(人間向け)。NDJSON に切り替えるには `--format jsonl` を指定します。
234
+
235
+ ```bash
236
+ jira-cli issue search PROJECT --format jsonl
237
+ ```
238
+
239
+ ### コンパクト出力
240
+
241
+ パイプや非 TTY 環境では自動的にコンパクト出力になります(AI エージェント向けのトークン削減)。
242
+
243
+ ```bash
244
+ # 強制的にコンパクト出力
245
+ JIRA_CLI_COMPACT=1 iw-jira-cli issue search PROJECT
246
+
247
+ # 強制的にフル出力
248
+ JIRA_CLI_COMPACT=0 iw-jira-cli issue search PROJECT
249
+ ```
250
+
251
+ ## Mention syntax
252
+
253
+ 説明文・コメントで以下の記法でメンションを指定できます。
254
+
255
+ | 記法 | 説明 |
256
+ |------|------|
257
+ | `@[712020:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx]` | accountId で直接指定 |
258
+ | `@[email:user@example.com]` | メールアドレスで指定(内部で accountId に解決) |
259
+
260
+ accountId は `iw-jira-cli user search <名前>` で確認できます。
261
+
262
+ ## Development
263
+
264
+ ```bash
265
+ # 依存関係のインストール
266
+ npm install
267
+
268
+ # TypeScript のビルド
269
+ npm run build
270
+
271
+ # ビルドなしで直接実行(開発時)
272
+ npm run dev -- issue search
273
+ ```
274
+
275
+ ## ライセンス
276
+
277
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { JiraCredentials } from './config.js';
2
+ /** `@[...]` を含むときだけ API で解決。含まなければ同期の plainTextToAdfBody。 */
3
+ export declare function descriptionToAdf(env: JiraCredentials, text: string): Promise<Record<string, unknown>>;
@@ -0,0 +1,103 @@
1
+ import { jiraRequest } from './jira-client.js';
2
+ import { plainTextToAdfBody } from './adf.js';
3
+ /**
4
+ * テキスト内の `@[accountId]` または `@[email:user@example.com]` を Jira Cloud ADF の mention ノードに変換する。
5
+ * accountId は Atlassian の形式(例: 712020:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)。
6
+ */
7
+ const MENTION_PATTERN = /@\[([^\]]+)\]/g;
8
+ function containsMentionPlaceholder(text) {
9
+ return /@\[([^\]]+)\]/.test(text);
10
+ }
11
+ function splitLineWithMentions(line) {
12
+ const segments = [];
13
+ let last = 0;
14
+ let m;
15
+ const re = new RegExp(MENTION_PATTERN.source, 'g');
16
+ while ((m = re.exec(line)) !== null) {
17
+ if (m.index > last) {
18
+ segments.push({ kind: 'text', value: line.slice(last, m.index) });
19
+ }
20
+ segments.push({ kind: 'mention', value: m[1].trim() });
21
+ last = m.index + m[0].length;
22
+ }
23
+ if (last < line.length) {
24
+ segments.push({ kind: 'text', value: line.slice(last) });
25
+ }
26
+ return segments;
27
+ }
28
+ async function resolveMentionInner(inner, env) {
29
+ const emailPrefix = /^email:/i;
30
+ if (emailPrefix.test(inner)) {
31
+ const email = inner.replace(emailPrefix, '').trim();
32
+ if (!email) {
33
+ throw new Error('@[email:...] にメールアドレスがありません');
34
+ }
35
+ const users = await jiraRequest(env, `/user/search?query=${encodeURIComponent(email)}&maxResults=25`);
36
+ const exact = users.find((u) => u.emailAddress?.toLowerCase() === email.toLowerCase());
37
+ const u = exact ?? users[0];
38
+ if (!u) {
39
+ throw new Error(`ユーザーが見つかりません: ${email}`);
40
+ }
41
+ return {
42
+ accountId: u.accountId,
43
+ label: u.displayName ?? u.emailAddress ?? email,
44
+ };
45
+ }
46
+ const accountId = inner;
47
+ try {
48
+ const u = await jiraRequest(env, `/user?accountId=${encodeURIComponent(accountId)}`);
49
+ return {
50
+ accountId: u.accountId,
51
+ label: u.displayName ?? accountId,
52
+ };
53
+ }
54
+ catch {
55
+ return { accountId, label: accountId };
56
+ }
57
+ }
58
+ async function lineToParagraphContent(line, env) {
59
+ const segments = splitLineWithMentions(line);
60
+ if (segments.length === 0) {
61
+ return [];
62
+ }
63
+ const content = [];
64
+ for (const seg of segments) {
65
+ if (seg.kind === 'text') {
66
+ if (seg.value.length > 0) {
67
+ content.push({ type: 'text', text: seg.value });
68
+ }
69
+ }
70
+ else {
71
+ const { accountId, label } = await resolveMentionInner(seg.value, env);
72
+ content.push({
73
+ type: 'mention',
74
+ attrs: {
75
+ id: accountId,
76
+ text: `@${label}`,
77
+ accessLevel: '',
78
+ },
79
+ });
80
+ }
81
+ }
82
+ return content;
83
+ }
84
+ /** `@[...]` を含むときだけ API で解決。含まなければ同期の plainTextToAdfBody。 */
85
+ export async function descriptionToAdf(env, text) {
86
+ if (!containsMentionPlaceholder(text)) {
87
+ return plainTextToAdfBody(text);
88
+ }
89
+ const lines = text.split(/\r?\n/);
90
+ const content = [];
91
+ for (const line of lines) {
92
+ const paraContent = await lineToParagraphContent(line, env);
93
+ content.push({
94
+ type: 'paragraph',
95
+ content: paraContent.length ? paraContent : [],
96
+ });
97
+ }
98
+ return {
99
+ type: 'doc',
100
+ version: 1,
101
+ content: content.length ? content : [{ type: 'paragraph', content: [] }],
102
+ };
103
+ }
package/dist/adf.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Atlassian Document Format からプレーンテキストを抽出(簡易) */
2
+ export declare function plainTextFromAdf(node: unknown): string;
3
+ /** 1 行でも複数行でも ADF doc に変換(課題の description 用) */
4
+ export declare function plainTextToAdfBody(text: string): Record<string, unknown>;
package/dist/adf.js ADDED
@@ -0,0 +1,38 @@
1
+ /** Atlassian Document Format からプレーンテキストを抽出(簡易) */
2
+ export function plainTextFromAdf(node) {
3
+ if (node == null)
4
+ return '';
5
+ if (typeof node !== 'object')
6
+ return '';
7
+ const n = node;
8
+ if (n.type === 'text' && typeof n.text === 'string')
9
+ return n.text;
10
+ if (n.type === 'mention') {
11
+ const a = n.attrs;
12
+ if (a && typeof a.text === 'string')
13
+ return a.text;
14
+ if (a && typeof a.id === 'string')
15
+ return `@[${a.id}]`;
16
+ return '';
17
+ }
18
+ if (!Array.isArray(n.content))
19
+ return '';
20
+ const inner = n.content.map((c) => plainTextFromAdf(c)).filter(Boolean);
21
+ if (n.type === 'paragraph' || n.type === 'heading') {
22
+ return inner.join('') + '\n';
23
+ }
24
+ return inner.join('\n');
25
+ }
26
+ /** 1 行でも複数行でも ADF doc に変換(課題の description 用) */
27
+ export function plainTextToAdfBody(text) {
28
+ const lines = text.split(/\r?\n/);
29
+ const content = lines.map((line) => ({
30
+ type: 'paragraph',
31
+ content: line.length ? [{ type: 'text', text: line }] : [],
32
+ }));
33
+ return {
34
+ type: 'doc',
35
+ version: 1,
36
+ content: content.length ? content : [{ type: 'paragraph', content: [] }],
37
+ };
38
+ }
@@ -0,0 +1,164 @@
1
+ import type { IssueSummary } from './jira-client.js';
2
+ /** `JIRA_CLI_COMPACT`: 1/true=常に圧縮、0/false=常にフル、未設定=非TTY のときだけ圧縮 */
3
+ export declare function wantCompact(agent: boolean): boolean;
4
+ export declare function truncateText(s: string, max: number): string;
5
+ /** エージェント向け: id/self を落とし、空フィールド省略、説明は切り詰め */
6
+ export declare function slimIssue(i: IssueSummary): Record<string, unknown>;
7
+ export declare function slimIssueEnvelope(issue: IssueSummary, compact: boolean): {
8
+ issue: IssueSummary;
9
+ } | {
10
+ issue: Record<string, unknown>;
11
+ };
12
+ export declare function slimSearch(data: {
13
+ count: number;
14
+ isLast?: boolean;
15
+ nextPageToken: string | null | undefined;
16
+ issues: IssueSummary[];
17
+ jql: string;
18
+ }, compact: boolean): {
19
+ count: number;
20
+ isLast?: boolean;
21
+ nextPageToken: string | null | undefined;
22
+ issues: IssueSummary[];
23
+ jql: string;
24
+ } | {
25
+ /** issues(短縮キー i) */
26
+ i: Record<string, unknown>[];
27
+ next?: string | undefined;
28
+ last?: boolean | undefined;
29
+ n: number;
30
+ };
31
+ export declare function slimMyself(data: {
32
+ accountId: string;
33
+ displayName: string;
34
+ emailAddress?: string;
35
+ active?: boolean;
36
+ }, compact: boolean): Record<string, unknown> | {
37
+ accountId: string;
38
+ displayName: string;
39
+ emailAddress?: string;
40
+ active?: boolean;
41
+ };
42
+ export declare function slimComments(data: {
43
+ key: string;
44
+ total: number;
45
+ comments: Array<{
46
+ id: string;
47
+ author: string;
48
+ created: string;
49
+ body: string;
50
+ }>;
51
+ }, compact: boolean): {
52
+ key: string;
53
+ total: number;
54
+ comments: Array<{
55
+ id: string;
56
+ author: string;
57
+ created: string;
58
+ body: string;
59
+ }>;
60
+ } | {
61
+ k: string;
62
+ n: number;
63
+ c: {
64
+ id: string;
65
+ a: string | undefined;
66
+ t: string;
67
+ b: string | undefined;
68
+ }[];
69
+ };
70
+ export declare function slimProjects(data: {
71
+ count: number;
72
+ projects: Array<{
73
+ key: string;
74
+ id: string;
75
+ name: string;
76
+ projectTypeKey: string;
77
+ }>;
78
+ }, compact: boolean): {
79
+ count: number;
80
+ projects: Array<{
81
+ key: string;
82
+ id: string;
83
+ name: string;
84
+ projectTypeKey: string;
85
+ }>;
86
+ } | {
87
+ n: number;
88
+ p: {
89
+ k: string;
90
+ name: string;
91
+ type: string;
92
+ }[];
93
+ };
94
+ export declare function slimUsers(data: {
95
+ count: number;
96
+ users: Array<{
97
+ accountId: string;
98
+ displayName: string;
99
+ emailAddress: string;
100
+ mention: string;
101
+ }>;
102
+ hint: string;
103
+ }, compact: boolean): {
104
+ count: number;
105
+ users: Array<{
106
+ accountId: string;
107
+ displayName: string;
108
+ emailAddress: string;
109
+ mention: string;
110
+ }>;
111
+ hint: string;
112
+ } | {
113
+ n: number;
114
+ u: {
115
+ id: string;
116
+ n: string;
117
+ m: string | undefined;
118
+ ph: string;
119
+ }[];
120
+ };
121
+ export declare function slimCreate(data: {
122
+ key: string;
123
+ id: string;
124
+ self: string;
125
+ }, compact: boolean): {
126
+ key: string;
127
+ id: string;
128
+ self: string;
129
+ } | {
130
+ k: string;
131
+ id: string;
132
+ };
133
+ export declare function slimUpdate(data: {
134
+ ok: boolean;
135
+ key: string;
136
+ }, compact: boolean): {
137
+ ok: boolean;
138
+ key: string;
139
+ } | {
140
+ ok: boolean;
141
+ k: string;
142
+ };
143
+ export declare function slimTransitions(data: {
144
+ key: string;
145
+ transitions: Array<{
146
+ id: string;
147
+ name: string;
148
+ toStatus: string;
149
+ }>;
150
+ }, compact: boolean): {
151
+ key: string;
152
+ transitions: Array<{
153
+ id: string;
154
+ name: string;
155
+ toStatus: string;
156
+ }>;
157
+ } | {
158
+ k: string;
159
+ t: {
160
+ id: string;
161
+ n: string;
162
+ s: string;
163
+ }[];
164
+ };