meetfy 1.0.3 → 1.0.5
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 +308 -353
- package/package.json +22 -27
- package/README.md +0 -43
package/dist/index.js
CHANGED
|
@@ -1,457 +1,412 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
// src/
|
|
4
|
+
// src/cli.ts
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
+
import chalk2 from "chalk";
|
|
6
7
|
|
|
7
|
-
// src/
|
|
8
|
+
// src/auth.ts
|
|
9
|
+
import http from "node:http";
|
|
8
10
|
import { google } from "googleapis";
|
|
9
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
11
11
|
|
|
12
|
-
// src/
|
|
12
|
+
// src/config.ts
|
|
13
13
|
import Conf from "conf";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
var conf = new Conf({
|
|
15
|
+
projectName: "meetfy",
|
|
16
|
+
configName: "meetfy-config"
|
|
17
|
+
});
|
|
18
|
+
function getConfig(key) {
|
|
19
|
+
return conf.get(key);
|
|
20
|
+
}
|
|
21
|
+
function setConfig(key, value) {
|
|
22
|
+
conf.set(key, value);
|
|
23
|
+
}
|
|
24
|
+
function clearConfig() {
|
|
25
|
+
conf.clear();
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
// src/
|
|
22
|
-
|
|
23
|
-
var
|
|
28
|
+
// src/auth.ts
|
|
29
|
+
var WORKER_URL = (process.env.MEETFY_AUTH_URL ?? "https://meetfy.eduardoborges.dev").replace(/\/$/, "");
|
|
30
|
+
var REDIRECT_PORT = 3434;
|
|
31
|
+
var HTML_OK = `
|
|
24
32
|
<!DOCTYPE html>
|
|
25
33
|
<html>
|
|
26
|
-
<head
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
};
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="utf-8">
|
|
36
|
+
<title>Meetfy</title>
|
|
37
|
+
<style>
|
|
38
|
+
body {
|
|
39
|
+
font-family: system-ui;
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
height: 100vh;
|
|
45
|
+
margin: 0;
|
|
46
|
+
}
|
|
47
|
+
h1 {
|
|
48
|
+
color: #22c55e;
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<h1>\u2705 Authenticated!</h1>
|
|
54
|
+
<p>You can close this tab.</p>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|
|
57
|
+
`;
|
|
58
|
+
function makeClient(clientId, tokens) {
|
|
59
|
+
const client = new google.auth.OAuth2(clientId, "");
|
|
60
|
+
client.setCredentials(tokens);
|
|
61
|
+
return client;
|
|
55
62
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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" };
|
|
63
|
+
async function getClient() {
|
|
64
|
+
const stored = getConfig("googleTokens");
|
|
65
|
+
const clientId = getConfig("googleClientId");
|
|
66
|
+
if (!stored || !clientId) return null;
|
|
67
|
+
let tokens = stored;
|
|
68
|
+
if (stored.expiry_date && Date.now() > stored.expiry_date && stored.refresh_token) {
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(`${WORKER_URL}/refresh`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ refresh_token: stored.refresh_token })
|
|
74
|
+
});
|
|
75
|
+
if (res.ok) {
|
|
76
|
+
tokens = { ...stored, ...await res.json() };
|
|
77
|
+
setConfig("googleTokens", tokens);
|
|
108
78
|
}
|
|
109
|
-
}
|
|
110
|
-
async logout() {
|
|
111
|
-
config.clear();
|
|
79
|
+
} catch {
|
|
112
80
|
}
|
|
113
|
-
}
|
|
81
|
+
}
|
|
82
|
+
return makeClient(clientId, tokens);
|
|
114
83
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
};
|
|
161
|
-
} catch {
|
|
162
|
-
return null;
|
|
84
|
+
async function authenticate() {
|
|
85
|
+
const client = await getClient();
|
|
86
|
+
if (client) return { type: "ok", client };
|
|
87
|
+
const forward = `http://localhost:${REDIRECT_PORT}`;
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(`${WORKER_URL}/auth/url?forward=${encodeURIComponent(forward)}`);
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
const err = await res.json().catch(() => ({}));
|
|
92
|
+
return { type: "error", message: err.error ?? "Failed to get auth URL" };
|
|
93
|
+
}
|
|
94
|
+
const { authUrl } = await res.json();
|
|
95
|
+
return {
|
|
96
|
+
type: "need_code",
|
|
97
|
+
authUrl,
|
|
98
|
+
waitForTokens: () => waitForTokensThenSave(REDIRECT_PORT)
|
|
99
|
+
};
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return { type: "error", message: e.message ?? "Auth service unreachable" };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function waitForTokensThenSave(port) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
let settled = false;
|
|
107
|
+
const once = (err, client) => {
|
|
108
|
+
if (settled) return;
|
|
109
|
+
settled = true;
|
|
110
|
+
if (err) reject(err);
|
|
111
|
+
else resolve(client);
|
|
112
|
+
};
|
|
113
|
+
const server = http.createServer({ maxHeaderSize: 64 * 1024 }, (req, res) => {
|
|
114
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
115
|
+
const raw = url.searchParams.get("tokens");
|
|
116
|
+
if (!raw) {
|
|
117
|
+
res.writeHead(400, { "Content-Type": "text/plain" }).end("Missing tokens");
|
|
118
|
+
return;
|
|
163
119
|
}
|
|
164
|
-
},
|
|
165
|
-
async getNextEvent(client) {
|
|
166
120
|
try {
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
};
|
|
121
|
+
const json2 = Buffer.from(raw, "base64").toString("utf-8");
|
|
122
|
+
const { client_id: clientId, ...tokens } = JSON.parse(json2);
|
|
123
|
+
if (!clientId || !tokens.access_token) throw new Error("Incomplete payload");
|
|
124
|
+
res.writeHead(200, { "Content-Type": "text/html", Connection: "close" }).end(HTML_OK);
|
|
125
|
+
setConfig("googleTokens", tokens);
|
|
126
|
+
setConfig("googleClientId", clientId);
|
|
127
|
+
once(null, makeClient(clientId, tokens));
|
|
128
|
+
server.close();
|
|
190
129
|
} catch {
|
|
191
|
-
|
|
130
|
+
res.writeHead(400, { "Content-Type": "text/plain", Connection: "close" }).end("Invalid tokens");
|
|
131
|
+
once(new Error("Invalid tokens"));
|
|
132
|
+
server.close();
|
|
192
133
|
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// src/use-cases/authenticate.ts
|
|
198
|
-
function makeAuthenticate(authGateway) {
|
|
199
|
-
return async function authenticate() {
|
|
200
|
-
return authGateway.authenticate();
|
|
201
|
-
};
|
|
134
|
+
});
|
|
135
|
+
server.on("error", (err) => once(err));
|
|
136
|
+
server.listen(port);
|
|
137
|
+
});
|
|
202
138
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
function makeLogout(authGateway) {
|
|
206
|
-
return async function logout() {
|
|
207
|
-
return authGateway.logout();
|
|
208
|
-
};
|
|
139
|
+
function logout() {
|
|
140
|
+
clearConfig();
|
|
209
141
|
}
|
|
210
142
|
|
|
211
|
-
// src/
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
143
|
+
// src/calendar.ts
|
|
144
|
+
import { google as google2 } from "googleapis";
|
|
145
|
+
async function createMeeting(client, input) {
|
|
146
|
+
try {
|
|
147
|
+
const calendar = google2.calendar({ version: "v3", auth: client });
|
|
148
|
+
const now = /* @__PURE__ */ new Date();
|
|
149
|
+
const startTime = new Date(now.getTime() + 5 * 60 * 1e3);
|
|
150
|
+
const endTime = new Date(startTime.getTime() + 30 * 60 * 1e3);
|
|
151
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
152
|
+
const res = await calendar.events.insert({
|
|
153
|
+
calendarId: "primary",
|
|
154
|
+
requestBody: {
|
|
155
|
+
summary: input.title,
|
|
156
|
+
description: input.description,
|
|
157
|
+
start: { dateTime: startTime.toISOString(), timeZone: tz },
|
|
158
|
+
end: { dateTime: endTime.toISOString(), timeZone: tz },
|
|
159
|
+
attendees: input.participants.map((email) => ({ email: email.trim() })),
|
|
160
|
+
conferenceData: {
|
|
161
|
+
createRequest: {
|
|
162
|
+
requestId: `meetfy-${Date.now()}`,
|
|
163
|
+
conferenceSolutionKey: { type: "hangoutsMeet" }
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
reminders: {
|
|
167
|
+
useDefault: false,
|
|
168
|
+
overrides: [
|
|
169
|
+
{ method: "email", minutes: 10 },
|
|
170
|
+
{ method: "popup", minutes: 5 }
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
conferenceDataVersion: 1,
|
|
175
|
+
sendUpdates: "all"
|
|
176
|
+
});
|
|
177
|
+
if (!res.data.id || !res.data.hangoutLink) return null;
|
|
178
|
+
return {
|
|
179
|
+
id: res.data.id,
|
|
180
|
+
title: input.title,
|
|
181
|
+
startTime: startTime.toLocaleString(),
|
|
182
|
+
endTime: endTime.toLocaleString(),
|
|
183
|
+
hangoutLink: res.data.hangoutLink
|
|
184
|
+
};
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
218
188
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
189
|
+
async function getNextMeeting(client) {
|
|
190
|
+
try {
|
|
191
|
+
const calendar = google2.calendar({ version: "v3", auth: client });
|
|
192
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
193
|
+
const res = await calendar.events.list({
|
|
194
|
+
calendarId: "primary",
|
|
195
|
+
timeMin: now,
|
|
196
|
+
singleEvents: true,
|
|
197
|
+
orderBy: "startTime",
|
|
198
|
+
maxResults: 1
|
|
199
|
+
});
|
|
200
|
+
const event = res.data.items?.[0];
|
|
201
|
+
if (!event) return null;
|
|
202
|
+
const start = event.start?.dateTime || event.start?.date;
|
|
203
|
+
const end = event.end?.dateTime || event.end?.date;
|
|
204
|
+
if (!start || !end) return null;
|
|
205
|
+
return {
|
|
206
|
+
id: event.id ?? "",
|
|
207
|
+
title: event.summary ?? "Untitled",
|
|
208
|
+
startTime: new Date(start).toLocaleString(),
|
|
209
|
+
endTime: new Date(end).toLocaleString(),
|
|
210
|
+
hangoutLink: event.hangoutLink ?? void 0,
|
|
211
|
+
location: event.location ?? void 0
|
|
212
|
+
};
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
227
216
|
}
|
|
228
217
|
|
|
229
|
-
// src/
|
|
230
|
-
|
|
231
|
-
function
|
|
218
|
+
// src/format.ts
|
|
219
|
+
import chalk from "chalk";
|
|
220
|
+
function welcome() {
|
|
232
221
|
return [
|
|
233
222
|
"",
|
|
234
|
-
" Meetfy
|
|
235
|
-
"
|
|
223
|
+
chalk.cyan.bold(" Meetfy"),
|
|
224
|
+
chalk.dim(" Instant Meeting Creator \u2014 reserve time in Google Calendar"),
|
|
236
225
|
""
|
|
237
226
|
].join("\n");
|
|
238
227
|
}
|
|
239
|
-
function
|
|
228
|
+
function meeting(meet) {
|
|
240
229
|
const lines = [
|
|
241
|
-
` ${
|
|
242
|
-
` \u{1F550} ${
|
|
230
|
+
chalk.cyan.bold(` ${meet.title}`),
|
|
231
|
+
chalk.dim(` \u{1F550} ${meet.startTime} \u2013 ${meet.endTime}`)
|
|
243
232
|
];
|
|
244
|
-
if (
|
|
245
|
-
if (
|
|
233
|
+
if (meet.hangoutLink) lines.push(chalk.blue(` \u{1F517} ${meet.hangoutLink}`));
|
|
234
|
+
if (meet.location) lines.push(chalk.dim(` \u{1F4CD} ${meet.location}`));
|
|
246
235
|
return lines.join("\n");
|
|
247
236
|
}
|
|
248
|
-
function
|
|
237
|
+
function authNeedCode(authUrl) {
|
|
249
238
|
return [
|
|
250
|
-
"\
|
|
251
|
-
|
|
252
|
-
"
|
|
253
|
-
"
|
|
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"
|
|
239
|
+
chalk.cyan("\u{1F510} Authorize this app by visiting this URL:"),
|
|
240
|
+
chalk.blue(authUrl),
|
|
241
|
+
"",
|
|
242
|
+
chalk.green("Press Enter to copy URL and open in browser.")
|
|
258
243
|
].join("\n");
|
|
259
244
|
}
|
|
260
|
-
function
|
|
245
|
+
function authWaiting() {
|
|
246
|
+
return chalk.dim("\u23F3 Waiting for code on port 3434...");
|
|
247
|
+
}
|
|
248
|
+
function authSuccess() {
|
|
261
249
|
return [
|
|
262
|
-
"\
|
|
263
|
-
authUrl,
|
|
250
|
+
chalk.green("\u2705 Authentication successful!"),
|
|
264
251
|
"",
|
|
265
|
-
"
|
|
252
|
+
chalk.dim("Available commands:"),
|
|
253
|
+
chalk.cyan(" meetfy create") + chalk.dim(" Create an instant meeting (30 min)"),
|
|
254
|
+
chalk.cyan(" meetfy next") + chalk.dim(" Show your next meeting"),
|
|
255
|
+
chalk.cyan(" meetfy logout") + chalk.dim(" Log out from Google")
|
|
266
256
|
].join("\n");
|
|
267
257
|
}
|
|
268
|
-
function
|
|
269
|
-
return "\u23F3 Waiting for code on port 3434...";
|
|
270
|
-
}
|
|
271
|
-
function formatAuthSuccess() {
|
|
258
|
+
function createSuccess(m) {
|
|
272
259
|
return [
|
|
273
|
-
"\u2705
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
" meetfy next Show your next meeting",
|
|
278
|
-
" meetfy logout Log out from Google"
|
|
260
|
+
chalk.green("\u2705 Meeting created successfully!"),
|
|
261
|
+
chalk.cyan(`\u{1F4C5} ${m.title}`),
|
|
262
|
+
chalk.blue(`\u{1F517} ${m.hangoutLink}`),
|
|
263
|
+
chalk.dim(`\u23F0 ${m.startTime} \u2013 ${m.endTime}`)
|
|
279
264
|
].join("\n");
|
|
280
265
|
}
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
266
|
+
function logoutSuccess() {
|
|
267
|
+
return chalk.green("\u2705 Logged out successfully!");
|
|
268
|
+
}
|
|
269
|
+
function noMeetings() {
|
|
270
|
+
return chalk.yellow("\u{1F4ED} No upcoming meetings found.");
|
|
271
|
+
}
|
|
272
|
+
function nextMeetingTitle() {
|
|
273
|
+
return chalk.cyan("\u{1F4C5} Next meeting:\n");
|
|
274
|
+
}
|
|
275
|
+
function authErrorJson(result) {
|
|
276
|
+
return result.type === "error" ? result.message : "auth_required";
|
|
285
277
|
}
|
|
286
278
|
|
|
287
|
-
// src/
|
|
279
|
+
// src/prompts.ts
|
|
288
280
|
import * as readline from "node:readline";
|
|
289
|
-
function
|
|
281
|
+
function createRl() {
|
|
290
282
|
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
291
283
|
}
|
|
292
|
-
function question(rl, prompt,
|
|
284
|
+
function question(rl, prompt, defaultVal = "") {
|
|
293
285
|
return new Promise((resolve) => {
|
|
294
|
-
const p =
|
|
295
|
-
rl.question(p, (answer) =>
|
|
296
|
-
resolve(answer.trim() || defaultValue.trim());
|
|
297
|
-
});
|
|
286
|
+
const p = defaultVal ? `${prompt} (${defaultVal}): ` : `${prompt}: `;
|
|
287
|
+
rl.question(p, (answer) => resolve(answer.trim() || defaultVal.trim()));
|
|
298
288
|
});
|
|
299
289
|
}
|
|
300
|
-
function
|
|
290
|
+
function closeRl(rl) {
|
|
301
291
|
rl.close();
|
|
302
292
|
}
|
|
303
293
|
|
|
304
|
-
// src/
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
parts.push("Could not copy");
|
|
313
|
-
}
|
|
314
|
-
try {
|
|
315
|
-
const open = (await import("open")).default;
|
|
316
|
-
await open(url);
|
|
317
|
-
parts.push("Opening in browser...");
|
|
318
|
-
} catch {
|
|
319
|
-
parts.push("Could not open browser");
|
|
320
|
-
}
|
|
321
|
-
return parts.join(". ");
|
|
294
|
+
// src/browser.ts
|
|
295
|
+
import open from "open";
|
|
296
|
+
import clipboardy from "clipboardy";
|
|
297
|
+
function copyAndOpenUrl(url) {
|
|
298
|
+
clipboardy.write(url).catch(() => {
|
|
299
|
+
});
|
|
300
|
+
open(url).catch(() => {
|
|
301
|
+
});
|
|
322
302
|
}
|
|
323
303
|
|
|
324
|
-
// src/
|
|
325
|
-
|
|
326
|
-
function outputJson(obj) {
|
|
304
|
+
// src/cli.ts
|
|
305
|
+
function json(obj) {
|
|
327
306
|
console.log(JSON.stringify(obj, null, 0));
|
|
328
307
|
}
|
|
329
308
|
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
309
|
const program = new Command();
|
|
337
310
|
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
311
|
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
|
|
340
|
-
if (
|
|
312
|
+
const useJson = program.opts().json;
|
|
313
|
+
if (useJson) {
|
|
341
314
|
const auth = await authenticate();
|
|
342
315
|
if (auth.type !== "ok") {
|
|
343
|
-
|
|
316
|
+
json({ success: false, error: authErrorJson(auth) });
|
|
344
317
|
process.exit(1);
|
|
345
318
|
}
|
|
346
319
|
const title2 = opts.title?.trim() || "Instant Meeting";
|
|
347
320
|
const description2 = opts.description?.trim() || "Instant meeting created via Meetfy CLI";
|
|
348
321
|
const participants2 = (opts.participants ?? "").split(",").map((e) => e.trim()).filter(Boolean);
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
outputJson({ success: false, error: "Failed to create meeting" });
|
|
354
|
-
process.exit(1);
|
|
355
|
-
}
|
|
356
|
-
return;
|
|
322
|
+
const result2 = await createMeeting(auth.client, { title: title2, description: description2, participants: participants2 });
|
|
323
|
+
if (result2) json({ success: true, meeting: result2 });
|
|
324
|
+
else json({ success: false, error: "Failed to create meeting" });
|
|
325
|
+
process.exit(result2 ? 0 : 1);
|
|
357
326
|
}
|
|
358
|
-
console.log(
|
|
359
|
-
const rl =
|
|
327
|
+
console.log(welcome());
|
|
328
|
+
const rl = createRl();
|
|
360
329
|
const title = opts.title?.trim() || await question(rl, "Meeting title", "Instant Meeting");
|
|
361
330
|
const description = opts.description?.trim() || await question(rl, "Meeting description", "Instant meeting created via Meetfy CLI");
|
|
362
331
|
const participantsStr = opts.participants ?? await question(rl, "Participant emails (comma-separated)", "");
|
|
363
|
-
|
|
332
|
+
closeRl(rl);
|
|
333
|
+
const client = await getClient();
|
|
334
|
+
if (!client) {
|
|
335
|
+
console.error(chalk2.red("\u274C Not authenticated. Run meetfy auth first."));
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
364
338
|
const participants = participantsStr.split(",").map((e) => e.trim()).filter(Boolean);
|
|
365
|
-
console.log("\n\u23F3 Creating meeting...");
|
|
366
|
-
const
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
console.
|
|
370
|
-
console.log(`\u{1F517} ${meeting.hangoutLink}`);
|
|
371
|
-
console.log(`\u23F0 ${meeting.startTime} \u2013 ${meeting.endTime}`);
|
|
372
|
-
} else {
|
|
373
|
-
console.error("\n\u274C Failed to create meeting. Run meetfy auth if needed.");
|
|
339
|
+
console.log(chalk2.dim("\n\u23F3 Creating meeting..."));
|
|
340
|
+
const result = await createMeeting(client, { title, description, participants });
|
|
341
|
+
if (result) console.log("\n" + createSuccess(result));
|
|
342
|
+
else {
|
|
343
|
+
console.error(chalk2.red("\n\u274C Failed to create meeting. Run meetfy auth if needed."));
|
|
374
344
|
process.exit(1);
|
|
375
345
|
}
|
|
376
346
|
});
|
|
377
347
|
program.command("auth").description("Authenticate with Google Calendar").action(async () => {
|
|
378
|
-
const
|
|
348
|
+
const useJson = program.opts().json;
|
|
379
349
|
const auth = await authenticate();
|
|
380
|
-
if (
|
|
381
|
-
if (auth.type === "ok") {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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);
|
|
392
|
-
}
|
|
393
|
-
return;
|
|
350
|
+
if (useJson) {
|
|
351
|
+
if (auth.type === "ok") json({ success: true });
|
|
352
|
+
else if (auth.type === "need_code") json({ success: false, authRequired: true, authUrl: auth.authUrl });
|
|
353
|
+
else json({ success: false, error: auth.message });
|
|
354
|
+
process.exit(auth.type === "ok" ? 0 : 1);
|
|
394
355
|
}
|
|
395
|
-
console.log(
|
|
356
|
+
console.log(welcome());
|
|
396
357
|
if (auth.type === "ok") {
|
|
397
|
-
console.log(
|
|
358
|
+
console.log(authSuccess());
|
|
398
359
|
process.exit(0);
|
|
399
360
|
}
|
|
400
|
-
if (auth.type === "no_credentials") {
|
|
401
|
-
console.log(formatAuthNoCredentials());
|
|
402
|
-
process.exit(1);
|
|
403
|
-
}
|
|
404
361
|
if (auth.type === "error") {
|
|
405
|
-
console.error("\u274C", auth.message);
|
|
362
|
+
console.error(chalk2.red("\u274C"), auth.message);
|
|
406
363
|
process.exit(1);
|
|
407
364
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (client) {
|
|
421
|
-
console.log("\n" + formatAuthSuccess());
|
|
422
|
-
} else {
|
|
423
|
-
console.error("\n\u274C Failed to get token.");
|
|
365
|
+
const tokensPromise = auth.waitForTokens();
|
|
366
|
+
console.log(authNeedCode(auth.authUrl));
|
|
367
|
+
console.log(authWaiting());
|
|
368
|
+
const rl = createRl();
|
|
369
|
+
await new Promise((r) => rl.once("line", r));
|
|
370
|
+
closeRl(rl);
|
|
371
|
+
copyAndOpenUrl(auth.authUrl);
|
|
372
|
+
try {
|
|
373
|
+
await tokensPromise;
|
|
374
|
+
console.log("\n" + authSuccess());
|
|
375
|
+
} catch {
|
|
376
|
+
console.error(chalk2.red("\n\u274C Failed to get token."));
|
|
424
377
|
process.exit(1);
|
|
425
378
|
}
|
|
426
379
|
process.exit(0);
|
|
427
380
|
});
|
|
428
381
|
program.command("logout").description("Logout from Google Calendar").action(async () => {
|
|
429
|
-
|
|
430
|
-
if (program.opts().json) {
|
|
431
|
-
|
|
432
|
-
} else {
|
|
433
|
-
console.log("\u2705 Logged out successfully!");
|
|
434
|
-
}
|
|
382
|
+
logout();
|
|
383
|
+
if (program.opts().json) json({ success: true });
|
|
384
|
+
else console.log(logoutSuccess());
|
|
435
385
|
});
|
|
436
386
|
program.command("next").description("Show your next scheduled meeting").action(async () => {
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
outputJson({ success: false, error: authErrorForJson(auth) });
|
|
387
|
+
const useJson = program.opts().json;
|
|
388
|
+
const client = await getClient();
|
|
389
|
+
if (useJson) {
|
|
390
|
+
if (!client) {
|
|
391
|
+
json({ success: false, error: "auth_required" });
|
|
443
392
|
process.exit(1);
|
|
444
393
|
}
|
|
445
|
-
|
|
394
|
+
const result2 = await getNextMeeting(client);
|
|
395
|
+
json({ success: true, meeting: result2 ?? null });
|
|
446
396
|
return;
|
|
447
397
|
}
|
|
448
|
-
console.log(
|
|
449
|
-
if (!
|
|
450
|
-
console.
|
|
398
|
+
console.log(welcome());
|
|
399
|
+
if (!client) {
|
|
400
|
+
console.error(chalk2.red("\u274C Not authenticated. Run meetfy auth first."));
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
const result = await getNextMeeting(client);
|
|
404
|
+
if (!result) {
|
|
405
|
+
console.log(noMeetings());
|
|
451
406
|
return;
|
|
452
407
|
}
|
|
453
|
-
console.log(
|
|
454
|
-
console.log(
|
|
408
|
+
console.log(nextMeetingTitle());
|
|
409
|
+
console.log(meeting(result));
|
|
455
410
|
});
|
|
456
411
|
program.parse();
|
|
457
412
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meetfy",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"packageManager": "pnpm@10.31.0",
|
|
3
|
+
"version": "1.0.5",
|
|
5
4
|
"description": "CLI tool for creating instant meetings and reserving time in Google Calendar",
|
|
6
5
|
"main": "dist/index.js",
|
|
7
6
|
"type": "module",
|
|
@@ -12,14 +11,11 @@
|
|
|
12
11
|
"dist",
|
|
13
12
|
"README.md"
|
|
14
13
|
],
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"build": "node build.mjs",
|
|
18
|
-
"prepublishOnly": "pnpm run build",
|
|
19
|
-
"lint": "eslint src/**/*.ts"
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"registry": "https://registry.npmjs.org"
|
|
20
16
|
},
|
|
21
17
|
"engines": {
|
|
22
|
-
"node": ">=
|
|
18
|
+
"node": ">=22"
|
|
23
19
|
},
|
|
24
20
|
"keywords": [
|
|
25
21
|
"cli",
|
|
@@ -27,28 +23,27 @@
|
|
|
27
23
|
"google-calendar",
|
|
28
24
|
"typescript"
|
|
29
25
|
],
|
|
30
|
-
"author": "",
|
|
31
26
|
"license": "ISC",
|
|
32
27
|
"dependencies": {
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"google-auth-library": "
|
|
38
|
-
"googleapis": "
|
|
39
|
-
"open": "
|
|
40
|
-
"
|
|
41
|
-
"typescript": "5.5"
|
|
28
|
+
"chalk": "5.6.2",
|
|
29
|
+
"clipboardy": "5.3.1",
|
|
30
|
+
"commander": "14.0.3",
|
|
31
|
+
"conf": "15.1.0",
|
|
32
|
+
"google-auth-library": "10.6.1",
|
|
33
|
+
"googleapis": "171.4.0",
|
|
34
|
+
"open": "11.0.0",
|
|
35
|
+
"typescript": "5.9.3"
|
|
42
36
|
},
|
|
43
37
|
"devDependencies": {
|
|
44
|
-
"@types/
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
38
|
+
"@types/node": "25.4.0",
|
|
39
|
+
"esbuild": "0.27.3",
|
|
40
|
+
"oxlint": "^1.20.0",
|
|
41
|
+
"tsx": "4.21.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"start": "tsx src/index.ts",
|
|
45
|
+
"build": "tsx scripts/build.ts",
|
|
46
|
+
"lint": "oxlint",
|
|
47
|
+
"deploy": "npm publish"
|
|
53
48
|
}
|
|
54
49
|
}
|
package/README.md
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<h1>📆 meetfy</h1>
|
|
3
|
-
<h4>A CLI tool for creating instant meetings and reserving time in Google Calendar.</h4>
|
|
4
|
-
|
|
5
|
-
  
|
|
6
|
-
|
|
7
|
-
</div>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## Features
|
|
11
|
-
- 🎯 Create instant meetings and reserve time in Google Calendar.
|
|
12
|
-
- 📝 Add participants to the meeting.
|
|
13
|
-
- 🕒 Choose the duration of the meeting.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
## Install
|
|
17
|
-
|
|
18
|
-
```sh
|
|
19
|
-
npm i -g meetfy
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Quickstart
|
|
23
|
-
|
|
24
|
-
```sh
|
|
25
|
-
npm i -g meetfy
|
|
26
|
-
meetfy auth # authenticate with Google Calendar
|
|
27
|
-
meetfy create # create a new meeting
|
|
28
|
-
meetfy next # show your next meeting
|
|
29
|
-
meetfy logout # log out
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Use `--json` for machine-readable output (e.g. `meetfy --json next`).
|
|
33
|
-
|
|
34
|
-
## Publishing (maintainers)
|
|
35
|
-
|
|
36
|
-
```sh
|
|
37
|
-
pnpm run build # build dist/index.js
|
|
38
|
-
npm publish # runs prepublishOnly (build) then publishes
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## License
|
|
42
|
-
|
|
43
|
-
MIT © [@eduardoborges](https://github.com/eduardoborges)
|