@rui.branco/jira-mcp 1.7.11 → 1.7.12

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.
Files changed (5) hide show
  1. package/README.md +358 -358
  2. package/config-store.js +553 -553
  3. package/index.js +5773 -5763
  4. package/package.json +36 -36
  5. package/setup.js +393 -392
package/setup.js CHANGED
@@ -1,392 +1,393 @@
1
- #!/usr/bin/env node
2
-
3
- const readline = require("readline");
4
- const fs = require("fs");
5
- const path = require("path");
6
- const configStore = require("./config-store.js");
7
-
8
- const configPath = configStore.configPath;
9
-
10
- // Check for command line arguments
11
- let args = process.argv.slice(2);
12
- // Skip "setup" arg if called via index.js
13
- if (args[0] === "setup") args = args.slice(1);
14
-
15
- // Load existing config if present
16
- function loadConfig() {
17
- return configStore.loadConfig();
18
- }
19
-
20
- function saveConfig(mutator) {
21
- return configStore.updateConfigSync(mutator);
22
- }
23
-
24
- // Non-interactive: setup add <name> <email> <token> <baseUrl> <projects>
25
- if (args[0] === "add" && args.length >= 5) {
26
- const [, name, email, token, baseUrl, ...projectArgs] = args;
27
- const projects = projectArgs.length > 0
28
- ? configStore.normalizeProjects(projectArgs.join(",").split(","))
29
- : [];
30
-
31
- let saved;
32
- try {
33
- saved = saveConfig((config) => {
34
- configStore.migrateLegacyConfig(config);
35
- const existingIdx = config.instances.findIndex((instance) => instance.name === name);
36
- const previous = existingIdx >= 0 ? config.instances[existingIdx] : {};
37
- const mergedProjects = configStore.normalizeProjects([
38
- ...(previous.projects || []),
39
- ...projects,
40
- ]);
41
- configStore.assertNoDuplicateProjectOwnership(config, name, mergedProjects);
42
- const instance = {
43
- ...previous,
44
- name,
45
- email,
46
- token,
47
- baseUrl: baseUrl.replace(/\/$/, ""),
48
- projects: mergedProjects,
49
- };
50
- if (existingIdx >= 0) {
51
- config.instances[existingIdx] = instance;
52
- } else {
53
- config.instances.push(instance);
54
- }
55
- if (!config.defaultInstance) config.defaultInstance = name;
56
- return { instance };
57
- });
58
- } catch (error) {
59
- console.error("Setup failed:", error.message);
60
- process.exit(1);
61
- }
62
-
63
- console.log(`Instance "${name}" saved to ${configPath}`);
64
- if (saved.instance.projects.length > 0) {
65
- console.log(`Projects: ${saved.instance.projects.join(", ")}`);
66
- }
67
- process.exit(0);
68
- }
69
-
70
- // Non-interactive: setup remove <name>
71
- if (args[0] === "remove" && args.length >= 2) {
72
- const name = args[1];
73
- let removed;
74
- try {
75
- removed = saveConfig((config) => {
76
- if (!Array.isArray(config.instances)) {
77
- throw new Error("No multi-instance config found.");
78
- }
79
- config.instances = config.instances.filter((instance) => instance.name !== name);
80
- if (config.defaultInstance === name) {
81
- config.defaultInstance = config.instances[0]?.name || null;
82
- }
83
- return { defaultInstance: config.defaultInstance };
84
- });
85
- } catch (error) {
86
- console.error(error.message);
87
- process.exit(1);
88
- }
89
-
90
- console.log(`Instance "${name}" removed.`);
91
- process.exit(0);
92
- }
93
-
94
- // Non-interactive: setup <email> <token> <baseUrl> (legacy single-instance)
95
- if (args.length >= 3 && args[0] !== "add" && args[0] !== "remove") {
96
- const [email, token, baseUrl] = args;
97
- try {
98
- saveConfig((config) => {
99
- const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
100
- if (Array.isArray(config.instances)) {
101
- const instance =
102
- config.instances.find((candidate) => candidate.name === config.defaultInstance) ||
103
- config.instances[0];
104
- if (!instance) throw new Error("No Jira instances are configured.");
105
- instance.email = email;
106
- instance.token = token;
107
- instance.baseUrl = normalizedBaseUrl;
108
- } else {
109
- if (config.instances !== undefined) {
110
- throw new Error("Jira config has an invalid instances value.");
111
- }
112
- config.email = email;
113
- config.token = token;
114
- config.baseUrl = normalizedBaseUrl;
115
- }
116
- });
117
- } catch (error) {
118
- console.error("Setup failed:", error.message);
119
- process.exit(1);
120
- }
121
- console.log(`Config saved to ${configPath}`);
122
- process.exit(0);
123
- }
124
-
125
- // Interactive mode
126
- const rl = readline.createInterface({
127
- input: process.stdin,
128
- output: process.stdout,
129
- });
130
-
131
- function ask(question) {
132
- return new Promise((resolve) => {
133
- rl.question(question, resolve);
134
- });
135
- }
136
-
137
- async function setup() {
138
- const existing = loadConfig();
139
- const hasInstances = existing?.instances?.length > 0;
140
- const isOldFormat = existing?.email && !existing?.instances;
141
-
142
- console.log("\n=== Jira MCP Setup ===\n");
143
-
144
- if (hasInstances || isOldFormat) {
145
- if (isOldFormat) {
146
- console.log(`Existing single-instance config found (${existing.baseUrl})\n`);
147
- } else {
148
- console.log("Existing instances:");
149
- const missingTeam = [];
150
- for (const inst of existing.instances) {
151
- const isDefault = inst.name === existing.defaultInstance ? " (default)" : "";
152
- const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
153
- const team = inst.defaultTeam ? ` | Team: ${inst.defaultTeam.name}` : "";
154
- console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl}${projs}${team}`);
155
- if (!inst.defaultTeam) missingTeam.push(inst.name);
156
- }
157
- if (missingTeam.length > 0) {
158
- console.log(`\n ⚠ No default team: ${missingTeam.join(", ")}`);
159
- }
160
- console.log();
161
- }
162
-
163
- const action = await ask("Add new instance, set default team, or fresh setup? (add/team/fresh): ");
164
- const choice = action.trim().toLowerCase();
165
- if (choice === "fresh") {
166
- // Fall through to single setup
167
- } else if (choice === "team") {
168
- return await setInstanceTeam(existing);
169
- } else {
170
- // Add instance to multi-instance config
171
- return await addInstance(existing);
172
- }
173
- }
174
-
175
- console.log("To get your Jira API token:");
176
- console.log("1. Go to https://id.atlassian.com/manage-profile/security/api-tokens");
177
- console.log("2. Click 'Create API token'");
178
- console.log("3. Copy the token\n");
179
-
180
- const email = await ask("Jira email: ");
181
- const token = await ask("Jira API token: ");
182
- const baseUrl = await ask("Jira base URL (e.g., https://company.atlassian.net): ");
183
-
184
- const config = { email, token, baseUrl: baseUrl.replace(/\/$/, "") };
185
- const authStr = Buffer.from(`${email}:${token}`).toString("base64");
186
- const team = await pickDefaultTeam(baseUrl.replace(/\/$/, ""), authStr);
187
- if (team) config.defaultTeam = team;
188
-
189
- saveConfig((latest) => {
190
- for (const key of Object.keys(latest)) delete latest[key];
191
- Object.assign(latest, config);
192
- });
193
- console.log(`\nConfig saved to ${configPath}`);
194
-
195
- printFigmaStatus();
196
- printSetupComplete();
197
- rl.close();
198
- }
199
-
200
- async function pickDefaultTeam(baseUrl, authStr) {
201
- console.log("\nFetching available teams from Jira...");
202
- try {
203
- const response = await fetch(`${baseUrl}/rest/teams/1.0/teams/find?query=&excludeMembers=true`, {
204
- headers: {
205
- Authorization: `Basic ${authStr}`,
206
- Accept: "application/json",
207
- },
208
- });
209
- if (!response.ok) {
210
- console.log("Could not fetch teams (API returned " + response.status + "). Skipping team setup.");
211
- return null;
212
- }
213
- const teams = JSON.parse(await response.text());
214
- if (!teams || teams.length === 0) {
215
- console.log("No teams found. Skipping team setup.");
216
- return null;
217
- }
218
-
219
- console.log("\nAvailable teams:");
220
- for (let i = 0; i < teams.length; i++) {
221
- console.log(` ${i + 1}. ${teams[i].title}`);
222
- }
223
- const choice = await ask("\nSelect default team number (or press Enter to skip): ");
224
- const idx = parseInt(choice, 10) - 1;
225
- if (isNaN(idx) || idx < 0 || idx >= teams.length) {
226
- console.log("No default team set.");
227
- return null;
228
- }
229
- const selected = teams[idx];
230
- const teamObj = { name: selected.title, id: `${selected.organizationId}-${selected.id}` };
231
- console.log(`Default team set to: ${teamObj.name}`);
232
- return teamObj;
233
- } catch (e) {
234
- console.log("Could not fetch teams: " + e.message + ". Skipping team setup.");
235
- return null;
236
- }
237
- }
238
-
239
- async function setInstanceTeam(config) {
240
- if (!config.instances || config.instances.length === 0) {
241
- console.log("No instances configured.");
242
- rl.close();
243
- return;
244
- }
245
-
246
- let target;
247
- if (config.instances.length === 1) {
248
- target = config.instances[0];
249
- console.log(`\nConfiguring team for "${target.name}"...`);
250
- } else {
251
- console.log("\nWhich instance?");
252
- for (let i = 0; i < config.instances.length; i++) {
253
- const inst = config.instances[i];
254
- const team = inst.defaultTeam ? ` (current: ${inst.defaultTeam.name})` : " (no team)";
255
- console.log(` ${i + 1}. ${inst.name}${team}`);
256
- }
257
- const choice = await ask("Select instance number: ");
258
- const idx = parseInt(choice, 10) - 1;
259
- if (isNaN(idx) || idx < 0 || idx >= config.instances.length) {
260
- console.log("Invalid selection.");
261
- rl.close();
262
- return;
263
- }
264
- target = config.instances[idx];
265
- }
266
-
267
- const authStr = Buffer.from(`${target.email}:${target.token}`).toString("base64");
268
- const team = await pickDefaultTeam(target.baseUrl, authStr);
269
- if (team) {
270
- const targetName = target.name;
271
- saveConfig((latest) => {
272
- const current = latest.instances?.find((instance) => instance.name === targetName);
273
- if (!current) throw new Error(`Instance "${targetName}" no longer exists.`);
274
- current.defaultTeam = team;
275
- });
276
- console.log(`\nSaved default team "${team.name}" for instance "${target.name}".`);
277
- } else {
278
- console.log("No changes made.");
279
- }
280
- rl.close();
281
- }
282
-
283
- async function addInstance(config) {
284
- let legacyName = null;
285
- let legacyProjects = [];
286
- if (config.email && !config.instances) {
287
- const oldName = await ask("Name for your existing instance (e.g., work): ");
288
- const oldProjects = await ask("Project prefixes for existing instance (comma-separated, e.g., MODS,ENG): ");
289
- legacyName = oldName.trim() || "default";
290
- legacyProjects = configStore.normalizeProjects(oldProjects.split(","));
291
- }
292
-
293
- console.log("\n--- Add New Instance ---\n");
294
- console.log("To get your Jira API token:");
295
- console.log("1. Go to https://id.atlassian.com/manage-profile/security/api-tokens");
296
- console.log("2. Click 'Create API token'");
297
- console.log("3. Copy the token\n");
298
-
299
- const name = await ask("Instance name (e.g., personal): ");
300
- const email = await ask("Jira email: ");
301
- const token = await ask("Jira API token: ");
302
- const baseUrl = await ask("Jira base URL (e.g., https://company.atlassian.net): ");
303
- const projectsInput = await ask("Project prefixes (comma-separated, e.g., SIDE,FUN): ");
304
- const projects = configStore.normalizeProjects(projectsInput.split(","));
305
-
306
- const trimmedBaseUrl = baseUrl.trim().replace(/\/$/, "");
307
- const authStr = Buffer.from(`${email.trim()}:${token.trim()}`).toString("base64");
308
- const team = await pickDefaultTeam(trimmedBaseUrl, authStr);
309
-
310
- const instance = {
311
- name: name.trim(),
312
- email: email.trim(),
313
- token: token.trim(),
314
- baseUrl: trimmedBaseUrl,
315
- projects,
316
- };
317
- if (team) instance.defaultTeam = team;
318
-
319
- const setDefault = await ask(`Set "${instance.name}" as default? (y/N): `);
320
- const makeDefault = setDefault.trim().toLowerCase() === "y";
321
-
322
- const savedConfig = saveConfig((latest) => {
323
- if (legacyName && !Array.isArray(latest.instances)) {
324
- configStore.migrateLegacyConfig(latest, legacyName);
325
- const migrated = latest.instances.find((item) => item.name === legacyName);
326
- migrated.projects = configStore.normalizeProjects([
327
- ...(migrated.projects || []),
328
- ...legacyProjects,
329
- ]);
330
- } else {
331
- configStore.migrateLegacyConfig(latest);
332
- }
333
-
334
- const idx = latest.instances.findIndex((item) => item.name === instance.name);
335
- const previous = idx >= 0 ? latest.instances[idx] : {};
336
- const savedProjects = configStore.normalizeProjects([
337
- ...(previous.projects || []),
338
- ...instance.projects,
339
- ]);
340
- configStore.assertNoDuplicateProjectOwnership(latest, instance.name, savedProjects);
341
- const toSave = {
342
- ...previous,
343
- ...instance,
344
- projects: savedProjects,
345
- };
346
- if (idx >= 0) {
347
- latest.instances[idx] = toSave;
348
- } else {
349
- latest.instances.push(toSave);
350
- }
351
- if (!latest.defaultInstance || makeDefault) {
352
- latest.defaultInstance = instance.name;
353
- }
354
- return latest;
355
- });
356
-
357
- console.log(`\nInstance "${instance.name}" saved to ${configPath}`);
358
-
359
- console.log("\nAll instances:");
360
- for (const inst of savedConfig.instances) {
361
- const isDefault = inst.name === savedConfig.defaultInstance ? " (default)" : "";
362
- const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
363
- console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl}${projs}`);
364
- }
365
-
366
- printFigmaStatus();
367
- printSetupComplete();
368
- rl.close();
369
- }
370
-
371
- function printFigmaStatus() {
372
- const figmaConfigPath = path.join(process.env.HOME, ".config/figma-mcp/config.json");
373
- if (fs.existsSync(figmaConfigPath)) {
374
- console.log("\n[OK] Figma MCP detected - Figma links in tickets will be fetched automatically");
375
- } else {
376
- console.log("\n[INFO] Figma MCP not installed - Figma links won't be fetched");
377
- console.log("To enable Figma integration, install figma-mcp");
378
- }
379
- }
380
-
381
- function printSetupComplete() {
382
- console.log("\n=== Setup Complete ===");
383
- console.log("\nIf you haven't already, add to Claude Code with:\n");
384
- console.log(" claude mcp add --transport stdio jira -- npx -y @rui.branco/jira-mcp");
385
- console.log("\nThen restart Claude Code and run /mcp to verify.");
386
- }
387
-
388
- setup().catch((e) => {
389
- console.error("Setup failed:", e.message);
390
- rl.close();
391
- process.exit(1);
392
- });
1
+ #!/usr/bin/env node
2
+
3
+ const readline = require("readline");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const configStore = require("./config-store.js");
8
+
9
+ const configPath = configStore.configPath;
10
+
11
+ // Check for command line arguments
12
+ let args = process.argv.slice(2);
13
+ // Skip "setup" arg if called via index.js
14
+ if (args[0] === "setup") args = args.slice(1);
15
+
16
+ // Load existing config if present
17
+ function loadConfig() {
18
+ return configStore.loadConfig();
19
+ }
20
+
21
+ function saveConfig(mutator) {
22
+ return configStore.updateConfigSync(mutator);
23
+ }
24
+
25
+ // Non-interactive: setup add <name> <email> <token> <baseUrl> <projects>
26
+ if (args[0] === "add" && args.length >= 5) {
27
+ const [, name, email, token, baseUrl, ...projectArgs] = args;
28
+ const projects = projectArgs.length > 0
29
+ ? configStore.normalizeProjects(projectArgs.join(",").split(","))
30
+ : [];
31
+
32
+ let saved;
33
+ try {
34
+ saved = saveConfig((config) => {
35
+ configStore.migrateLegacyConfig(config);
36
+ const existingIdx = config.instances.findIndex((instance) => instance.name === name);
37
+ const previous = existingIdx >= 0 ? config.instances[existingIdx] : {};
38
+ const mergedProjects = configStore.normalizeProjects([
39
+ ...(previous.projects || []),
40
+ ...projects,
41
+ ]);
42
+ configStore.assertNoDuplicateProjectOwnership(config, name, mergedProjects);
43
+ const instance = {
44
+ ...previous,
45
+ name,
46
+ email,
47
+ token,
48
+ baseUrl: baseUrl.replace(/\/$/, ""),
49
+ projects: mergedProjects,
50
+ };
51
+ if (existingIdx >= 0) {
52
+ config.instances[existingIdx] = instance;
53
+ } else {
54
+ config.instances.push(instance);
55
+ }
56
+ if (!config.defaultInstance) config.defaultInstance = name;
57
+ return { instance };
58
+ });
59
+ } catch (error) {
60
+ console.error("Setup failed:", error.message);
61
+ process.exit(1);
62
+ }
63
+
64
+ console.log(`Instance "${name}" saved to ${configPath}`);
65
+ if (saved.instance.projects.length > 0) {
66
+ console.log(`Projects: ${saved.instance.projects.join(", ")}`);
67
+ }
68
+ process.exit(0);
69
+ }
70
+
71
+ // Non-interactive: setup remove <name>
72
+ if (args[0] === "remove" && args.length >= 2) {
73
+ const name = args[1];
74
+ let removed;
75
+ try {
76
+ removed = saveConfig((config) => {
77
+ if (!Array.isArray(config.instances)) {
78
+ throw new Error("No multi-instance config found.");
79
+ }
80
+ config.instances = config.instances.filter((instance) => instance.name !== name);
81
+ if (config.defaultInstance === name) {
82
+ config.defaultInstance = config.instances[0]?.name || null;
83
+ }
84
+ return { defaultInstance: config.defaultInstance };
85
+ });
86
+ } catch (error) {
87
+ console.error(error.message);
88
+ process.exit(1);
89
+ }
90
+
91
+ console.log(`Instance "${name}" removed.`);
92
+ process.exit(0);
93
+ }
94
+
95
+ // Non-interactive: setup <email> <token> <baseUrl> (legacy single-instance)
96
+ if (args.length >= 3 && args[0] !== "add" && args[0] !== "remove") {
97
+ const [email, token, baseUrl] = args;
98
+ try {
99
+ saveConfig((config) => {
100
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
101
+ if (Array.isArray(config.instances)) {
102
+ const instance =
103
+ config.instances.find((candidate) => candidate.name === config.defaultInstance) ||
104
+ config.instances[0];
105
+ if (!instance) throw new Error("No Jira instances are configured.");
106
+ instance.email = email;
107
+ instance.token = token;
108
+ instance.baseUrl = normalizedBaseUrl;
109
+ } else {
110
+ if (config.instances !== undefined) {
111
+ throw new Error("Jira config has an invalid instances value.");
112
+ }
113
+ config.email = email;
114
+ config.token = token;
115
+ config.baseUrl = normalizedBaseUrl;
116
+ }
117
+ });
118
+ } catch (error) {
119
+ console.error("Setup failed:", error.message);
120
+ process.exit(1);
121
+ }
122
+ console.log(`Config saved to ${configPath}`);
123
+ process.exit(0);
124
+ }
125
+
126
+ // Interactive mode
127
+ const rl = readline.createInterface({
128
+ input: process.stdin,
129
+ output: process.stdout,
130
+ });
131
+
132
+ function ask(question) {
133
+ return new Promise((resolve) => {
134
+ rl.question(question, resolve);
135
+ });
136
+ }
137
+
138
+ async function setup() {
139
+ const existing = loadConfig();
140
+ const hasInstances = existing?.instances?.length > 0;
141
+ const isOldFormat = existing?.email && !existing?.instances;
142
+
143
+ console.log("\n=== Jira MCP Setup ===\n");
144
+
145
+ if (hasInstances || isOldFormat) {
146
+ if (isOldFormat) {
147
+ console.log(`Existing single-instance config found (${existing.baseUrl})\n`);
148
+ } else {
149
+ console.log("Existing instances:");
150
+ const missingTeam = [];
151
+ for (const inst of existing.instances) {
152
+ const isDefault = inst.name === existing.defaultInstance ? " (default)" : "";
153
+ const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
154
+ const team = inst.defaultTeam ? ` | Team: ${inst.defaultTeam.name}` : "";
155
+ console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl}${projs}${team}`);
156
+ if (!inst.defaultTeam) missingTeam.push(inst.name);
157
+ }
158
+ if (missingTeam.length > 0) {
159
+ console.log(`\n ⚠ No default team: ${missingTeam.join(", ")}`);
160
+ }
161
+ console.log();
162
+ }
163
+
164
+ const action = await ask("Add new instance, set default team, or fresh setup? (add/team/fresh): ");
165
+ const choice = action.trim().toLowerCase();
166
+ if (choice === "fresh") {
167
+ // Fall through to single setup
168
+ } else if (choice === "team") {
169
+ return await setInstanceTeam(existing);
170
+ } else {
171
+ // Add instance to multi-instance config
172
+ return await addInstance(existing);
173
+ }
174
+ }
175
+
176
+ console.log("To get your Jira API token:");
177
+ console.log("1. Go to https://id.atlassian.com/manage-profile/security/api-tokens");
178
+ console.log("2. Click 'Create API token'");
179
+ console.log("3. Copy the token\n");
180
+
181
+ const email = await ask("Jira email: ");
182
+ const token = await ask("Jira API token: ");
183
+ const baseUrl = await ask("Jira base URL (e.g., https://company.atlassian.net): ");
184
+
185
+ const config = { email, token, baseUrl: baseUrl.replace(/\/$/, "") };
186
+ const authStr = Buffer.from(`${email}:${token}`).toString("base64");
187
+ const team = await pickDefaultTeam(baseUrl.replace(/\/$/, ""), authStr);
188
+ if (team) config.defaultTeam = team;
189
+
190
+ saveConfig((latest) => {
191
+ for (const key of Object.keys(latest)) delete latest[key];
192
+ Object.assign(latest, config);
193
+ });
194
+ console.log(`\nConfig saved to ${configPath}`);
195
+
196
+ printFigmaStatus();
197
+ printSetupComplete();
198
+ rl.close();
199
+ }
200
+
201
+ async function pickDefaultTeam(baseUrl, authStr) {
202
+ console.log("\nFetching available teams from Jira...");
203
+ try {
204
+ const response = await fetch(`${baseUrl}/rest/teams/1.0/teams/find?query=&excludeMembers=true`, {
205
+ headers: {
206
+ Authorization: `Basic ${authStr}`,
207
+ Accept: "application/json",
208
+ },
209
+ });
210
+ if (!response.ok) {
211
+ console.log("Could not fetch teams (API returned " + response.status + "). Skipping team setup.");
212
+ return null;
213
+ }
214
+ const teams = JSON.parse(await response.text());
215
+ if (!teams || teams.length === 0) {
216
+ console.log("No teams found. Skipping team setup.");
217
+ return null;
218
+ }
219
+
220
+ console.log("\nAvailable teams:");
221
+ for (let i = 0; i < teams.length; i++) {
222
+ console.log(` ${i + 1}. ${teams[i].title}`);
223
+ }
224
+ const choice = await ask("\nSelect default team number (or press Enter to skip): ");
225
+ const idx = parseInt(choice, 10) - 1;
226
+ if (isNaN(idx) || idx < 0 || idx >= teams.length) {
227
+ console.log("No default team set.");
228
+ return null;
229
+ }
230
+ const selected = teams[idx];
231
+ const teamObj = { name: selected.title, id: `${selected.organizationId}-${selected.id}` };
232
+ console.log(`Default team set to: ${teamObj.name}`);
233
+ return teamObj;
234
+ } catch (e) {
235
+ console.log("Could not fetch teams: " + e.message + ". Skipping team setup.");
236
+ return null;
237
+ }
238
+ }
239
+
240
+ async function setInstanceTeam(config) {
241
+ if (!config.instances || config.instances.length === 0) {
242
+ console.log("No instances configured.");
243
+ rl.close();
244
+ return;
245
+ }
246
+
247
+ let target;
248
+ if (config.instances.length === 1) {
249
+ target = config.instances[0];
250
+ console.log(`\nConfiguring team for "${target.name}"...`);
251
+ } else {
252
+ console.log("\nWhich instance?");
253
+ for (let i = 0; i < config.instances.length; i++) {
254
+ const inst = config.instances[i];
255
+ const team = inst.defaultTeam ? ` (current: ${inst.defaultTeam.name})` : " (no team)";
256
+ console.log(` ${i + 1}. ${inst.name}${team}`);
257
+ }
258
+ const choice = await ask("Select instance number: ");
259
+ const idx = parseInt(choice, 10) - 1;
260
+ if (isNaN(idx) || idx < 0 || idx >= config.instances.length) {
261
+ console.log("Invalid selection.");
262
+ rl.close();
263
+ return;
264
+ }
265
+ target = config.instances[idx];
266
+ }
267
+
268
+ const authStr = Buffer.from(`${target.email}:${target.token}`).toString("base64");
269
+ const team = await pickDefaultTeam(target.baseUrl, authStr);
270
+ if (team) {
271
+ const targetName = target.name;
272
+ saveConfig((latest) => {
273
+ const current = latest.instances?.find((instance) => instance.name === targetName);
274
+ if (!current) throw new Error(`Instance "${targetName}" no longer exists.`);
275
+ current.defaultTeam = team;
276
+ });
277
+ console.log(`\nSaved default team "${team.name}" for instance "${target.name}".`);
278
+ } else {
279
+ console.log("No changes made.");
280
+ }
281
+ rl.close();
282
+ }
283
+
284
+ async function addInstance(config) {
285
+ let legacyName = null;
286
+ let legacyProjects = [];
287
+ if (config.email && !config.instances) {
288
+ const oldName = await ask("Name for your existing instance (e.g., work): ");
289
+ const oldProjects = await ask("Project prefixes for existing instance (comma-separated, e.g., MODS,ENG): ");
290
+ legacyName = oldName.trim() || "default";
291
+ legacyProjects = configStore.normalizeProjects(oldProjects.split(","));
292
+ }
293
+
294
+ console.log("\n--- Add New Instance ---\n");
295
+ console.log("To get your Jira API token:");
296
+ console.log("1. Go to https://id.atlassian.com/manage-profile/security/api-tokens");
297
+ console.log("2. Click 'Create API token'");
298
+ console.log("3. Copy the token\n");
299
+
300
+ const name = await ask("Instance name (e.g., personal): ");
301
+ const email = await ask("Jira email: ");
302
+ const token = await ask("Jira API token: ");
303
+ const baseUrl = await ask("Jira base URL (e.g., https://company.atlassian.net): ");
304
+ const projectsInput = await ask("Project prefixes (comma-separated, e.g., SIDE,FUN): ");
305
+ const projects = configStore.normalizeProjects(projectsInput.split(","));
306
+
307
+ const trimmedBaseUrl = baseUrl.trim().replace(/\/$/, "");
308
+ const authStr = Buffer.from(`${email.trim()}:${token.trim()}`).toString("base64");
309
+ const team = await pickDefaultTeam(trimmedBaseUrl, authStr);
310
+
311
+ const instance = {
312
+ name: name.trim(),
313
+ email: email.trim(),
314
+ token: token.trim(),
315
+ baseUrl: trimmedBaseUrl,
316
+ projects,
317
+ };
318
+ if (team) instance.defaultTeam = team;
319
+
320
+ const setDefault = await ask(`Set "${instance.name}" as default? (y/N): `);
321
+ const makeDefault = setDefault.trim().toLowerCase() === "y";
322
+
323
+ const savedConfig = saveConfig((latest) => {
324
+ if (legacyName && !Array.isArray(latest.instances)) {
325
+ configStore.migrateLegacyConfig(latest, legacyName);
326
+ const migrated = latest.instances.find((item) => item.name === legacyName);
327
+ migrated.projects = configStore.normalizeProjects([
328
+ ...(migrated.projects || []),
329
+ ...legacyProjects,
330
+ ]);
331
+ } else {
332
+ configStore.migrateLegacyConfig(latest);
333
+ }
334
+
335
+ const idx = latest.instances.findIndex((item) => item.name === instance.name);
336
+ const previous = idx >= 0 ? latest.instances[idx] : {};
337
+ const savedProjects = configStore.normalizeProjects([
338
+ ...(previous.projects || []),
339
+ ...instance.projects,
340
+ ]);
341
+ configStore.assertNoDuplicateProjectOwnership(latest, instance.name, savedProjects);
342
+ const toSave = {
343
+ ...previous,
344
+ ...instance,
345
+ projects: savedProjects,
346
+ };
347
+ if (idx >= 0) {
348
+ latest.instances[idx] = toSave;
349
+ } else {
350
+ latest.instances.push(toSave);
351
+ }
352
+ if (!latest.defaultInstance || makeDefault) {
353
+ latest.defaultInstance = instance.name;
354
+ }
355
+ return latest;
356
+ });
357
+
358
+ console.log(`\nInstance "${instance.name}" saved to ${configPath}`);
359
+
360
+ console.log("\nAll instances:");
361
+ for (const inst of savedConfig.instances) {
362
+ const isDefault = inst.name === savedConfig.defaultInstance ? " (default)" : "";
363
+ const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
364
+ console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl}${projs}`);
365
+ }
366
+
367
+ printFigmaStatus();
368
+ printSetupComplete();
369
+ rl.close();
370
+ }
371
+
372
+ function printFigmaStatus() {
373
+ const figmaConfigPath = path.join(os.homedir(), ".config/figma-mcp/config.json");
374
+ if (fs.existsSync(figmaConfigPath)) {
375
+ console.log("\n[OK] Figma MCP detected - Figma links in tickets will be fetched automatically");
376
+ } else {
377
+ console.log("\n[INFO] Figma MCP not installed - Figma links won't be fetched");
378
+ console.log("To enable Figma integration, install figma-mcp");
379
+ }
380
+ }
381
+
382
+ function printSetupComplete() {
383
+ console.log("\n=== Setup Complete ===");
384
+ console.log("\nIf you haven't already, add to Claude Code with:\n");
385
+ console.log(" claude mcp add --transport stdio jira -- npx -y @rui.branco/jira-mcp");
386
+ console.log("\nThen restart Claude Code and run /mcp to verify.");
387
+ }
388
+
389
+ setup().catch((e) => {
390
+ console.error("Setup failed:", e.message);
391
+ rl.close();
392
+ process.exit(1);
393
+ });