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 +37 -0
- package/LICENSE +15 -0
- package/README.md +21 -2
- package/package.json +13 -4
- package/services/autocomplete.js +8 -0
- package/services/enums/actions.enum.js +1 -0
- package/services/mr-status.js +306 -0
- package/services/process-commands.js +29 -3
- package/services/task-stats.js +22 -3
- package/services/utils.js +36 -9
- package/.commitlintrc.json +0 -10
- package/.editorconfig +0 -16
- package/.eslintrc.js +0 -29
- package/.gitattributes +0 -2
- package/.prettierrc +0 -5
- package/.releaserc.json +0 -34
- package/docs/AUTOCOMPLETE.md +0 -136
- package/docs/BACKUP.md +0 -343
- package/docs/BUILD.md +0 -120
- package/docs/BUILD_DEPLOY.md +0 -320
- package/docs/CLEANUP.md +0 -285
- package/docs/CREATE_BRANCH.md +0 -173
- package/docs/DEPLOY.md +0 -130
- package/docs/MERGE.md +0 -253
- package/docs/README.md +0 -277
- package/docs/REFACTOR.md +0 -375
- package/docs/REVIEW.md +0 -185
- package/docs/TIME_ENTRY.md +0 -422
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 >=
|
|
36
|
-
- npm >=
|
|
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.
|
|
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.
|
|
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",
|
package/services/autocomplete.js
CHANGED
|
@@ -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]: [],
|
|
@@ -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>
|
package/services/task-stats.js
CHANGED
|
@@ -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
|
};
|