@niiiiiiile/iw-backlog-cli 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/.env.example ADDED
@@ -0,0 +1,8 @@
1
+ # Backlog スペース URL(プロファイル未使用時のフォールバック)
2
+ BACKLOG_BASE_URL=https://yourspace.backlog.com
3
+
4
+ # デフォルトプロジェクトキー
5
+ BACKLOG_PROJECT_KEY=MYPROJECT
6
+
7
+ # Backlog API キー(スペース設定 > API から発行)
8
+ BACKLOG_API_KEY=your_api_key_here
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Teruaki Iwane
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,203 @@
1
+ # backlog-cli
2
+
3
+ `backlog-cli` は [Nulab Backlog](https://backlog.com/) の課題・プロジェクトをターミナルから操作する CLI ツールです。`setup` / `profile` による接続情報管理と、`issue` / `project` / `whoami` による日常操作を一貫したフローで提供します。
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
+ + [Install globally](#install-globally)
15
+ + [Run with npx](#run-with-npx)
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
+ + [Issue commands](#issue-commands)
24
+ + [Project commands](#project-commands)
25
+ + [Profile commands](#profile-commands)
26
+ * [Development](#development)
27
+ * [License](#license)
28
+
29
+ ## Overview
30
+
31
+ `backlog-cli` は Backlog の API をシンプルに扱うための CLI です。初回は `backlog setup` で接続情報を保存し、その後は `backlog issue ...` や `backlog project ...` をそのまま実行できます。複数スペースを扱う場合も `profile` で切り替えできます。
32
+
33
+ ## Features
34
+
35
+ - 課題の一覧取得・詳細確認・作成・更新・削除・コメント操作
36
+ - プロジェクト一覧取得・詳細確認
37
+ - `setup` / `profile` による複数スペース管理
38
+ - `whoami` による接続確認
39
+ - フラグ / プロファイル / 環境変数の優先順位による資格情報解決
40
+
41
+ ## Installation
42
+
43
+ ### Install globally
44
+
45
+ ```bash
46
+ npm install -g @niiiiiiile/iw-backlog-cli
47
+ ```
48
+
49
+ ### Run with npx
50
+
51
+ ```bash
52
+ npx @niiiiiiile/iw-backlog-cli --help
53
+ npx @niiiiiiile/iw-backlog-cli issue list
54
+ ```
55
+
56
+ ### Run from source
57
+
58
+ ```bash
59
+ git clone https://github.com/Niiiiile/backlog-cli.git
60
+ cd backlog-cli
61
+ npm install
62
+ npm run dev -- --help
63
+ ```
64
+
65
+ ## Quick start
66
+
67
+ ```bash
68
+ backlog setup \
69
+ --url https://yourspace.backlog.com \
70
+ --api-key YOUR_API_KEY \
71
+ --project-key MYPROJECT
72
+ ```
73
+
74
+ 初回登録時は `default` プロファイルに保存され、自動でデフォルトになります。
75
+
76
+ ## Profiles
77
+
78
+ 複数スペースを使い分ける場合は `profile` を使います。
79
+
80
+ ```bash
81
+ # work プロファイルを追加
82
+ backlog profile add work \
83
+ --url https://yourspace.backlog.com \
84
+ --api-key YOUR_API_KEY \
85
+ --project-key MYPROJECT
86
+
87
+ # プロファイル一覧
88
+ backlog profile list
89
+
90
+ # デフォルト切り替え
91
+ backlog profile use work
92
+
93
+ # 実行時だけ別プロファイルを使う
94
+ backlog issue list --profile work
95
+ ```
96
+
97
+ ## Environment variables
98
+
99
+ `.env.example` を参考に環境変数を設定することもできます。
100
+
101
+ ```bash
102
+ export BACKLOG_BASE_URL=https://yourspace.backlog.com
103
+ export BACKLOG_API_KEY=your_api_key_here
104
+ export BACKLOG_PROJECT_KEY=MYPROJECT
105
+ ```
106
+
107
+ ## Authentication check
108
+
109
+ ```bash
110
+ backlog whoami
111
+ ```
112
+
113
+ ## Credential precedence
114
+
115
+ 1. コマンドフラグ(`--url`, `--api-key`, `--project-key`)
116
+ 2. 設定ファイルのプロファイル(`--profile` または default)
117
+ 3. 環境変数(`BACKLOG_BASE_URL`, `BACKLOG_API_KEY`, `BACKLOG_PROJECT_KEY`)
118
+
119
+ ## Usage
120
+
121
+ ```bash
122
+ backlog --help
123
+ ```
124
+
125
+ ### Issue commands
126
+
127
+ | サブコマンド | 説明 |
128
+ |---|---|
129
+ | `list` | 課題の一覧を取得(キーワード・ステータス・担当者でフィルタ可) |
130
+ | `get <issueIdOrKey>` | 課題の詳細を取得 |
131
+ | `add` | 課題を作成 |
132
+ | `update <issueIdOrKey>` | 課題を更新(ステータス変更・コメント追加など) |
133
+ | `delete <issueIdOrKey>` | 課題を削除 |
134
+ | `comments <issueIdOrKey>` | コメント一覧を取得 |
135
+ | `comment-add <issueIdOrKey> <content>` | コメントを追加 |
136
+
137
+ ```bash
138
+ # 最新20件を取得
139
+ backlog issue list
140
+
141
+ # キーワード検索
142
+ backlog issue list --keyword バグ
143
+
144
+ # 未対応・処理中のみ
145
+ backlog issue list --status-id 1,2
146
+
147
+ # 課題の詳細
148
+ backlog issue get PROJ-123
149
+
150
+ # 課題を作成
151
+ backlog issue add --summary "画面表示のバグ" --issue-type-id 2 --priority-id 2
152
+
153
+ # ステータスを完了に変更しコメントを追加
154
+ backlog issue update PROJ-123 --status-id 4 --comment "対応完了"
155
+
156
+ # コメント一覧
157
+ backlog issue comments PROJ-123
158
+ ```
159
+
160
+ ### Project commands
161
+
162
+ | サブコマンド | 説明 |
163
+ |---|---|
164
+ | `list` | 参加しているプロジェクトの一覧を取得 |
165
+ | `get <projectIdOrKey>` | プロジェクトの詳細を取得 |
166
+
167
+ ```bash
168
+ backlog project list
169
+ backlog project get MYPROJECT
170
+ ```
171
+
172
+ ### Profile commands
173
+
174
+ | サブコマンド | 説明 |
175
+ |---|---|
176
+ | `list` | 登録済みプロファイルの一覧を表示 |
177
+ | `add <name>` | プロファイルを追加・更新 |
178
+ | `remove <name>` | プロファイルを削除 |
179
+ | `use <name>` | デフォルトプロファイルを変更 |
180
+
181
+ ```bash
182
+ backlog profile add personal --url https://myspace.backlog.jp --api-key KEY2 --project-key MYPROJ
183
+
184
+ # プロファイル一覧
185
+ backlog profile list
186
+ ```
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ # 依存関係のインストール
192
+ npm install
193
+
194
+ # TypeScript のビルド
195
+ npm run build
196
+
197
+ # ビルドなしで直接実行
198
+ npm run dev -- --help
199
+ ```
200
+
201
+ ## ライセンス
202
+
203
+ MIT
@@ -0,0 +1,7 @@
1
+ export interface BacklogClient {
2
+ get: <T>(path: string, params?: Record<string, unknown>) => Promise<T>;
3
+ post: <T>(path: string, body?: Record<string, unknown>) => Promise<T>;
4
+ patch: <T>(path: string, body?: Record<string, unknown>) => Promise<T>;
5
+ delete: <T>(path: string) => Promise<T>;
6
+ }
7
+ export declare function createClient(baseUrl: string, apiKey: string): BacklogClient;
package/dist/client.js ADDED
@@ -0,0 +1,45 @@
1
+ function toFormPairs(obj) {
2
+ const pairs = [];
3
+ for (const [key, value] of Object.entries(obj)) {
4
+ if (value === undefined || value === null)
5
+ continue;
6
+ if (Array.isArray(value)) {
7
+ for (const item of value) {
8
+ pairs.push([`${key}[]`, String(item)]);
9
+ }
10
+ }
11
+ else {
12
+ pairs.push([key, String(value)]);
13
+ }
14
+ }
15
+ return pairs;
16
+ }
17
+ export function createClient(baseUrl, apiKey) {
18
+ const base = baseUrl.replace(/\/$/, '');
19
+ async function request(method, path, params, body) {
20
+ const url = new URL(`${base}/api/v2${path}`);
21
+ url.searchParams.set('apiKey', apiKey);
22
+ if (params) {
23
+ for (const [k, v] of toFormPairs(params)) {
24
+ url.searchParams.append(k, v);
25
+ }
26
+ }
27
+ const init = { method };
28
+ if (body) {
29
+ init.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
30
+ init.body = new URLSearchParams(toFormPairs(body));
31
+ }
32
+ const res = await fetch(url.toString(), init);
33
+ if (!res.ok) {
34
+ const text = await res.text().catch(() => '');
35
+ throw new Error(`Backlog API エラー [${res.status}]: ${text}`);
36
+ }
37
+ return res.json();
38
+ }
39
+ return {
40
+ get: (path, params) => request('GET', path, params),
41
+ post: (path, body) => request('POST', path, undefined, body),
42
+ patch: (path, body) => request('PATCH', path, undefined, body),
43
+ delete: (path) => request('DELETE', path),
44
+ };
45
+ }
@@ -0,0 +1,2 @@
1
+ import { Cli } from 'incur';
2
+ export declare const issueCli: Cli.Cli<{}, undefined, undefined>;
@@ -0,0 +1,311 @@
1
+ import { Cli, z } from 'incur';
2
+ import { createClient } from '../client.js';
3
+ import { resolveCredentials } from '../config.js';
4
+ import { authOptions } from '../shared.js';
5
+ async function resolveProjectId(client, projectKey) {
6
+ const project = await client.get(`/projects/${projectKey}`);
7
+ return project.id;
8
+ }
9
+ function parseIds(value) {
10
+ if (!value)
11
+ return undefined;
12
+ return value
13
+ .split(',')
14
+ .map((s) => Number(s.trim()))
15
+ .filter((n) => !Number.isNaN(n));
16
+ }
17
+ function formatIssue(issue) {
18
+ return {
19
+ key: issue.issueKey,
20
+ summary: issue.summary,
21
+ status: issue.status.name,
22
+ priority: issue.priority.name,
23
+ issueType: issue.issueType.name,
24
+ assignee: issue.assignee?.name ?? null,
25
+ dueDate: issue.dueDate ?? null,
26
+ created: issue.created,
27
+ updated: issue.updated,
28
+ };
29
+ }
30
+ export const issueCli = Cli.create('issue', {
31
+ description: '課題操作',
32
+ });
33
+ issueCli.command('list', {
34
+ description: '課題の一覧を取得',
35
+ options: z.object({
36
+ keyword: z.string().optional().describe('キーワード検索'),
37
+ statusId: z
38
+ .string()
39
+ .optional()
40
+ .describe('ステータス ID(カンマ区切り)例: 1,2 (1=未対応,2=処理中,3=処理済み,4=完了)'),
41
+ assigneeId: z.string().optional().describe('担当者ユーザー ID(カンマ区切り)'),
42
+ count: z.coerce.number().default(20).describe('取得件数(1〜100)'),
43
+ offset: z.coerce.number().default(0).describe('オフセット'),
44
+ sort: z
45
+ .enum(['created', 'updated', 'status', 'priority', 'dueDate', 'summary'])
46
+ .optional()
47
+ .describe('ソートフィールド'),
48
+ order: z.enum(['asc', 'desc']).default('desc').describe('ソート順'),
49
+ ...authOptions.shape,
50
+ }),
51
+ examples: [
52
+ { description: 'デフォルトプロジェクトの課題一覧(最新20件)' },
53
+ { options: { keyword: 'バグ' }, description: 'キーワード "バグ" で検索' },
54
+ { options: { statusId: '1,2' }, description: '未対応・処理中の課題のみ' },
55
+ { options: { count: 50, sort: 'dueDate', order: 'asc' }, description: '期限日順で50件取得' },
56
+ { options: { profile: 'work' }, description: '別プロファイルで取得' },
57
+ ],
58
+ async run(c) {
59
+ const creds = resolveCredentials(c.options);
60
+ const client = createClient(creds.baseUrl, creds.apiKey);
61
+ const projectId = await resolveProjectId(client, creds.projectKey);
62
+ const params = {
63
+ projectId: [projectId],
64
+ count: c.options.count,
65
+ offset: c.options.offset,
66
+ order: c.options.order,
67
+ };
68
+ if (c.options.keyword)
69
+ params.keyword = c.options.keyword;
70
+ if (c.options.statusId)
71
+ params.statusId = parseIds(c.options.statusId);
72
+ if (c.options.assigneeId)
73
+ params.assigneeId = parseIds(c.options.assigneeId);
74
+ if (c.options.sort)
75
+ params.sort = c.options.sort;
76
+ const issues = await client.get('/issues', params);
77
+ return {
78
+ count: issues.length,
79
+ issues: issues.map(formatIssue),
80
+ };
81
+ },
82
+ });
83
+ issueCli.command('get', {
84
+ description: '課題の詳細を取得',
85
+ args: z.object({
86
+ issueIdOrKey: z.string().describe('課題 ID または課題キー(例: PROJ-123)'),
87
+ }),
88
+ options: authOptions,
89
+ examples: [
90
+ { args: { issueIdOrKey: 'PROJ-123' }, description: '課題キーで詳細取得' },
91
+ { args: { issueIdOrKey: '123456' }, description: '課題 ID で詳細取得' },
92
+ ],
93
+ async run(c) {
94
+ const creds = resolveCredentials(c.options);
95
+ const client = createClient(creds.baseUrl, creds.apiKey);
96
+ const issue = await client.get(`/issues/${c.args.issueIdOrKey}`);
97
+ return {
98
+ key: issue.issueKey,
99
+ summary: issue.summary,
100
+ description: issue.description ?? '',
101
+ status: issue.status.name,
102
+ priority: issue.priority.name,
103
+ issueType: issue.issueType.name,
104
+ assignee: issue.assignee?.name ?? null,
105
+ startDate: issue.startDate ?? null,
106
+ dueDate: issue.dueDate ?? null,
107
+ estimatedHours: issue.estimatedHours ?? null,
108
+ actualHours: issue.actualHours ?? null,
109
+ created: issue.created,
110
+ updated: issue.updated,
111
+ };
112
+ },
113
+ });
114
+ issueCli.command('add', {
115
+ description: '課題を作成',
116
+ options: z.object({
117
+ summary: z.string().describe('課題の件名'),
118
+ issueTypeId: z.coerce.number().describe('課題種別 ID'),
119
+ priorityId: z.coerce.number().describe('優先度 ID(2=高,3=中,4=低)'),
120
+ description: z.string().optional().describe('課題の詳細'),
121
+ assigneeId: z.coerce.number().optional().describe('担当者のユーザー ID'),
122
+ startDate: z.string().optional().describe('開始日(yyyy-MM-dd)'),
123
+ dueDate: z.string().optional().describe('期限日(yyyy-MM-dd)'),
124
+ estimatedHours: z.coerce.number().optional().describe('予定時間'),
125
+ parentIssueId: z.coerce.number().optional().describe('親課題 ID'),
126
+ ...authOptions.shape,
127
+ }),
128
+ examples: [
129
+ {
130
+ options: { summary: '画面表示のバグ', issueTypeId: 2, priorityId: 2 },
131
+ description: '最小構成で課題を作成(件名・種別・優先度)',
132
+ },
133
+ {
134
+ options: {
135
+ summary: '新機能の実装',
136
+ issueTypeId: 1,
137
+ priorityId: 3,
138
+ description: '詳細な説明',
139
+ dueDate: '2026-04-30',
140
+ estimatedHours: 8,
141
+ },
142
+ description: '詳細情報を含めて課題を作成',
143
+ },
144
+ ],
145
+ async run(c) {
146
+ const creds = resolveCredentials(c.options);
147
+ const client = createClient(creds.baseUrl, creds.apiKey);
148
+ const projectId = await resolveProjectId(client, creds.projectKey);
149
+ const body = {
150
+ projectId,
151
+ summary: c.options.summary,
152
+ issueTypeId: c.options.issueTypeId,
153
+ priorityId: c.options.priorityId,
154
+ };
155
+ if (c.options.description)
156
+ body.description = c.options.description;
157
+ if (c.options.assigneeId)
158
+ body.assigneeId = c.options.assigneeId;
159
+ if (c.options.startDate)
160
+ body.startDate = c.options.startDate;
161
+ if (c.options.dueDate)
162
+ body.dueDate = c.options.dueDate;
163
+ if (c.options.estimatedHours)
164
+ body.estimatedHours = c.options.estimatedHours;
165
+ if (c.options.parentIssueId)
166
+ body.parentIssueId = c.options.parentIssueId;
167
+ const issue = await client.post('/issues', body);
168
+ return c.ok({
169
+ key: issue.issueKey,
170
+ summary: issue.summary,
171
+ status: issue.status.name,
172
+ priority: issue.priority.name,
173
+ }, { cta: { commands: [`backlog issue get ${issue.issueKey}`] } });
174
+ },
175
+ });
176
+ issueCli.command('update', {
177
+ description: '課題を更新',
178
+ args: z.object({
179
+ issueIdOrKey: z.string().describe('課題 ID または課題キー(例: PROJ-123)'),
180
+ }),
181
+ options: z.object({
182
+ summary: z.string().optional().describe('課題の件名'),
183
+ description: z.string().optional().describe('課題の詳細'),
184
+ statusId: z.coerce
185
+ .number()
186
+ .optional()
187
+ .describe('ステータス ID(1=未対応,2=処理中,3=処理済み,4=完了)'),
188
+ priorityId: z.coerce.number().optional().describe('優先度 ID(2=高,3=中,4=低)'),
189
+ assigneeId: z.coerce.number().optional().describe('担当者のユーザー ID'),
190
+ startDate: z.string().optional().describe('開始日(yyyy-MM-dd)'),
191
+ dueDate: z.string().optional().describe('期限日(yyyy-MM-dd)'),
192
+ estimatedHours: z.coerce.number().optional().describe('予定時間'),
193
+ actualHours: z.coerce.number().optional().describe('実績時間'),
194
+ comment: z.string().optional().describe('更新時のコメント'),
195
+ ...authOptions.shape,
196
+ }),
197
+ examples: [
198
+ { args: { issueIdOrKey: 'PROJ-123' }, options: { statusId: 2 }, description: 'ステータスを処理中に変更' },
199
+ { args: { issueIdOrKey: 'PROJ-123' }, options: { statusId: 4, comment: '対応完了' }, description: 'ステータスを完了にしてコメント追加' },
200
+ { args: { issueIdOrKey: 'PROJ-123' }, options: { summary: '新しいタイトル', dueDate: '2026-05-01' }, description: '件名と期限日を更新' },
201
+ ],
202
+ async run(c) {
203
+ const creds = resolveCredentials(c.options);
204
+ const client = createClient(creds.baseUrl, creds.apiKey);
205
+ const body = {};
206
+ if (c.options.summary)
207
+ body.summary = c.options.summary;
208
+ if (c.options.description)
209
+ body.description = c.options.description;
210
+ if (c.options.statusId)
211
+ body.statusId = c.options.statusId;
212
+ if (c.options.priorityId)
213
+ body.priorityId = c.options.priorityId;
214
+ if (c.options.assigneeId)
215
+ body.assigneeId = c.options.assigneeId;
216
+ if (c.options.startDate)
217
+ body.startDate = c.options.startDate;
218
+ if (c.options.dueDate)
219
+ body.dueDate = c.options.dueDate;
220
+ if (c.options.estimatedHours !== undefined)
221
+ body.estimatedHours = c.options.estimatedHours;
222
+ if (c.options.actualHours !== undefined)
223
+ body.actualHours = c.options.actualHours;
224
+ if (c.options.comment)
225
+ body.comment = c.options.comment;
226
+ const issue = await client.patch(`/issues/${c.args.issueIdOrKey}`, body);
227
+ return {
228
+ key: issue.issueKey,
229
+ summary: issue.summary,
230
+ status: issue.status.name,
231
+ updated: issue.updated,
232
+ };
233
+ },
234
+ });
235
+ issueCli.command('delete', {
236
+ description: '課題を削除',
237
+ args: z.object({
238
+ issueIdOrKey: z.string().describe('課題 ID または課題キー(例: PROJ-123)'),
239
+ }),
240
+ options: authOptions,
241
+ examples: [
242
+ { args: { issueIdOrKey: 'PROJ-123' }, description: '課題を完全削除(取り消し不可)' },
243
+ ],
244
+ async run(c) {
245
+ const creds = resolveCredentials(c.options);
246
+ const client = createClient(creds.baseUrl, creds.apiKey);
247
+ const issue = await client.delete(`/issues/${c.args.issueIdOrKey}`);
248
+ return { deleted: issue.issueKey };
249
+ },
250
+ });
251
+ issueCli.command('comments', {
252
+ description: '課題のコメント一覧を取得',
253
+ args: z.object({
254
+ issueIdOrKey: z.string().describe('課題 ID または課題キー(例: PROJ-123)'),
255
+ }),
256
+ options: z.object({
257
+ count: z.coerce.number().default(20).describe('取得件数(1〜200)'),
258
+ minId: z.coerce.number().optional().describe('最小コメント ID(この ID より大きいコメントを取得)'),
259
+ maxId: z.coerce.number().optional().describe('最大コメント ID(この ID より小さいコメントを取得)'),
260
+ order: z.enum(['asc', 'desc']).default('asc').describe('ソート順'),
261
+ ...authOptions.shape,
262
+ }),
263
+ examples: [
264
+ { args: { issueIdOrKey: 'PROJ-123' }, description: '課題のコメントを時系列順で取得' },
265
+ { args: { issueIdOrKey: 'PROJ-123' }, options: { order: 'desc' }, description: '最新コメントから取得' },
266
+ ],
267
+ async run(c) {
268
+ const creds = resolveCredentials(c.options);
269
+ const client = createClient(creds.baseUrl, creds.apiKey);
270
+ const params = {
271
+ count: c.options.count,
272
+ order: c.options.order,
273
+ };
274
+ if (c.options.minId !== undefined)
275
+ params.minId = c.options.minId;
276
+ if (c.options.maxId !== undefined)
277
+ params.maxId = c.options.maxId;
278
+ const comments = await client.get(`/issues/${c.args.issueIdOrKey}/comments`, params);
279
+ return {
280
+ count: comments.length,
281
+ comments: comments.map((cm) => ({
282
+ id: cm.id,
283
+ content: cm.content,
284
+ createdUser: cm.createdUser.name,
285
+ created: cm.created,
286
+ updated: cm.updated,
287
+ })),
288
+ };
289
+ },
290
+ });
291
+ issueCli.command('comment-add', {
292
+ description: '課題にコメントを追加',
293
+ args: z.object({
294
+ issueIdOrKey: z.string().describe('課題 ID または課題キー(例: PROJ-123)'),
295
+ content: z.string().describe('コメント本文'),
296
+ }),
297
+ options: authOptions,
298
+ examples: [
299
+ { args: { issueIdOrKey: 'PROJ-123', content: '対応完了しました。確認をお願いします。' }, description: '課題にコメントを追加' },
300
+ ],
301
+ async run(c) {
302
+ const creds = resolveCredentials(c.options);
303
+ const client = createClient(creds.baseUrl, creds.apiKey);
304
+ const comment = await client.post(`/issues/${c.args.issueIdOrKey}/comments`, { content: c.args.content });
305
+ return {
306
+ id: comment.id,
307
+ content: comment.content,
308
+ created: comment.created,
309
+ };
310
+ },
311
+ });
@@ -0,0 +1,2 @@
1
+ import { Cli } from 'incur';
2
+ export declare const profileCli: Cli.Cli<{}, undefined, undefined>;
@@ -0,0 +1,108 @@
1
+ import { Cli, z } from 'incur';
2
+ import { readConfig, writeConfig } from '../config.js';
3
+ export const profileCli = Cli.create('profile', {
4
+ description: 'プロファイル管理(Backlog スペース接続情報)',
5
+ });
6
+ profileCli.command('list', {
7
+ description: '設定済みプロファイルの一覧を表示',
8
+ examples: [
9
+ { description: '登録済みプロファイルとデフォルトを確認' },
10
+ ],
11
+ run() {
12
+ const config = readConfig();
13
+ const profiles = Object.entries(config.profiles).map(([name, p]) => ({
14
+ name,
15
+ baseUrl: p.baseUrl,
16
+ projectKey: p.projectKey,
17
+ isDefault: name === config.default,
18
+ }));
19
+ return {
20
+ default: config.default ?? null,
21
+ profiles,
22
+ };
23
+ },
24
+ });
25
+ profileCli.command('add', {
26
+ description: 'プロファイルを追加・更新',
27
+ args: z.object({
28
+ name: z.string().describe('プロファイル名(例: work, personal)'),
29
+ }),
30
+ options: z.object({
31
+ url: z.string().describe('Backlog スペース URL(例: https://yourspace.backlog.com)'),
32
+ apiKey: z.string().describe('Backlog API キー'),
33
+ projectKey: z.string().describe('デフォルトプロジェクトキー(例: MYPROJECT)'),
34
+ default: z.boolean().optional().describe('このプロファイルをデフォルトに設定'),
35
+ }),
36
+ examples: [
37
+ {
38
+ args: { name: 'work' },
39
+ options: { url: 'https://yourspace.backlog.com', apiKey: 'YOUR_API_KEY', projectKey: 'MYPROJECT' },
40
+ description: 'work プロファイルを追加(初回登録時は自動でデフォルトになる)',
41
+ },
42
+ {
43
+ args: { name: 'personal' },
44
+ options: { url: 'https://myspace.backlog.jp', apiKey: 'ANOTHER_KEY', projectKey: 'MYPROJ', default: true },
45
+ description: 'personal プロファイルをデフォルトに設定しながら追加',
46
+ },
47
+ ],
48
+ run(c) {
49
+ const config = readConfig();
50
+ config.profiles[c.args.name] = {
51
+ baseUrl: c.options.url.replace(/\/$/, ''),
52
+ apiKey: c.options.apiKey,
53
+ projectKey: c.options.projectKey,
54
+ };
55
+ const isFirst = Object.keys(config.profiles).length === 1;
56
+ if (c.options.default || isFirst) {
57
+ config.default = c.args.name;
58
+ }
59
+ writeConfig(config);
60
+ return {
61
+ added: c.args.name,
62
+ isDefault: config.default === c.args.name,
63
+ };
64
+ },
65
+ });
66
+ profileCli.command('remove', {
67
+ description: 'プロファイルを削除',
68
+ args: z.object({
69
+ name: z.string().describe('削除するプロファイル名'),
70
+ }),
71
+ run(c) {
72
+ const config = readConfig();
73
+ if (!config.profiles[c.args.name]) {
74
+ return c.error({
75
+ code: 'PROFILE_NOT_FOUND',
76
+ message: `プロファイル "${c.args.name}" が見つかりません`,
77
+ retryable: false,
78
+ cta: { commands: ['backlog profile list'] },
79
+ });
80
+ }
81
+ delete config.profiles[c.args.name];
82
+ if (config.default === c.args.name) {
83
+ config.default = Object.keys(config.profiles)[0];
84
+ }
85
+ writeConfig(config);
86
+ return { removed: c.args.name, newDefault: config.default ?? null };
87
+ },
88
+ });
89
+ profileCli.command('use', {
90
+ description: 'デフォルトプロファイルを変更',
91
+ args: z.object({
92
+ name: z.string().describe('デフォルトに設定するプロファイル名'),
93
+ }),
94
+ run(c) {
95
+ const config = readConfig();
96
+ if (!config.profiles[c.args.name]) {
97
+ return c.error({
98
+ code: 'PROFILE_NOT_FOUND',
99
+ message: `プロファイル "${c.args.name}" が見つかりません`,
100
+ retryable: false,
101
+ cta: { commands: ['backlog profile list'] },
102
+ });
103
+ }
104
+ config.default = c.args.name;
105
+ writeConfig(config);
106
+ return { default: c.args.name };
107
+ },
108
+ });
@@ -0,0 +1,2 @@
1
+ import { Cli } from 'incur';
2
+ export declare const projectCli: Cli.Cli<{}, undefined, undefined>;
@@ -0,0 +1,62 @@
1
+ import { Cli, z } from 'incur';
2
+ import { createClient } from '../client.js';
3
+ import { resolveCredentials } from '../config.js';
4
+ import { authOptions } from '../shared.js';
5
+ export const projectCli = Cli.create('project', {
6
+ description: 'プロジェクト操作',
7
+ });
8
+ projectCli.command('list', {
9
+ description: '参加しているプロジェクトの一覧を取得',
10
+ options: z.object({
11
+ archived: z.boolean().optional().describe('アーカイブ済みプロジェクトのみ取得'),
12
+ all: z.boolean().optional().describe('全プロジェクトを取得(管理者のみ)'),
13
+ ...authOptions.shape,
14
+ }),
15
+ examples: [
16
+ { description: 'アクティブなプロジェクト一覧' },
17
+ { options: { archived: true }, description: 'アーカイブ済みプロジェクト一覧' },
18
+ ],
19
+ async run(c) {
20
+ const creds = resolveCredentials(c.options);
21
+ const client = createClient(creds.baseUrl, creds.apiKey);
22
+ const params = {};
23
+ if (c.options.archived !== undefined)
24
+ params.archived = c.options.archived;
25
+ if (c.options.all !== undefined)
26
+ params.all = c.options.all;
27
+ const projects = await client.get('/projects', params);
28
+ return {
29
+ count: projects.length,
30
+ projects: projects.map((p) => ({
31
+ id: p.id,
32
+ key: p.projectKey,
33
+ name: p.name,
34
+ archived: p.archived,
35
+ })),
36
+ };
37
+ },
38
+ });
39
+ projectCli.command('get', {
40
+ description: 'プロジェクトの詳細を取得',
41
+ args: z.object({
42
+ projectIdOrKey: z.string().describe('プロジェクト ID またはプロジェクトキー'),
43
+ }),
44
+ options: authOptions,
45
+ examples: [
46
+ { args: { projectIdOrKey: 'MYPROJECT' }, description: 'プロジェクトキーで詳細取得' },
47
+ ],
48
+ async run(c) {
49
+ const creds = resolveCredentials(c.options);
50
+ const client = createClient(creds.baseUrl, creds.apiKey);
51
+ const project = await client.get(`/projects/${c.args.projectIdOrKey}`);
52
+ return {
53
+ id: project.id,
54
+ key: project.projectKey,
55
+ name: project.name,
56
+ archived: project.archived,
57
+ useWiki: project.useWiki,
58
+ useGit: project.useGit,
59
+ textFormattingRule: project.textFormattingRule,
60
+ };
61
+ },
62
+ });
@@ -0,0 +1,13 @@
1
+ import { z } from 'incur';
2
+ export declare const setupOptions: z.ZodObject<{
3
+ profile: z.ZodOptional<z.ZodString>;
4
+ url: z.ZodString;
5
+ apiKey: z.ZodString;
6
+ projectKey: z.ZodString;
7
+ default: z.ZodOptional<z.ZodBoolean>;
8
+ }, z.core.$strip>;
9
+ export declare function runSetup(options: z.infer<typeof setupOptions>): {
10
+ saved: string;
11
+ isDefault: boolean;
12
+ hint: string;
13
+ };
@@ -0,0 +1,32 @@
1
+ import { z } from 'incur';
2
+ import { readConfig, writeConfig } from '../config.js';
3
+ const DEFAULT_PROFILE_NAME = 'default';
4
+ export const setupOptions = z.object({
5
+ profile: z
6
+ .string()
7
+ .optional()
8
+ .describe(`保存先プロファイル名(省略時: ${DEFAULT_PROFILE_NAME})`),
9
+ url: z.string().describe('Backlog スペース URL(例: https://yourspace.backlog.com)'),
10
+ apiKey: z.string().describe('Backlog API キー'),
11
+ projectKey: z.string().describe('デフォルトプロジェクトキー(例: MYPROJECT)'),
12
+ default: z.boolean().optional().describe('このプロファイルをデフォルトに設定'),
13
+ });
14
+ export function runSetup(options) {
15
+ const config = readConfig();
16
+ const name = options.profile ?? DEFAULT_PROFILE_NAME;
17
+ config.profiles[name] = {
18
+ baseUrl: options.url.replace(/\/$/, ''),
19
+ apiKey: options.apiKey,
20
+ projectKey: options.projectKey,
21
+ };
22
+ const isFirst = Object.keys(config.profiles).length === 1;
23
+ if (options.default || isFirst || !config.default) {
24
+ config.default = name;
25
+ }
26
+ writeConfig(config);
27
+ return {
28
+ saved: name,
29
+ isDefault: config.default === name,
30
+ hint: '接続確認は `backlog whoami` を実行してください',
31
+ };
32
+ }
@@ -0,0 +1,13 @@
1
+ export declare function loadWhoami(opts: {
2
+ profile?: string;
3
+ url?: string;
4
+ apiKey?: string;
5
+ projectKey?: string;
6
+ }): Promise<{
7
+ id: number;
8
+ userId: string;
9
+ name: string;
10
+ mailAddress: string | null;
11
+ lang: string | null;
12
+ projectKey: string | null;
13
+ }>;
@@ -0,0 +1,25 @@
1
+ import { createClient } from '../client.js';
2
+ import { readConfig } from '../config.js';
3
+ export async function loadWhoami(opts) {
4
+ const config = readConfig();
5
+ const profileName = opts.profile ?? config.default;
6
+ const profileData = profileName ? config.profiles[profileName] : undefined;
7
+ const baseUrl = opts.url ?? profileData?.baseUrl ?? process.env.BACKLOG_BASE_URL;
8
+ const apiKey = opts.apiKey ?? profileData?.apiKey ?? process.env.BACKLOG_API_KEY;
9
+ if (!baseUrl) {
10
+ throw new Error('Backlog URL が未設定です。--url フラグ、プロファイル、または BACKLOG_BASE_URL 環境変数を設定してください。');
11
+ }
12
+ if (!apiKey) {
13
+ throw new Error('API キーが未設定です。--api-key フラグ、プロファイル、または BACKLOG_API_KEY 環境変数を設定してください。');
14
+ }
15
+ const client = createClient(baseUrl.replace(/\/$/, ''), apiKey);
16
+ const me = await client.get('/users/myself');
17
+ return {
18
+ id: me.id,
19
+ userId: me.userId,
20
+ name: me.name,
21
+ mailAddress: me.mailAddress ?? null,
22
+ lang: me.lang ?? null,
23
+ projectKey: opts.projectKey ?? profileData?.projectKey ?? process.env.BACKLOG_PROJECT_KEY ?? null,
24
+ };
25
+ }
@@ -0,0 +1,29 @@
1
+ export interface Profile {
2
+ baseUrl: string;
3
+ projectKey: string;
4
+ apiKey: string;
5
+ }
6
+ export interface Config {
7
+ default?: string;
8
+ profiles: Record<string, Profile>;
9
+ }
10
+ export interface Credentials {
11
+ baseUrl: string;
12
+ projectKey: string;
13
+ apiKey: string;
14
+ }
15
+ export interface ResolveOptions {
16
+ profile?: string;
17
+ url?: string;
18
+ apiKey?: string;
19
+ projectKey?: string;
20
+ }
21
+ export declare function readConfig(): Config;
22
+ export declare function writeConfig(config: Config): void;
23
+ /**
24
+ * 認証情報を以下の優先順位で解決する:
25
+ * 1. コマンドフラグ (--url / --api-key / --project-key)
26
+ * 2. 設定ファイルのプロファイル (--profile または default)
27
+ * 3. 環境変数 (BACKLOG_BASE_URL / BACKLOG_API_KEY / BACKLOG_PROJECT_KEY)
28
+ */
29
+ export declare function resolveCredentials(opts: ResolveOptions): Credentials;
package/dist/config.js ADDED
@@ -0,0 +1,39 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ const CONFIG_PATH = join(homedir(), '.config', 'backlog-cli', 'config.json');
5
+ export function readConfig() {
6
+ if (!existsSync(CONFIG_PATH))
7
+ return { profiles: {} };
8
+ try {
9
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
10
+ }
11
+ catch {
12
+ return { profiles: {} };
13
+ }
14
+ }
15
+ export function writeConfig(config) {
16
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
17
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
18
+ }
19
+ /**
20
+ * 認証情報を以下の優先順位で解決する:
21
+ * 1. コマンドフラグ (--url / --api-key / --project-key)
22
+ * 2. 設定ファイルのプロファイル (--profile または default)
23
+ * 3. 環境変数 (BACKLOG_BASE_URL / BACKLOG_API_KEY / BACKLOG_PROJECT_KEY)
24
+ */
25
+ export function resolveCredentials(opts) {
26
+ const config = readConfig();
27
+ const profileName = opts.profile ?? config.default;
28
+ const profileData = profileName ? config.profiles[profileName] : undefined;
29
+ const baseUrl = opts.url ?? profileData?.baseUrl ?? process.env.BACKLOG_BASE_URL;
30
+ const apiKey = opts.apiKey ?? profileData?.apiKey ?? process.env.BACKLOG_API_KEY;
31
+ const projectKey = opts.projectKey ?? profileData?.projectKey ?? process.env.BACKLOG_PROJECT_KEY;
32
+ if (!baseUrl)
33
+ throw new Error('Backlog URL が未設定です。--url フラグ、プロファイル、または BACKLOG_BASE_URL 環境変数を設定してください。');
34
+ if (!apiKey)
35
+ throw new Error('API キーが未設定です。--api-key フラグ、プロファイル、または BACKLOG_API_KEY 環境変数を設定してください。');
36
+ if (!projectKey)
37
+ throw new Error('プロジェクトキーが未設定です。--project-key フラグ、プロファイル、または BACKLOG_PROJECT_KEY 環境変数を設定してください。');
38
+ return { baseUrl: baseUrl.replace(/\/$/, ''), apiKey, projectKey };
39
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { Cli } from 'incur';
3
+ declare const cli: Cli.Cli<{}, undefined, undefined>;
4
+ export default cli;
package/dist/index.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { Cli } from 'incur';
3
+ import { issueCli } from './commands/issue.js';
4
+ import { profileCli } from './commands/profile.js';
5
+ import { projectCli } from './commands/project.js';
6
+ import { runSetup, setupOptions } from './commands/setup.js';
7
+ import { loadWhoami } from './commands/whoami.js';
8
+ import { authOptions } from './shared.js';
9
+ const cli = Cli.create('backlog', {
10
+ version: '0.1.0',
11
+ description: 'Backlog API CLI — setup/profile、課題・プロジェクト管理',
12
+ });
13
+ cli
14
+ .command('setup', {
15
+ description: '初回セットアップ向けにプロファイルを保存',
16
+ options: setupOptions,
17
+ run(c) {
18
+ return runSetup(c.options);
19
+ },
20
+ })
21
+ .command('whoami', {
22
+ description: '認証ユーザー情報を取得',
23
+ options: authOptions,
24
+ async run(c) {
25
+ return loadWhoami(c.options);
26
+ },
27
+ })
28
+ .command(issueCli)
29
+ .command(projectCli)
30
+ .command(profileCli)
31
+ .serve();
32
+ export default cli;
@@ -0,0 +1,8 @@
1
+ import { z } from 'incur';
2
+ /** 全コマンド共通の認証オプション */
3
+ export declare const authOptions: z.ZodObject<{
4
+ profile: z.ZodOptional<z.ZodString>;
5
+ url: z.ZodOptional<z.ZodString>;
6
+ apiKey: z.ZodOptional<z.ZodString>;
7
+ projectKey: z.ZodOptional<z.ZodString>;
8
+ }, z.core.$strip>;
package/dist/shared.js ADDED
@@ -0,0 +1,8 @@
1
+ import { z } from 'incur';
2
+ /** 全コマンド共通の認証オプション */
3
+ export const authOptions = z.object({
4
+ profile: z.string().optional().describe('使用するプロファイル名'),
5
+ url: z.string().optional().describe('Backlog スペース URL(上書き)'),
6
+ apiKey: z.string().optional().describe('API キー(上書き)'),
7
+ projectKey: z.string().optional().describe('プロジェクトキー(上書き)'),
8
+ });
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@niiiiiiile/iw-backlog-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for Nulab Backlog issue and project management",
5
+ "type": "module",
6
+ "bin": {
7
+ "backlog": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ ".env.example"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsx src/index.ts",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "backlog",
22
+ "nulab",
23
+ "cli",
24
+ "issue",
25
+ "project-management"
26
+ ],
27
+ "author": "Teruaki Iwane",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Niiiiile/backlog-cli.git"
32
+ },
33
+ "homepage": "https://github.com/Niiiiile/backlog-cli#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/Niiiiile/backlog-cli/issues"
36
+ },
37
+ "engines": {
38
+ "node": ">=20.0.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "incur": "^0.3.25"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.0.0",
48
+ "tsx": "^4.19.0",
49
+ "typescript": "^5.8.0"
50
+ }
51
+ }