@rui.branco/jira-mcp 1.6.14 → 1.6.16
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/index.js +180 -7
- package/package.json +1 -1
- package/setup.js +103 -5
package/index.js
CHANGED
|
@@ -184,6 +184,48 @@ async function fetchJiraAgile(endpoint, options = {}, instance = defaultInstance
|
|
|
184
184
|
return text ? JSON.parse(text) : {};
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
async function fetchJiraTeams(endpoint, options = {}, instance = defaultInstance) {
|
|
188
|
+
const { method = "GET", body } = options;
|
|
189
|
+
const headers = {
|
|
190
|
+
Authorization: `Basic ${instance.auth}`,
|
|
191
|
+
Accept: "application/json",
|
|
192
|
+
};
|
|
193
|
+
if (body) {
|
|
194
|
+
headers["Content-Type"] = "application/json";
|
|
195
|
+
}
|
|
196
|
+
const response = await fetch(`${instance.baseUrl}/rest/teams/1.0${endpoint}`, {
|
|
197
|
+
method,
|
|
198
|
+
headers,
|
|
199
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
200
|
+
});
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
const errorBody = await response.text().catch(() => "");
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Jira Teams API error: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const text = await response.text();
|
|
208
|
+
return text ? JSON.parse(text) : {};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function resolveTeamId(teamName, instance) {
|
|
212
|
+
const teams = await fetchJiraTeams(
|
|
213
|
+
`/teams/find?query=${encodeURIComponent(teamName)}&excludeMembers=true`,
|
|
214
|
+
{},
|
|
215
|
+
instance,
|
|
216
|
+
);
|
|
217
|
+
const match = teams.find(
|
|
218
|
+
(t) => t.title.toLowerCase() === teamName.toLowerCase(),
|
|
219
|
+
);
|
|
220
|
+
if (!match) {
|
|
221
|
+
const available = teams.map((t) => t.title).join(", ");
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Team "${teamName}" not found.${available ? ` Similar teams: ${available}` : ""}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return `${match.organizationId}-${match.id}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
187
229
|
async function downloadAttachment(url, filename, issueKey, instance) {
|
|
188
230
|
const issueDir = path.join(attachmentDir, issueKey);
|
|
189
231
|
if (!fs.existsSync(issueDir)) {
|
|
@@ -848,6 +890,13 @@ async function getTicket(issueKey, downloadImages = true, fetchFigma = true, ins
|
|
|
848
890
|
if (fields.resolutiondate) output += `**Resolved:** ${fields.resolutiondate}\n`;
|
|
849
891
|
if (fields.resolution) output += `**Resolution:** ${fields.resolution.name}\n`;
|
|
850
892
|
|
|
893
|
+
// Team
|
|
894
|
+
if (fields.customfield_10001) {
|
|
895
|
+
const teamVal = fields.customfield_10001;
|
|
896
|
+
const teamName = typeof teamVal === "object" ? teamVal.name || teamVal.title || teamVal.value || JSON.stringify(teamVal) : teamVal;
|
|
897
|
+
output += `**Team:** ${teamName}\n`;
|
|
898
|
+
}
|
|
899
|
+
|
|
851
900
|
// Story points
|
|
852
901
|
if (storyPoints != null) output += `**Story Points:** ${storyPoints}\n`;
|
|
853
902
|
|
|
@@ -1477,7 +1526,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1477
1526
|
{
|
|
1478
1527
|
name: "jira_add_comment",
|
|
1479
1528
|
description:
|
|
1480
|
-
"Add a comment to a Jira ticket. IMPORTANT:
|
|
1529
|
+
"Add a comment to a Jira ticket. IMPORTANT RULES: (1) Write comments at a HIGH LEVEL for stakeholders, product owners, and managers. Focus on WHAT was done and the business impact, NOT how it was done. Example: 'Implemented the new filtering feature for the dashboard' instead of 'Added a useEffect hook with debounced API calls to filter endpoint'. (2) NEVER mention technical details: no code, no function names, no file paths, no git branches, no commit hashes, no 'pushed to main', no framework-specific terms (React, Angular, hooks, components, etc.). (3) Use @DisplayName (e.g. @Julia Pereszta) for mentions, NOT [~accountId:...] syntax. (4) NEVER use em dashes (—) or en dashes (–), use commas, periods, or rewrite the sentence instead.",
|
|
1481
1530
|
inputSchema: {
|
|
1482
1531
|
type: "object",
|
|
1483
1532
|
properties: {
|
|
@@ -1646,6 +1695,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1646
1695
|
items: { type: "string" },
|
|
1647
1696
|
description: "Labels to set on the ticket",
|
|
1648
1697
|
},
|
|
1698
|
+
team: {
|
|
1699
|
+
type: "string",
|
|
1700
|
+
description: "Team name to assign (e.g., 'Site Surveys (MODS)'). Resolved automatically via Jira Teams API.",
|
|
1701
|
+
},
|
|
1649
1702
|
instance: {
|
|
1650
1703
|
type: "string",
|
|
1651
1704
|
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
@@ -1740,6 +1793,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1740
1793
|
type: "boolean",
|
|
1741
1794
|
description: "Set this instance as the default (default: false)",
|
|
1742
1795
|
},
|
|
1796
|
+
defaultTeam: {
|
|
1797
|
+
type: "string",
|
|
1798
|
+
description: "Default team name to auto-assign when creating tickets (e.g., 'Site Surveys (MODS)'). Resolved and validated via Jira Teams API. Pass empty string to clear.",
|
|
1799
|
+
},
|
|
1743
1800
|
},
|
|
1744
1801
|
required: ["name"],
|
|
1745
1802
|
},
|
|
@@ -1827,6 +1884,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1827
1884
|
type: "number",
|
|
1828
1885
|
description: "Sprint ID to add the ticket to. Use jira_get_sprints to find sprint IDs.",
|
|
1829
1886
|
},
|
|
1887
|
+
team: {
|
|
1888
|
+
type: "string",
|
|
1889
|
+
description: "Team name to assign (e.g., 'Site Surveys (MODS)'). Resolved automatically via Jira Teams API.",
|
|
1890
|
+
},
|
|
1830
1891
|
instance: {
|
|
1831
1892
|
type: "string",
|
|
1832
1893
|
description: "Instance name override. Auto-detected from project key if omitted.",
|
|
@@ -1867,6 +1928,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1867
1928
|
items: { type: "string" },
|
|
1868
1929
|
description: "Labels to set on the subtask",
|
|
1869
1930
|
},
|
|
1931
|
+
team: {
|
|
1932
|
+
type: "string",
|
|
1933
|
+
description: "Team name to assign (e.g., 'Site Surveys (MODS)'). Resolved automatically via Jira Teams API.",
|
|
1934
|
+
},
|
|
1870
1935
|
instance: {
|
|
1871
1936
|
type: "string",
|
|
1872
1937
|
description: "Instance name override. Auto-detected from parent issue key prefix if omitted.",
|
|
@@ -2733,6 +2798,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2733
2798
|
if (args.labels) {
|
|
2734
2799
|
fields.labels = args.labels;
|
|
2735
2800
|
}
|
|
2801
|
+
if (args.team) {
|
|
2802
|
+
fields.customfield_10001 = await resolveTeamId(args.team, inst);
|
|
2803
|
+
}
|
|
2736
2804
|
|
|
2737
2805
|
if (Object.keys(fields).length === 0) {
|
|
2738
2806
|
return {
|
|
@@ -2786,7 +2854,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2786
2854
|
const projects = args.projects ? args.projects.map((p) => p.toUpperCase()) : (existing.projects || []);
|
|
2787
2855
|
const authStr = Buffer.from(`${email}:${token}`).toString("base64");
|
|
2788
2856
|
|
|
2789
|
-
|
|
2857
|
+
// Resolve defaultTeam if provided
|
|
2858
|
+
let defaultTeam = existing.defaultTeam || undefined;
|
|
2859
|
+
if (args.defaultTeam !== undefined) {
|
|
2860
|
+
if (args.defaultTeam === "") {
|
|
2861
|
+
defaultTeam = undefined;
|
|
2862
|
+
} else {
|
|
2863
|
+
// Validate team exists via Jira Teams API
|
|
2864
|
+
const tempInst = { baseUrl, auth: authStr };
|
|
2865
|
+
try {
|
|
2866
|
+
const teamId = await resolveTeamId(args.defaultTeam, tempInst);
|
|
2867
|
+
defaultTeam = { name: args.defaultTeam, id: teamId };
|
|
2868
|
+
} catch (e) {
|
|
2869
|
+
// Try to list available teams for a helpful error
|
|
2870
|
+
try {
|
|
2871
|
+
const teams = await fetchJiraTeams(
|
|
2872
|
+
`/teams/find?query=&excludeMembers=true`,
|
|
2873
|
+
{},
|
|
2874
|
+
tempInst,
|
|
2875
|
+
);
|
|
2876
|
+
const available = teams.map((t) => t.title).join(", ");
|
|
2877
|
+
return {
|
|
2878
|
+
content: [{ type: "text", text: `Team "${args.defaultTeam}" not found. Available teams: ${available}` }],
|
|
2879
|
+
isError: true,
|
|
2880
|
+
};
|
|
2881
|
+
} catch {
|
|
2882
|
+
return {
|
|
2883
|
+
content: [{ type: "text", text: e.message }],
|
|
2884
|
+
isError: true,
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
const newInstance = { name: instName, email, token, baseUrl, projects, auth: authStr, defaultTeam };
|
|
2790
2892
|
|
|
2791
2893
|
// Update in-memory instances
|
|
2792
2894
|
if (isUpdate) {
|
|
@@ -2818,6 +2920,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2818
2920
|
|
|
2819
2921
|
// Save without the computed auth field
|
|
2820
2922
|
const toSave = { name: instName, email, token, baseUrl, projects };
|
|
2923
|
+
if (defaultTeam) toSave.defaultTeam = defaultTeam;
|
|
2821
2924
|
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
2822
2925
|
if (savedIdx >= 0) {
|
|
2823
2926
|
savedConfig.instances[savedIdx] = toSave;
|
|
@@ -2833,6 +2936,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2833
2936
|
let text = `${action} instance "${instName}" (${baseUrl}).`;
|
|
2834
2937
|
if (projects.length > 0) text += ` Projects: ${projects.join(", ")}.`;
|
|
2835
2938
|
if (args.setDefault) text += " Set as default.";
|
|
2939
|
+
if (defaultTeam) {
|
|
2940
|
+
text += ` Default team: ${defaultTeam.name}.`;
|
|
2941
|
+
} else {
|
|
2942
|
+
// No default team — fetch available teams and prompt
|
|
2943
|
+
try {
|
|
2944
|
+
const tempInst = { baseUrl, auth: authStr };
|
|
2945
|
+
const teams = await fetchJiraTeams(
|
|
2946
|
+
`/teams/find?query=&excludeMembers=true`, {}, tempInst,
|
|
2947
|
+
);
|
|
2948
|
+
if (teams && teams.length > 0) {
|
|
2949
|
+
const list = teams.map((t, i) => `${i + 1}. ${t.title}`).join("\n");
|
|
2950
|
+
text += `\n\n⚠ No default team configured. Available teams:\n${list}\n0. None\n\nAsk the user which team to set as default. If they pick one, call jira_add_instance with name="${instName}" and defaultTeam="<team name>".`;
|
|
2951
|
+
}
|
|
2952
|
+
} catch {
|
|
2953
|
+
// Teams API not available, skip
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2836
2956
|
|
|
2837
2957
|
return { content: [{ type: "text", text }] };
|
|
2838
2958
|
|
|
@@ -2877,7 +2997,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2877
2997
|
for (const inst of instances) {
|
|
2878
2998
|
const isDefault = inst.name === currentDefault ? " **(default)**" : "";
|
|
2879
2999
|
const projs = inst.projects?.length > 0 ? `\n Projects: ${inst.projects.join(", ")}` : "";
|
|
2880
|
-
|
|
3000
|
+
const team = inst.defaultTeam ? `\n Default team: ${inst.defaultTeam.name}` : "";
|
|
3001
|
+
text += `- **${inst.name}**${isDefault}: ${inst.baseUrl} (${inst.email})${projs}${team}\n`;
|
|
2881
3002
|
}
|
|
2882
3003
|
return { content: [{ type: "text", text }] };
|
|
2883
3004
|
|
|
@@ -2921,6 +3042,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2921
3042
|
if (args.parentKey) {
|
|
2922
3043
|
fields.parent = { key: args.parentKey };
|
|
2923
3044
|
}
|
|
3045
|
+
let teamPrompt = "";
|
|
3046
|
+
if (args.team) {
|
|
3047
|
+
fields.customfield_10001 = await resolveTeamId(args.team, inst);
|
|
3048
|
+
} else if (inst.defaultTeam) {
|
|
3049
|
+
fields.customfield_10001 = inst.defaultTeam.id;
|
|
3050
|
+
} else {
|
|
3051
|
+
// No team param and no default — fetch available teams and prompt user
|
|
3052
|
+
try {
|
|
3053
|
+
const teams = await fetchJiraTeams(
|
|
3054
|
+
`/teams/find?query=&excludeMembers=true`, {}, inst,
|
|
3055
|
+
);
|
|
3056
|
+
if (teams && teams.length > 0) {
|
|
3057
|
+
const list = teams.map((t, i) => `${i + 1}. ${t.title}`).join("\n");
|
|
3058
|
+
teamPrompt = `\n\n⚠ No team assigned and no default team configured for instance "${inst.name}". Available teams:\n${list}\n0. None\n\nTo assign a team to this ticket, call jira_update_ticket with issueKey and team parameter.\nIf a team is selected, ask the user if it should also be saved as the default team for instance "${inst.name}" (via jira_add_instance with defaultTeam).`;
|
|
3059
|
+
}
|
|
3060
|
+
} catch {
|
|
3061
|
+
// Teams API not available, proceed without team
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
2924
3064
|
|
|
2925
3065
|
const result = await fetchJira("/issue", { method: "POST", body: { fields } }, inst);
|
|
2926
3066
|
const newKey = result.key;
|
|
@@ -2935,6 +3075,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2935
3075
|
|
|
2936
3076
|
let text = `Created ${newKey}: ${args.summary}\nURL: ${inst.baseUrl}/browse/${newKey}`;
|
|
2937
3077
|
if (args.sprintId) text += `\nAdded to sprint ${args.sprintId}.`;
|
|
3078
|
+
text += teamPrompt;
|
|
2938
3079
|
return { content: [{ type: "text", text }] };
|
|
2939
3080
|
|
|
2940
3081
|
} else if (name === "jira_create_subtask") {
|
|
@@ -2967,6 +3108,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2967
3108
|
if (args.labels) {
|
|
2968
3109
|
fields.labels = args.labels;
|
|
2969
3110
|
}
|
|
3111
|
+
let teamWarning = "";
|
|
3112
|
+
if (args.team) {
|
|
3113
|
+
fields.customfield_10001 = await resolveTeamId(args.team, inst);
|
|
3114
|
+
} else {
|
|
3115
|
+
// Inherit team from parent ticket
|
|
3116
|
+
const parentIssue = await fetchJira(
|
|
3117
|
+
`/issue/${args.parentKey}?fields=customfield_10001`, {}, inst,
|
|
3118
|
+
);
|
|
3119
|
+
if (parentIssue.fields?.customfield_10001) {
|
|
3120
|
+
fields.customfield_10001 = parentIssue.fields.customfield_10001;
|
|
3121
|
+
} else if (inst.defaultTeam) {
|
|
3122
|
+
teamWarning = `\n\nNote: Parent ${args.parentKey} has no team assigned. Instance default team is "${inst.defaultTeam.name}". Use team parameter to assign it.`;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
2970
3125
|
|
|
2971
3126
|
const result = await fetchJira("/issue", { method: "POST", body: { fields } }, inst);
|
|
2972
3127
|
const newKey = result.key;
|
|
@@ -2974,7 +3129,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2974
3129
|
content: [
|
|
2975
3130
|
{
|
|
2976
3131
|
type: "text",
|
|
2977
|
-
text: `Created subtask ${newKey} under ${args.parentKey}: ${args.summary}\nURL: ${inst.baseUrl}/browse/${newKey}`,
|
|
3132
|
+
text: `Created subtask ${newKey} under ${args.parentKey}: ${args.summary}\nURL: ${inst.baseUrl}/browse/${newKey}${teamWarning}`,
|
|
2978
3133
|
},
|
|
2979
3134
|
],
|
|
2980
3135
|
};
|
|
@@ -3286,7 +3441,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3286
3441
|
? getInstanceByName(args.instance)
|
|
3287
3442
|
: getInstanceForKey(args.issueKey);
|
|
3288
3443
|
const original = await fetchJira(
|
|
3289
|
-
`/issue/${args.issueKey}?fields=summary,description,priority,labels,components,issuetype,project`,
|
|
3444
|
+
`/issue/${args.issueKey}?fields=summary,description,priority,labels,components,issuetype,project,customfield_10001`,
|
|
3290
3445
|
{},
|
|
3291
3446
|
inst,
|
|
3292
3447
|
);
|
|
@@ -3311,6 +3466,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3311
3466
|
if (of.components?.length > 0) {
|
|
3312
3467
|
fields.components = of.components.map((c) => ({ name: c.name }));
|
|
3313
3468
|
}
|
|
3469
|
+
let teamPrompt = "";
|
|
3470
|
+
if (of.customfield_10001) {
|
|
3471
|
+
fields.customfield_10001 = of.customfield_10001;
|
|
3472
|
+
} else if (inst.defaultTeam) {
|
|
3473
|
+
fields.customfield_10001 = inst.defaultTeam.id;
|
|
3474
|
+
} else {
|
|
3475
|
+
try {
|
|
3476
|
+
const teams = await fetchJiraTeams(
|
|
3477
|
+
`/teams/find?query=&excludeMembers=true`, {}, inst,
|
|
3478
|
+
);
|
|
3479
|
+
if (teams && teams.length > 0) {
|
|
3480
|
+
const list = teams.map((t, i) => `${i + 1}. ${t.title}`).join("\n");
|
|
3481
|
+
teamPrompt = `\n\n⚠ No team assigned (original had none) and no default team configured for instance "${inst.name}". Available teams:\n${list}\n0. None\n\nTo assign a team to this ticket, call jira_update_ticket with issueKey and team parameter.\nIf a team is selected, ask the user if it should also be saved as the default team for instance "${inst.name}" (via jira_add_instance with defaultTeam).`;
|
|
3482
|
+
}
|
|
3483
|
+
} catch {
|
|
3484
|
+
// Teams API not available, proceed without team
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3314
3487
|
|
|
3315
3488
|
const result = await fetchJira("/issue", { method: "POST", body: { fields } }, inst);
|
|
3316
3489
|
const newKey = result.key;
|
|
@@ -3347,7 +3520,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3347
3520
|
content: [
|
|
3348
3521
|
{
|
|
3349
3522
|
type: "text",
|
|
3350
|
-
text: `Cloned ${args.issueKey} -> ${newKey}: ${prefix}${of.summary}\nURL: ${inst.baseUrl}/browse/${newKey}`,
|
|
3523
|
+
text: `Cloned ${args.issueKey} -> ${newKey}: ${prefix}${of.summary}\nURL: ${inst.baseUrl}/browse/${newKey}${teamPrompt}`,
|
|
3351
3524
|
},
|
|
3352
3525
|
],
|
|
3353
3526
|
};
|
|
@@ -3521,5 +3694,5 @@ if (require.main === module) {
|
|
|
3521
3694
|
|
|
3522
3695
|
// Export for testing
|
|
3523
3696
|
if (typeof module !== "undefined") {
|
|
3524
|
-
module.exports = { buildCommentADF, parseInlineFormatting, findJiraTicketKeys };
|
|
3697
|
+
module.exports = { buildCommentADF, parseInlineFormatting, findJiraTicketKeys, resolveTeamId, fetchJiraTeams };
|
|
3525
3698
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rui.branco/jira-mcp",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.16",
|
|
4
4
|
"description": "Jira MCP server for Claude Code - fetch tickets, search with JQL, update tickets, manage comments, change status, and get Figma designs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/setup.js
CHANGED
|
@@ -126,17 +126,26 @@ async function setup() {
|
|
|
126
126
|
console.log(`Existing single-instance config found (${existing.baseUrl})\n`);
|
|
127
127
|
} else {
|
|
128
128
|
console.log("Existing instances:");
|
|
129
|
+
const missingTeam = [];
|
|
129
130
|
for (const inst of existing.instances) {
|
|
130
131
|
const isDefault = inst.name === existing.defaultInstance ? " (default)" : "";
|
|
131
132
|
const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
|
|
132
|
-
|
|
133
|
+
const team = inst.defaultTeam ? ` | Team: ${inst.defaultTeam.name}` : "";
|
|
134
|
+
console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl}${projs}${team}`);
|
|
135
|
+
if (!inst.defaultTeam) missingTeam.push(inst.name);
|
|
136
|
+
}
|
|
137
|
+
if (missingTeam.length > 0) {
|
|
138
|
+
console.log(`\n ⚠ No default team: ${missingTeam.join(", ")}`);
|
|
133
139
|
}
|
|
134
140
|
console.log();
|
|
135
141
|
}
|
|
136
142
|
|
|
137
|
-
const action = await ask("Add new instance, or fresh setup? (add/fresh): ");
|
|
138
|
-
|
|
143
|
+
const action = await ask("Add new instance, set default team, or fresh setup? (add/team/fresh): ");
|
|
144
|
+
const choice = action.trim().toLowerCase();
|
|
145
|
+
if (choice === "fresh") {
|
|
139
146
|
// Fall through to single setup
|
|
147
|
+
} else if (choice === "team") {
|
|
148
|
+
return await setInstanceTeam(existing);
|
|
140
149
|
} else {
|
|
141
150
|
// Add instance to multi-instance config
|
|
142
151
|
return await addInstance(existing);
|
|
@@ -152,7 +161,12 @@ async function setup() {
|
|
|
152
161
|
const token = await ask("Jira API token: ");
|
|
153
162
|
const baseUrl = await ask("Jira base URL (e.g., https://company.atlassian.net): ");
|
|
154
163
|
|
|
155
|
-
|
|
164
|
+
const config = { email, token, baseUrl: baseUrl.replace(/\/$/, "") };
|
|
165
|
+
const authStr = Buffer.from(`${email}:${token}`).toString("base64");
|
|
166
|
+
const team = await pickDefaultTeam(baseUrl.replace(/\/$/, ""), authStr);
|
|
167
|
+
if (team) config.defaultTeam = team;
|
|
168
|
+
|
|
169
|
+
saveConfig(config);
|
|
156
170
|
console.log(`\nConfig saved to ${configPath}`);
|
|
157
171
|
|
|
158
172
|
printFigmaStatus();
|
|
@@ -160,6 +174,85 @@ async function setup() {
|
|
|
160
174
|
rl.close();
|
|
161
175
|
}
|
|
162
176
|
|
|
177
|
+
async function pickDefaultTeam(baseUrl, authStr) {
|
|
178
|
+
console.log("\nFetching available teams from Jira...");
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch(`${baseUrl}/rest/teams/1.0/teams/find?query=&excludeMembers=true`, {
|
|
181
|
+
headers: {
|
|
182
|
+
Authorization: `Basic ${authStr}`,
|
|
183
|
+
Accept: "application/json",
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
console.log("Could not fetch teams (API returned " + response.status + "). Skipping team setup.");
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const teams = JSON.parse(await response.text());
|
|
191
|
+
if (!teams || teams.length === 0) {
|
|
192
|
+
console.log("No teams found. Skipping team setup.");
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log("\nAvailable teams:");
|
|
197
|
+
for (let i = 0; i < teams.length; i++) {
|
|
198
|
+
console.log(` ${i + 1}. ${teams[i].title}`);
|
|
199
|
+
}
|
|
200
|
+
const choice = await ask("\nSelect default team number (or press Enter to skip): ");
|
|
201
|
+
const idx = parseInt(choice, 10) - 1;
|
|
202
|
+
if (isNaN(idx) || idx < 0 || idx >= teams.length) {
|
|
203
|
+
console.log("No default team set.");
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const selected = teams[idx];
|
|
207
|
+
const teamObj = { name: selected.title, id: `${selected.organizationId}-${selected.id}` };
|
|
208
|
+
console.log(`Default team set to: ${teamObj.name}`);
|
|
209
|
+
return teamObj;
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.log("Could not fetch teams: " + e.message + ". Skipping team setup.");
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function setInstanceTeam(config) {
|
|
217
|
+
if (!config.instances || config.instances.length === 0) {
|
|
218
|
+
console.log("No instances configured.");
|
|
219
|
+
rl.close();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let target;
|
|
224
|
+
if (config.instances.length === 1) {
|
|
225
|
+
target = config.instances[0];
|
|
226
|
+
console.log(`\nConfiguring team for "${target.name}"...`);
|
|
227
|
+
} else {
|
|
228
|
+
console.log("\nWhich instance?");
|
|
229
|
+
for (let i = 0; i < config.instances.length; i++) {
|
|
230
|
+
const inst = config.instances[i];
|
|
231
|
+
const team = inst.defaultTeam ? ` (current: ${inst.defaultTeam.name})` : " (no team)";
|
|
232
|
+
console.log(` ${i + 1}. ${inst.name}${team}`);
|
|
233
|
+
}
|
|
234
|
+
const choice = await ask("Select instance number: ");
|
|
235
|
+
const idx = parseInt(choice, 10) - 1;
|
|
236
|
+
if (isNaN(idx) || idx < 0 || idx >= config.instances.length) {
|
|
237
|
+
console.log("Invalid selection.");
|
|
238
|
+
rl.close();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
target = config.instances[idx];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const authStr = Buffer.from(`${target.email}:${target.token}`).toString("base64");
|
|
245
|
+
const team = await pickDefaultTeam(target.baseUrl, authStr);
|
|
246
|
+
if (team) {
|
|
247
|
+
target.defaultTeam = team;
|
|
248
|
+
saveConfig(config);
|
|
249
|
+
console.log(`\nSaved default team "${team.name}" for instance "${target.name}".`);
|
|
250
|
+
} else {
|
|
251
|
+
console.log("No changes made.");
|
|
252
|
+
}
|
|
253
|
+
rl.close();
|
|
254
|
+
}
|
|
255
|
+
|
|
163
256
|
async function addInstance(config) {
|
|
164
257
|
// Migrate old format if needed
|
|
165
258
|
if (config.email && !config.instances) {
|
|
@@ -192,13 +285,18 @@ async function addInstance(config) {
|
|
|
192
285
|
const projectsInput = await ask("Project prefixes (comma-separated, e.g., SIDE,FUN): ");
|
|
193
286
|
const projects = projectsInput.split(",").map((p) => p.trim().toUpperCase()).filter(Boolean);
|
|
194
287
|
|
|
288
|
+
const trimmedBaseUrl = baseUrl.trim().replace(/\/$/, "");
|
|
289
|
+
const authStr = Buffer.from(`${email.trim()}:${token.trim()}`).toString("base64");
|
|
290
|
+
const team = await pickDefaultTeam(trimmedBaseUrl, authStr);
|
|
291
|
+
|
|
195
292
|
const instance = {
|
|
196
293
|
name: name.trim(),
|
|
197
294
|
email: email.trim(),
|
|
198
295
|
token: token.trim(),
|
|
199
|
-
baseUrl:
|
|
296
|
+
baseUrl: trimmedBaseUrl,
|
|
200
297
|
projects,
|
|
201
298
|
};
|
|
299
|
+
if (team) instance.defaultTeam = team;
|
|
202
300
|
|
|
203
301
|
// Replace or add
|
|
204
302
|
const idx = config.instances.findIndex((i) => i.name === instance.name);
|