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.
Files changed (3) hide show
  1. package/dist/index.js +308 -353
  2. package/package.json +22 -27
  3. 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/adapters/cli/index.ts
4
+ // src/cli.ts
5
5
  import { Command } from "commander";
6
+ import chalk2 from "chalk";
6
7
 
7
- // src/adapters/google/auth-adapter.ts
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/infrastructure/config.ts
12
+ // src/config.ts
13
13
  import Conf from "conf";
14
- function createConfig() {
15
- return new Conf({
16
- projectName: "meetfy",
17
- configName: "meetfy-config"
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/infrastructure/web-server.ts
22
- import express from "express";
23
- var HTML = `
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><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
- };
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
- // src/adapters/google/auth-adapter.ts
58
- var SCOPES = [
59
- "https://www.googleapis.com/auth/calendar",
60
- "https://www.googleapis.com/auth/calendar.events"
61
- ];
62
- var CREDENTIALS_PATH = join(process.cwd(), "credentials.json");
63
- function createGoogleAuthAdapter(redirectPort) {
64
- const config = createConfig();
65
- const getCodeServer = createCodeServer(redirectPort);
66
- return {
67
- async authenticate() {
68
- try {
69
- if (!existsSync(CREDENTIALS_PATH)) {
70
- return { type: "no_credentials" };
71
- }
72
- const credentials = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf-8"));
73
- const { client_secret: clientSecret, client_id: clientId } = credentials.installed || credentials.web;
74
- const redirectUri = `http://localhost:${redirectPort}`;
75
- const oAuth2Client = new google.auth.OAuth2(
76
- clientId,
77
- clientSecret,
78
- redirectUri
79
- );
80
- const storedTokens = config.get("googleTokens");
81
- if (storedTokens) {
82
- oAuth2Client.setCredentials(storedTokens);
83
- if (storedTokens.expiry_date && Date.now() > storedTokens.expiry_date) {
84
- const { credentials: newTokens } = await oAuth2Client.refreshAccessToken();
85
- oAuth2Client.setCredentials(newTokens);
86
- config.set("googleTokens", newTokens);
87
- }
88
- return { type: "ok", client: oAuth2Client };
89
- }
90
- const authUrl = oAuth2Client.generateAuthUrl({
91
- access_type: "offline",
92
- scope: SCOPES
93
- });
94
- const fetchToken = async (callbacks) => {
95
- try {
96
- const code = await getCodeServer(callbacks.onWaiting);
97
- const { tokens } = await oAuth2Client.getToken(code);
98
- oAuth2Client.setCredentials(tokens);
99
- config.set("googleTokens", tokens);
100
- return oAuth2Client;
101
- } catch {
102
- return null;
103
- }
104
- };
105
- return { type: "need_code", authUrl, fetchToken };
106
- } catch {
107
- return { type: "error", message: "Authentication failed" };
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
- // src/adapters/google/calendar-adapter.ts
117
- import { google as google2 } from "googleapis";
118
- function createGoogleCalendarAdapter() {
119
- return {
120
- async createEvent(client, input) {
121
- try {
122
- const auth = client;
123
- const calendar = google2.calendar({ version: "v3", auth });
124
- const now = /* @__PURE__ */ new Date();
125
- const startTime = new Date(now.getTime() + 5 * 60 * 1e3);
126
- const endTime = new Date(startTime.getTime() + 30 * 60 * 1e3);
127
- const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
128
- const response = await calendar.events.insert({
129
- calendarId: "primary",
130
- requestBody: {
131
- summary: input.title,
132
- description: input.description,
133
- start: { dateTime: startTime.toISOString(), timeZone: tz },
134
- end: { dateTime: endTime.toISOString(), timeZone: tz },
135
- attendees: input.participants.map((email) => ({ email: email.trim() })),
136
- conferenceData: {
137
- createRequest: {
138
- requestId: `meetfy-${Date.now()}`,
139
- conferenceSolutionKey: { type: "hangoutsMeet" }
140
- }
141
- },
142
- reminders: {
143
- useDefault: false,
144
- overrides: [
145
- { method: "email", minutes: 10 },
146
- { method: "popup", minutes: 5 }
147
- ]
148
- }
149
- },
150
- conferenceDataVersion: 1,
151
- sendUpdates: "all"
152
- });
153
- if (!response.data.id || !response.data.hangoutLink) return null;
154
- return {
155
- id: response.data.id,
156
- title: input.title,
157
- startTime: startTime.toLocaleString(),
158
- endTime: endTime.toLocaleString(),
159
- hangoutLink: response.data.hangoutLink
160
- };
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 auth = client;
168
- const calendar = google2.calendar({ version: "v3", auth });
169
- const now = (/* @__PURE__ */ new Date()).toISOString();
170
- const response = await calendar.events.list({
171
- calendarId: "primary",
172
- timeMin: now,
173
- singleEvents: true,
174
- orderBy: "startTime",
175
- maxResults: 1
176
- });
177
- const event = response.data.items?.[0];
178
- if (!event) return null;
179
- const start = event.start?.dateTime || event.start?.date;
180
- const end = event.end?.dateTime || event.end?.date;
181
- if (!start || !end) return null;
182
- return {
183
- id: event.id ?? "",
184
- title: event.summary ?? "Untitled",
185
- startTime: new Date(start).toLocaleString(),
186
- endTime: new Date(end).toLocaleString(),
187
- hangoutLink: event.hangoutLink ?? void 0,
188
- location: event.location ?? void 0
189
- };
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
- return null;
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
- // src/use-cases/logout.ts
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/use-cases/create-meeting.ts
212
- function makeCreateMeeting(authGateway, calendarGateway) {
213
- return async function createMeeting(input) {
214
- const auth = await authGateway.authenticate();
215
- if (auth.type !== "ok") return null;
216
- return calendarGateway.createEvent(auth.client, input);
217
- };
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
- // src/use-cases/get-next-meeting.ts
221
- function makeGetNextMeeting(authGateway, calendarGateway) {
222
- return async function getNextMeeting() {
223
- const auth = await authGateway.authenticate();
224
- if (auth.type !== "ok") return null;
225
- return calendarGateway.getNextEvent(auth.client);
226
- };
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/adapters/cli/format.ts
230
- var REDIRECT_URI = "http://localhost:3434";
231
- function formatWelcome() {
218
+ // src/format.ts
219
+ import chalk from "chalk";
220
+ function welcome() {
232
221
  return [
233
222
  "",
234
- " Meetfy \u2014 Instant Meeting Creator",
235
- " Create instant meetings and reserve time in Google Calendar",
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 formatMeeting(meeting) {
228
+ function meeting(meet) {
240
229
  const lines = [
241
- ` ${meeting.title}`,
242
- ` \u{1F550} ${meeting.startTime} \u2013 ${meeting.endTime}`
230
+ chalk.cyan.bold(` ${meet.title}`),
231
+ chalk.dim(` \u{1F550} ${meet.startTime} \u2013 ${meet.endTime}`)
243
232
  ];
244
- if (meeting.hangoutLink) lines.push(` \u{1F517} ${meeting.hangoutLink}`);
245
- if (meeting.location) lines.push(` \u{1F4CD} ${meeting.location}`);
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 formatAuthNoCredentials() {
237
+ function authNeedCode(authUrl) {
249
238
  return [
250
- "\u26A0\uFE0F Google Calendar credentials not found.",
251
- "\u{1F4DD} Steps:",
252
- " 1. Go to https://console.cloud.google.com",
253
- " 2. Create/select project \u2192 Enable Google Calendar API",
254
- " 3. Create OAuth 2.0 credentials (Web application)",
255
- ` 4. Add Authorized redirect URI: ${REDIRECT_URI}`,
256
- " 5. Download credentials.json to project root",
257
- " 6. Run: meetfy auth"
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 formatAuthNeedCode(authUrl) {
245
+ function authWaiting() {
246
+ return chalk.dim("\u23F3 Waiting for code on port 3434...");
247
+ }
248
+ function authSuccess() {
261
249
  return [
262
- "\u{1F510} Authorize this app by visiting this URL:",
263
- authUrl,
250
+ chalk.green("\u2705 Authentication successful!"),
264
251
  "",
265
- "Press Enter to copy URL and open in browser."
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 formatAuthWaiting() {
269
- return "\u23F3 Waiting for code on port 3434...";
270
- }
271
- function formatAuthSuccess() {
258
+ function createSuccess(m) {
272
259
  return [
273
- "\u2705 Authentication successful!",
274
- "",
275
- "Available commands:",
276
- " meetfy create Create an instant meeting (30 min)",
277
- " meetfy next Show your next meeting",
278
- " meetfy logout Log out from Google"
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 authErrorForJson(result) {
282
- if (result.type === "no_credentials") return "no_credentials";
283
- if (result.type === "error") return result.message;
284
- return "auth_required";
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/adapters/cli/prompts.ts
279
+ // src/prompts.ts
288
280
  import * as readline from "node:readline";
289
- function createReadlineInterface() {
281
+ function createRl() {
290
282
  return readline.createInterface({ input: process.stdin, output: process.stdout });
291
283
  }
292
- function question(rl, prompt, defaultValue = "") {
284
+ function question(rl, prompt, defaultVal = "") {
293
285
  return new Promise((resolve) => {
294
- const p = defaultValue ? `${prompt} (${defaultValue}): ` : `${prompt}: `;
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 closeReadline(rl) {
290
+ function closeRl(rl) {
301
291
  rl.close();
302
292
  }
303
293
 
304
- // src/adapters/cli/browser.ts
305
- async function copyAndOpenUrl(url) {
306
- const parts = [];
307
- try {
308
- const { default: clipboardy } = await import("clipboardy");
309
- await clipboardy.write(url);
310
- parts.push("URL copied to clipboard");
311
- } catch {
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/adapters/cli/index.ts
325
- var REDIRECT_PORT = 3434;
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 json = program.opts().json;
340
- if (json) {
312
+ const useJson = program.opts().json;
313
+ if (useJson) {
341
314
  const auth = await authenticate();
342
315
  if (auth.type !== "ok") {
343
- outputJson({ success: false, error: authErrorForJson(auth) });
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 meeting2 = await createMeeting({ title: title2, description: description2, participants: participants2 });
350
- if (meeting2) {
351
- outputJson({ success: true, meeting: meeting2 });
352
- } else {
353
- outputJson({ success: false, error: "Failed to create meeting" });
354
- process.exit(1);
355
- }
356
- return;
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(formatWelcome());
359
- const rl = createReadlineInterface();
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
- closeReadline(rl);
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 meeting = await createMeeting({ title, description, participants });
367
- if (meeting) {
368
- console.log("\n\u2705 Meeting created successfully!");
369
- console.log(`\u{1F4C5} ${meeting.title}`);
370
- console.log(`\u{1F517} ${meeting.hangoutLink}`);
371
- console.log(`\u23F0 ${meeting.startTime} \u2013 ${meeting.endTime}`);
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 json = program.opts().json;
348
+ const useJson = program.opts().json;
379
349
  const auth = await authenticate();
380
- if (json) {
381
- if (auth.type === "ok") {
382
- outputJson({ success: true });
383
- } else if (auth.type === "need_code") {
384
- outputJson({ success: false, authRequired: true, authUrl: auth.authUrl });
385
- process.exit(1);
386
- } else if (auth.type === "no_credentials") {
387
- outputJson({ success: false, error: "no_credentials" });
388
- process.exit(1);
389
- } else {
390
- outputJson({ success: false, error: auth.message });
391
- process.exit(1);
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(formatWelcome());
356
+ console.log(welcome());
396
357
  if (auth.type === "ok") {
397
- console.log(formatAuthSuccess());
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
- console.log(formatAuthNeedCode(auth.authUrl));
409
- const rl = createReadlineInterface();
410
- await new Promise((resolve) => {
411
- rl.once("line", () => resolve());
412
- });
413
- await copyAndOpenUrl(auth.authUrl);
414
- closeReadline(rl);
415
- console.log("\n" + formatAuthWaiting());
416
- const client = await auth.fetchToken({
417
- onWaiting: () => {
418
- }
419
- });
420
- if (client) {
421
- console.log("\n" + formatAuthSuccess());
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
- await logout();
430
- if (program.opts().json) {
431
- outputJson({ success: true });
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 json = program.opts().json;
438
- const meeting = await getNextMeeting();
439
- if (json) {
440
- const auth = await authenticate();
441
- if (auth.type !== "ok") {
442
- outputJson({ success: false, error: authErrorForJson(auth) });
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
- outputJson({ success: true, meeting: meeting ?? null });
394
+ const result2 = await getNextMeeting(client);
395
+ json({ success: true, meeting: result2 ?? null });
446
396
  return;
447
397
  }
448
- console.log(formatWelcome());
449
- if (!meeting) {
450
- console.log("\u{1F4ED} No upcoming meetings found.");
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("\u{1F4C5} Next meeting:\n");
454
- console.log(formatMeeting(meeting));
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.3",
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
- "scripts": {
16
- "start": "tsx src/index.ts",
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"
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
- "clipboardy": "^4.0.0",
34
- "commander": "^11.1.0",
35
- "conf": "^12.0.0",
36
- "express": "^5.1.0",
37
- "google-auth-library": "^9.0.0",
38
- "googleapis": "^128.0.0",
39
- "open": "^10.2.0",
40
- "tsx": "^4.20.3",
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/express": "^5.0.3",
45
- "@types/node": "^20.10.0",
46
- "@typescript-eslint/eslint-plugin": "^7.18.0",
47
- "@typescript-eslint/parser": "^7.18.0",
48
- "esbuild": "^0.25.8",
49
- "eslint": "^8.57.1",
50
- "eslint-config-airbnb": "^19.0.4",
51
- "eslint-config-airbnb-typescript": "^18.0.0",
52
- "eslint-plugin-import": "^2.32.0"
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
- ![npm](https://img.shields.io/npm/v/meetfy) ![npm package minimized gzipped size (select exports)](https://img.shields.io/bundlejs/size/meetfy?color=green-light) ![npm](https://img.shields.io/npm/dw/meetfy)
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)