m365-agent-cli 1.2.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/LICENSE +22 -0
- package/README.md +916 -0
- package/package.json +50 -0
- package/src/cli.ts +100 -0
- package/src/commands/auto-reply.ts +182 -0
- package/src/commands/calendar.ts +576 -0
- package/src/commands/counter.ts +87 -0
- package/src/commands/create-event.ts +544 -0
- package/src/commands/delegates.ts +286 -0
- package/src/commands/delete-event.ts +321 -0
- package/src/commands/drafts.ts +502 -0
- package/src/commands/files.ts +532 -0
- package/src/commands/find.ts +195 -0
- package/src/commands/findtime.ts +270 -0
- package/src/commands/folders.ts +177 -0
- package/src/commands/forward-event.ts +49 -0
- package/src/commands/graph-calendar.ts +217 -0
- package/src/commands/login.ts +195 -0
- package/src/commands/mail.ts +950 -0
- package/src/commands/oof.ts +263 -0
- package/src/commands/outlook-categories.ts +173 -0
- package/src/commands/outlook-graph.ts +880 -0
- package/src/commands/planner.ts +1678 -0
- package/src/commands/respond.ts +291 -0
- package/src/commands/rooms.ts +210 -0
- package/src/commands/rules.ts +511 -0
- package/src/commands/schedule.ts +109 -0
- package/src/commands/send.ts +204 -0
- package/src/commands/serve.ts +14 -0
- package/src/commands/sharepoint.ts +179 -0
- package/src/commands/site-pages.ts +163 -0
- package/src/commands/subscribe.ts +103 -0
- package/src/commands/subscriptions.ts +29 -0
- package/src/commands/suggest.ts +155 -0
- package/src/commands/todo.ts +2092 -0
- package/src/commands/update-event.ts +608 -0
- package/src/commands/update.ts +88 -0
- package/src/commands/verify-token.ts +62 -0
- package/src/commands/whoami.ts +74 -0
- package/src/index.ts +190 -0
- package/src/lib/atomic-write.ts +20 -0
- package/src/lib/attach-link-spec.test.ts +24 -0
- package/src/lib/attach-link-spec.ts +70 -0
- package/src/lib/attachments.ts +79 -0
- package/src/lib/auth.ts +192 -0
- package/src/lib/calendar-range.test.ts +41 -0
- package/src/lib/calendar-range.ts +103 -0
- package/src/lib/dates.test.ts +74 -0
- package/src/lib/dates.ts +137 -0
- package/src/lib/delegate-client.test.ts +74 -0
- package/src/lib/delegate-client.ts +322 -0
- package/src/lib/ews-client.ts +3418 -0
- package/src/lib/git-commit.ts +4 -0
- package/src/lib/glitchtip-eligibility.ts +220 -0
- package/src/lib/glitchtip.ts +253 -0
- package/src/lib/global-env.ts +3 -0
- package/src/lib/graph-auth.ts +223 -0
- package/src/lib/graph-calendar-client.test.ts +118 -0
- package/src/lib/graph-calendar-client.ts +112 -0
- package/src/lib/graph-client.test.ts +107 -0
- package/src/lib/graph-client.ts +1058 -0
- package/src/lib/graph-constants.ts +12 -0
- package/src/lib/graph-directory.ts +116 -0
- package/src/lib/graph-event.ts +134 -0
- package/src/lib/graph-schedule.ts +173 -0
- package/src/lib/graph-subscriptions.ts +94 -0
- package/src/lib/graph-user-path.ts +13 -0
- package/src/lib/jwt-utils.ts +34 -0
- package/src/lib/markdown.test.ts +21 -0
- package/src/lib/markdown.ts +174 -0
- package/src/lib/mime-type.ts +106 -0
- package/src/lib/oof-client.test.ts +59 -0
- package/src/lib/oof-client.ts +122 -0
- package/src/lib/outlook-graph-client.test.ts +146 -0
- package/src/lib/outlook-graph-client.ts +649 -0
- package/src/lib/outlook-master-categories.ts +145 -0
- package/src/lib/package-info.ts +59 -0
- package/src/lib/places-client.ts +144 -0
- package/src/lib/planner-client.ts +1226 -0
- package/src/lib/rules-client.ts +178 -0
- package/src/lib/sharepoint-client.ts +101 -0
- package/src/lib/site-pages-client.ts +73 -0
- package/src/lib/todo-client.test.ts +298 -0
- package/src/lib/todo-client.ts +1309 -0
- package/src/lib/url-validation.ts +40 -0
- package/src/lib/utils.ts +45 -0
- package/src/lib/webhook-server.ts +51 -0
- package/src/test/auth.test.ts +104 -0
- package/src/test/cli.integration.test.ts +1083 -0
- package/src/test/ews-client.test.ts +268 -0
- package/src/test/mocks/index.ts +375 -0
- package/src/test/mocks/responses.ts +861 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolveGraphAuth } from '../lib/graph-auth.js';
|
|
3
|
+
import {
|
|
4
|
+
type GraphCalendarEvent,
|
|
5
|
+
getCalendar,
|
|
6
|
+
getEvent,
|
|
7
|
+
listCalendars,
|
|
8
|
+
listCalendarView
|
|
9
|
+
} from '../lib/graph-calendar-client.js';
|
|
10
|
+
import { acceptEventInvitation, declineEventInvitation, tentativelyAcceptEventInvitation } from '../lib/graph-event.js';
|
|
11
|
+
import { checkReadOnly } from '../lib/utils.js';
|
|
12
|
+
|
|
13
|
+
export const graphCalendarCommand = new Command('graph-calendar').description(
|
|
14
|
+
'Microsoft Graph calendar REST: calendars, calendarView, events, invitation responses (distinct from EWS `calendar` / `respond`)'
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
function formatEventLine(e: GraphCalendarEvent): string {
|
|
18
|
+
const subj = e.subject?.trim() || '(no subject)';
|
|
19
|
+
const start = e.start?.dateTime;
|
|
20
|
+
const end = e.end?.dateTime;
|
|
21
|
+
const tz = e.start?.timeZone || '';
|
|
22
|
+
const when =
|
|
23
|
+
start && end ? `${start} → ${end}${tz ? ` (${tz})` : ''}` : start ? `${start}${tz ? ` (${tz})` : ''}` : '?';
|
|
24
|
+
const allDay = e.isAllDay ? ' [all-day]' : '';
|
|
25
|
+
return `${when}${allDay}\t${subj}\t${e.id}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
graphCalendarCommand
|
|
29
|
+
.command('list-calendars')
|
|
30
|
+
.description('List calendars (Graph GET /calendars)')
|
|
31
|
+
.option('--json', 'Output as JSON')
|
|
32
|
+
.option('--token <token>', 'Graph access token')
|
|
33
|
+
.option('--identity <name>', 'Graph token cache identity (default: default)')
|
|
34
|
+
.option('--user <email>', 'Target mailbox (delegation)')
|
|
35
|
+
.action(async (opts: { json?: boolean; token?: string; identity?: string; user?: string }) => {
|
|
36
|
+
const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
|
|
37
|
+
if (!auth.success || !auth.token) {
|
|
38
|
+
console.error(`Auth error: ${auth.error}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const r = await listCalendars(auth.token, opts.user);
|
|
42
|
+
if (!r.ok || !r.data) {
|
|
43
|
+
console.error(`Error: ${r.error?.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (opts.json) {
|
|
47
|
+
console.log(JSON.stringify(r.data, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const c of r.data) {
|
|
51
|
+
const label = c.name || '(unnamed)';
|
|
52
|
+
console.log(`${label}\t${c.id}`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
graphCalendarCommand
|
|
57
|
+
.command('get-calendar')
|
|
58
|
+
.description('Get one calendar by id')
|
|
59
|
+
.argument('<calendarId>', 'Calendar id')
|
|
60
|
+
.option('--json', 'Output as JSON')
|
|
61
|
+
.option('--token <token>', 'Graph access token')
|
|
62
|
+
.option('--identity <name>', 'Graph token cache identity (default: default)')
|
|
63
|
+
.option('--user <email>', 'Target mailbox (delegation)')
|
|
64
|
+
.action(async (calendarId: string, opts: { json?: boolean; token?: string; identity?: string; user?: string }) => {
|
|
65
|
+
const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
|
|
66
|
+
if (!auth.success || !auth.token) {
|
|
67
|
+
console.error(`Auth error: ${auth.error}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const r = await getCalendar(auth.token, calendarId, opts.user);
|
|
71
|
+
if (!r.ok || !r.data) {
|
|
72
|
+
console.error(`Error: ${r.error?.message}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
if (opts.json) console.log(JSON.stringify(r.data, null, 2));
|
|
76
|
+
else console.log(JSON.stringify(r.data, null, 2));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
graphCalendarCommand
|
|
80
|
+
.command('list-view')
|
|
81
|
+
.description('List events in a time window (Graph GET .../calendarView)')
|
|
82
|
+
.requiredOption('--start <iso>', 'Start (ISO 8601, e.g. 2026-04-01T00:00:00Z)')
|
|
83
|
+
.requiredOption('--end <iso>', 'End (ISO 8601, exclusive upper bound in many cases — see Graph docs)')
|
|
84
|
+
.option('-c, --calendar <calendarId>', 'Calendar id (omit for default calendar)')
|
|
85
|
+
.option('--json', 'Output as JSON')
|
|
86
|
+
.option('--token <token>', 'Graph access token')
|
|
87
|
+
.option('--identity <name>', 'Graph token cache identity (default: default)')
|
|
88
|
+
.option('--user <email>', 'Target mailbox (delegation)')
|
|
89
|
+
.action(
|
|
90
|
+
async (opts: {
|
|
91
|
+
start: string;
|
|
92
|
+
end: string;
|
|
93
|
+
calendar?: string;
|
|
94
|
+
json?: boolean;
|
|
95
|
+
token?: string;
|
|
96
|
+
identity?: string;
|
|
97
|
+
user?: string;
|
|
98
|
+
}) => {
|
|
99
|
+
const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
|
|
100
|
+
if (!auth.success || !auth.token) {
|
|
101
|
+
console.error(`Auth error: ${auth.error}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
const r = await listCalendarView(auth.token, opts.start, opts.end, {
|
|
105
|
+
calendarId: opts.calendar,
|
|
106
|
+
user: opts.user
|
|
107
|
+
});
|
|
108
|
+
if (!r.ok || !r.data) {
|
|
109
|
+
console.error(`Error: ${r.error?.message}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
if (opts.json) {
|
|
113
|
+
console.log(JSON.stringify(r.data, null, 2));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const e of r.data) {
|
|
117
|
+
console.log(formatEventLine(e));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
graphCalendarCommand
|
|
123
|
+
.command('get-event')
|
|
124
|
+
.description('Get a single event by id (Graph GET /events/{id})')
|
|
125
|
+
.argument('<eventId>', 'Event id')
|
|
126
|
+
.option('--select <fields>', 'OData $select (comma-separated)')
|
|
127
|
+
.option('--json', 'Output as JSON')
|
|
128
|
+
.option('--token <token>', 'Graph access token')
|
|
129
|
+
.option('--identity <name>', 'Graph token cache identity (default: default)')
|
|
130
|
+
.option('--user <email>', 'Mailbox that owns the event (delegation)')
|
|
131
|
+
.action(
|
|
132
|
+
async (
|
|
133
|
+
eventId: string,
|
|
134
|
+
opts: {
|
|
135
|
+
select?: string;
|
|
136
|
+
json?: boolean;
|
|
137
|
+
token?: string;
|
|
138
|
+
identity?: string;
|
|
139
|
+
user?: string;
|
|
140
|
+
}
|
|
141
|
+
) => {
|
|
142
|
+
const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
|
|
143
|
+
if (!auth.success || !auth.token) {
|
|
144
|
+
console.error(`Auth error: ${auth.error}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
const r = await getEvent(auth.token, eventId, opts.user, opts.select);
|
|
148
|
+
if (!r.ok || !r.data) {
|
|
149
|
+
console.error(`Error: ${r.error?.message}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
if (opts.json) console.log(JSON.stringify(r.data, null, 2));
|
|
153
|
+
else console.log(JSON.stringify(r.data, null, 2));
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
function addRespondCommand(
|
|
158
|
+
name: string,
|
|
159
|
+
description: string,
|
|
160
|
+
fn: (o: {
|
|
161
|
+
token: string;
|
|
162
|
+
eventId: string;
|
|
163
|
+
comment?: string;
|
|
164
|
+
sendResponse: boolean;
|
|
165
|
+
user?: string;
|
|
166
|
+
}) => ReturnType<typeof acceptEventInvitation>
|
|
167
|
+
) {
|
|
168
|
+
graphCalendarCommand
|
|
169
|
+
.command(name)
|
|
170
|
+
.description(description)
|
|
171
|
+
.argument('<eventId>', 'Event id')
|
|
172
|
+
.option('--comment <text>', 'Optional comment to organizer')
|
|
173
|
+
.option('--no-notify', "Don't send response to organizer")
|
|
174
|
+
.option('--token <token>', 'Graph access token')
|
|
175
|
+
.option('--identity <name>', 'Graph token cache identity (default: default)')
|
|
176
|
+
.option('--user <email>', 'Mailbox that owns the invitation (delegation)')
|
|
177
|
+
.action(
|
|
178
|
+
async (
|
|
179
|
+
eventId: string,
|
|
180
|
+
opts: {
|
|
181
|
+
comment?: string;
|
|
182
|
+
notify: boolean;
|
|
183
|
+
token?: string;
|
|
184
|
+
identity?: string;
|
|
185
|
+
user?: string;
|
|
186
|
+
},
|
|
187
|
+
cmd: any
|
|
188
|
+
) => {
|
|
189
|
+
checkReadOnly(cmd);
|
|
190
|
+
const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
|
|
191
|
+
if (!auth.success || !auth.token) {
|
|
192
|
+
console.error(`Auth error: ${auth.error}`);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
const r = await fn({
|
|
196
|
+
token: auth.token,
|
|
197
|
+
eventId,
|
|
198
|
+
comment: opts.comment,
|
|
199
|
+
sendResponse: opts.notify !== false,
|
|
200
|
+
user: opts.user
|
|
201
|
+
});
|
|
202
|
+
if (!r.ok) {
|
|
203
|
+
console.error(`Error: ${r.error?.message}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
console.log('Done.');
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
addRespondCommand('accept', 'Accept a meeting request (Graph POST .../accept)', acceptEventInvitation);
|
|
212
|
+
addRespondCommand('decline', 'Decline a meeting request (Graph POST .../decline)', declineEventInvitation);
|
|
213
|
+
addRespondCommand(
|
|
214
|
+
'tentative',
|
|
215
|
+
'Tentatively accept without proposing a new time (Graph POST .../tentativelyAccept)',
|
|
216
|
+
tentativelyAcceptEventInvitation
|
|
217
|
+
);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { atomicWriteUtf8File } from '../lib/atomic-write.js';
|
|
8
|
+
import { getMicrosoftTenantPathSegment } from '../lib/jwt-utils.js';
|
|
9
|
+
|
|
10
|
+
async function performDeviceCodeFlow(clientId: string, tenant: string, scope: string, label: string): Promise<string> {
|
|
11
|
+
console.log(`\nInitiating Device Code flow for ${label}...`);
|
|
12
|
+
|
|
13
|
+
const deviceCodeRes = await fetch(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/devicecode`, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
16
|
+
body: new URLSearchParams({
|
|
17
|
+
client_id: clientId,
|
|
18
|
+
scope: scope
|
|
19
|
+
}).toString()
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const deviceCodeJson = await deviceCodeRes.json();
|
|
23
|
+
|
|
24
|
+
if (!deviceCodeRes.ok) {
|
|
25
|
+
console.error(`Failed to initiate ${label} device code flow:`, deviceCodeJson);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log('\n=========================================================');
|
|
30
|
+
console.log(deviceCodeJson.message);
|
|
31
|
+
console.log('=========================================================\n');
|
|
32
|
+
|
|
33
|
+
const deviceCode = deviceCodeJson.device_code;
|
|
34
|
+
const interval = (deviceCodeJson.interval || 5) * 1000;
|
|
35
|
+
const expiresAt = Date.now() + (deviceCodeJson.expires_in || 900) * 1000;
|
|
36
|
+
|
|
37
|
+
let authenticated = false;
|
|
38
|
+
let refreshToken = '';
|
|
39
|
+
let pollInterval = interval;
|
|
40
|
+
|
|
41
|
+
console.log(`Waiting for ${label} authentication...`);
|
|
42
|
+
|
|
43
|
+
while (!authenticated) {
|
|
44
|
+
if (Date.now() > expiresAt) {
|
|
45
|
+
console.error(`\n${label} device code expired. Please run the command again.`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
50
|
+
|
|
51
|
+
const tokenRes = await fetch(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
54
|
+
body: new URLSearchParams({
|
|
55
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
56
|
+
client_id: clientId,
|
|
57
|
+
device_code: deviceCode
|
|
58
|
+
}).toString()
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const tokenJson = await tokenRes.json();
|
|
62
|
+
|
|
63
|
+
if (tokenRes.ok) {
|
|
64
|
+
authenticated = true;
|
|
65
|
+
refreshToken = tokenJson.refresh_token;
|
|
66
|
+
// Extract username from access token
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const parts = tokenJson.access_token.split('.');
|
|
70
|
+
|
|
71
|
+
if (parts.length === 3) {
|
|
72
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
73
|
+
|
|
74
|
+
const rawUsername = payload.upn || payload.email;
|
|
75
|
+
const username = rawUsername ? rawUsername.replace(/[\r\n]/g, '') : undefined;
|
|
76
|
+
|
|
77
|
+
if (username) {
|
|
78
|
+
let envContent = '';
|
|
79
|
+
|
|
80
|
+
const configDir = join(homedir(), '.config', 'm365-agent-cli');
|
|
81
|
+
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
82
|
+
const envPath = join(configDir, '.env');
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
envContent = await readFile(envPath, 'utf8');
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
if (err.code !== 'ENOENT') throw err;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (/^EWS_USERNAME=.*$/m.test(envContent)) {
|
|
91
|
+
envContent = envContent.replace(/^EWS_USERNAME=.*$/m, () => `EWS_USERNAME=${username}`);
|
|
92
|
+
} else {
|
|
93
|
+
envContent += `\nEWS_USERNAME=${username}\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await atomicWriteUtf8File(envPath, `${envContent.trim()}\n`, 0o600);
|
|
97
|
+
|
|
98
|
+
console.log(`Saved EWS_USERNAME (${username}) to ${envPath}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (_e) {
|
|
102
|
+
/* ignore parse errors */
|
|
103
|
+
}
|
|
104
|
+
if (!refreshToken) {
|
|
105
|
+
console.error(`\nFailed to obtain ${label} refresh token. Ensure the offline_access scope is granted.`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
} else if (tokenJson.error === 'authorization_pending') {
|
|
109
|
+
// Continue polling
|
|
110
|
+
} else if (tokenJson.error === 'slow_down') {
|
|
111
|
+
pollInterval += 5000;
|
|
112
|
+
} else {
|
|
113
|
+
console.error(`\n${label} authentication failed:`, tokenJson.error_description || tokenJson.error);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(`\n${label} authentication successful!`);
|
|
119
|
+
|
|
120
|
+
return refreshToken;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const loginCommand = new Command('login')
|
|
124
|
+
.description('Interactive login to obtain refresh tokens via OAuth2 Device Code flow')
|
|
125
|
+
.action(async () => {
|
|
126
|
+
let clientId = process.env.EWS_CLIENT_ID;
|
|
127
|
+
|
|
128
|
+
// Read existing .env if present
|
|
129
|
+
const configDir = join(homedir(), '.config', 'm365-agent-cli');
|
|
130
|
+
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
131
|
+
const envPath = join(configDir, '.env');
|
|
132
|
+
let envContent = '';
|
|
133
|
+
if (existsSync(envPath)) {
|
|
134
|
+
envContent = await readFile(envPath, 'utf8');
|
|
135
|
+
if (!clientId) {
|
|
136
|
+
const match = envContent.match(/^EWS_CLIENT_ID=(.*)$/m);
|
|
137
|
+
if (match) {
|
|
138
|
+
clientId = match[1].trim();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!clientId) {
|
|
144
|
+
const rl = createInterface({
|
|
145
|
+
input: process.stdin,
|
|
146
|
+
output: process.stdout
|
|
147
|
+
});
|
|
148
|
+
clientId = await rl.question('Enter your EWS_CLIENT_ID: ');
|
|
149
|
+
rl.close();
|
|
150
|
+
clientId = clientId.trim();
|
|
151
|
+
|
|
152
|
+
if (!clientId) {
|
|
153
|
+
console.error('EWS_CLIENT_ID is required.');
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Save it to .env
|
|
158
|
+
envContent += `\nEWS_CLIENT_ID=${clientId}\n`;
|
|
159
|
+
await atomicWriteUtf8File(envPath, `${envContent.trim()}\n`, 0o600);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const tenant = getMicrosoftTenantPathSegment();
|
|
163
|
+
|
|
164
|
+
// Use a single Graph Device Code flow to obtain a multi-resource refresh token
|
|
165
|
+
const graphScope =
|
|
166
|
+
'offline_access User.Read Calendars.ReadWrite Mail.ReadWrite Files.ReadWrite.All Sites.ReadWrite.All Tasks.ReadWrite Group.ReadWrite.All';
|
|
167
|
+
const rawToken = await performDeviceCodeFlow(clientId, tenant, graphScope, 'Microsoft 365');
|
|
168
|
+
const refreshToken = rawToken.replace(/[\r\n]/g, '');
|
|
169
|
+
|
|
170
|
+
// Save tokens immediately
|
|
171
|
+
try {
|
|
172
|
+
envContent = await readFile(envPath, 'utf8');
|
|
173
|
+
} catch (err: any) {
|
|
174
|
+
if (err.code !== 'ENOENT') throw err;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Update or append EWS_REFRESH_TOKEN
|
|
178
|
+
if (/^EWS_REFRESH_TOKEN=.*$/m.test(envContent)) {
|
|
179
|
+
envContent = envContent.replace(/^EWS_REFRESH_TOKEN=.*$/m, () => `EWS_REFRESH_TOKEN=${refreshToken}`);
|
|
180
|
+
} else {
|
|
181
|
+
envContent += `\nEWS_REFRESH_TOKEN=${refreshToken}\n`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Update or append GRAPH_REFRESH_TOKEN
|
|
185
|
+
if (/^GRAPH_REFRESH_TOKEN=.*$/m.test(envContent)) {
|
|
186
|
+
envContent = envContent.replace(/^GRAPH_REFRESH_TOKEN=.*$/m, () => `GRAPH_REFRESH_TOKEN=${refreshToken}`);
|
|
187
|
+
} else {
|
|
188
|
+
envContent += `\nGRAPH_REFRESH_TOKEN=${refreshToken}\n`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
envContent = envContent.replace(/\n{3,}/g, '\n\n');
|
|
192
|
+
await atomicWriteUtf8File(envPath, `${envContent.trim()}\n`, 0o600);
|
|
193
|
+
|
|
194
|
+
console.log(`Saved GRAPH_REFRESH_TOKEN and EWS_REFRESH_TOKEN to ${envPath}`);
|
|
195
|
+
});
|