meetfy 1.0.2 → 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 +304 -657
- package/package.json +17 -27
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,236 +1,176 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
// src/
|
|
5
|
-
import { render } from "ink";
|
|
4
|
+
// src/cli.ts
|
|
6
5
|
import { Command } from "commander";
|
|
6
|
+
import chalk2 from "chalk";
|
|
7
7
|
|
|
8
|
-
// src/
|
|
9
|
-
import
|
|
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
|
|
8
|
+
// src/auth.ts
|
|
9
|
+
import http from "node:http";
|
|
41
10
|
import { google } from "googleapis";
|
|
42
|
-
import { readFileSync, existsSync } from "fs";
|
|
43
|
-
import { join } from "path";
|
|
44
11
|
|
|
45
|
-
// src/
|
|
12
|
+
// src/config.ts
|
|
46
13
|
import Conf from "conf";
|
|
47
|
-
var
|
|
14
|
+
var conf = new Conf({
|
|
48
15
|
projectName: "meetfy",
|
|
49
16
|
configName: "meetfy-config"
|
|
50
17
|
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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();
|
|
26
|
+
}
|
|
58
27
|
|
|
59
|
-
// src/
|
|
60
|
-
var
|
|
61
|
-
var
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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;
|
|
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);
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
97
56
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"https://www.googleapis.com/auth/calendar.events"
|
|
106
|
-
];
|
|
107
|
-
var CREDENTIALS_PATH = join(process.cwd(), "credentials.json");
|
|
108
|
-
var REDIRECT_URI = `http://localhost:${PORT_NUMBER}`;
|
|
109
|
-
var authenticateGoogle = async () => {
|
|
57
|
+
}
|
|
58
|
+
return makeClient(clientId, tokens);
|
|
59
|
+
}
|
|
60
|
+
async function authenticate() {
|
|
61
|
+
const client = await getClient();
|
|
62
|
+
if (client) return { type: "ok", client };
|
|
63
|
+
const forward = `http://localhost:${REDIRECT_PORT}`;
|
|
110
64
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
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" };
|
|
113
69
|
}
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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;
|
|
131
88
|
}
|
|
132
|
-
return { type: "ok", client: oAuth2Client };
|
|
133
|
-
}
|
|
134
|
-
const authUrl = oAuth2Client.generateAuthUrl({
|
|
135
|
-
access_type: "offline",
|
|
136
|
-
scope: SCOPES
|
|
137
|
-
});
|
|
138
|
-
const fetchToken = async (callbacks) => {
|
|
139
89
|
try {
|
|
140
|
-
const
|
|
141
|
-
const { tokens } =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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();
|
|
145
98
|
} catch {
|
|
146
|
-
|
|
99
|
+
res.writeHead(400, { "Content-Type": "text/plain", Connection: "close" }).end("Invalid tokens");
|
|
100
|
+
reject(new Error("Invalid tokens"));
|
|
101
|
+
server.close();
|
|
147
102
|
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
};
|
|
103
|
+
});
|
|
104
|
+
server.on("error", reject);
|
|
105
|
+
server.listen(port);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function logout() {
|
|
109
|
+
clearConfig();
|
|
110
|
+
}
|
|
157
111
|
|
|
158
|
-
// src/
|
|
112
|
+
// src/calendar.ts
|
|
159
113
|
import { google as google2 } from "googleapis";
|
|
160
|
-
|
|
114
|
+
async function createMeeting(client, input) {
|
|
161
115
|
try {
|
|
162
|
-
const calendar = google2.calendar({ version: "v3", auth });
|
|
116
|
+
const calendar = google2.calendar({ version: "v3", auth: client });
|
|
163
117
|
const now = /* @__PURE__ */ new Date();
|
|
164
118
|
const startTime = new Date(now.getTime() + 5 * 60 * 1e3);
|
|
165
119
|
const endTime = new Date(startTime.getTime() + 30 * 60 * 1e3);
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
createRequest: {
|
|
180
|
-
requestId: `meetfy-${Date.now()}`,
|
|
181
|
-
conferenceSolutionKey: {
|
|
182
|
-
type: "hangoutsMeet"
|
|
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" }
|
|
183
133
|
}
|
|
134
|
+
},
|
|
135
|
+
reminders: {
|
|
136
|
+
useDefault: false,
|
|
137
|
+
overrides: [
|
|
138
|
+
{ method: "email", minutes: 10 },
|
|
139
|
+
{ method: "popup", minutes: 5 }
|
|
140
|
+
]
|
|
184
141
|
}
|
|
185
142
|
},
|
|
186
|
-
reminders: {
|
|
187
|
-
useDefault: false,
|
|
188
|
-
overrides: [
|
|
189
|
-
{ method: "email", minutes: 10 },
|
|
190
|
-
{ method: "popup", minutes: 5 }
|
|
191
|
-
]
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
const response = await calendar.events.insert({
|
|
195
|
-
calendarId: "primary",
|
|
196
|
-
requestBody: event,
|
|
197
143
|
conferenceDataVersion: 1,
|
|
198
144
|
sendUpdates: "all"
|
|
199
145
|
});
|
|
200
|
-
if (!
|
|
201
|
-
throw new Error("Failed to create meeting with Google Meet link");
|
|
202
|
-
}
|
|
146
|
+
if (!res.data.id || !res.data.hangoutLink) return null;
|
|
203
147
|
return {
|
|
204
|
-
id:
|
|
205
|
-
|
|
148
|
+
id: res.data.id,
|
|
149
|
+
title: input.title,
|
|
206
150
|
startTime: startTime.toLocaleString(),
|
|
207
151
|
endTime: endTime.toLocaleString(),
|
|
208
|
-
|
|
152
|
+
hangoutLink: res.data.hangoutLink
|
|
209
153
|
};
|
|
210
154
|
} catch {
|
|
211
155
|
return null;
|
|
212
156
|
}
|
|
213
|
-
}
|
|
214
|
-
|
|
157
|
+
}
|
|
158
|
+
async function getNextMeeting(client) {
|
|
215
159
|
try {
|
|
216
|
-
const calendar = google2.calendar({ version: "v3", auth });
|
|
160
|
+
const calendar = google2.calendar({ version: "v3", auth: client });
|
|
217
161
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
218
|
-
const
|
|
162
|
+
const res = await calendar.events.list({
|
|
219
163
|
calendarId: "primary",
|
|
220
164
|
timeMin: now,
|
|
221
165
|
singleEvents: true,
|
|
222
166
|
orderBy: "startTime",
|
|
223
167
|
maxResults: 1
|
|
224
168
|
});
|
|
225
|
-
const event =
|
|
226
|
-
if (!event)
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
169
|
+
const event = res.data.items?.[0];
|
|
170
|
+
if (!event) return null;
|
|
229
171
|
const start = event.start?.dateTime || event.start?.date;
|
|
230
172
|
const end = event.end?.dateTime || event.end?.date;
|
|
231
|
-
if (!start || !end)
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
173
|
+
if (!start || !end) return null;
|
|
234
174
|
return {
|
|
235
175
|
id: event.id ?? "",
|
|
236
176
|
title: event.summary ?? "Untitled",
|
|
@@ -242,496 +182,203 @@ var getNextMeeting = async (auth) => {
|
|
|
242
182
|
} catch {
|
|
243
183
|
return null;
|
|
244
184
|
}
|
|
245
|
-
}
|
|
185
|
+
}
|
|
246
186
|
|
|
247
|
-
// src/
|
|
248
|
-
import
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
187
|
+
// src/format.ts
|
|
188
|
+
import chalk from "chalk";
|
|
189
|
+
function welcome() {
|
|
190
|
+
return [
|
|
191
|
+
"",
|
|
192
|
+
chalk.cyan.bold(" Meetfy"),
|
|
193
|
+
chalk.dim(" Instant Meeting Creator \u2014 reserve time in Google Calendar"),
|
|
194
|
+
""
|
|
195
|
+
].join("\n");
|
|
255
196
|
}
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
initialDescription,
|
|
265
|
-
initialParticipants
|
|
266
|
-
}));
|
|
267
|
-
const [title, setTitle] = useState(initialTitle || "Instant Meeting");
|
|
268
|
-
const [description, setDescription] = useState(initialDescription ?? "Instant meeting created via Meetfy CLI");
|
|
269
|
-
const [participants, setParticipants] = useState(initialParticipants ?? "");
|
|
270
|
-
const [meeting, setMeeting] = useState(null);
|
|
271
|
-
const [errorMessage, setErrorMessage] = useState("");
|
|
272
|
-
const handleTitleSubmit = useCallback(() => {
|
|
273
|
-
if (!title.trim()) return;
|
|
274
|
-
if (initialDescription !== void 0) setStep("participants");
|
|
275
|
-
else setStep("description");
|
|
276
|
-
}, [title, initialDescription]);
|
|
277
|
-
const handleDescriptionSubmit = useCallback(() => {
|
|
278
|
-
if (initialParticipants !== void 0) setStep("creating");
|
|
279
|
-
else setStep("participants");
|
|
280
|
-
}, [initialParticipants]);
|
|
281
|
-
const handleParticipantsSubmit = useCallback(() => {
|
|
282
|
-
setStep("creating");
|
|
283
|
-
}, []);
|
|
284
|
-
useEffect(() => {
|
|
285
|
-
if (step !== "creating") return;
|
|
286
|
-
let cancelled = false;
|
|
287
|
-
(async () => {
|
|
288
|
-
const authResult = await authenticateGoogle();
|
|
289
|
-
if (cancelled) return;
|
|
290
|
-
if (authResult.type !== "ok") {
|
|
291
|
-
setStep("no_auth");
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
const options = {
|
|
295
|
-
title: title.trim(),
|
|
296
|
-
description: description.trim(),
|
|
297
|
-
participants: participants.split(",").map((e) => e.trim()).filter(Boolean)
|
|
298
|
-
};
|
|
299
|
-
const result = await createInstantMeeting(authResult.client, options);
|
|
300
|
-
if (cancelled) return;
|
|
301
|
-
if (result) {
|
|
302
|
-
setMeeting(result);
|
|
303
|
-
setStep("success");
|
|
304
|
-
setTimeout(() => exit(), 3e3);
|
|
305
|
-
} else {
|
|
306
|
-
setStep("error");
|
|
307
|
-
setErrorMessage("Failed to create meeting");
|
|
308
|
-
}
|
|
309
|
-
})();
|
|
310
|
-
return () => {
|
|
311
|
-
cancelled = true;
|
|
312
|
-
};
|
|
313
|
-
}, [step, title, description, participants, exit]);
|
|
314
|
-
if (step === "title") {
|
|
315
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
316
|
-
/* @__PURE__ */ jsx3(Box3, { marginRight: 1, children: /* @__PURE__ */ jsx3(Text2, { children: "Meeting title:" }) }),
|
|
317
|
-
/* @__PURE__ */ jsx3(
|
|
318
|
-
TextInput,
|
|
319
|
-
{
|
|
320
|
-
value: title,
|
|
321
|
-
onChange: setTitle,
|
|
322
|
-
onSubmit: handleTitleSubmit,
|
|
323
|
-
placeholder: "Instant Meeting"
|
|
324
|
-
}
|
|
325
|
-
)
|
|
326
|
-
] });
|
|
327
|
-
}
|
|
328
|
-
if (step === "description") {
|
|
329
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
330
|
-
/* @__PURE__ */ jsx3(Box3, { marginRight: 1, children: /* @__PURE__ */ jsx3(Text2, { children: "Meeting description (optional):" }) }),
|
|
331
|
-
/* @__PURE__ */ jsx3(
|
|
332
|
-
TextInput,
|
|
333
|
-
{
|
|
334
|
-
value: description,
|
|
335
|
-
onChange: setDescription,
|
|
336
|
-
onSubmit: handleDescriptionSubmit,
|
|
337
|
-
placeholder: "Instant meeting created via Meetfy CLI"
|
|
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;
|
|
197
|
+
function meeting(meeting2) {
|
|
198
|
+
const lines = [
|
|
199
|
+
chalk.cyan.bold(` ${meeting2.title}`),
|
|
200
|
+
chalk.dim(` \u{1F550} ${meeting2.startTime} \u2013 ${meeting2.endTime}`)
|
|
201
|
+
];
|
|
202
|
+
if (meeting2.hangoutLink) lines.push(chalk.blue(` \u{1F517} ${meeting2.hangoutLink}`));
|
|
203
|
+
if (meeting2.location) lines.push(chalk.dim(` \u{1F4CD} ${meeting2.location}`));
|
|
204
|
+
return lines.join("\n");
|
|
394
205
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
useInput
|
|
403
|
-
} from "ink";
|
|
404
|
-
import Spinner2 from "ink-spinner";
|
|
405
|
-
|
|
406
|
-
// src/utils/openAuthUrl.ts
|
|
407
|
-
async function copyAndOpenUrl(url) {
|
|
408
|
-
const results = [];
|
|
409
|
-
try {
|
|
410
|
-
const { default: clipboardy } = await import("clipboardy");
|
|
411
|
-
await clipboardy.write(url);
|
|
412
|
-
results.push("URL copied to clipboard");
|
|
413
|
-
} catch {
|
|
414
|
-
results.push("Could not copy to clipboard");
|
|
415
|
-
}
|
|
416
|
-
try {
|
|
417
|
-
const open = (await import("open")).default;
|
|
418
|
-
await open(url);
|
|
419
|
-
results.push("Opening in browser...");
|
|
420
|
-
} catch {
|
|
421
|
-
results.push("Could not open browser");
|
|
422
|
-
}
|
|
423
|
-
return results.join(". ");
|
|
206
|
+
function authNeedCode(authUrl) {
|
|
207
|
+
return [
|
|
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.")
|
|
212
|
+
].join("\n");
|
|
424
213
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
function
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
] });
|
|
545
|
-
}
|
|
546
|
-
return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text3, { color: "red", children: [
|
|
547
|
-
"\u274C ",
|
|
548
|
-
errorMessage
|
|
549
|
-
] }) });
|
|
214
|
+
function authWaiting() {
|
|
215
|
+
return chalk.dim("\u23F3 Waiting for code on port 3434...");
|
|
216
|
+
}
|
|
217
|
+
function authSuccess() {
|
|
218
|
+
return [
|
|
219
|
+
chalk.green("\u2705 Authentication successful!"),
|
|
220
|
+
"",
|
|
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")
|
|
225
|
+
].join("\n");
|
|
226
|
+
}
|
|
227
|
+
function createSuccess(meeting2) {
|
|
228
|
+
return [
|
|
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}`)
|
|
233
|
+
].join("\n");
|
|
234
|
+
}
|
|
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";
|
|
550
246
|
}
|
|
551
247
|
|
|
552
|
-
// src/
|
|
553
|
-
import
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
248
|
+
// src/prompts.ts
|
|
249
|
+
import * as readline from "node:readline";
|
|
250
|
+
function createRl() {
|
|
251
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
252
|
+
}
|
|
253
|
+
function question(rl, prompt, defaultVal = "") {
|
|
254
|
+
return new Promise((resolve) => {
|
|
255
|
+
const p = defaultVal ? `${prompt} (${defaultVal}): ` : `${prompt}: `;
|
|
256
|
+
rl.question(p, (answer) => resolve(answer.trim() || defaultVal.trim()));
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
function closeRl(rl) {
|
|
260
|
+
rl.close();
|
|
564
261
|
}
|
|
565
262
|
|
|
566
|
-
// src/
|
|
567
|
-
import
|
|
568
|
-
import
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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." }) });
|
|
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
|
+
});
|
|
638
271
|
}
|
|
639
272
|
|
|
640
|
-
// src/
|
|
641
|
-
|
|
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");
|
|
644
|
-
function outputJson(obj) {
|
|
273
|
+
// src/cli.ts
|
|
274
|
+
function json(obj) {
|
|
645
275
|
console.log(JSON.stringify(obj, null, 0));
|
|
646
276
|
}
|
|
647
|
-
function
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
277
|
+
function runCli() {
|
|
278
|
+
const program = new Command();
|
|
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");
|
|
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) => {
|
|
281
|
+
const useJson = program.opts().json;
|
|
282
|
+
if (useJson) {
|
|
283
|
+
const auth = await authenticate();
|
|
284
|
+
if (auth.type !== "ok") {
|
|
285
|
+
json({ success: false, error: authErrorJson(auth) });
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
const title2 = opts.title?.trim() || "Instant Meeting";
|
|
289
|
+
const description2 = opts.description?.trim() || "Instant meeting created via Meetfy CLI";
|
|
290
|
+
const participants2 = (opts.participants ?? "").split(",").map((e) => e.trim()).filter(Boolean);
|
|
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);
|
|
658
295
|
}
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
outputJson({ success: true, meeting });
|
|
669
|
-
} else {
|
|
670
|
-
outputJson({ success: false, error: "Failed to create meeting" });
|
|
296
|
+
console.log(welcome());
|
|
297
|
+
const rl = createRl();
|
|
298
|
+
const title = opts.title?.trim() || await question(rl, "Meeting title", "Instant Meeting");
|
|
299
|
+
const description = opts.description?.trim() || await question(rl, "Meeting description", "Instant meeting created via Meetfy CLI");
|
|
300
|
+
const participantsStr = opts.participants ?? await question(rl, "Participant emails (comma-separated)", "");
|
|
301
|
+
closeRl(rl);
|
|
302
|
+
const client = await getClient();
|
|
303
|
+
if (!client) {
|
|
304
|
+
console.error(chalk2.red("\u274C Not authenticated. Run meetfy auth first."));
|
|
671
305
|
process.exit(1);
|
|
672
306
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
initialTitle: options.title,
|
|
680
|
-
initialDescription: options.description,
|
|
681
|
-
initialParticipants: options.participants
|
|
682
|
-
}
|
|
683
|
-
) })
|
|
684
|
-
);
|
|
685
|
-
await waitUntilExit();
|
|
686
|
-
});
|
|
687
|
-
program.command("auth").description("Authenticate with Google Calendar").action(async () => {
|
|
688
|
-
if (program.opts().json) {
|
|
689
|
-
const authResult = await authenticateGoogle();
|
|
690
|
-
if (authResult.type === "ok") {
|
|
691
|
-
outputJson({ success: true });
|
|
692
|
-
} else if (authResult.type === "need_code") {
|
|
693
|
-
outputJson({ success: false, authRequired: true, authUrl: authResult.authUrl });
|
|
307
|
+
const participants = participantsStr.split(",").map((e) => e.trim()).filter(Boolean);
|
|
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."));
|
|
694
313
|
process.exit(1);
|
|
695
|
-
}
|
|
696
|
-
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
program.command("auth").description("Authenticate with Google Calendar").action(async () => {
|
|
317
|
+
const useJson = program.opts().json;
|
|
318
|
+
const auth = await authenticate();
|
|
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);
|
|
324
|
+
}
|
|
325
|
+
console.log(welcome());
|
|
326
|
+
if (auth.type === "ok") {
|
|
327
|
+
console.log(authSuccess());
|
|
328
|
+
process.exit(0);
|
|
329
|
+
}
|
|
330
|
+
if (auth.type === "error") {
|
|
331
|
+
console.error(chalk2.red("\u274C"), auth.message);
|
|
697
332
|
process.exit(1);
|
|
698
|
-
}
|
|
699
|
-
|
|
333
|
+
}
|
|
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."));
|
|
700
346
|
process.exit(1);
|
|
701
347
|
}
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (authResult.type !== "ok") {
|
|
725
|
-
outputJson({ success: false, error: authErrorForJson(authResult) });
|
|
348
|
+
process.exit(0);
|
|
349
|
+
});
|
|
350
|
+
program.command("logout").description("Logout from Google Calendar").action(async () => {
|
|
351
|
+
logout();
|
|
352
|
+
if (program.opts().json) json({ success: true });
|
|
353
|
+
else console.log(logoutSuccess());
|
|
354
|
+
});
|
|
355
|
+
program.command("next").description("Show your next scheduled meeting").action(async () => {
|
|
356
|
+
const useJson = program.opts().json;
|
|
357
|
+
const client = await getClient();
|
|
358
|
+
if (useJson) {
|
|
359
|
+
if (!client) {
|
|
360
|
+
json({ success: false, error: "auth_required" });
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
const result2 = await getNextMeeting(client);
|
|
364
|
+
json({ success: true, meeting: result2 ?? null });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
console.log(welcome());
|
|
368
|
+
if (!client) {
|
|
369
|
+
console.error(chalk2.red("\u274C Not authenticated. Run meetfy auth first."));
|
|
726
370
|
process.exit(1);
|
|
727
371
|
}
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
372
|
+
const result = await getNextMeeting(client);
|
|
373
|
+
if (!result) {
|
|
374
|
+
console.log(noMeetings());
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
console.log(nextMeetingTitle());
|
|
378
|
+
console.log(meeting(result));
|
|
379
|
+
});
|
|
380
|
+
program.parse();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/index.ts
|
|
384
|
+
runCli();
|
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",
|
|
@@ -13,13 +12,14 @@
|
|
|
13
12
|
"README.md"
|
|
14
13
|
],
|
|
15
14
|
"scripts": {
|
|
16
|
-
"start": "tsx src/index.
|
|
17
|
-
"build": "
|
|
15
|
+
"start": "tsx src/index.ts",
|
|
16
|
+
"build": "tsx scripts/build.ts",
|
|
18
17
|
"prepublishOnly": "pnpm run build",
|
|
19
|
-
"lint": "eslint src/**/*.
|
|
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,36 +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
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"ink-text-input": "^6.0.0",
|
|
42
|
-
"open": "^10.2.0",
|
|
43
|
-
"react": "^18.3.1",
|
|
44
|
-
"tsx": "^4.20.3",
|
|
45
|
-
"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"
|
|
46
40
|
},
|
|
47
41
|
"devDependencies": {
|
|
48
|
-
"@types/
|
|
49
|
-
"@types/node": "^20.10.0",
|
|
50
|
-
"@types/react": "^18.3.0",
|
|
42
|
+
"@types/node": "25.4.0",
|
|
51
43
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
52
44
|
"@typescript-eslint/parser": "^7.18.0",
|
|
53
|
-
"esbuild": "
|
|
45
|
+
"esbuild": "0.27.3",
|
|
54
46
|
"eslint": "^8.57.1",
|
|
55
47
|
"eslint-config-airbnb": "^19.0.4",
|
|
56
48
|
"eslint-config-airbnb-typescript": "^18.0.0",
|
|
57
49
|
"eslint-plugin-import": "^2.32.0",
|
|
58
|
-
"
|
|
59
|
-
"eslint-plugin-react": "^7.37.5",
|
|
60
|
-
"eslint-plugin-react-hooks": "^4.6.2"
|
|
50
|
+
"tsx": "4.21.0"
|
|
61
51
|
}
|
|
62
52
|
}
|