meetfy 1.0.2 → 1.0.3
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/dist/index.js +403 -680
- package/package.json +4 -12
package/dist/index.js
CHANGED
|
@@ -1,737 +1,460 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
// src/index.
|
|
5
|
-
import { render } from "ink";
|
|
4
|
+
// src/adapters/cli/index.ts
|
|
6
5
|
import { Command } from "commander";
|
|
7
6
|
|
|
8
|
-
// src/
|
|
9
|
-
import { Box as Box2 } from "ink";
|
|
10
|
-
|
|
11
|
-
// src/components/Welcome.tsx
|
|
12
|
-
import { Box, Text } from "ink";
|
|
13
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
-
function Welcome() {
|
|
15
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
16
|
-
/* @__PURE__ */ jsx(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "center", children: [
|
|
17
|
-
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Meetfy" }),
|
|
18
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Instant Meeting Creator" })
|
|
19
|
-
] }) }),
|
|
20
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Create instant meetings and reserve time in Google Calendar" }),
|
|
21
|
-
/* @__PURE__ */ jsx(Text, { children: "\n" })
|
|
22
|
-
] });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// src/components/App.tsx
|
|
26
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
27
|
-
function App({ children }) {
|
|
28
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
29
|
-
/* @__PURE__ */ jsx2(Welcome, {}),
|
|
30
|
-
children
|
|
31
|
-
] });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// src/components/CreateMeetingView.tsx
|
|
35
|
-
import { useState, useCallback } from "react";
|
|
36
|
-
import { Box as Box3, Text as Text2, useApp } from "ink";
|
|
37
|
-
import TextInput from "ink-text-input";
|
|
38
|
-
import Spinner from "ink-spinner";
|
|
39
|
-
|
|
40
|
-
// src/services/authService.ts
|
|
7
|
+
// src/adapters/google/auth-adapter.ts
|
|
41
8
|
import { google } from "googleapis";
|
|
42
|
-
import { readFileSync, existsSync } from "fs";
|
|
43
|
-
import { join } from "path";
|
|
9
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
44
11
|
|
|
45
|
-
// src/
|
|
12
|
+
// src/infrastructure/config.ts
|
|
46
13
|
import Conf from "conf";
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
14
|
+
function createConfig() {
|
|
15
|
+
return new Conf({
|
|
16
|
+
projectName: "meetfy",
|
|
17
|
+
configName: "meetfy-config"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
52
20
|
|
|
53
|
-
// src/
|
|
21
|
+
// src/infrastructure/web-server.ts
|
|
54
22
|
import express from "express";
|
|
23
|
+
var HTML = `
|
|
24
|
+
<!DOCTYPE html>
|
|
25
|
+
<html>
|
|
26
|
+
<head><meta charset="utf-8"><title>Meetfy</title></head>
|
|
27
|
+
<body style="font-family:system-ui;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;margin:0;">
|
|
28
|
+
<h1>Meetfy</h1>
|
|
29
|
+
<p>You can close this window now.</p>
|
|
30
|
+
</body>
|
|
31
|
+
</html>`;
|
|
32
|
+
function createCodeServer(port) {
|
|
33
|
+
return function getCode(onListening) {
|
|
34
|
+
const app = express();
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
app.get("/", (req, res) => {
|
|
37
|
+
const code = req.query.code;
|
|
38
|
+
if (code) {
|
|
39
|
+
resolve(code);
|
|
40
|
+
res.send(HTML);
|
|
41
|
+
server.close();
|
|
42
|
+
} else {
|
|
43
|
+
reject(new Error("No code in callback"));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const server = app.listen(port, (err) => {
|
|
47
|
+
if (err) {
|
|
48
|
+
reject(err);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
onListening?.();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
}
|
|
55
56
|
|
|
56
|
-
// src/
|
|
57
|
-
var PORT_NUMBER = 3434;
|
|
58
|
-
|
|
59
|
-
// src/services/webServer.ts
|
|
60
|
-
var server = express();
|
|
61
|
-
var getCodeServer = async (onListening) => new Promise((resolve, reject) => {
|
|
62
|
-
let instance;
|
|
63
|
-
server.get("/", (req, res) => {
|
|
64
|
-
const { code } = req.query;
|
|
65
|
-
if (code) {
|
|
66
|
-
resolve(code);
|
|
67
|
-
res.send(`
|
|
68
|
-
<html>
|
|
69
|
-
<style>
|
|
70
|
-
body {
|
|
71
|
-
display: flex;
|
|
72
|
-
flex-direction: column;
|
|
73
|
-
align-items: center;
|
|
74
|
-
justify-content: center;
|
|
75
|
-
height: 100vh;
|
|
76
|
-
background-color: #f0f0f0;
|
|
77
|
-
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
78
|
-
}
|
|
79
|
-
h1 { color: #000; }
|
|
80
|
-
p { color: #000; }
|
|
81
|
-
</style>
|
|
82
|
-
<body>
|
|
83
|
-
<h1>Meetfy</h1>
|
|
84
|
-
<p>You can close this window now.</p>
|
|
85
|
-
</body>
|
|
86
|
-
</html>
|
|
87
|
-
`);
|
|
88
|
-
instance.close();
|
|
89
|
-
} else {
|
|
90
|
-
reject(new Error("No code found"));
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
instance = server.listen(PORT_NUMBER, (error) => {
|
|
94
|
-
if (error) {
|
|
95
|
-
reject(error);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
onListening?.();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// src/services/authService.ts
|
|
57
|
+
// src/adapters/google/auth-adapter.ts
|
|
103
58
|
var SCOPES = [
|
|
104
59
|
"https://www.googleapis.com/auth/calendar",
|
|
105
60
|
"https://www.googleapis.com/auth/calendar.events"
|
|
106
61
|
];
|
|
107
62
|
var CREDENTIALS_PATH = join(process.cwd(), "credentials.json");
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
63
|
+
function createGoogleAuthAdapter(redirectPort) {
|
|
64
|
+
const config = createConfig();
|
|
65
|
+
const getCodeServer = createCodeServer(redirectPort);
|
|
66
|
+
return {
|
|
67
|
+
async authenticate() {
|
|
68
|
+
try {
|
|
69
|
+
if (!existsSync(CREDENTIALS_PATH)) {
|
|
70
|
+
return { type: "no_credentials" };
|
|
71
|
+
}
|
|
72
|
+
const credentials = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf-8"));
|
|
73
|
+
const { client_secret: clientSecret, client_id: clientId } = credentials.installed || credentials.web;
|
|
74
|
+
const redirectUri = `http://localhost:${redirectPort}`;
|
|
75
|
+
const oAuth2Client = new google.auth.OAuth2(
|
|
76
|
+
clientId,
|
|
77
|
+
clientSecret,
|
|
78
|
+
redirectUri
|
|
79
|
+
);
|
|
80
|
+
const storedTokens = config.get("googleTokens");
|
|
81
|
+
if (storedTokens) {
|
|
82
|
+
oAuth2Client.setCredentials(storedTokens);
|
|
83
|
+
if (storedTokens.expiry_date && Date.now() > storedTokens.expiry_date) {
|
|
84
|
+
const { credentials: newTokens } = await oAuth2Client.refreshAccessToken();
|
|
85
|
+
oAuth2Client.setCredentials(newTokens);
|
|
86
|
+
config.set("googleTokens", newTokens);
|
|
87
|
+
}
|
|
88
|
+
return { type: "ok", client: oAuth2Client };
|
|
89
|
+
}
|
|
90
|
+
const authUrl = oAuth2Client.generateAuthUrl({
|
|
91
|
+
access_type: "offline",
|
|
92
|
+
scope: SCOPES
|
|
93
|
+
});
|
|
94
|
+
const fetchToken = async (callbacks) => {
|
|
95
|
+
try {
|
|
96
|
+
const code = await getCodeServer(callbacks.onWaiting);
|
|
97
|
+
const { tokens } = await oAuth2Client.getToken(code);
|
|
98
|
+
oAuth2Client.setCredentials(tokens);
|
|
99
|
+
config.set("googleTokens", tokens);
|
|
100
|
+
return oAuth2Client;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
return { type: "need_code", authUrl, fetchToken };
|
|
106
|
+
} catch {
|
|
107
|
+
return { type: "error", message: "Authentication failed" };
|
|
131
108
|
}
|
|
132
|
-
|
|
109
|
+
},
|
|
110
|
+
async logout() {
|
|
111
|
+
config.clear();
|
|
133
112
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/adapters/google/calendar-adapter.ts
|
|
117
|
+
import { google as google2 } from "googleapis";
|
|
118
|
+
function createGoogleCalendarAdapter() {
|
|
119
|
+
return {
|
|
120
|
+
async createEvent(client, input) {
|
|
139
121
|
try {
|
|
140
|
-
const
|
|
141
|
-
const {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
122
|
+
const auth = client;
|
|
123
|
+
const calendar = google2.calendar({ version: "v3", auth });
|
|
124
|
+
const now = /* @__PURE__ */ new Date();
|
|
125
|
+
const startTime = new Date(now.getTime() + 5 * 60 * 1e3);
|
|
126
|
+
const endTime = new Date(startTime.getTime() + 30 * 60 * 1e3);
|
|
127
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
128
|
+
const response = await calendar.events.insert({
|
|
129
|
+
calendarId: "primary",
|
|
130
|
+
requestBody: {
|
|
131
|
+
summary: input.title,
|
|
132
|
+
description: input.description,
|
|
133
|
+
start: { dateTime: startTime.toISOString(), timeZone: tz },
|
|
134
|
+
end: { dateTime: endTime.toISOString(), timeZone: tz },
|
|
135
|
+
attendees: input.participants.map((email) => ({ email: email.trim() })),
|
|
136
|
+
conferenceData: {
|
|
137
|
+
createRequest: {
|
|
138
|
+
requestId: `meetfy-${Date.now()}`,
|
|
139
|
+
conferenceSolutionKey: { type: "hangoutsMeet" }
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
reminders: {
|
|
143
|
+
useDefault: false,
|
|
144
|
+
overrides: [
|
|
145
|
+
{ method: "email", minutes: 10 },
|
|
146
|
+
{ method: "popup", minutes: 5 }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
conferenceDataVersion: 1,
|
|
151
|
+
sendUpdates: "all"
|
|
152
|
+
});
|
|
153
|
+
if (!response.data.id || !response.data.hangoutLink) return null;
|
|
154
|
+
return {
|
|
155
|
+
id: response.data.id,
|
|
156
|
+
title: input.title,
|
|
157
|
+
startTime: startTime.toLocaleString(),
|
|
158
|
+
endTime: endTime.toLocaleString(),
|
|
159
|
+
hangoutLink: response.data.hangoutLink
|
|
160
|
+
};
|
|
145
161
|
} catch {
|
|
146
162
|
return null;
|
|
147
163
|
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
},
|
|
177
|
-
attendees: options.participants.map((email) => ({ email: email.trim() })),
|
|
178
|
-
conferenceData: {
|
|
179
|
-
createRequest: {
|
|
180
|
-
requestId: `meetfy-${Date.now()}`,
|
|
181
|
-
conferenceSolutionKey: {
|
|
182
|
-
type: "hangoutsMeet"
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
reminders: {
|
|
187
|
-
useDefault: false,
|
|
188
|
-
overrides: [
|
|
189
|
-
{ method: "email", minutes: 10 },
|
|
190
|
-
{ method: "popup", minutes: 5 }
|
|
191
|
-
]
|
|
164
|
+
},
|
|
165
|
+
async getNextEvent(client) {
|
|
166
|
+
try {
|
|
167
|
+
const auth = client;
|
|
168
|
+
const calendar = google2.calendar({ version: "v3", auth });
|
|
169
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
170
|
+
const response = await calendar.events.list({
|
|
171
|
+
calendarId: "primary",
|
|
172
|
+
timeMin: now,
|
|
173
|
+
singleEvents: true,
|
|
174
|
+
orderBy: "startTime",
|
|
175
|
+
maxResults: 1
|
|
176
|
+
});
|
|
177
|
+
const event = response.data.items?.[0];
|
|
178
|
+
if (!event) return null;
|
|
179
|
+
const start = event.start?.dateTime || event.start?.date;
|
|
180
|
+
const end = event.end?.dateTime || event.end?.date;
|
|
181
|
+
if (!start || !end) return null;
|
|
182
|
+
return {
|
|
183
|
+
id: event.id ?? "",
|
|
184
|
+
title: event.summary ?? "Untitled",
|
|
185
|
+
startTime: new Date(start).toLocaleString(),
|
|
186
|
+
endTime: new Date(end).toLocaleString(),
|
|
187
|
+
hangoutLink: event.hangoutLink ?? void 0,
|
|
188
|
+
location: event.location ?? void 0
|
|
189
|
+
};
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
192
|
}
|
|
193
|
-
};
|
|
194
|
-
const response = await calendar.events.insert({
|
|
195
|
-
calendarId: "primary",
|
|
196
|
-
requestBody: event,
|
|
197
|
-
conferenceDataVersion: 1,
|
|
198
|
-
sendUpdates: "all"
|
|
199
|
-
});
|
|
200
|
-
if (!response.data.id || !response.data.hangoutLink) {
|
|
201
|
-
throw new Error("Failed to create meeting with Google Meet link");
|
|
202
|
-
}
|
|
203
|
-
return {
|
|
204
|
-
id: response.data.id,
|
|
205
|
-
hangoutLink: response.data.hangoutLink,
|
|
206
|
-
startTime: startTime.toLocaleString(),
|
|
207
|
-
endTime: endTime.toLocaleString(),
|
|
208
|
-
title: options.title
|
|
209
|
-
};
|
|
210
|
-
} catch {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
var getNextMeeting = async (auth) => {
|
|
215
|
-
try {
|
|
216
|
-
const calendar = google2.calendar({ version: "v3", auth });
|
|
217
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
218
|
-
const response = await calendar.events.list({
|
|
219
|
-
calendarId: "primary",
|
|
220
|
-
timeMin: now,
|
|
221
|
-
singleEvents: true,
|
|
222
|
-
orderBy: "startTime",
|
|
223
|
-
maxResults: 1
|
|
224
|
-
});
|
|
225
|
-
const event = response.data.items?.[0];
|
|
226
|
-
if (!event) {
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
const start = event.start?.dateTime || event.start?.date;
|
|
230
|
-
const end = event.end?.dateTime || event.end?.date;
|
|
231
|
-
if (!start || !end) {
|
|
232
|
-
return null;
|
|
233
193
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
title: event.summary ?? "Untitled",
|
|
237
|
-
startTime: new Date(start).toLocaleString(),
|
|
238
|
-
endTime: new Date(end).toLocaleString(),
|
|
239
|
-
hangoutLink: event.hangoutLink ?? void 0,
|
|
240
|
-
location: event.location ?? void 0
|
|
241
|
-
};
|
|
242
|
-
} catch {
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
};
|
|
194
|
+
};
|
|
195
|
+
}
|
|
246
196
|
|
|
247
|
-
// src/
|
|
248
|
-
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (initialDescription === void 0) return "description";
|
|
253
|
-
if (initialParticipants === void 0) return "participants";
|
|
254
|
-
return "creating";
|
|
197
|
+
// src/use-cases/authenticate.ts
|
|
198
|
+
function makeAuthenticate(authGateway) {
|
|
199
|
+
return async function authenticate() {
|
|
200
|
+
return authGateway.authenticate();
|
|
201
|
+
};
|
|
255
202
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
)
|
|
340
|
-
] });
|
|
341
|
-
}
|
|
342
|
-
if (step === "participants") {
|
|
343
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
344
|
-
/* @__PURE__ */ jsx3(Box3, { marginRight: 1, children: /* @__PURE__ */ jsx3(Text2, { children: "Participant emails (comma-separated, optional):" }) }),
|
|
345
|
-
/* @__PURE__ */ jsx3(
|
|
346
|
-
TextInput,
|
|
347
|
-
{
|
|
348
|
-
value: participants,
|
|
349
|
-
onChange: setParticipants,
|
|
350
|
-
onSubmit: handleParticipantsSubmit,
|
|
351
|
-
placeholder: ""
|
|
352
|
-
}
|
|
353
|
-
)
|
|
354
|
-
] });
|
|
355
|
-
}
|
|
356
|
-
if (step === "creating") {
|
|
357
|
-
return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text2, { color: "gray", children: [
|
|
358
|
-
/* @__PURE__ */ jsx3(Spinner, { type: "dots" }),
|
|
359
|
-
" Creating meeting and reserving calendar..."
|
|
360
|
-
] }) });
|
|
361
|
-
}
|
|
362
|
-
if (step === "success" && meeting) {
|
|
363
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
364
|
-
/* @__PURE__ */ jsx3(Text2, { color: "green", children: "\u2705 Meeting created successfully!" }),
|
|
365
|
-
/* @__PURE__ */ jsxs3(Text2, { color: "cyan", children: [
|
|
366
|
-
"\u{1F4C5} Meeting ID: ",
|
|
367
|
-
meeting.id
|
|
368
|
-
] }),
|
|
369
|
-
/* @__PURE__ */ jsxs3(Text2, { color: "cyan", children: [
|
|
370
|
-
"\u{1F517} Join URL: ",
|
|
371
|
-
meeting.hangoutLink
|
|
372
|
-
] }),
|
|
373
|
-
/* @__PURE__ */ jsx3(Text2, { color: "cyan", children: "\u23F0 Duration: 30 minutes" }),
|
|
374
|
-
/* @__PURE__ */ jsxs3(Text2, { color: "cyan", children: [
|
|
375
|
-
"\u{1F4C5} Start Time: ",
|
|
376
|
-
meeting.startTime
|
|
377
|
-
] })
|
|
378
|
-
] });
|
|
379
|
-
}
|
|
380
|
-
if (step === "no_auth") {
|
|
381
|
-
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
382
|
-
/* @__PURE__ */ jsx3(Text2, { color: "red", children: "\u274C Authentication failed. Please run " }),
|
|
383
|
-
/* @__PURE__ */ jsx3(Text2, { color: "cyan", children: "meetfy auth" }),
|
|
384
|
-
/* @__PURE__ */ jsx3(Text2, { color: "red", children: " first." })
|
|
385
|
-
] });
|
|
386
|
-
}
|
|
387
|
-
if (step === "error") {
|
|
388
|
-
return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text2, { color: "red", children: [
|
|
389
|
-
"\u274C ",
|
|
390
|
-
errorMessage
|
|
391
|
-
] }) });
|
|
392
|
-
}
|
|
393
|
-
return null;
|
|
203
|
+
|
|
204
|
+
// src/use-cases/logout.ts
|
|
205
|
+
function makeLogout(authGateway) {
|
|
206
|
+
return async function logout() {
|
|
207
|
+
return authGateway.logout();
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/use-cases/create-meeting.ts
|
|
212
|
+
function makeCreateMeeting(authGateway, calendarGateway) {
|
|
213
|
+
return async function createMeeting(input) {
|
|
214
|
+
const auth = await authGateway.authenticate();
|
|
215
|
+
if (auth.type !== "ok") return null;
|
|
216
|
+
return calendarGateway.createEvent(auth.client, input);
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/use-cases/get-next-meeting.ts
|
|
221
|
+
function makeGetNextMeeting(authGateway, calendarGateway) {
|
|
222
|
+
return async function getNextMeeting() {
|
|
223
|
+
const auth = await authGateway.authenticate();
|
|
224
|
+
if (auth.type !== "ok") return null;
|
|
225
|
+
return calendarGateway.getNextEvent(auth.client);
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/adapters/cli/format.ts
|
|
230
|
+
var REDIRECT_URI = "http://localhost:3434";
|
|
231
|
+
function formatWelcome() {
|
|
232
|
+
return [
|
|
233
|
+
"",
|
|
234
|
+
" Meetfy \u2014 Instant Meeting Creator",
|
|
235
|
+
" Create instant meetings and reserve time in Google Calendar",
|
|
236
|
+
""
|
|
237
|
+
].join("\n");
|
|
238
|
+
}
|
|
239
|
+
function formatMeeting(meeting) {
|
|
240
|
+
const lines = [
|
|
241
|
+
` ${meeting.title}`,
|
|
242
|
+
` \u{1F550} ${meeting.startTime} \u2013 ${meeting.endTime}`
|
|
243
|
+
];
|
|
244
|
+
if (meeting.hangoutLink) lines.push(` \u{1F517} ${meeting.hangoutLink}`);
|
|
245
|
+
if (meeting.location) lines.push(` \u{1F4CD} ${meeting.location}`);
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
function formatAuthNoCredentials() {
|
|
249
|
+
return [
|
|
250
|
+
"\u26A0\uFE0F Google Calendar credentials not found.",
|
|
251
|
+
"\u{1F4DD} Steps:",
|
|
252
|
+
" 1. Go to https://console.cloud.google.com",
|
|
253
|
+
" 2. Create/select project \u2192 Enable Google Calendar API",
|
|
254
|
+
" 3. Create OAuth 2.0 credentials (Web application)",
|
|
255
|
+
` 4. Add Authorized redirect URI: ${REDIRECT_URI}`,
|
|
256
|
+
" 5. Download credentials.json to project root",
|
|
257
|
+
" 6. Run: meetfy auth"
|
|
258
|
+
].join("\n");
|
|
259
|
+
}
|
|
260
|
+
function formatAuthNeedCode(authUrl) {
|
|
261
|
+
return [
|
|
262
|
+
"\u{1F510} Authorize this app by visiting this URL:",
|
|
263
|
+
authUrl,
|
|
264
|
+
"",
|
|
265
|
+
"Press Enter to copy URL and open in browser."
|
|
266
|
+
].join("\n");
|
|
267
|
+
}
|
|
268
|
+
function formatAuthWaiting() {
|
|
269
|
+
return "\u23F3 Waiting for code on port 3434...";
|
|
270
|
+
}
|
|
271
|
+
function formatAuthSuccess() {
|
|
272
|
+
return [
|
|
273
|
+
"\u2705 Authentication successful!",
|
|
274
|
+
"",
|
|
275
|
+
"Available commands:",
|
|
276
|
+
" meetfy create Create an instant meeting (30 min)",
|
|
277
|
+
" meetfy next Show your next meeting",
|
|
278
|
+
" meetfy logout Log out from Google"
|
|
279
|
+
].join("\n");
|
|
280
|
+
}
|
|
281
|
+
function authErrorForJson(result) {
|
|
282
|
+
if (result.type === "no_credentials") return "no_credentials";
|
|
283
|
+
if (result.type === "error") return result.message;
|
|
284
|
+
return "auth_required";
|
|
394
285
|
}
|
|
395
286
|
|
|
396
|
-
// src/
|
|
397
|
-
import
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
287
|
+
// src/adapters/cli/prompts.ts
|
|
288
|
+
import * as readline from "node:readline";
|
|
289
|
+
function createReadlineInterface() {
|
|
290
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
291
|
+
}
|
|
292
|
+
function question(rl, prompt, defaultValue = "") {
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
const p = defaultValue ? `${prompt} (${defaultValue}): ` : `${prompt}: `;
|
|
295
|
+
rl.question(p, (answer) => {
|
|
296
|
+
resolve(answer.trim() || defaultValue.trim());
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function closeReadline(rl) {
|
|
301
|
+
rl.close();
|
|
302
|
+
}
|
|
405
303
|
|
|
406
|
-
// src/
|
|
304
|
+
// src/adapters/cli/browser.ts
|
|
407
305
|
async function copyAndOpenUrl(url) {
|
|
408
|
-
const
|
|
306
|
+
const parts = [];
|
|
409
307
|
try {
|
|
410
308
|
const { default: clipboardy } = await import("clipboardy");
|
|
411
309
|
await clipboardy.write(url);
|
|
412
|
-
|
|
310
|
+
parts.push("URL copied to clipboard");
|
|
413
311
|
} catch {
|
|
414
|
-
|
|
312
|
+
parts.push("Could not copy");
|
|
415
313
|
}
|
|
416
314
|
try {
|
|
417
315
|
const open = (await import("open")).default;
|
|
418
316
|
await open(url);
|
|
419
|
-
|
|
317
|
+
parts.push("Opening in browser...");
|
|
420
318
|
} catch {
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
return results.join(". ");
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// src/components/AuthView.tsx
|
|
427
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
428
|
-
function AuthView() {
|
|
429
|
-
const { exit } = useApp2();
|
|
430
|
-
const [status, setStatus] = useState2("loading");
|
|
431
|
-
const [authUrl, setAuthUrl] = useState2("");
|
|
432
|
-
const [errorMessage, setErrorMessage] = useState2("");
|
|
433
|
-
const [actionMessage, setActionMessage] = useState2(null);
|
|
434
|
-
const handleCopyAndOpen = useCallback2(async () => {
|
|
435
|
-
if (!authUrl) return;
|
|
436
|
-
setActionMessage(null);
|
|
437
|
-
const msg = await copyAndOpenUrl(authUrl);
|
|
438
|
-
setActionMessage(msg);
|
|
439
|
-
}, [authUrl]);
|
|
440
|
-
useInput(
|
|
441
|
-
(input, key) => {
|
|
442
|
-
if (key.return && (status === "need_code" || status === "waiting")) {
|
|
443
|
-
handleCopyAndOpen();
|
|
444
|
-
}
|
|
445
|
-
},
|
|
446
|
-
{ isActive: status === "need_code" || status === "waiting" }
|
|
447
|
-
);
|
|
448
|
-
useEffect2(() => {
|
|
449
|
-
let cancelled = false;
|
|
450
|
-
(async () => {
|
|
451
|
-
const result = await authenticateGoogle();
|
|
452
|
-
if (cancelled) return;
|
|
453
|
-
if (result.type === "ok") {
|
|
454
|
-
setStatus("success");
|
|
455
|
-
setTimeout(() => exit(), 2500);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
if (result.type === "no_credentials") {
|
|
459
|
-
setStatus("no_credentials");
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
if (result.type === "need_code") {
|
|
463
|
-
setAuthUrl(result.authUrl);
|
|
464
|
-
setStatus("need_code");
|
|
465
|
-
const client = await result.fetchToken({
|
|
466
|
-
onWaiting: () => {
|
|
467
|
-
if (!cancelled) setStatus("waiting");
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
if (cancelled) return;
|
|
471
|
-
if (client) {
|
|
472
|
-
setStatus("success");
|
|
473
|
-
setTimeout(() => exit(), 2500);
|
|
474
|
-
} else {
|
|
475
|
-
setStatus("error");
|
|
476
|
-
setErrorMessage("Failed to get token");
|
|
477
|
-
}
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
setStatus("error");
|
|
481
|
-
setErrorMessage(result.message);
|
|
482
|
-
})();
|
|
483
|
-
return () => {
|
|
484
|
-
cancelled = true;
|
|
485
|
-
};
|
|
486
|
-
}, [exit]);
|
|
487
|
-
if (status === "loading") {
|
|
488
|
-
return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text3, { color: "gray", children: [
|
|
489
|
-
/* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
|
|
490
|
-
" Checking authentication..."
|
|
491
|
-
] }) });
|
|
492
|
-
}
|
|
493
|
-
if (status === "no_credentials") {
|
|
494
|
-
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
495
|
-
/* @__PURE__ */ jsx4(Text3, { color: "yellow", children: "\u26A0\uFE0F Google Calendar credentials not found." }),
|
|
496
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", children: "\u{1F4DD} Please follow these steps:" }),
|
|
497
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 1. Go to https://console.cloud.google.com" }),
|
|
498
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 2. Create a new project or select existing one" }),
|
|
499
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 3. Enable Google Calendar API" }),
|
|
500
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 4. Create OAuth 2.0 credentials (Desktop app)" }),
|
|
501
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 5. Add Authorized redirect URI: " }),
|
|
502
|
-
/* @__PURE__ */ jsxs4(Text3, { color: "yellow", children: [
|
|
503
|
-
" ",
|
|
504
|
-
REDIRECT_URI
|
|
505
|
-
] }),
|
|
506
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 6. Download credentials.json and place it in the project root" }),
|
|
507
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 7. Run: meetfy auth" })
|
|
508
|
-
] });
|
|
509
|
-
}
|
|
510
|
-
if (status === "need_code" || status === "waiting") {
|
|
511
|
-
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
512
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", children: "\u{1F510} Authorize this app by visiting this URL:" }),
|
|
513
|
-
/* @__PURE__ */ jsx4(Text3, { color: "blue", children: authUrl }),
|
|
514
|
-
/* @__PURE__ */ jsx4(Text3, { children: "\n" }),
|
|
515
|
-
/* @__PURE__ */ jsx4(Text3, { color: "green", children: "Press Enter to copy URL and open in browser" }),
|
|
516
|
-
actionMessage && /* @__PURE__ */ jsxs4(Text3, { color: "gray", children: [
|
|
517
|
-
" ",
|
|
518
|
-
actionMessage
|
|
519
|
-
] }),
|
|
520
|
-
/* @__PURE__ */ jsx4(Text3, { children: "\n" }),
|
|
521
|
-
/* @__PURE__ */ jsx4(Text3, { dimColor: true, children: 'If you see "redirect_uri_mismatch", add this exact URI in Google Cloud Console:' }),
|
|
522
|
-
/* @__PURE__ */ jsx4(Text3, { dimColor: true, children: " Credentials \u2192 your OAuth 2.0 Client ID \u2192 Authorized redirect URIs" }),
|
|
523
|
-
/* @__PURE__ */ jsxs4(Text3, { color: "yellow", children: [
|
|
524
|
-
" ",
|
|
525
|
-
REDIRECT_URI
|
|
526
|
-
] }),
|
|
527
|
-
/* @__PURE__ */ jsx4(Text3, { children: "\n" }),
|
|
528
|
-
status === "waiting" && /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text3, { color: "gray", children: [
|
|
529
|
-
/* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
|
|
530
|
-
" Waiting for code on port 3434..."
|
|
531
|
-
] }) })
|
|
532
|
-
] });
|
|
533
|
-
}
|
|
534
|
-
if (status === "success") {
|
|
535
|
-
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
536
|
-
/* @__PURE__ */ jsx4(Text3, { color: "green", children: "\u2705 Authentication successful!" }),
|
|
537
|
-
/* @__PURE__ */ jsx4(Text3, { children: "\n" }),
|
|
538
|
-
/* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "Available commands:" }),
|
|
539
|
-
/* @__PURE__ */ jsx4(Text3, { children: " meetfy create Create an instant meeting (30 min)" }),
|
|
540
|
-
/* @__PURE__ */ jsx4(Text3, { children: " meetfy next Show your next meeting" }),
|
|
541
|
-
/* @__PURE__ */ jsx4(Text3, { children: " meetfy logout Log out from Google" }),
|
|
542
|
-
/* @__PURE__ */ jsx4(Text3, { children: "\n" }),
|
|
543
|
-
/* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "Exiting..." })
|
|
544
|
-
] });
|
|
319
|
+
parts.push("Could not open browser");
|
|
545
320
|
}
|
|
546
|
-
return
|
|
547
|
-
"\u274C ",
|
|
548
|
-
errorMessage
|
|
549
|
-
] }) });
|
|
321
|
+
return parts.join(". ");
|
|
550
322
|
}
|
|
551
323
|
|
|
552
|
-
// src/
|
|
553
|
-
|
|
554
|
-
import { Box as Box5, Text as Text4, useApp as useApp3 } from "ink";
|
|
555
|
-
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
556
|
-
function LogoutView() {
|
|
557
|
-
const { exit } = useApp3();
|
|
558
|
-
useEffect3(() => {
|
|
559
|
-
logoutGoogle().then(() => {
|
|
560
|
-
setTimeout(() => exit(), 800);
|
|
561
|
-
});
|
|
562
|
-
}, [exit]);
|
|
563
|
-
return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text4, { color: "green", children: "\u2705 Logged out successfully!" }) });
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// src/components/NextMeetingView.tsx
|
|
567
|
-
import { useState as useState3, useEffect as useEffect4 } from "react";
|
|
568
|
-
import { Box as Box6, Text as Text5, useApp as useApp4 } from "ink";
|
|
569
|
-
import Spinner3 from "ink-spinner";
|
|
570
|
-
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
571
|
-
function NextMeetingView() {
|
|
572
|
-
const { exit } = useApp4();
|
|
573
|
-
const [status, setStatus] = useState3("loading");
|
|
574
|
-
const [meeting, setMeeting] = useState3(null);
|
|
575
|
-
useEffect4(() => {
|
|
576
|
-
let cancelled = false;
|
|
577
|
-
(async () => {
|
|
578
|
-
const authResult = await authenticateGoogle();
|
|
579
|
-
if (cancelled) return;
|
|
580
|
-
if (authResult.type !== "ok") {
|
|
581
|
-
setStatus("no_auth");
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
const next = await getNextMeeting(authResult.client);
|
|
585
|
-
if (cancelled) return;
|
|
586
|
-
if (!next) {
|
|
587
|
-
setStatus("no_meeting");
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
setMeeting(next);
|
|
591
|
-
setStatus("success");
|
|
592
|
-
})();
|
|
593
|
-
return () => {
|
|
594
|
-
cancelled = true;
|
|
595
|
-
};
|
|
596
|
-
}, []);
|
|
597
|
-
if (status === "loading") {
|
|
598
|
-
return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
599
|
-
/* @__PURE__ */ jsx6(Spinner3, { type: "dots" }),
|
|
600
|
-
" Loading next meeting..."
|
|
601
|
-
] }) });
|
|
602
|
-
}
|
|
603
|
-
if (status === "no_auth") {
|
|
604
|
-
return /* @__PURE__ */ jsxs5(Box6, { children: [
|
|
605
|
-
/* @__PURE__ */ jsx6(Text5, { color: "red", children: "\u274C Authentication failed. Please run " }),
|
|
606
|
-
/* @__PURE__ */ jsx6(Text5, { color: "cyan", children: "meetfy auth" }),
|
|
607
|
-
/* @__PURE__ */ jsx6(Text5, { color: "red", children: " first." })
|
|
608
|
-
] });
|
|
609
|
-
}
|
|
610
|
-
if (status === "no_meeting") {
|
|
611
|
-
return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text5, { color: "yellow", children: "\u{1F4ED} No upcoming meetings found." }) });
|
|
612
|
-
}
|
|
613
|
-
if (meeting) {
|
|
614
|
-
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
|
|
615
|
-
/* @__PURE__ */ jsx6(Text5, { bold: true, color: "green", children: "\u{1F4C5} Next meeting" }),
|
|
616
|
-
/* @__PURE__ */ jsx6(Text5, { children: "\n" }),
|
|
617
|
-
/* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
618
|
-
" ",
|
|
619
|
-
meeting.title
|
|
620
|
-
] }),
|
|
621
|
-
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
622
|
-
" \u{1F550} ",
|
|
623
|
-
meeting.startTime,
|
|
624
|
-
" \u2013 ",
|
|
625
|
-
meeting.endTime
|
|
626
|
-
] }),
|
|
627
|
-
meeting.hangoutLink && /* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
628
|
-
" \u{1F517} ",
|
|
629
|
-
meeting.hangoutLink
|
|
630
|
-
] }),
|
|
631
|
-
meeting.location && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
632
|
-
" \u{1F4CD} ",
|
|
633
|
-
meeting.location
|
|
634
|
-
] })
|
|
635
|
-
] });
|
|
636
|
-
}
|
|
637
|
-
return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text5, { color: "red", children: "\u274C Error fetching next meeting." }) });
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// src/index.tsx
|
|
641
|
-
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
642
|
-
var program = new Command();
|
|
643
|
-
program.name("meetfy").description("CLI tool for creating instant meetings and reserving time in Google Calendar").version("1.0.0").option("--json", "Output result as JSON");
|
|
324
|
+
// src/adapters/cli/index.ts
|
|
325
|
+
var REDIRECT_PORT = 3434;
|
|
644
326
|
function outputJson(obj) {
|
|
645
327
|
console.log(JSON.stringify(obj, null, 0));
|
|
646
328
|
}
|
|
647
|
-
function
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
329
|
+
function runCli() {
|
|
330
|
+
const authGateway = createGoogleAuthAdapter(REDIRECT_PORT);
|
|
331
|
+
const calendarGateway = createGoogleCalendarAdapter();
|
|
332
|
+
const authenticate = makeAuthenticate(authGateway);
|
|
333
|
+
const logout = makeLogout(authGateway);
|
|
334
|
+
const createMeeting = makeCreateMeeting(authGateway, calendarGateway);
|
|
335
|
+
const getNextMeeting = makeGetNextMeeting(authGateway, calendarGateway);
|
|
336
|
+
const program = new Command();
|
|
337
|
+
program.name("meetfy").description("CLI tool for creating instant meetings and reserving time in Google Calendar").version("1.0.0").option("--json", "Output result as JSON");
|
|
338
|
+
program.command("create").description("Create an instant meeting and reserve 30 minutes in your Google Calendar").option("-t, --title <title>", "Meeting title").option("-d, --description <description>", "Meeting description").option("-p, --participants <emails>", "Comma-separated list of participant emails").action(async (opts) => {
|
|
339
|
+
const json = program.opts().json;
|
|
340
|
+
if (json) {
|
|
341
|
+
const auth = await authenticate();
|
|
342
|
+
if (auth.type !== "ok") {
|
|
343
|
+
outputJson({ success: false, error: authErrorForJson(auth) });
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
const title2 = opts.title?.trim() || "Instant Meeting";
|
|
347
|
+
const description2 = opts.description?.trim() || "Instant meeting created via Meetfy CLI";
|
|
348
|
+
const participants2 = (opts.participants ?? "").split(",").map((e) => e.trim()).filter(Boolean);
|
|
349
|
+
const meeting2 = await createMeeting({ title: title2, description: description2, participants: participants2 });
|
|
350
|
+
if (meeting2) {
|
|
351
|
+
outputJson({ success: true, meeting: meeting2 });
|
|
352
|
+
} else {
|
|
353
|
+
outputJson({ success: false, error: "Failed to create meeting" });
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
658
357
|
}
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
358
|
+
console.log(formatWelcome());
|
|
359
|
+
const rl = createReadlineInterface();
|
|
360
|
+
const title = opts.title?.trim() || await question(rl, "Meeting title", "Instant Meeting");
|
|
361
|
+
const description = opts.description?.trim() || await question(rl, "Meeting description", "Instant meeting created via Meetfy CLI");
|
|
362
|
+
const participantsStr = opts.participants ?? await question(rl, "Participant emails (comma-separated)", "");
|
|
363
|
+
closeReadline(rl);
|
|
364
|
+
const participants = participantsStr.split(",").map((e) => e.trim()).filter(Boolean);
|
|
365
|
+
console.log("\n\u23F3 Creating meeting...");
|
|
366
|
+
const meeting = await createMeeting({ title, description, participants });
|
|
667
367
|
if (meeting) {
|
|
668
|
-
|
|
368
|
+
console.log("\n\u2705 Meeting created successfully!");
|
|
369
|
+
console.log(`\u{1F4C5} ${meeting.title}`);
|
|
370
|
+
console.log(`\u{1F517} ${meeting.hangoutLink}`);
|
|
371
|
+
console.log(`\u23F0 ${meeting.startTime} \u2013 ${meeting.endTime}`);
|
|
669
372
|
} else {
|
|
670
|
-
|
|
373
|
+
console.error("\n\u274C Failed to create meeting. Run meetfy auth if needed.");
|
|
671
374
|
process.exit(1);
|
|
672
375
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
{
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
376
|
+
});
|
|
377
|
+
program.command("auth").description("Authenticate with Google Calendar").action(async () => {
|
|
378
|
+
const json = program.opts().json;
|
|
379
|
+
const auth = await authenticate();
|
|
380
|
+
if (json) {
|
|
381
|
+
if (auth.type === "ok") {
|
|
382
|
+
outputJson({ success: true });
|
|
383
|
+
} else if (auth.type === "need_code") {
|
|
384
|
+
outputJson({ success: false, authRequired: true, authUrl: auth.authUrl });
|
|
385
|
+
process.exit(1);
|
|
386
|
+
} else if (auth.type === "no_credentials") {
|
|
387
|
+
outputJson({ success: false, error: "no_credentials" });
|
|
388
|
+
process.exit(1);
|
|
389
|
+
} else {
|
|
390
|
+
outputJson({ success: false, error: auth.message });
|
|
391
|
+
process.exit(1);
|
|
682
392
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if (
|
|
691
|
-
|
|
692
|
-
} else if (authResult.type === "need_code") {
|
|
693
|
-
outputJson({ success: false, authRequired: true, authUrl: authResult.authUrl });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
console.log(formatWelcome());
|
|
396
|
+
if (auth.type === "ok") {
|
|
397
|
+
console.log(formatAuthSuccess());
|
|
398
|
+
process.exit(0);
|
|
399
|
+
}
|
|
400
|
+
if (auth.type === "no_credentials") {
|
|
401
|
+
console.log(formatAuthNoCredentials());
|
|
694
402
|
process.exit(1);
|
|
695
|
-
}
|
|
696
|
-
|
|
403
|
+
}
|
|
404
|
+
if (auth.type === "error") {
|
|
405
|
+
console.error("\u274C", auth.message);
|
|
697
406
|
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
console.log(formatAuthNeedCode(auth.authUrl));
|
|
409
|
+
const rl = createReadlineInterface();
|
|
410
|
+
await new Promise((resolve) => {
|
|
411
|
+
rl.once("line", () => resolve());
|
|
412
|
+
});
|
|
413
|
+
await copyAndOpenUrl(auth.authUrl);
|
|
414
|
+
closeReadline(rl);
|
|
415
|
+
console.log("\n" + formatAuthWaiting());
|
|
416
|
+
const client = await auth.fetchToken({
|
|
417
|
+
onWaiting: () => {
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
if (client) {
|
|
421
|
+
console.log("\n" + formatAuthSuccess());
|
|
698
422
|
} else {
|
|
699
|
-
|
|
423
|
+
console.error("\n\u274C Failed to get token.");
|
|
700
424
|
process.exit(1);
|
|
701
425
|
}
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
program.command("logout").description("Logout from Google Calendar").action(async () => {
|
|
711
|
-
if (program.opts().json) {
|
|
712
|
-
await logoutGoogle();
|
|
713
|
-
outputJson({ success: true });
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
const { waitUntilExit } = render(
|
|
717
|
-
/* @__PURE__ */ jsx7(App, { children: /* @__PURE__ */ jsx7(LogoutView, {}) })
|
|
718
|
-
);
|
|
719
|
-
await waitUntilExit();
|
|
720
|
-
});
|
|
721
|
-
program.command("next").description("Show your next scheduled meeting").action(async () => {
|
|
722
|
-
if (program.opts().json) {
|
|
723
|
-
const authResult = await authenticateGoogle();
|
|
724
|
-
if (authResult.type !== "ok") {
|
|
725
|
-
outputJson({ success: false, error: authErrorForJson(authResult) });
|
|
726
|
-
process.exit(1);
|
|
426
|
+
process.exit(0);
|
|
427
|
+
});
|
|
428
|
+
program.command("logout").description("Logout from Google Calendar").action(async () => {
|
|
429
|
+
await logout();
|
|
430
|
+
if (program.opts().json) {
|
|
431
|
+
outputJson({ success: true });
|
|
432
|
+
} else {
|
|
433
|
+
console.log("\u2705 Logged out successfully!");
|
|
727
434
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
435
|
+
});
|
|
436
|
+
program.command("next").description("Show your next scheduled meeting").action(async () => {
|
|
437
|
+
const json = program.opts().json;
|
|
438
|
+
const meeting = await getNextMeeting();
|
|
439
|
+
if (json) {
|
|
440
|
+
const auth = await authenticate();
|
|
441
|
+
if (auth.type !== "ok") {
|
|
442
|
+
outputJson({ success: false, error: authErrorForJson(auth) });
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
outputJson({ success: true, meeting: meeting ?? null });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
console.log(formatWelcome());
|
|
449
|
+
if (!meeting) {
|
|
450
|
+
console.log("\u{1F4ED} No upcoming meetings found.");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
console.log("\u{1F4C5} Next meeting:\n");
|
|
454
|
+
console.log(formatMeeting(meeting));
|
|
455
|
+
});
|
|
456
|
+
program.parse();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/index.ts
|
|
460
|
+
runCli();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meetfy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"packageManager": "pnpm@10.31.0",
|
|
5
5
|
"description": "CLI tool for creating instant meetings and reserving time in Google Calendar",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"start": "tsx src/index.
|
|
16
|
+
"start": "tsx src/index.ts",
|
|
17
17
|
"build": "node build.mjs",
|
|
18
18
|
"prepublishOnly": "pnpm run build",
|
|
19
|
-
"lint": "eslint src/**/*.
|
|
19
|
+
"lint": "eslint src/**/*.ts"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=18"
|
|
@@ -36,27 +36,19 @@
|
|
|
36
36
|
"express": "^5.1.0",
|
|
37
37
|
"google-auth-library": "^9.0.0",
|
|
38
38
|
"googleapis": "^128.0.0",
|
|
39
|
-
"ink": "^5.0.0",
|
|
40
|
-
"ink-spinner": "^5.0.0",
|
|
41
|
-
"ink-text-input": "^6.0.0",
|
|
42
39
|
"open": "^10.2.0",
|
|
43
|
-
"react": "^18.3.1",
|
|
44
40
|
"tsx": "^4.20.3",
|
|
45
41
|
"typescript": "5.5"
|
|
46
42
|
},
|
|
47
43
|
"devDependencies": {
|
|
48
44
|
"@types/express": "^5.0.3",
|
|
49
45
|
"@types/node": "^20.10.0",
|
|
50
|
-
"@types/react": "^18.3.0",
|
|
51
46
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
52
47
|
"@typescript-eslint/parser": "^7.18.0",
|
|
53
48
|
"esbuild": "^0.25.8",
|
|
54
49
|
"eslint": "^8.57.1",
|
|
55
50
|
"eslint-config-airbnb": "^19.0.4",
|
|
56
51
|
"eslint-config-airbnb-typescript": "^18.0.0",
|
|
57
|
-
"eslint-plugin-import": "^2.32.0"
|
|
58
|
-
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
59
|
-
"eslint-plugin-react": "^7.37.5",
|
|
60
|
-
"eslint-plugin-react-hooks": "^4.6.2"
|
|
52
|
+
"eslint-plugin-import": "^2.32.0"
|
|
61
53
|
}
|
|
62
54
|
}
|