@falldownthesystem/devtool 0.4.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,220 @@
1
+ import { stripVTControlCharacters } from 'node:util';
2
+ import { createRequire } from 'node:module';
3
+ import { table, heading, field } from '../utils/output.js';
4
+ import { outputJson } from '../utils/mode.js';
5
+
6
+ const require = createRequire(import.meta.url);
7
+ let convert;
8
+
9
+ export function graphText(value) {
10
+ return stripVTControlCharacters(String(value ?? '')).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, '');
11
+ }
12
+
13
+ function bodyText(body) {
14
+ if (!body) return '';
15
+ if (body.contentType?.toLowerCase() !== 'html') return graphText(body.content);
16
+ convert ||= require('html-to-text').convert;
17
+ return graphText(convert(body.content || '', { wordwrap: false, selectors: [{ selector: 'img', format: 'skip' }] }));
18
+ }
19
+
20
+ function person(recipient) {
21
+ return recipient ? { name: recipient.emailAddress?.name || null, address: recipient.emailAddress?.address || null } : null;
22
+ }
23
+
24
+ function people(recipients) {
25
+ return (recipients || []).map(person);
26
+ }
27
+
28
+ export function curateCalendarJson(calendar) {
29
+ return {
30
+ id: calendar.id,
31
+ name: calendar.name || '',
32
+ owner: calendar.owner || null,
33
+ isDefault: calendar.isDefaultCalendar ?? false,
34
+ canViewPrivateItems: calendar.canViewPrivateItems ?? false,
35
+ };
36
+ }
37
+
38
+ export function curateEventJson(event) {
39
+ return {
40
+ id: event.id,
41
+ subject: event.subject || '',
42
+ start: event.start || null,
43
+ end: event.end || null,
44
+ isAllDay: event.isAllDay ?? false,
45
+ isCancelled: event.isCancelled ?? false,
46
+ response: event.responseStatus?.response || null,
47
+ organizer: person(event.organizer),
48
+ attendees: (event.attendees || []).map((a) => ({ ...person(a), type: a.type || null, response: a.status?.response || null })),
49
+ location: event.location?.displayName || null,
50
+ type: event.type || null,
51
+ seriesId: event.seriesMasterId || null,
52
+ body: bodyText(event.body),
53
+ joinUrl: event.onlineMeeting?.joinUrl || null,
54
+ url: event.webLink || null,
55
+ };
56
+ }
57
+
58
+ export function curateNextEventJson(result) {
59
+ return {
60
+ from: result.from,
61
+ to: result.to,
62
+ event: result.event ? curateEventJson(result.event) : null,
63
+ };
64
+ }
65
+
66
+ export function curateAvailabilityJson(result) {
67
+ return {
68
+ from: result.from, to: result.to, intervalMinutes: result.intervalMinutes,
69
+ legend: { 0: 'free or working elsewhere', 1: 'tentative', 2: 'busy', 3: 'out of office' },
70
+ schedules: result.schedules.map((schedule) => ({
71
+ address: schedule.scheduleId,
72
+ error: schedule.error ? { code: schedule.error.responseCode || null, message: schedule.error.message || 'Availability is unknown.' } : null,
73
+ availabilityView: schedule.error ? null : schedule.availabilityView ?? null,
74
+ items: (schedule.scheduleItems || []).map((item) => ({
75
+ status: item.status || 'unknown', start: item.start || null, end: item.end || null,
76
+ subject: item.subject || null, location: item.location || null, isPrivate: item.isPrivate ?? null,
77
+ })),
78
+ })),
79
+ };
80
+ }
81
+
82
+ export function formatNextEvent(data, options) {
83
+ if (options.mode === 'json') { outputJson(data); return; }
84
+ formatGraph(data.event ? { from: data.from, to: data.to, ...data.event } : {
85
+ from: data.from, to: data.to, message: 'No upcoming event found in this calendar and date range.',
86
+ }, options);
87
+ }
88
+
89
+ export function formatAvailability(data, options) {
90
+ if (options.mode === 'json') { outputJson(data); return; }
91
+ console.log(`Window: ${data.from} to ${data.to} (UTC; ${data.intervalMinutes}-minute slots)`);
92
+ for (const schedule of data.schedules) {
93
+ formatGraph(schedule, { ...options, title: `Availability: ${schedule.address}` });
94
+ }
95
+ console.log('Slot codes: 0=free/working elsewhere, 1=tentative, 2=busy, 3=out of office. Errors mean unknown, not free.');
96
+ }
97
+
98
+ export function curateChatJson(chat) {
99
+ return {
100
+ id: chat.id,
101
+ topic: chat.topic || null,
102
+ type: chat.chatType || null,
103
+ members: (chat.members || []).map((m) => ({ id: m.userId || m.id, name: m.displayName || null, email: m.email || null })),
104
+ createdAt: chat.createdDateTime || null,
105
+ updatedAt: chat.lastUpdatedDateTime || null,
106
+ url: chat.webUrl || null,
107
+ };
108
+ }
109
+
110
+ export function curateChatMessageJson(message) {
111
+ return {
112
+ id: message.id,
113
+ chatId: message.chatId || null,
114
+ type: message.messageType || null,
115
+ from: message.from?.user?.displayName || message.from?.application?.displayName || null,
116
+ createdAt: message.createdDateTime || null,
117
+ editedAt: message.lastEditedDateTime || null,
118
+ deletedAt: message.deletedDateTime || null,
119
+ body: bodyText(message.body),
120
+ attachments: (message.attachments || []).map((a) => ({ id: a.id, name: a.name || null, type: a.contentType || null })),
121
+ url: message.webUrl || null,
122
+ };
123
+ }
124
+
125
+ export function curateMailJson(message) {
126
+ return {
127
+ id: message.id,
128
+ subject: message.subject || '',
129
+ from: person(message.from),
130
+ to: people(message.toRecipients),
131
+ ...(message.ccRecipients ? { cc: people(message.ccRecipients) } : {}),
132
+ receivedAt: message.receivedDateTime || null,
133
+ sentAt: message.sentDateTime || null,
134
+ isRead: message.isRead ?? null,
135
+ hasAttachments: message.hasAttachments ?? false,
136
+ folderId: message.parentFolderId || null,
137
+ preview: message.bodyPreview || '',
138
+ ...(message.body ? { body: bodyText(message.body) } : {}),
139
+ url: message.webLink || null,
140
+ };
141
+ }
142
+
143
+ export function curateFolderJson(folder) {
144
+ return { id: folder.id, name: folder.displayName || '', total: folder.totalItemCount ?? 0, unread: folder.unreadItemCount ?? 0, childFolders: folder.childFolderCount ?? 0 };
145
+ }
146
+
147
+ export function curateSiteJson(site) {
148
+ return { id: site.id, name: site.displayName || site.name || '', description: site.description || '', url: site.webUrl || null };
149
+ }
150
+
151
+ export function curateDriveJson(drive) {
152
+ return { id: drive.id, name: drive.name || '', type: drive.driveType || null, description: drive.description || '', url: drive.webUrl || null };
153
+ }
154
+
155
+ export function curateFileJson(file) {
156
+ return {
157
+ id: file.id,
158
+ name: file.name || '',
159
+ type: file.folder ? 'folder' : file.file ? 'file' : 'item',
160
+ size: file.size ?? null,
161
+ mimeType: file.file?.mimeType || null,
162
+ childCount: file.folder?.childCount ?? null,
163
+ driveId: file.parentReference?.driveId || null,
164
+ parentId: file.parentReference?.id || null,
165
+ updatedAt: file.lastModifiedDateTime || null,
166
+ url: file.webUrl || null,
167
+ };
168
+ }
169
+
170
+ export function curateListJson(list) {
171
+ return { id: list.id, name: list.displayName || list.name || '', description: list.description || '', template: list.list?.template || null, url: list.webUrl || null };
172
+ }
173
+
174
+ export function curateListItemJson(item) {
175
+ return {
176
+ id: item.id,
177
+ fields: Object.fromEntries(Object.entries(item.fields || {}).filter(([key]) => !key.startsWith('@odata.'))),
178
+ createdAt: item.createdDateTime || null,
179
+ updatedAt: item.lastModifiedDateTime || null,
180
+ url: item.webUrl || null,
181
+ };
182
+ }
183
+
184
+ function display(value) {
185
+ if (value === null || value === undefined) return '';
186
+ return graphText(typeof value === 'object' ? JSON.stringify(value) : value);
187
+ }
188
+
189
+ export function formatGraph(data, { mode, title, columns = [], records = false }) {
190
+ if (mode === 'json') { outputJson(data); return; }
191
+ const items = Array.isArray(data) ? data : [data];
192
+ if (!items.length) { console.log('No results found.'); return; }
193
+ if (mode === 'rich') heading(graphText(title));
194
+ if (Array.isArray(data) && !records) {
195
+ const rows = items.map((item) => columns.map(([, value]) => display(value(item))));
196
+ if (mode === 'plain') {
197
+ for (const row of rows) console.log(row.map((cell) => cell.replace(/\s+/g, ' ')).join('\t'));
198
+ } else {
199
+ const widths = columns.map(([, , width]) => width || 32);
200
+ const fits = widths.reduce((sum, width) => sum + width, columns.length + 1) <= (process.stdout.columns || 120)
201
+ && rows.every((row) => row.every((cell, index) => cell.split('\n').every((line) => line.length <= widths[index] - 2)));
202
+ if (fits) console.log(table(columns.map(([label]) => label), rows, { colWidths: widths }));
203
+ else {
204
+ // Resource IDs must remain complete and copyable when a table would truncate them.
205
+ for (const row of rows) {
206
+ for (const [index, cell] of row.entries()) field(columns[index][0], cell);
207
+ console.log('');
208
+ }
209
+ }
210
+ }
211
+ return;
212
+ }
213
+ for (const [index, item] of items.entries()) {
214
+ if (index) console.log('');
215
+ for (const [key, value] of Object.entries(item)) {
216
+ if (mode === 'rich') field(key, display(value));
217
+ else console.log(`${key}: ${display(value)}`);
218
+ }
219
+ }
220
+ }
package/src/index.js CHANGED
@@ -1,42 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { createRequire } from 'node:module';
3
4
  import { Command } from 'commander';
4
- import { isConfigured, runSetup } from './config/store.js';
5
5
  import configCmd from './commands/config.js';
6
6
  import prCmd from './commands/pr.js';
7
7
  import jiraCmd from './commands/jira.js';
8
8
  import confluenceCmd from './commands/confluence.js';
9
9
  import sdCmd from './commands/sd.js';
10
+ import graphCmd from './commands/graph.js';
11
+
12
+ const { version } = createRequire(import.meta.url)('../package.json');
10
13
 
11
14
  const program = new Command();
12
15
 
13
16
  program
14
17
  .name('dev')
15
- .description('CLI for Bitbucket PRs, Jira tickets, Confluence pages, and Service Desk tickets')
16
- .version('0.4.1')
18
+ .description('CLI for Bitbucket, Jira, Confluence, Service Desk, and Microsoft Graph')
19
+ .version(version)
17
20
  .option('--json', 'Output raw JSON')
18
- .option('--plain', 'Output compact plain text (no tables, no colors)');
21
+ .option('--plain', 'Output compact plain text (no tables, no colors)')
22
+ .option('-c, --connection <name>', 'Connection to use (defaults to the default connection of the command\'s type)');
19
23
 
20
24
  program.addCommand(configCmd);
21
25
  program.addCommand(prCmd);
22
26
  program.addCommand(jiraCmd);
23
27
  program.addCommand(confluenceCmd);
24
28
  program.addCommand(sdCmd);
25
-
26
- program.hook('preAction', async (thisCommand, actionCommand) => {
27
- const cmdChain = [];
28
- let cmd = actionCommand;
29
- while (cmd) {
30
- cmdChain.unshift(cmd.name());
31
- cmd = cmd.parent;
32
- }
33
- if (cmdChain.includes('config')) return;
34
-
35
- if (!isConfigured()) {
36
- console.log('First run detected. Let\'s set up your configuration.\n');
37
- await runSetup();
38
- console.log('');
39
- }
40
- });
29
+ program.addCommand(graphCmd);
41
30
 
42
31
  program.parse();
@@ -0,0 +1,5 @@
1
+ import { getConnection } from '../config/store.js';
2
+
3
+ export function connectionFor(cmd, type) {
4
+ return getConnection(type, cmd.optsWithGlobals().connection);
5
+ }