@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.
@@ -0,0 +1,24 @@
1
+ import { config } from 'dotenv';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /**
5
+ * 1. カレントディレクトリの `.env`(上書きなし=シェルや既存 env を尊重)
6
+ * 2. エントリと同じディレクトリの `.env`(`node dist/cli.js` なら `dist/.env`)を override で反映
7
+ * 3. `tsx src/cli.ts` のときは `dist/.env` をさらに override で読み、`dist` の値が最優先
8
+ */
9
+ /** dotenv 後に、よくある typo `IRA_HOST` を `JIRA_HOST` に寄せる */
10
+ function aliasIraHost() {
11
+ const j = process.env.JIRA_HOST?.trim();
12
+ const i = process.env.IRA_HOST?.trim();
13
+ if (!j && i)
14
+ process.env.JIRA_HOST = i;
15
+ }
16
+ export function loadDotenvFiles() {
17
+ const cliDir = dirname(fileURLToPath(import.meta.url));
18
+ config({ path: join(process.cwd(), '.env') });
19
+ config({ path: join(cliDir, '.env'), override: true });
20
+ if (basename(cliDir) === 'src') {
21
+ config({ path: join(cliDir, '..', 'dist', '.env'), override: true });
22
+ }
23
+ aliasIraHost();
24
+ }
@@ -0,0 +1,7 @@
1
+ type OutputContext = {
2
+ agent: boolean;
3
+ format: string;
4
+ };
5
+ export declare function finalizeOutput<T>(context: OutputContext, data: T): unknown;
6
+ export declare function finalizeCompactOutput<T>(context: OutputContext, full: T, slim: (value: T) => unknown): unknown;
7
+ export {};
package/dist/output.js ADDED
@@ -0,0 +1,9 @@
1
+ import { wantCompact } from './agent-compact.js';
2
+ import { outJsonlIfNeeded } from './jsonl-lines.js';
3
+ export function finalizeOutput(context, data) {
4
+ return outJsonlIfNeeded(data, context.format);
5
+ }
6
+ export function finalizeCompactOutput(context, full, slim) {
7
+ const data = wantCompact(context.agent) ? slim(full) : full;
8
+ return finalizeOutput(context, data);
9
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 課題キー(WEC-41)または Jira の課題 URL から課題キーを得る。
3
+ * browse URL・selectedIssue・パス末尾のキーなどに対応。
4
+ */
5
+ export declare function parseIssueKey(input: string): string;
6
+ /**
7
+ * プロジェクトキー(WEC)・課題キー(WEC-41 なら WEC 部分)・ボード/プロジェクト URL からプロジェクトキーを得る。
8
+ */
9
+ export declare function parseProjectRef(input: string): string;
@@ -0,0 +1,97 @@
1
+ /** /projects/KEY/ … からプロジェクトキーを取る */
2
+ function projectKeyFromPath(pathname) {
3
+ const m = pathname.match(/\/projects\/([A-Za-z][A-Za-z0-9]*)(?:\/|$)/);
4
+ return m ? m[1].toUpperCase() : null;
5
+ }
6
+ function withHttpsIfHostLooksLikeAtlassian(s) {
7
+ const t = s.trim();
8
+ if (/^https?:\/\//i.test(t))
9
+ return t;
10
+ if (/\.atlassian\.net/i.test(t))
11
+ return `https://${t.replace(/^\/+/, '')}`;
12
+ return t;
13
+ }
14
+ /**
15
+ * 課題キー(WEC-41)または Jira の課題 URL から課題キーを得る。
16
+ * browse URL・selectedIssue・パス末尾のキーなどに対応。
17
+ */
18
+ export function parseIssueKey(input) {
19
+ const raw = input.trim();
20
+ if (!raw) {
21
+ throw new Error('課題キーまたは URL が空です');
22
+ }
23
+ const fromSelected = raw.match(/[?&]selectedIssue=([A-Za-z][A-Za-z0-9]*-\d+)/i);
24
+ if (fromSelected) {
25
+ return normalizeIssueKey(fromSelected[1]);
26
+ }
27
+ const fromBrowse = raw.match(/\/browse\/([A-Za-z][A-Za-z0-9]*-\d+)/i);
28
+ if (fromBrowse) {
29
+ return normalizeIssueKey(fromBrowse[1]);
30
+ }
31
+ const bare = raw.match(/^([A-Za-z][A-Za-z0-9]*)-(\d+)$/);
32
+ if (bare) {
33
+ return `${bare[1].toUpperCase()}-${bare[2]}`;
34
+ }
35
+ const asUrl = withHttpsIfHostLooksLikeAtlassian(raw);
36
+ try {
37
+ const u = new URL(asUrl);
38
+ const browse = u.pathname.match(/\/browse\/([A-Za-z][A-Za-z0-9]*-\d+)/i);
39
+ if (browse) {
40
+ return normalizeIssueKey(browse[1]);
41
+ }
42
+ const sel = u.searchParams.get('selectedIssue');
43
+ if (sel && /^[A-Za-z][A-Za-z0-9]*-\d+$/i.test(sel)) {
44
+ return normalizeIssueKey(sel);
45
+ }
46
+ const seg = u.pathname.split('/').filter(Boolean);
47
+ const last = seg[seg.length - 1];
48
+ if (last && /^[A-Za-z][A-Za-z0-9]*-\d+$/i.test(last)) {
49
+ return normalizeIssueKey(last);
50
+ }
51
+ }
52
+ catch {
53
+ /* 相対パス等は下で失敗 */
54
+ }
55
+ throw new Error(`課題キー(例: WEC-41)または課題 URL(.../browse/WEC-41 等)として解釈できません: ${raw.slice(0, 120)}${raw.length > 120 ? '…' : ''}`);
56
+ }
57
+ function normalizeIssueKey(key) {
58
+ const m = key.match(/^([A-Za-z][A-Za-z0-9]*)-(\d+)$/i);
59
+ if (!m)
60
+ return key.toUpperCase();
61
+ return `${m[1].toUpperCase()}-${m[2]}`;
62
+ }
63
+ /**
64
+ * プロジェクトキー(WEC)・課題キー(WEC-41 なら WEC 部分)・ボード/プロジェクト URL からプロジェクトキーを得る。
65
+ */
66
+ export function parseProjectRef(input) {
67
+ const raw = input.trim();
68
+ if (!raw) {
69
+ throw new Error('プロジェクトキーまたは URL が空です');
70
+ }
71
+ const asUrl = withHttpsIfHostLooksLikeAtlassian(raw);
72
+ try {
73
+ const u = new URL(asUrl);
74
+ const fromPath = projectKeyFromPath(u.pathname);
75
+ if (fromPath)
76
+ return fromPath;
77
+ const pk = u.searchParams.get('projectKey');
78
+ if (pk && /^[A-Za-z][A-Za-z0-9]*$/i.test(pk)) {
79
+ return pk.toUpperCase();
80
+ }
81
+ }
82
+ catch {
83
+ /* 続行 */
84
+ }
85
+ const fromPathLoose = projectKeyFromPath(raw);
86
+ if (fromPathLoose)
87
+ return fromPathLoose;
88
+ const fromIssue = raw.match(/^([A-Za-z][A-Za-z0-9]*)-\d+$/i);
89
+ if (fromIssue) {
90
+ return fromIssue[1].toUpperCase();
91
+ }
92
+ const bare = raw.match(/^([A-Za-z][A-Za-z0-9]*)$/i);
93
+ if (bare) {
94
+ return bare[1].toUpperCase();
95
+ }
96
+ throw new Error(`プロジェクトキー(例: WEC)またはプロジェクト URL(.../projects/WEC/...)として解釈できません: ${raw.slice(0, 120)}${raw.length > 120 ? '…' : ''}`);
97
+ }
@@ -0,0 +1,2 @@
1
+ import { Cli } from 'incur';
2
+ export declare const profileCli: Cli.Cli<{}, undefined, undefined>;
@@ -0,0 +1,105 @@
1
+ import { Cli, z } from 'incur';
2
+ import { readConfig, writeConfig } from './config.js';
3
+ export const profileCli = Cli.create('profile', {
4
+ description: 'プロファイル管理(Jira サイト接続情報)',
5
+ });
6
+ profileCli.command('list', {
7
+ description: '設定済みプロファイルの一覧を表示',
8
+ examples: [{ description: '登録済みプロファイルとデフォルトを確認' }],
9
+ run() {
10
+ const config = readConfig();
11
+ const profiles = Object.entries(config.profiles).map(([name, profile]) => ({
12
+ name,
13
+ host: profile.host,
14
+ email: profile.email,
15
+ isDefault: name === config.default,
16
+ }));
17
+ return {
18
+ default: config.default ?? null,
19
+ profiles,
20
+ };
21
+ },
22
+ });
23
+ profileCli.command('add', {
24
+ description: 'プロファイルを追加・更新',
25
+ args: z.object({
26
+ name: z.string().describe('プロファイル名(例: work, personal)'),
27
+ }),
28
+ options: z.object({
29
+ host: z.string().describe('Jira サイトホストまたは URL'),
30
+ email: z.string().email().describe('Atlassian アカウントのメール'),
31
+ apiToken: z.string().describe('Jira API トークン'),
32
+ default: z.boolean().optional().describe('このプロファイルをデフォルトに設定'),
33
+ }),
34
+ examples: [
35
+ {
36
+ args: { name: 'work' },
37
+ options: {
38
+ host: 'your-company.atlassian.net',
39
+ email: 'user@example.com',
40
+ apiToken: 'YOUR_API_TOKEN',
41
+ },
42
+ description: 'work プロファイルを追加(初回登録時は自動でデフォルトになる)',
43
+ },
44
+ ],
45
+ run(c) {
46
+ const config = readConfig();
47
+ config.profiles[c.args.name] = {
48
+ host: c.options.host.trim(),
49
+ email: c.options.email.trim(),
50
+ apiToken: c.options.apiToken.trim(),
51
+ };
52
+ const isFirst = Object.keys(config.profiles).length === 1;
53
+ if (c.options.default || isFirst || !config.default) {
54
+ config.default = c.args.name;
55
+ }
56
+ writeConfig(config);
57
+ return {
58
+ added: c.args.name,
59
+ isDefault: config.default === c.args.name,
60
+ };
61
+ },
62
+ });
63
+ profileCli.command('remove', {
64
+ description: 'プロファイルを削除',
65
+ args: z.object({
66
+ name: z.string().describe('削除するプロファイル名'),
67
+ }),
68
+ run(c) {
69
+ const config = readConfig();
70
+ if (!config.profiles[c.args.name]) {
71
+ return c.error({
72
+ code: 'PROFILE_NOT_FOUND',
73
+ message: `プロファイル "${c.args.name}" が見つかりません`,
74
+ retryable: false,
75
+ cta: { commands: ['iw-jira-cli profile list'] },
76
+ });
77
+ }
78
+ delete config.profiles[c.args.name];
79
+ if (config.default === c.args.name) {
80
+ config.default = Object.keys(config.profiles)[0];
81
+ }
82
+ writeConfig(config);
83
+ return { removed: c.args.name, newDefault: config.default ?? null };
84
+ },
85
+ });
86
+ profileCli.command('use', {
87
+ description: 'デフォルトプロファイルを変更',
88
+ args: z.object({
89
+ name: z.string().describe('デフォルトに設定するプロファイル名'),
90
+ }),
91
+ run(c) {
92
+ const config = readConfig();
93
+ if (!config.profiles[c.args.name]) {
94
+ return c.error({
95
+ code: 'PROFILE_NOT_FOUND',
96
+ message: `プロファイル "${c.args.name}" が見つかりません`,
97
+ retryable: false,
98
+ cta: { commands: ['iw-jira-cli profile list'] },
99
+ });
100
+ }
101
+ config.default = c.args.name;
102
+ writeConfig(config);
103
+ return { default: c.args.name };
104
+ },
105
+ });
@@ -0,0 +1,14 @@
1
+ import { Cli } from 'incur';
2
+ export declare const projectCli: Cli.Cli<{
3
+ list: {
4
+ args: {};
5
+ options: {
6
+ limit: number;
7
+ profile?: string | undefined;
8
+ host?: string | undefined;
9
+ email?: string | undefined;
10
+ apiToken?: string | undefined;
11
+ query?: string | undefined;
12
+ };
13
+ };
14
+ }, undefined, undefined>;
@@ -0,0 +1,36 @@
1
+ import { Cli, z } from 'incur';
2
+ import { slimProjects } from './agent-compact.js';
3
+ import { resolveCredentials } from './config.js';
4
+ import { jiraRequest } from './jira-client.js';
5
+ import { finalizeCompactOutput } from './output.js';
6
+ import { authOptions } from './shared.js';
7
+ export const projectCli = Cli.create('project', {
8
+ description: 'プロジェクトの検索・一覧(非TTY 時は圧縮出力)',
9
+ }).command('list', {
10
+ description: 'アクセス可能なプロジェクトを検索',
11
+ options: z.object({
12
+ query: z.string().optional().describe('名前・キーに対する部分一致(省略時は広く取得)'),
13
+ limit: z.coerce.number().int().min(1).max(100).default(50).describe('最大件数'),
14
+ ...authOptions.shape,
15
+ }),
16
+ output: z.any(),
17
+ async run(c) {
18
+ const q = new URLSearchParams();
19
+ const creds = resolveCredentials(c.options);
20
+ q.set('maxResults', String(c.options.limit));
21
+ if (c.options.query !== undefined && c.options.query.length > 0) {
22
+ q.set('query', c.options.query);
23
+ }
24
+ const res = await jiraRequest(creds, `/project/search?${q.toString()}`);
25
+ const full = {
26
+ count: res.values.length,
27
+ projects: res.values.map((p) => ({
28
+ key: p.key,
29
+ id: p.id,
30
+ name: p.name,
31
+ projectTypeKey: p.projectTypeKey,
32
+ })),
33
+ };
34
+ return finalizeCompactOutput(c, full, (data) => slimProjects(data, true));
35
+ },
36
+ });
@@ -0,0 +1,8 @@
1
+ import { z } from 'incur';
2
+ /** 全コマンド共通の認証オプション */
3
+ export declare const authOptions: z.ZodObject<{
4
+ profile: z.ZodOptional<z.ZodString>;
5
+ host: z.ZodOptional<z.ZodString>;
6
+ email: z.ZodOptional<z.ZodString>;
7
+ apiToken: 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
+ host: z.string().optional().describe('Jira サイトホストまたは URL(上書き)'),
6
+ email: z.string().optional().describe('Atlassian アカウントのメール(上書き)'),
7
+ apiToken: z.string().optional().describe('Jira API トークン(上書き)'),
8
+ });
@@ -0,0 +1,15 @@
1
+ import { Cli } from 'incur';
2
+ export declare const userCli: Cli.Cli<{
3
+ search: {
4
+ args: {
5
+ query: string;
6
+ };
7
+ options: {
8
+ limit: number;
9
+ profile?: string | undefined;
10
+ host?: string | undefined;
11
+ email?: string | undefined;
12
+ apiToken?: string | undefined;
13
+ };
14
+ };
15
+ }, undefined, undefined>;
@@ -0,0 +1,37 @@
1
+ import { Cli, z } from 'incur';
2
+ import { slimUsers } from './agent-compact.js';
3
+ import { resolveCredentials } from './config.js';
4
+ import { jiraRequest } from './jira-client.js';
5
+ import { finalizeCompactOutput } from './output.js';
6
+ import { authOptions } from './shared.js';
7
+ export const userCli = Cli.create('user', {
8
+ description: 'ユーザー検索(メンション用 accountId の確認など)',
9
+ }).command('search', {
10
+ description: '表示名・メールの部分一致で検索(GET /user/search)',
11
+ args: z.object({
12
+ query: z.string().min(1).describe('検索語(メール・名前の一部)'),
13
+ }),
14
+ options: z.object({
15
+ limit: z.coerce.number().int().min(1).max(50).default(15).describe('最大件数'),
16
+ ...authOptions.shape,
17
+ }),
18
+ output: z.any(),
19
+ async run(c) {
20
+ const creds = resolveCredentials(c.options);
21
+ const users = await jiraRequest(creds, `/user/search?query=${encodeURIComponent(c.args.query)}&maxResults=${c.options.limit}`);
22
+ const list = users.map((u) => ({
23
+ accountId: u.accountId,
24
+ displayName: u.displayName ?? '',
25
+ emailAddress: u.emailAddress ?? '',
26
+ active: u.active,
27
+ /** CLI メンション用プレースホルダー例 */
28
+ mention: `@[${u.accountId}]`,
29
+ }));
30
+ const data = {
31
+ count: list.length,
32
+ users: list,
33
+ hint: '本文では @[accountId] または @[email:mail@example.com] でメンションできます',
34
+ };
35
+ return finalizeCompactOutput(c, data, (value) => slimUsers(value, true));
36
+ },
37
+ });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@niiiiiiile/iw-jira-cli",
3
+ "version": "0.5.0",
4
+ "description": "CLI for Jira Cloud issue management with setup, profiles, and compact agent output",
5
+ "type": "module",
6
+ "files": [
7
+ "dist/**/*.js",
8
+ "dist/**/*.d.ts",
9
+ "README.md",
10
+ ".env.example"
11
+ ],
12
+ "bin": {
13
+ "iw-jira-cli": "./dist/cli.js"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsx src/cli.ts",
18
+ "prepare": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "jira",
22
+ "atlassian",
23
+ "cli",
24
+ "issue-tracker",
25
+ "productivity"
26
+ ],
27
+ "author": "Teruaki Iwane",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Niiiiile/jira-cli.git"
32
+ },
33
+ "homepage": "https://github.com/Niiiiile/jira-cli#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/Niiiiile/jira-cli/issues"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "dotenv": "^16.4.7",
45
+ "incur": "^0.3.25"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.10.0",
49
+ "tsx": "^4.19.2",
50
+ "typescript": "^5.7.2"
51
+ }
52
+ }