jira-subtask-cli 1.0.1
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 +152 -0
- package/bin/jira-subtask.js +113 -0
- package/package.json +31 -0
- package/src/config.js +39 -0
- package/src/jira.js +57 -0
- package/templates/config.default.json +7 -0
- package/templates/tasks.default.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sayali
|
|
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,152 @@
|
|
|
1
|
+
# Jira Subtask CLI
|
|
2
|
+
|
|
3
|
+
A lightweight **Node.js CLI utility** to automatically create standard **DEV / QA / UX Jira sub-tasks** for a parent Jira issue — without requiring Jira Automation or admin access.
|
|
4
|
+
|
|
5
|
+
This tool is designed to save time for **developers and testers** by eliminating repetitive manual sub-task creation.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- Create multiple Jira sub-tasks using **one command**
|
|
12
|
+
- Works without **Jira Automation** or Jira admin permissions
|
|
13
|
+
- Globally installable via `npm`
|
|
14
|
+
- User-editable configuration and task templates
|
|
15
|
+
- Cross-platform support (Windows / macOS / Linux)
|
|
16
|
+
- Uses Jira **Cloud REST APIs**
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📋 Default Sub-tasks Created
|
|
21
|
+
|
|
22
|
+
By default, the utility creates the following sub-tasks:
|
|
23
|
+
|
|
24
|
+
- DEV – Story Analysis
|
|
25
|
+
- DEV – Code Changes
|
|
26
|
+
- DEV – Unit Testing
|
|
27
|
+
- DEV – Dev Testing
|
|
28
|
+
- QA – Functional Testing
|
|
29
|
+
- DEV – Buddy Testing *(unassigned)*
|
|
30
|
+
- UX Review *(unassigned)*
|
|
31
|
+
|
|
32
|
+
> Sub-tasks can be freely customized by editing `tasks.json`.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 🧰 Prerequisites
|
|
37
|
+
|
|
38
|
+
- **Node.js 18+**
|
|
39
|
+
- **npm**
|
|
40
|
+
- Jira **Cloud** account (`*.atlassian.net`)
|
|
41
|
+
- Jira **API Token**
|
|
42
|
+
|
|
43
|
+
Check versions:
|
|
44
|
+
```bash
|
|
45
|
+
node -v
|
|
46
|
+
npm -v
|
|
47
|
+
|
|
48
|
+
🚀 Installation (Global)
|
|
49
|
+
|
|
50
|
+
Install directly fusing below command:
|
|
51
|
+
npm install -g https://github.com/sayaligp/Jira_Subtask_Utility.git
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
Verify installation:
|
|
55
|
+
jira-subtask
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
Expected output:
|
|
59
|
+
❌ Usage: jira-subtask <ISSUE-KEY> [--dev-only | --qa-only | --skip-ux]
|
|
60
|
+
|
|
61
|
+
⚙️ First-time Setup
|
|
62
|
+
On first execution, the utility automatically creates user-specific files:
|
|
63
|
+
|
|
64
|
+
jira-subtask ABC-1
|
|
65
|
+
|
|
66
|
+
This creates:
|
|
67
|
+
|
|
68
|
+
~/.jira-subtask/
|
|
69
|
+
├── config.json
|
|
70
|
+
└── tasks.json
|
|
71
|
+
|
|
72
|
+
🔐 Configure Jira Credentials
|
|
73
|
+
|
|
74
|
+
Open the config file:
|
|
75
|
+
|
|
76
|
+
jira-subtask config
|
|
77
|
+
|
|
78
|
+
Update it with your Jira details:
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
"baseUrl": "https://yourcompany.atlassian.net",
|
|
82
|
+
"email": "your.email@company.com",
|
|
83
|
+
"apiToken": "YOUR_JIRA_API_TOKEN",
|
|
84
|
+
"projectId": "your project id"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Configuration notes
|
|
88
|
+
|
|
89
|
+
email → Jira login email
|
|
90
|
+
apiToken → Jira API token (NOT your password)
|
|
91
|
+
projectId → Jira project ID (not project key)
|
|
92
|
+
|
|
93
|
+
Create an API token here:
|
|
94
|
+
https://id.atlassian.com/manage-profile/security/api-tokens
|
|
95
|
+
|
|
96
|
+
📝 Customize Sub-tasks (Optional)
|
|
97
|
+
Open task template file:
|
|
98
|
+
jira-subtask tasks
|
|
99
|
+
|
|
100
|
+
Each task entry looks like:
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
"type": "DEV",
|
|
104
|
+
"summary": "[DEV] Code Changes",
|
|
105
|
+
"description": "Implement required code changes",
|
|
106
|
+
"assignee": "SELF"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Assignee behavior
|
|
110
|
+
"SELF" → assigned to the current user
|
|
111
|
+
"NONE" → created unassigned
|
|
112
|
+
|
|
113
|
+
▶️ Usage
|
|
114
|
+
Create all sub-tasks (DEV + QA + UX)
|
|
115
|
+
jira-subtask ABC-123
|
|
116
|
+
|
|
117
|
+
Create DEV + UX sub-tasks only
|
|
118
|
+
jira-subtask ABC-123 --dev-only
|
|
119
|
+
|
|
120
|
+
Create QA sub-tasks only
|
|
121
|
+
jira-subtask ABC-123 --qa-only
|
|
122
|
+
|
|
123
|
+
Create DEV sub-tasks only (without UX)
|
|
124
|
+
jira-subtask ABC-123 --skip-ux
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
⚠️ Only one flag at a time is supported.
|
|
128
|
+
|
|
129
|
+
🧪 Sample Output
|
|
130
|
+
Fetching Jira account...
|
|
131
|
+
🚀 Creating 6 sub-tasks for ABC-123
|
|
132
|
+
✅ ABC-201 → [DEV] Code Changes
|
|
133
|
+
✅ ABC-202 → [QA] Functional Testing
|
|
134
|
+
🎉 Done!
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
🧯 Troubleshooting
|
|
138
|
+
❌ 401 Unauthorized
|
|
139
|
+
Ensure API token is correct
|
|
140
|
+
Email must match Jira login email
|
|
141
|
+
Verify credentials using:
|
|
142
|
+
curl -u email:token https://yourcompany.atlassian.net/rest/api/3/myself
|
|
143
|
+
|
|
144
|
+
🔐 Security
|
|
145
|
+
No credentials are committed to the repository
|
|
146
|
+
Each user uses their own Jira API token
|
|
147
|
+
Tokens are stored locally on the user’s machine
|
|
148
|
+
|
|
149
|
+
📌 Notes
|
|
150
|
+
Works only with Jira Cloud
|
|
151
|
+
No Jira admin permissions required
|
|
152
|
+
Intended for internal team productivity
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { exec } from "child_process";
|
|
4
|
+
import { ensureUserFiles, loadConfig, loadTasks, CONFIG_PATH, TASKS_PATH } from "../src/config.js";
|
|
5
|
+
import { getMyAccountId, createSubtask } from "../src/jira.js";
|
|
6
|
+
|
|
7
|
+
// Ensure user files exist
|
|
8
|
+
ensureUserFiles();
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ------------------------------------
|
|
14
|
+
* HELPER COMMANDS (NEW)
|
|
15
|
+
* ------------------------------------
|
|
16
|
+
*/
|
|
17
|
+
if (args[0] === "config") {
|
|
18
|
+
const editor = process.env.EDITOR || "notepad";
|
|
19
|
+
exec(`"${editor}" "${CONFIG_PATH}"`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (args[0] === "tasks") {
|
|
24
|
+
const editor = process.env.EDITOR || "notepad";
|
|
25
|
+
exec(`"${editor}" "${TASKS_PATH}"`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const parentKey = args[0];
|
|
31
|
+
|
|
32
|
+
const devOnly = args.includes("--dev-only");
|
|
33
|
+
const qaOnly = args.includes("--qa-only");
|
|
34
|
+
const skipUx = args.includes("--skip-ux");
|
|
35
|
+
|
|
36
|
+
// Validate input
|
|
37
|
+
if (!parentKey) {
|
|
38
|
+
console.error(
|
|
39
|
+
"Usage: jira-subtask <ISSUE-KEY> [--dev-only | --qa-only | --skip-ux]"
|
|
40
|
+
);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Only one flag allowed
|
|
45
|
+
const flagCount = [devOnly, qaOnly, skipUx].filter(Boolean).length;
|
|
46
|
+
if (flagCount > 1) {
|
|
47
|
+
console.error(
|
|
48
|
+
"Only one flag is allowed: --dev-only OR --qa-only OR --skip-ux"
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
const tasks = loadTasks();
|
|
55
|
+
|
|
56
|
+
// --------------------
|
|
57
|
+
// FINAL FILTER LOGIC
|
|
58
|
+
// --------------------
|
|
59
|
+
let filteredTasks;
|
|
60
|
+
|
|
61
|
+
if (devOnly) {
|
|
62
|
+
// DEV + UX
|
|
63
|
+
filteredTasks = tasks.filter(
|
|
64
|
+
t => t.type === "DEV" || t.type === "UX"
|
|
65
|
+
);
|
|
66
|
+
} else if (qaOnly) {
|
|
67
|
+
// QA only
|
|
68
|
+
filteredTasks = tasks.filter(t => t.type === "QA");
|
|
69
|
+
} else if (skipUx) {
|
|
70
|
+
// DEV only
|
|
71
|
+
filteredTasks = tasks.filter(t => t.type === "DEV");
|
|
72
|
+
} else {
|
|
73
|
+
// DEV + QA + UX
|
|
74
|
+
filteredTasks = tasks;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (filteredTasks.length === 0) {
|
|
78
|
+
console.log("No tasks to create.");
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --------------------
|
|
83
|
+
// Execute
|
|
84
|
+
// --------------------
|
|
85
|
+
(async () => {
|
|
86
|
+
try {
|
|
87
|
+
console.log("Fetching Jira account...");
|
|
88
|
+
const myAccountId = await getMyAccountId();
|
|
89
|
+
|
|
90
|
+
console.log(
|
|
91
|
+
`Creating ${filteredTasks.length} sub-tasks for ${parentKey}`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
for (const task of filteredTasks) {
|
|
95
|
+
let assigneeId = null;
|
|
96
|
+
if(task.assignee === "SELF"){
|
|
97
|
+
assigneeId = myAccountId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
const key = await createSubtask(parentKey, task, assigneeId);
|
|
102
|
+
console.log(`${key} → ${task.summary}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log("Done!");
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(
|
|
108
|
+
"Error:",
|
|
109
|
+
err.response?.data?.errorMessages || err.message
|
|
110
|
+
);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jira-subtask-cli",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "CLI to auto-create Jira sub-tasks",
|
|
5
|
+
"bin": {
|
|
6
|
+
"jira-subtask": "./bin/jira-subtask.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"axios": "^1.6.0"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"templates"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"jira",
|
|
19
|
+
"cli",
|
|
20
|
+
"automation"
|
|
21
|
+
],
|
|
22
|
+
"author": "Sayali Pokharkar",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/sayaligp/Jira_Subtask_Utility.git"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
const BASE_DIR = path.join(os.homedir(), ".jira-subtask");
|
|
9
|
+
const CONFIG_PATH = path.join(BASE_DIR, "config.json");
|
|
10
|
+
const TASKS_PATH = path.join(BASE_DIR, "tasks.json");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG = path.join(__dirname, "../templates/config.default.json");
|
|
13
|
+
const DEFAULT_TASKS = path.join(__dirname, "../templates/tasks.default.json");
|
|
14
|
+
|
|
15
|
+
export function ensureUserFiles() {
|
|
16
|
+
if (!fs.existsSync(BASE_DIR)) {
|
|
17
|
+
fs.mkdirSync(BASE_DIR);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
21
|
+
fs.copyFileSync(DEFAULT_CONFIG, CONFIG_PATH);
|
|
22
|
+
console.log("📄 config.json created at ~/.jira-subtask (please update it)");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(TASKS_PATH)) {
|
|
26
|
+
fs.copyFileSync(DEFAULT_TASKS, TASKS_PATH);
|
|
27
|
+
console.log("📄 tasks.json created at ~/.jira-subtask (editable)");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function loadConfig() {
|
|
32
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function loadTasks() {
|
|
36
|
+
return JSON.parse(fs.readFileSync(TASKS_PATH, "utf-8"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { CONFIG_PATH, TASKS_PATH };
|
package/src/jira.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
|
|
4
|
+
export async function getMyAccountId() {
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
|
|
7
|
+
const res = await axios.get(
|
|
8
|
+
`${config.baseUrl}/rest/api/3/myself`,
|
|
9
|
+
{
|
|
10
|
+
auth: {
|
|
11
|
+
username: config.email,
|
|
12
|
+
password: config.apiToken
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return res.data.accountId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function createSubtask(parentKey, task, assigneeId) {
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const fields= {
|
|
25
|
+
project: { id: config.projectId },
|
|
26
|
+
parent: { key: parentKey },
|
|
27
|
+
issuetype: { name: "Sub-task" },
|
|
28
|
+
summary: task.summary,
|
|
29
|
+
description: {
|
|
30
|
+
type: "doc",
|
|
31
|
+
version: 1,
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "paragraph",
|
|
35
|
+
content: [{ type: "text", text: task.description }]
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if(assigneeId){
|
|
42
|
+
fields.assignee= { id: assigneeId }
|
|
43
|
+
}
|
|
44
|
+
const payload = {fields};
|
|
45
|
+
const res = await axios.post(
|
|
46
|
+
`${config.baseUrl}/rest/api/3/issue`,
|
|
47
|
+
payload,
|
|
48
|
+
{
|
|
49
|
+
auth: {
|
|
50
|
+
username: config.email,
|
|
51
|
+
password: config.apiToken
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return res.data.key;
|
|
57
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"type": "DEV",
|
|
4
|
+
"summary": "[DEV] Story Analysis",
|
|
5
|
+
"description": "Analyze requirements and acceptance criteria",
|
|
6
|
+
"assignee": "SELF"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "DEV",
|
|
10
|
+
"summary": "[DEV] Code Changes",
|
|
11
|
+
"description": "Implement and commit required code changes",
|
|
12
|
+
"assignee": "SELF"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"type": "DEV",
|
|
16
|
+
"summary": "[DEV] Unit Testing",
|
|
17
|
+
"description": "Write and execute unit tests",
|
|
18
|
+
"assignee": "SELF"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "DEV",
|
|
22
|
+
"summary": "[DEV] Dev Testing",
|
|
23
|
+
"description": "Perform functional testing",
|
|
24
|
+
"assignee": "SELF"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "DEV",
|
|
28
|
+
"summary": "[DEV] Buddy Testing",
|
|
29
|
+
"description": "Buddy testing of implemented changes",
|
|
30
|
+
"assignee": "NONE"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"type": "QA",
|
|
34
|
+
"summary": "[QA] Test cases preparation",
|
|
35
|
+
"description": "Test cases preparation",
|
|
36
|
+
"assignee": "QA"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"type": "UX",
|
|
40
|
+
"summary": "UX Review",
|
|
41
|
+
"description": "Final UX review and validation",
|
|
42
|
+
"assignee": "None"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
|