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.
Files changed (3) hide show
  1. package/README.md +3 -41
  2. package/dist/index.js +279 -355
  3. package/package.json +17 -19
package/README.md CHANGED
@@ -1,43 +1,5 @@
1
- <div align="center">
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
- ![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)
3
+ Install globally: `npm i -g meetfy`
6
4
 
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)
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/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);
19
20
  }
20
-
21
- // src/infrastructure/web-server.ts
22
- import express from "express";
23
- var HTML = `
24
- <!DOCTYPE html>
25
- <html>
26
- <head><meta charset="utf-8"><title>Meetfy</title></head>
27
- <body style="font-family:system-ui;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;margin:0;">
28
- <h1>Meetfy</h1>
29
- <p>You can close this window now.</p>
30
- </body>
31
- </html>`;
32
- function createCodeServer(port) {
33
- return function getCode(onListening) {
34
- const app = express();
35
- return new Promise((resolve, reject) => {
36
- app.get("/", (req, res) => {
37
- const code = req.query.code;
38
- if (code) {
39
- resolve(code);
40
- res.send(HTML);
41
- server.close();
42
- } else {
43
- reject(new Error("No code in callback"));
44
- }
45
- });
46
- const server = app.listen(port, (err) => {
47
- if (err) {
48
- reject(err);
49
- return;
50
- }
51
- onListening?.();
52
- });
53
- });
54
- };
21
+ function setConfig(key, value) {
22
+ conf.set(key, value);
23
+ }
24
+ function clearConfig() {
25
+ conf.clear();
55
26
  }
56
27
 
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" };
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">&#10003; 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
- // 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;
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 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
- };
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
- return null;
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
- // src/use-cases/logout.ts
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/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
- };
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
- // 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
- };
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/adapters/cli/format.ts
230
- var REDIRECT_URI = "http://localhost:3434";
231
- function formatWelcome() {
187
+ // src/format.ts
188
+ import chalk from "chalk";
189
+ function welcome() {
232
190
  return [
233
191
  "",
234
- " Meetfy \u2014 Instant Meeting Creator",
235
- " Create instant meetings and reserve time in Google Calendar",
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 formatMeeting(meeting) {
197
+ function meeting(meeting2) {
240
198
  const lines = [
241
- ` ${meeting.title}`,
242
- ` \u{1F550} ${meeting.startTime} \u2013 ${meeting.endTime}`
199
+ chalk.cyan.bold(` ${meeting2.title}`),
200
+ chalk.dim(` \u{1F550} ${meeting2.startTime} \u2013 ${meeting2.endTime}`)
243
201
  ];
244
- if (meeting.hangoutLink) lines.push(` \u{1F517} ${meeting.hangoutLink}`);
245
- if (meeting.location) lines.push(` \u{1F4CD} ${meeting.location}`);
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 formatAuthNoCredentials() {
206
+ function authNeedCode(authUrl) {
249
207
  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"
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 formatAuthNeedCode(authUrl) {
214
+ function authWaiting() {
215
+ return chalk.dim("\u23F3 Waiting for code on port 3434...");
216
+ }
217
+ function authSuccess() {
261
218
  return [
262
- "\u{1F510} Authorize this app by visiting this URL:",
263
- authUrl,
219
+ chalk.green("\u2705 Authentication successful!"),
264
220
  "",
265
- "Press Enter to copy URL and open in browser."
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 formatAuthWaiting() {
269
- return "\u23F3 Waiting for code on port 3434...";
270
- }
271
- function formatAuthSuccess() {
227
+ function createSuccess(meeting2) {
272
228
  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"
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 authErrorForJson(result) {
282
- if (result.type === "no_credentials") return "no_credentials";
283
- if (result.type === "error") return result.message;
284
- return "auth_required";
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/adapters/cli/prompts.ts
248
+ // src/prompts.ts
288
249
  import * as readline from "node:readline";
289
- function createReadlineInterface() {
250
+ function createRl() {
290
251
  return readline.createInterface({ input: process.stdin, output: process.stdout });
291
252
  }
292
- function question(rl, prompt, defaultValue = "") {
253
+ function question(rl, prompt, defaultVal = "") {
293
254
  return new Promise((resolve) => {
294
- const p = defaultValue ? `${prompt} (${defaultValue}): ` : `${prompt}: `;
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 closeReadline(rl) {
259
+ function closeRl(rl) {
301
260
  rl.close();
302
261
  }
303
262
 
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(". ");
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/adapters/cli/index.ts
325
- var REDIRECT_PORT = 3434;
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 json = program.opts().json;
340
- if (json) {
281
+ const useJson = program.opts().json;
282
+ if (useJson) {
341
283
  const auth = await authenticate();
342
284
  if (auth.type !== "ok") {
343
- outputJson({ success: false, error: authErrorForJson(auth) });
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 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;
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(formatWelcome());
359
- const rl = createReadlineInterface();
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
- closeReadline(rl);
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 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.");
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 json = program.opts().json;
317
+ const useJson = program.opts().json;
379
318
  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;
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(formatWelcome());
325
+ console.log(welcome());
396
326
  if (auth.type === "ok") {
397
- console.log(formatAuthSuccess());
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
- 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.");
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
- await logout();
430
- if (program.opts().json) {
431
- outputJson({ success: true });
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 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) });
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
- outputJson({ success: true, meeting: meeting ?? null });
363
+ const result2 = await getNextMeeting(client);
364
+ json({ success: true, meeting: result2 ?? null });
446
365
  return;
447
366
  }
448
- console.log(formatWelcome());
449
- if (!meeting) {
450
- console.log("\u{1F4ED} No upcoming meetings found.");
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("\u{1F4C5} Next meeting:\n");
454
- console.log(formatMeeting(meeting));
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.3",
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": "node build.mjs",
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": ">=18"
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
- "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"
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/express": "^5.0.3",
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": "^0.25.8",
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
  }