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