nitor 1.5.0 → 1.7.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/CHANGELOG.md CHANGED
@@ -46,3 +46,40 @@ For a complete list of changes and discussion, see [Issue #1](https://github.com
46
46
  - Add table display for get-task command.
47
47
 
48
48
  - [Issue #9](https://github.com/codebynithin/nitor/issues/9)
49
+
50
+ ## [1.6.0] - 2026-04-20
51
+
52
+ ### New Features
53
+
54
+ - Added `mr-status` command to list GitLab repositories you contribute to and
55
+ view their merge requests rendered as a branch tree (with filtering by state
56
+ and repository search).
57
+ - `task-stats` and `get-task` output now render GitLab issue IDs, merge request
58
+ IDs, and Zoho task IDs as clickable terminal hyperlinks (OSC 8) for
59
+ supporting terminals.
60
+ - New env variables `ZOHO_WORKSPACE` and `ZOHO_PROJECT_PREFIX` for building
61
+ Zoho Sprints task URLs used by the clickable `Task` column.
62
+
63
+ ### Enhancements
64
+
65
+ - `printTable` is now ANSI/hyperlink-aware and computes visible column widths
66
+ correctly when cell values contain escape sequences.
67
+ - Autocomplete updated to include the new `mr-status` command and its
68
+ `-state` / `-search` options.
69
+
70
+ - [Issue #10](https://github.com/codebynithin/nitor/issues/10)
71
+
72
+ ## [1.7.0] - 2026-04-20
73
+
74
+ ### New Features
75
+
76
+ ### Security & Packaging
77
+
78
+ - Bumped `axios` to `~1.15.1` to resolve high-severity advisories
79
+ (GHSA-43fc-jf86-j433, GHSA-3p68-rc4w-qgx5, GHSA-fvcv-3m26-pcqx) and a
80
+ moderate `follow-redirects` advisory pulled in transitively.
81
+ - Added a top-level `LICENSE` file (ISC) and a `files` allowlist in
82
+ `package.json` so published tarballs only ship runtime assets.
83
+ - Removed unused `commander` and `compression` dependencies.
84
+ - Declared `engines.node >= 18` to reflect the actual minimum supported by
85
+ runtime dependencies (notably `@google/generative-ai`).
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025 Nithin V <mails2nithin@gmail.com>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -29,11 +29,13 @@ A CLI utility toolkit for automating and managing build, deploy, and status oper
29
29
  - Task statistics with merge request details
30
30
  - Extract Zoho task IDs from GitLab issue descriptions
31
31
  - Merge request status tracking for active tasks
32
+ - Branch-tree view of GitLab merge requests across repositories (`mr-status`)
33
+ - Clickable terminal hyperlinks for task IDs, GitLab issues and merge requests in stats output
32
34
 
33
35
  ## Requirements
34
36
 
35
- - Node.js >= 14.x
36
- - npm >= 6.x
37
+ - Node.js >= 18.x
38
+ - npm >= 9.x
37
39
  - A properly configured `.env.nu` file in your `~/Desktop` directory with required tokens and URLs:
38
40
  - `CSRF_TOKEN` - CSRF token for Gitlab (Copy from browser)
39
41
  - `COOKIE` - Cookie for Gitlab (Copy from browser)
@@ -57,6 +59,8 @@ A CLI utility toolkit for automating and managing build, deploy, and status oper
57
59
  - `ZOHO_TOKEN` - Zoho CSRF token
58
60
  - `ZOHO_URI` - Zoho API URL
59
61
  - `ZOHO_USERID` - Zoho user ID
62
+ - `ZOHO_WORKSPACE` - Zoho workspace name (used to build clickable task links)
63
+ - `ZOHO_PROJECT_PREFIX` - Zoho project prefix, e.g. `P29` (used to build clickable task links)
60
64
 
61
65
  ## Installation
62
66
 
@@ -184,6 +188,18 @@ Once enabled, you can use Tab to autocomplete:
184
188
  ```bash
185
189
  nitor get-task -task <task numbers with space>
186
190
  ```
191
+ - **Merge Request Status (Tree View):**
192
+
193
+ ```bash
194
+ # Interactive: pick a repository, then view MRs as a branch tree
195
+ nitor mr-status
196
+
197
+ # Filter by MR state (opened | merged | closed | all)
198
+ nitor mr-status -state opened
199
+
200
+ # Pre-filter repositories by name
201
+ nitor mr-status -search <text>
202
+ ```
187
203
 
188
204
  ### Command Reference
189
205
 
@@ -211,6 +227,7 @@ Once enabled, you can use Tab to autocomplete:
211
227
  - `time-switch` : Switch between default projects
212
228
  - `task-stats` : View task statistics with GitLab merge request details
213
229
  - `get-task` : Extract Zoho task IDs from GitLab issue descriptions
230
+ - `mr-status` : List GitLab repositories you contribute to and view merge requests as a branch tree
214
231
 
215
232
  ## MongoDB Backup & Restore
216
233
 
@@ -359,6 +376,8 @@ The backup service is designed to work on Windows, macOS, and Linux:
359
376
  - `-mergeId` or `-mId` : Merge ID
360
377
  - `-source` or `-so` : Source branch name
361
378
  - `-target` or `-ta` : Target branch name
379
+ - `-state` or `-st` : MR state filter for `mr-status` (`opened`, `merged`, `closed`, `all`)
380
+ - `-search` or `-q` : Repository search text for `mr-status`
362
381
 
363
382
  #### Time Entry Options
364
383
 
package/package.json CHANGED
@@ -1,10 +1,21 @@
1
1
  {
2
2
  "name": "nitor",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "A comprehensive CLI toolkit for automating GitLab operations, AI-powered code review, build/deploy automation, MongoDB backup/restore, and developer productivity tools",
5
5
  "main": "index.js",
6
6
  "author": "Nithin V <mails2nithin@gmail.com>",
7
7
  "bin": "index.js",
8
+ "files": [
9
+ "index.js",
10
+ "services/",
11
+ "favicon.png",
12
+ "README.md",
13
+ "CHANGELOG.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
8
19
  "scripts": {
9
20
  "start": "node index",
10
21
  "start:dev": "nodemon index",
@@ -53,11 +64,9 @@
53
64
  "homepage": "https://github.com/codebynithin/nitor#readme",
54
65
  "dependencies": {
55
66
  "@google/generative-ai": "~0.24.1",
56
- "axios": "~1.12.2",
67
+ "axios": "~1.15.1",
57
68
  "chalk": "~4.1.2",
58
69
  "cheerio": "~1.0.0-rc.12",
59
- "commander": "~14.0.1",
60
- "compression": "~1.8.1",
61
70
  "date-fns": "~2.30.0",
62
71
  "date-fns-tz": "~2.0.1",
63
72
  "dotenv": "~16.4.5",
@@ -228,6 +228,14 @@ const setupAutocomplete = () => {
228
228
  '-help': [],
229
229
  '--h': [],
230
230
  },
231
+ [ACTIONS.MR_STATUS]: {
232
+ '-state': ['opened', 'merged', 'closed', 'all'],
233
+ '-st': ['opened', 'merged', 'closed', 'all'],
234
+ '-search': [],
235
+ '-q': [],
236
+ '-help': [],
237
+ '--h': [],
238
+ },
231
239
  [ACTIONS.TIME_INIT]: [],
232
240
  [ACTIONS.TIME_SWITCH]: [],
233
241
  [ACTIONS.TIME_ADD]: [],
@@ -13,6 +13,7 @@ module.exports = {
13
13
  VERSION: 'version',
14
14
  TASK_STATS: 'task-stats',
15
15
  GET_TASK: 'get-task',
16
+ MR_STATUS: 'mr-status',
16
17
 
17
18
  // Time entry commands
18
19
  TIME_INIT: 'time-init',
@@ -0,0 +1,306 @@
1
+ const axios = require('axios');
2
+ const readline = require('readline');
3
+ const { gitlabConfig, createSpinner } = require('./utils');
4
+
5
+ const STATES = ['opened', 'merged', 'closed', 'all'];
6
+
7
+ const C = {
8
+ reset: '\x1b[0m',
9
+ bold: '\x1b[1m',
10
+ dim: '\x1b[2m',
11
+ cyan: '\x1b[36m',
12
+ green: '\x1b[32m',
13
+ yellow: '\x1b[33m',
14
+ magenta: '\x1b[35m',
15
+ blue: '\x1b[34m',
16
+ red: '\x1b[31m',
17
+ };
18
+
19
+ const apiBase = () => `${(gitlabConfig.url || '').replace(/\/+$/, '')}/api/v4`;
20
+
21
+ const gitlabGet = async (path, params = {}) => {
22
+ const response = await axios.get(`${apiBase()}${path}`, {
23
+ headers: { 'PRIVATE-TOKEN': gitlabConfig.token },
24
+ params,
25
+ timeout: 20000,
26
+ });
27
+
28
+ return response.data;
29
+ };
30
+
31
+ const fetchProjects = async (search = '') => {
32
+ return gitlabGet('/projects', {
33
+ membership: true,
34
+ order_by: 'last_activity_at',
35
+ sort: 'desc',
36
+ per_page: 100,
37
+ simple: true,
38
+ search: search || undefined,
39
+ });
40
+ };
41
+
42
+ const fetchMergeRequests = async (projectId, state = 'opened') => {
43
+ return gitlabGet(`/projects/${encodeURIComponent(String(projectId))}/merge_requests`, {
44
+ state,
45
+ per_page: 100,
46
+ order_by: 'updated_at',
47
+ sort: 'desc',
48
+ scope: 'all',
49
+ });
50
+ };
51
+
52
+ const buildTree = (mrs) => {
53
+ const byTarget = new Map();
54
+ const sourceSet = new Set(mrs.map((m) => m.source_branch));
55
+
56
+ for (const mr of mrs) {
57
+ if (!byTarget.has(mr.target_branch)) {
58
+ byTarget.set(mr.target_branch, []);
59
+ }
60
+
61
+ byTarget.get(mr.target_branch).push(mr);
62
+ }
63
+
64
+ const rootTargets = [...byTarget.keys()].filter((t) => !sourceSet.has(t));
65
+ const roots = rootTargets.length ? rootTargets : [...byTarget.keys()];
66
+
67
+ const buildNode = (branch, visited = new Set()) => {
68
+ if (visited.has(branch)) {
69
+ return { branch, mrs: [], cyclic: true };
70
+ }
71
+
72
+ visited.add(branch);
73
+
74
+ const list = byTarget.get(branch) || [];
75
+
76
+ return {
77
+ branch,
78
+ mrs: list.map((mr) => ({
79
+ mr,
80
+ child: byTarget.has(mr.source_branch)
81
+ ? buildNode(mr.source_branch, new Set(visited))
82
+ : null,
83
+ })),
84
+ };
85
+ };
86
+
87
+ return roots.sort().map((b) => buildNode(b));
88
+ };
89
+
90
+ const stateColor = (state) => {
91
+ switch (state) {
92
+ case 'opened':
93
+ return C.green;
94
+ case 'merged':
95
+ return C.blue;
96
+ case 'closed':
97
+ return C.red;
98
+ default:
99
+ return C.dim;
100
+ }
101
+ };
102
+
103
+ const fmtDate = (s) => {
104
+ if (!s) return '';
105
+
106
+ return new Date(s).toISOString().replace('T', ' ').slice(0, 16);
107
+ };
108
+
109
+ const TREE = {
110
+ branch: '├── ',
111
+ lastBranch: '└── ',
112
+ vertical: '│ ',
113
+ space: ' ',
114
+ };
115
+
116
+ // Render tree connectors at normal brightness (not dim) so the hierarchy is
117
+ // clearly visible against the rest of the output.
118
+ const treeStyle = (s) => s;
119
+
120
+ const printMR = (mr, prefix, connector, continuation) => {
121
+ const draft = mr.draft || mr.work_in_progress ? ` ${C.yellow}[draft]${C.reset}` : '';
122
+ const state = `${stateColor(mr.state)}${mr.state}${C.reset}`;
123
+ const author = mr.author?.name || mr.author?.username || '-';
124
+ const linePrefix = `${prefix}${treeStyle(continuation)}`;
125
+
126
+ console.log(
127
+ `${prefix}${treeStyle(connector)}${C.cyan}!${mr.iid}${C.reset} ${C.bold}${mr.title}${C.reset}${draft}`,
128
+ );
129
+ console.log(
130
+ `${linePrefix} ${C.dim}${mr.source_branch} → ${mr.target_branch}${C.reset} [${state}] ${C.magenta}${author}${C.reset} ${C.dim}${fmtDate(mr.updated_at)}${C.reset}`,
131
+ );
132
+ console.log(`${linePrefix} ${C.dim}${mr.web_url}${C.reset}`);
133
+ };
134
+
135
+ const printBranchNode = (node, prefix = '') => {
136
+ const count = node.mrs.length;
137
+ const cyclic = node.cyclic ? ` ${C.yellow}(cycle)${C.reset}` : '';
138
+
139
+ console.log(
140
+ `${prefix}${C.bold}${C.blue}⎇ ${node.branch}${C.reset} ${C.dim}(${count} MR${count === 1 ? '' : 's'})${C.reset}${cyclic}`,
141
+ );
142
+
143
+ node.mrs.forEach(({ mr, child }, idx) => {
144
+ const isLast = idx === node.mrs.length - 1;
145
+ const connector = isLast ? TREE.lastBranch : TREE.branch;
146
+ // When an MR has a child branch, keep the vertical in the continuation so
147
+ // the child is visibly connected to the MR even if this MR is the last
148
+ // sibling at its level.
149
+ const continuation = isLast ? TREE.space : TREE.vertical;
150
+ // const continuation = isLast && !child ? TREE.space : TREE.vertical;
151
+
152
+ printMR(mr, prefix, connector, continuation);
153
+
154
+ if (child) {
155
+ printBranchNode(child, `${prefix}${treeStyle(continuation)}`);
156
+ }
157
+ });
158
+ };
159
+
160
+ const askQuestion = (question) =>
161
+ new Promise((resolve) => {
162
+ // terminal: false keeps stdin in cooked mode so the terminal itself handles
163
+ // echo. Using the default (terminal: true) alongside tooling that tweaks
164
+ // the TTY (e.g. the spinner's cursor-hide escapes) can cause each typed
165
+ // character to render twice (e.g. "1" appearing as "11").
166
+ const rl = readline.createInterface({
167
+ input: process.stdin,
168
+ output: process.stdout,
169
+ terminal: false,
170
+ });
171
+
172
+ process.stdout.write(question);
173
+
174
+ rl.question('', (answer) => {
175
+ rl.close();
176
+ resolve((answer || '').trim());
177
+ });
178
+ });
179
+
180
+ const selectRepo = async (projects) => {
181
+ console.log('');
182
+
183
+ projects.forEach((p, i) => {
184
+ console.log(
185
+ ` ${C.cyan}${String(i + 1).padStart(3)}${C.reset}. ${C.bold}${p.name}${C.reset} ${C.dim}(${p.path_with_namespace})${C.reset}`,
186
+ );
187
+ });
188
+
189
+ console.log('');
190
+
191
+ for (;;) {
192
+ const answer = await askQuestion(`Select repository (1-${projects.length}) [default: 1]: `);
193
+
194
+ if (!answer) {
195
+ return projects[0];
196
+ }
197
+
198
+ const idx = parseInt(answer, 10);
199
+
200
+ if (Number.isInteger(idx) && idx >= 1 && idx <= projects.length) {
201
+ return projects[idx - 1];
202
+ }
203
+
204
+ console.log(`${C.yellow}Invalid selection. Try again.${C.reset}`);
205
+ }
206
+ };
207
+
208
+ const selectState = async (preset) => {
209
+ const lower = (preset || '').toLowerCase();
210
+
211
+ if (STATES.includes(lower)) {
212
+ return lower;
213
+ }
214
+
215
+ console.log('');
216
+
217
+ STATES.forEach((s, i) => console.log(` ${C.cyan}${i + 1}${C.reset}. ${s}`));
218
+
219
+ const answer = await askQuestion(`Select MR state (1-${STATES.length}) [default: 1=opened]: `);
220
+
221
+ if (!answer) {
222
+ return 'opened';
223
+ }
224
+
225
+ const idx = parseInt(answer, 10);
226
+
227
+ return Number.isInteger(idx) && idx >= 1 && idx <= STATES.length ? STATES[idx - 1] : 'opened';
228
+ };
229
+
230
+ const getMr = async (values = {}) => {
231
+ if (!gitlabConfig.url || !gitlabConfig.token) {
232
+ console.log(
233
+ `${C.yellow}GitLab configuration missing.${C.reset} Set GITLAB_URI and GITLAB_TOKEN in your env file.`,
234
+ );
235
+
236
+ process.exit(1);
237
+ }
238
+
239
+ const repoSpinner = createSpinner('Fetching repositories');
240
+
241
+ repoSpinner.start();
242
+
243
+ let projects;
244
+
245
+ try {
246
+ projects = await fetchProjects(values.search || '');
247
+
248
+ repoSpinner.stop(
249
+ `Loaded ${projects.length} repositor${projects.length === 1 ? 'y' : 'ies'}`,
250
+ projects.length > 0,
251
+ );
252
+ } catch (error) {
253
+ repoSpinner.stop('Failed to fetch repositories', false);
254
+ console.error(error.response?.data?.message || error.message);
255
+
256
+ return;
257
+ }
258
+
259
+ if (!projects.length) {
260
+ console.log('No repositories found.');
261
+
262
+ return;
263
+ }
264
+
265
+ const selected = await selectRepo(projects);
266
+ const state = await selectState(values.state);
267
+
268
+ const mrSpinner = createSpinner(`Fetching ${state} merge requests for ${selected.name}`);
269
+
270
+ mrSpinner.start();
271
+
272
+ let mrs;
273
+
274
+ try {
275
+ mrs = await fetchMergeRequests(selected.id, state);
276
+ mrSpinner.stop(
277
+ `Found ${mrs.length} merge request${mrs.length === 1 ? '' : 's'}`,
278
+ mrs.length > 0,
279
+ );
280
+ } catch (error) {
281
+ mrSpinner.stop('Failed to fetch merge requests', false);
282
+ console.error(error.response?.data?.message || error.message);
283
+
284
+ return;
285
+ }
286
+
287
+ if (!mrs.length) {
288
+ console.log(`${C.dim}No ${state} merge requests.${C.reset}`);
289
+
290
+ return;
291
+ }
292
+
293
+ console.log('');
294
+ console.log(
295
+ `${C.bold}${selected.name}${C.reset} ${C.dim}${selected.path_with_namespace}${C.reset}`,
296
+ );
297
+ console.log('');
298
+
299
+ const tree = buildTree(mrs);
300
+
301
+ tree.forEach((node) => printBranchNode(node));
302
+
303
+ console.log('');
304
+ };
305
+
306
+ module.exports = { getMr };
@@ -2,13 +2,14 @@ const axios = require('axios');
2
2
  const { build, buildStatus } = require('./build');
3
3
  const { deploy } = require('./deploy');
4
4
  const { ACTIONS } = require('./enums/actions.enum');
5
- const { convertParamsToMap, wait, printTable, createSpinner } = require('./utils');
5
+ const { convertParamsToMap, wait, printTable, createSpinner, makeLink } = require('./utils');
6
6
  const { createBranch } = require('./create-branch');
7
7
  const { mrAIReview } = require('./review');
8
8
  const { refactor } = require('./refactor');
9
9
  const { backup } = require('./mongodb-backup');
10
10
  const { merge } = require('./merge');
11
11
  const { cleanup } = require('./cleanup');
12
+ const { getMr } = require('./mr-status');
12
13
 
13
14
  // Time entry imports
14
15
  const { getSprints } = require('./time-entry/utils');
@@ -23,6 +24,7 @@ const {
23
24
  getGitIdStats,
24
25
  getGitlabIssueMergeRequests,
25
26
  getGitMergeRequestDetails,
27
+ getTaskBrowserUrl,
26
28
  } = require('./task-stats');
27
29
 
28
30
  const processArgs = async (type, value) => {
@@ -333,8 +335,10 @@ Options:
333
335
 
334
336
  const mrDetails = await getGitlabIssueMergeRequests(gitIdDetails);
335
337
 
338
+ const taskUrl = getTaskBrowserUrl(task);
339
+
336
340
  return mrDetails.map((mrDetail) => ({
337
- Task: task,
341
+ Task: taskUrl ? makeLink(taskUrl, task) : task,
338
342
  Owner: taskDetail.owner,
339
343
  ...mrDetail,
340
344
  }));
@@ -355,6 +359,25 @@ Options:
355
359
  break;
356
360
  }
357
361
 
362
+ case ACTIONS.MR_STATUS: {
363
+ if (value === '-help' || value === '--h') {
364
+ console.log(`usage: \tnu mr-status [-state <opened|merged|closed|all>] [-search <text>]
365
+ \tnu mr-status [-st <state>] [-q <text>]
366
+
367
+ List GitLab repositories you are a member of, select one, and view its merge requests in a branch tree view.
368
+
369
+ Options:
370
+ -st, --state <state> MR state filter | opened (default), merged, closed, all
371
+ -q, --search <text> Pre-filter repositories by name`);
372
+
373
+ break;
374
+ }
375
+
376
+ await getMr(values || {});
377
+
378
+ break;
379
+ }
380
+
358
381
  case ACTIONS.GET_TASK: {
359
382
  if (value === '-help' || value === '--h') {
360
383
  console.log(`usage: \tnu get-task [-task <task numbers with space>]
@@ -539,7 +562,7 @@ Options:
539
562
  \t[${ACTIONS.BUILD}] [${ACTIONS.DEPLOY}] [${ACTIONS.BUILD_DEPLOY}]
540
563
  \t[${ACTIONS.CREATE_BRANCH}] [${ACTIONS.REVIEW}] [${ACTIONS.MERGE}]
541
564
  \t[${ACTIONS.CLEANUP}] [${ACTIONS.BACKUP}] [${ACTIONS.REFACTOR}]
542
- \t[${ACTIONS.TASK_STATS}] [${ACTIONS.GET_TASK}]
565
+ \t[${ACTIONS.TASK_STATS}] [${ACTIONS.GET_TASK}] [${ACTIONS.MR_STATUS}]
543
566
  \t[${ACTIONS.TIME_INIT}] [${ACTIONS.TIME_ADD}] [${ACTIONS.TIME_STATUS}]\n
544
567
  Available commands:\n
545
568
  backup : Backup MongoDB databases
@@ -549,6 +572,7 @@ Available commands:\n
549
572
  completion : Setup shell autocomplete
550
573
  create-branch : Create git branch
551
574
  deploy : Deploy specified components
575
+ mr-status : List GitLab repos you contribute to and view merge requests as a tree
552
576
  get-task : Get task details from GitLab merge requests
553
577
  merge : Merge source branch into target branch
554
578
  refactor : Refactor the provided text for improved clarity, conciseness, and professional quality
@@ -577,6 +601,8 @@ Example usage:\n
577
601
  nitor cleanup
578
602
  nitor create-branch -task <task number> -type <feat|fix> -description <description> -project <project short name>
579
603
  nitor deploy -project <project> -components <components> -instance <instance>
604
+ nitor mr-status
605
+ nitor mr-status -state opened
580
606
  nitor get-task -task <task numbers with space>
581
607
  nitor merge -source <source branch> -target <target branch>
582
608
  nitor refactor <text>
@@ -1,5 +1,5 @@
1
1
  const axios = require('axios');
2
- const { zohoConfig, gitlabConfig } = require('./utils');
2
+ const { zohoConfig, gitlabConfig, makeLink } = require('./utils');
3
3
 
4
4
  const getZohoUrl = (type, taskNumber) => {
5
5
  switch (type) {
@@ -11,6 +11,20 @@ const getZohoUrl = (type, taskNumber) => {
11
11
  }
12
12
  };
13
13
 
14
+ // Browser URL for a Zoho Sprints task, e.g.
15
+ // https://externalusers.zohosprints.com/workspace/4medica/client/wmoku#P29/itemdetails/I8856
16
+ // Requires ZOHO_WORKSPACE (e.g. "4medica") and ZOHO_PROJECT_PREFIX (e.g. "P29")
17
+ // to be set in the env file; returns an empty string if either is missing.
18
+ const getTaskBrowserUrl = (taskNumber) => {
19
+ const { url, workspace, projectPrefix } = zohoConfig;
20
+
21
+ if (!url || !workspace || !projectPrefix || !taskNumber) {
22
+ return '';
23
+ }
24
+
25
+ return `${url}/workspace/${workspace}/client/wmoku#${projectPrefix}/itemdetails/I${taskNumber}`;
26
+ };
27
+
14
28
  const getZohoConfig = (type, taskNumber) => {
15
29
  return {
16
30
  method: 'get',
@@ -153,10 +167,14 @@ const getGitlabIssueMergeRequests = async (gitDetails) => {
153
167
  const approvalResponse = await axios.request(approvalConfig);
154
168
  const approvalData = approvalResponse.data;
155
169
 
170
+ // Build browser URLs for IDs so they render as clickable links.
171
+ const issueUrl = gitDetail.web_url;
172
+ const mrUrl = mrData.web_url;
173
+
156
174
  // Merge approval data with MR data
157
175
  return {
158
- GitID: gitDetail.iid,
159
- MRID: mrData.iid,
176
+ GitID: issueUrl ? makeLink(issueUrl, gitDetail.iid) : gitDetail.iid,
177
+ MRID: mrUrl ? makeLink(mrUrl, mrData.iid) : mrData.iid,
160
178
  MRStatus: mrData.state,
161
179
  MRAssignedTo: mrData.assignee?.name,
162
180
  MRTarget: mrData.target_branch,
@@ -202,4 +220,5 @@ module.exports = {
202
220
  getGitlabIssueDetails,
203
221
  getGitlabIssueMergeRequests,
204
222
  getGitMergeRequestDetails,
223
+ getTaskBrowserUrl,
205
224
  };