meetfy 1.0.1 → 1.0.3
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 +25 -140
- package/dist/index.js +460 -0
- package/package.json +19 -16
- package/.eslintrc +0 -13
- package/src/constants.ts +0 -1
- package/src/index.ts +0 -81
- package/src/resources/index.html +0 -50
- package/src/services/authService.ts +0 -104
- package/src/services/configService.ts +0 -8
- package/src/services/meetingService.ts +0 -139
- package/src/services/webServer.ts +0 -54
- package/src/utils/cliUtils.ts +0 -14
- package/test.html +0 -0
- package/tsconfig.json +0 -20
package/README.md
CHANGED
|
@@ -1,158 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>📆 meetfy</h1>
|
|
3
|
+
<h4>A CLI tool for creating instant meetings and reserving time in Google Calendar.</h4>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
  
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- 🚀 Create instant meetings with Google Meet
|
|
8
|
-
- 📅 Automatically reserve 30 minutes in your Google Calendar
|
|
9
|
-
- 👥 Add participants via email
|
|
10
|
-
- 🔐 Secure Google OAuth2 authentication
|
|
11
|
-
- 💾 Persistent authentication tokens
|
|
12
|
-
- 🎨 Beautiful CLI interface with colors and spinners
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
1. Clone the repository:
|
|
17
|
-
```bash
|
|
18
|
-
git clone <repository-url>
|
|
19
|
-
cd meetfy
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
2. Install dependencies:
|
|
23
|
-
```bash
|
|
24
|
-
npm install
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
3. Build the project:
|
|
28
|
-
```bash
|
|
29
|
-
npm run build
|
|
30
|
-
```
|
|
7
|
+
</div>
|
|
31
8
|
|
|
32
|
-
4. Link globally (optional):
|
|
33
|
-
```bash
|
|
34
|
-
npm link
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Setup
|
|
38
|
-
|
|
39
|
-
### 1. Google Calendar API Setup
|
|
40
|
-
|
|
41
|
-
1. Go to [Google Cloud Console](https://console.cloud.google.com)
|
|
42
|
-
2. Create a new project or select an existing one
|
|
43
|
-
3. Enable the Google Calendar API:
|
|
44
|
-
- Go to "APIs & Services" → "Library"
|
|
45
|
-
- Search for "Google Calendar API"
|
|
46
|
-
- Click "Enable"
|
|
47
|
-
4. Create OAuth 2.0 credentials:
|
|
48
|
-
- Go to "APIs & Services" → "Credentials"
|
|
49
|
-
- Click "Create Credentials" → "OAuth 2.0 Client IDs"
|
|
50
|
-
- Choose "Desktop application"
|
|
51
|
-
- Download the credentials file
|
|
52
|
-
5. Rename the downloaded file to `credentials.json` and place it in the project root
|
|
53
|
-
|
|
54
|
-
The `credentials.json` file should look like this:
|
|
55
|
-
```json
|
|
56
|
-
{
|
|
57
|
-
"installed": {
|
|
58
|
-
"client_id": "your-client-id.apps.googleusercontent.com",
|
|
59
|
-
"project_id": "your-project-id",
|
|
60
|
-
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
61
|
-
"token_uri": "https://oauth2.googleapis.com/token",
|
|
62
|
-
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
63
|
-
"client_secret": "your-client-secret",
|
|
64
|
-
"redirect_uris": ["http://localhost"]
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### 2. Authentication
|
|
70
|
-
|
|
71
|
-
Run the authentication command:
|
|
72
|
-
```bash
|
|
73
|
-
npm run dev auth
|
|
74
|
-
# or if linked globally:
|
|
75
|
-
meetfy auth
|
|
76
|
-
```
|
|
77
9
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
81
14
|
|
|
82
|
-
### Create an Instant Meeting
|
|
83
15
|
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
npm run dev create
|
|
87
|
-
# or if linked globally:
|
|
88
|
-
meetfy create
|
|
89
|
-
```
|
|
16
|
+
## Install
|
|
90
17
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
meetfy create -t "Team Standup" -d "Daily team sync" -p "john@example.com,jane@example.com"
|
|
18
|
+
```sh
|
|
19
|
+
npm i -g meetfy
|
|
94
20
|
```
|
|
95
21
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
- `-t, --title <title>`: Meeting title
|
|
99
|
-
- `-d, --description <description>`: Meeting description
|
|
100
|
-
- `-p, --participants <emails>`: Comma-separated list of participant emails
|
|
101
|
-
|
|
102
|
-
### Authentication
|
|
22
|
+
## Quickstart
|
|
103
23
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
meetfy auth
|
|
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
|
|
107
30
|
```
|
|
108
31
|
|
|
109
|
-
|
|
32
|
+
Use `--json` for machine-readable output (e.g. `meetfy --json next`).
|
|
110
33
|
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
npm run dev
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### Build for Production
|
|
117
|
-
```bash
|
|
118
|
-
npm run build
|
|
119
|
-
```
|
|
34
|
+
## Publishing (maintainers)
|
|
120
35
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
npm
|
|
36
|
+
```sh
|
|
37
|
+
pnpm run build # build dist/index.js
|
|
38
|
+
npm publish # runs prepublishOnly (build) then publishes
|
|
124
39
|
```
|
|
125
40
|
|
|
126
|
-
## Project Structure
|
|
127
|
-
|
|
128
|
-
```
|
|
129
|
-
src/
|
|
130
|
-
├── features/
|
|
131
|
-
│ ├── auth/
|
|
132
|
-
│ │ └── authService.ts # Google OAuth2 authentication
|
|
133
|
-
│ └── meeting/
|
|
134
|
-
│ └── meetingService.ts # Meeting creation and calendar integration
|
|
135
|
-
├── utils/
|
|
136
|
-
│ └── cliUtils.ts # CLI utilities and formatting
|
|
137
|
-
└── index.ts # Main CLI entry point
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Features
|
|
141
|
-
|
|
142
|
-
- **Instant Meeting Creation**: Creates meetings that start in 5 minutes
|
|
143
|
-
- **30-Minute Duration**: Automatically sets 30-minute duration
|
|
144
|
-
- **Google Meet Integration**: Includes Google Meet link in calendar events
|
|
145
|
-
- **Participant Management**: Add participants via email addresses
|
|
146
|
-
- **Calendar Reminders**: Sets up email and popup reminders
|
|
147
|
-
- **Persistent Authentication**: Saves and reuses authentication tokens
|
|
148
|
-
- **Interactive CLI**: User-friendly prompts and colorful output
|
|
149
|
-
|
|
150
|
-
## Requirements
|
|
151
|
-
|
|
152
|
-
- Node.js 18+
|
|
153
|
-
- Google Calendar API access
|
|
154
|
-
- Google OAuth2 credentials
|
|
155
|
-
|
|
156
41
|
## License
|
|
157
42
|
|
|
158
|
-
|
|
43
|
+
MIT © [@eduardoborges](https://github.com/eduardoborges)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// src/adapters/cli/index.ts
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/adapters/google/auth-adapter.ts
|
|
8
|
+
import { google } from "googleapis";
|
|
9
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
// src/infrastructure/config.ts
|
|
13
|
+
import Conf from "conf";
|
|
14
|
+
function createConfig() {
|
|
15
|
+
return new Conf({
|
|
16
|
+
projectName: "meetfy",
|
|
17
|
+
configName: "meetfy-config"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
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
|
+
};
|
|
55
|
+
}
|
|
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" };
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
async logout() {
|
|
111
|
+
config.clear();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
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;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
async getNextEvent(client) {
|
|
166
|
+
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
|
+
};
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/use-cases/authenticate.ts
|
|
198
|
+
function makeAuthenticate(authGateway) {
|
|
199
|
+
return async function authenticate() {
|
|
200
|
+
return authGateway.authenticate();
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/use-cases/logout.ts
|
|
205
|
+
function makeLogout(authGateway) {
|
|
206
|
+
return async function logout() {
|
|
207
|
+
return authGateway.logout();
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
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
|
+
};
|
|
218
|
+
}
|
|
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
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/adapters/cli/format.ts
|
|
230
|
+
var REDIRECT_URI = "http://localhost:3434";
|
|
231
|
+
function formatWelcome() {
|
|
232
|
+
return [
|
|
233
|
+
"",
|
|
234
|
+
" Meetfy \u2014 Instant Meeting Creator",
|
|
235
|
+
" Create instant meetings and reserve time in Google Calendar",
|
|
236
|
+
""
|
|
237
|
+
].join("\n");
|
|
238
|
+
}
|
|
239
|
+
function formatMeeting(meeting) {
|
|
240
|
+
const lines = [
|
|
241
|
+
` ${meeting.title}`,
|
|
242
|
+
` \u{1F550} ${meeting.startTime} \u2013 ${meeting.endTime}`
|
|
243
|
+
];
|
|
244
|
+
if (meeting.hangoutLink) lines.push(` \u{1F517} ${meeting.hangoutLink}`);
|
|
245
|
+
if (meeting.location) lines.push(` \u{1F4CD} ${meeting.location}`);
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
function formatAuthNoCredentials() {
|
|
249
|
+
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"
|
|
258
|
+
].join("\n");
|
|
259
|
+
}
|
|
260
|
+
function formatAuthNeedCode(authUrl) {
|
|
261
|
+
return [
|
|
262
|
+
"\u{1F510} Authorize this app by visiting this URL:",
|
|
263
|
+
authUrl,
|
|
264
|
+
"",
|
|
265
|
+
"Press Enter to copy URL and open in browser."
|
|
266
|
+
].join("\n");
|
|
267
|
+
}
|
|
268
|
+
function formatAuthWaiting() {
|
|
269
|
+
return "\u23F3 Waiting for code on port 3434...";
|
|
270
|
+
}
|
|
271
|
+
function formatAuthSuccess() {
|
|
272
|
+
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"
|
|
279
|
+
].join("\n");
|
|
280
|
+
}
|
|
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";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/adapters/cli/prompts.ts
|
|
288
|
+
import * as readline from "node:readline";
|
|
289
|
+
function createReadlineInterface() {
|
|
290
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
291
|
+
}
|
|
292
|
+
function question(rl, prompt, defaultValue = "") {
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
const p = defaultValue ? `${prompt} (${defaultValue}): ` : `${prompt}: `;
|
|
295
|
+
rl.question(p, (answer) => {
|
|
296
|
+
resolve(answer.trim() || defaultValue.trim());
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function closeReadline(rl) {
|
|
301
|
+
rl.close();
|
|
302
|
+
}
|
|
303
|
+
|
|
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(". ");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/adapters/cli/index.ts
|
|
325
|
+
var REDIRECT_PORT = 3434;
|
|
326
|
+
function outputJson(obj) {
|
|
327
|
+
console.log(JSON.stringify(obj, null, 0));
|
|
328
|
+
}
|
|
329
|
+
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
|
+
const program = new Command();
|
|
337
|
+
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
|
+
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) {
|
|
341
|
+
const auth = await authenticate();
|
|
342
|
+
if (auth.type !== "ok") {
|
|
343
|
+
outputJson({ success: false, error: authErrorForJson(auth) });
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
const title2 = opts.title?.trim() || "Instant Meeting";
|
|
347
|
+
const description2 = opts.description?.trim() || "Instant meeting created via Meetfy CLI";
|
|
348
|
+
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;
|
|
357
|
+
}
|
|
358
|
+
console.log(formatWelcome());
|
|
359
|
+
const rl = createReadlineInterface();
|
|
360
|
+
const title = opts.title?.trim() || await question(rl, "Meeting title", "Instant Meeting");
|
|
361
|
+
const description = opts.description?.trim() || await question(rl, "Meeting description", "Instant meeting created via Meetfy CLI");
|
|
362
|
+
const participantsStr = opts.participants ?? await question(rl, "Participant emails (comma-separated)", "");
|
|
363
|
+
closeReadline(rl);
|
|
364
|
+
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.");
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
program.command("auth").description("Authenticate with Google Calendar").action(async () => {
|
|
378
|
+
const json = program.opts().json;
|
|
379
|
+
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;
|
|
394
|
+
}
|
|
395
|
+
console.log(formatWelcome());
|
|
396
|
+
if (auth.type === "ok") {
|
|
397
|
+
console.log(formatAuthSuccess());
|
|
398
|
+
process.exit(0);
|
|
399
|
+
}
|
|
400
|
+
if (auth.type === "no_credentials") {
|
|
401
|
+
console.log(formatAuthNoCredentials());
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
if (auth.type === "error") {
|
|
405
|
+
console.error("\u274C", auth.message);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
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.");
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
process.exit(0);
|
|
427
|
+
});
|
|
428
|
+
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
|
+
}
|
|
435
|
+
});
|
|
436
|
+
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) });
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
outputJson({ success: true, meeting: meeting ?? null });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
console.log(formatWelcome());
|
|
449
|
+
if (!meeting) {
|
|
450
|
+
console.log("\u{1F4ED} No upcoming meetings found.");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
console.log("\u{1F4C5} Next meeting:\n");
|
|
454
|
+
console.log(formatMeeting(meeting));
|
|
455
|
+
});
|
|
456
|
+
program.parse();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/index.ts
|
|
460
|
+
runCli();
|
package/package.json
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meetfy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"packageManager": "pnpm@10.31.0",
|
|
4
5
|
"description": "CLI tool for creating instant meetings and reserving time in Google Calendar",
|
|
5
|
-
"main": "
|
|
6
|
+
"main": "dist/index.js",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"bin": {
|
|
8
|
-
"meetfy": "./
|
|
9
|
+
"meetfy": "./dist/index.js"
|
|
9
10
|
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
10
15
|
"scripts": {
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
16
|
+
"start": "tsx src/index.ts",
|
|
17
|
+
"build": "node build.mjs",
|
|
18
|
+
"prepublishOnly": "pnpm run build",
|
|
14
19
|
"lint": "eslint src/**/*.ts"
|
|
15
20
|
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
16
24
|
"keywords": [
|
|
17
25
|
"cli",
|
|
18
26
|
"meeting",
|
|
@@ -22,30 +30,25 @@
|
|
|
22
30
|
"author": "",
|
|
23
31
|
"license": "ISC",
|
|
24
32
|
"dependencies": {
|
|
25
|
-
"
|
|
26
|
-
"chalk": "^5.3.0",
|
|
33
|
+
"clipboardy": "^4.0.0",
|
|
27
34
|
"commander": "^11.1.0",
|
|
28
35
|
"conf": "^12.0.0",
|
|
29
36
|
"express": "^5.1.0",
|
|
30
37
|
"google-auth-library": "^9.0.0",
|
|
31
38
|
"googleapis": "^128.0.0",
|
|
32
|
-
"
|
|
33
|
-
"ora": "^7.0.1",
|
|
39
|
+
"open": "^10.2.0",
|
|
34
40
|
"tsx": "^4.20.3",
|
|
35
41
|
"typescript": "5.5"
|
|
36
42
|
},
|
|
37
43
|
"devDependencies": {
|
|
38
44
|
"@types/express": "^5.0.3",
|
|
39
|
-
"@types/inquirer": "^9.0.7",
|
|
40
45
|
"@types/node": "^20.10.0",
|
|
41
46
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
42
47
|
"@typescript-eslint/parser": "^7.18.0",
|
|
48
|
+
"esbuild": "^0.25.8",
|
|
43
49
|
"eslint": "^8.57.1",
|
|
44
50
|
"eslint-config-airbnb": "^19.0.4",
|
|
45
51
|
"eslint-config-airbnb-typescript": "^18.0.0",
|
|
46
|
-
"eslint-plugin-import": "^2.32.0"
|
|
47
|
-
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
48
|
-
"eslint-plugin-react": "^7.37.5",
|
|
49
|
-
"eslint-plugin-react-hooks": "^4.6.2"
|
|
52
|
+
"eslint-plugin-import": "^2.32.0"
|
|
50
53
|
}
|
|
51
|
-
}
|
|
54
|
+
}
|
package/.eslintrc
DELETED
package/src/constants.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const PORT_NUMBER = 3434;
|
package/src/index.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx --no-deprecation
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { createInstantMeeting } from './services/meetingService.js';
|
|
6
|
-
import { authenticateGoogle, logoutGoogle } from './services/authService.js';
|
|
7
|
-
import { showWelcomeMessage } from './utils/cliUtils.js';
|
|
8
|
-
|
|
9
|
-
const program = new Command();
|
|
10
|
-
|
|
11
|
-
program
|
|
12
|
-
.name('meetfy')
|
|
13
|
-
.description('CLI tool for creating instant meetings and reserving time in Google Calendar')
|
|
14
|
-
.version('1.0.0');
|
|
15
|
-
|
|
16
|
-
program
|
|
17
|
-
.command('create')
|
|
18
|
-
.description('Create an instant meeting and reserve 30 minutes in your Google Calendar')
|
|
19
|
-
.option('-t, --title <title>', 'Meeting title')
|
|
20
|
-
.option('-d, --description <description>', 'Meeting description')
|
|
21
|
-
.option('-p, --participants <emails>', 'Comma-separated list of participant emails')
|
|
22
|
-
.action(async (options) => {
|
|
23
|
-
try {
|
|
24
|
-
showWelcomeMessage();
|
|
25
|
-
|
|
26
|
-
// Authenticate with Google
|
|
27
|
-
const auth = await authenticateGoogle();
|
|
28
|
-
if (!auth) {
|
|
29
|
-
console.log(chalk.red('❌ Authentication failed. Please try again.'));
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Create instant meeting
|
|
34
|
-
const meeting = await createInstantMeeting(auth, options);
|
|
35
|
-
|
|
36
|
-
if (meeting) {
|
|
37
|
-
console.log(chalk.green('✅ Meeting created successfully!'));
|
|
38
|
-
console.log(chalk.cyan(`📅 Meeting ID: ${meeting.id}`));
|
|
39
|
-
console.log(chalk.cyan(`🔗 Join URL: ${meeting.hangoutLink}`));
|
|
40
|
-
console.log(chalk.cyan('⏰ Duration: 30 minutes'));
|
|
41
|
-
console.log(chalk.cyan(`📅 Start Time: ${meeting.startTime}`));
|
|
42
|
-
}
|
|
43
|
-
} catch (error) {
|
|
44
|
-
console.error(chalk.red('❌ Error creating meeting:'), error);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
program
|
|
50
|
-
.command('auth')
|
|
51
|
-
.description('Authenticate with Google Calendar')
|
|
52
|
-
.addHelpText('after', `
|
|
53
|
-
\nExamples:
|
|
54
|
-
\n meetfy auth
|
|
55
|
-
\n meetfy auth --help
|
|
56
|
-
`)
|
|
57
|
-
.action(async () => {
|
|
58
|
-
try {
|
|
59
|
-
showWelcomeMessage();
|
|
60
|
-
const auth = await authenticateGoogle();
|
|
61
|
-
if (auth) {
|
|
62
|
-
console.log(chalk.green('✅ Authentication successful!'));
|
|
63
|
-
} else {
|
|
64
|
-
console.log(chalk.red('❌ Authentication failed.'));
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
} catch (error) {
|
|
68
|
-
console.error(chalk.red('❌ Authentication error:'), error);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
program
|
|
74
|
-
.command('logout')
|
|
75
|
-
.description('Logout from Google Calendar')
|
|
76
|
-
.action(async () => {
|
|
77
|
-
await logoutGoogle();
|
|
78
|
-
console.log(chalk.green('✅ Logged out successfully!'));
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
program.parse();
|
package/src/resources/index.html
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Document</title>
|
|
7
|
-
<style>
|
|
8
|
-
.container {
|
|
9
|
-
display: flex;
|
|
10
|
-
flex-direction: column;
|
|
11
|
-
height: 100vh;
|
|
12
|
-
}
|
|
13
|
-
.header {
|
|
14
|
-
background-color: #f00;
|
|
15
|
-
height: 100px;
|
|
16
|
-
}
|
|
17
|
-
.content {
|
|
18
|
-
display: grid;
|
|
19
|
-
grid-template-columns: 1fr;
|
|
20
|
-
}
|
|
21
|
-
.left {
|
|
22
|
-
background-color: #0f0;
|
|
23
|
-
}
|
|
24
|
-
.right {
|
|
25
|
-
background-color: #00f;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
@media (min-width: 768px) {
|
|
29
|
-
.content {
|
|
30
|
-
grid-template-columns: 300px 1fr;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
</style>
|
|
34
|
-
</head>
|
|
35
|
-
<body>
|
|
36
|
-
<div class="container">
|
|
37
|
-
<div class="header">
|
|
38
|
-
<h1>Header</h1>
|
|
39
|
-
</div>
|
|
40
|
-
<div class="content">
|
|
41
|
-
<div class="left">
|
|
42
|
-
<h1>Left</h1>
|
|
43
|
-
</div>
|
|
44
|
-
<div class="right">
|
|
45
|
-
<h1>Right</h1>
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
</body>
|
|
50
|
-
</html>
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { google } from 'googleapis';
|
|
2
|
-
import { OAuth2Client } from 'google-auth-library';
|
|
3
|
-
import { readFileSync, existsSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
import config from './configService.js';
|
|
7
|
-
import { getCodeServer } from './webServer.js';
|
|
8
|
-
import { PORT_NUMBER } from '../constants.js';
|
|
9
|
-
|
|
10
|
-
const SCOPES = [
|
|
11
|
-
'https://www.googleapis.com/auth/calendar',
|
|
12
|
-
'https://www.googleapis.com/auth/calendar.events',
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const CREDENTIALS_PATH = join(process.cwd(), 'credentials.json');
|
|
16
|
-
const REDIRECT_URI = `http://localhost:${PORT_NUMBER}/`;
|
|
17
|
-
|
|
18
|
-
interface AuthTokens {
|
|
19
|
-
access_token: string;
|
|
20
|
-
refresh_token: string;
|
|
21
|
-
scope: string;
|
|
22
|
-
token_type: string;
|
|
23
|
-
expiry_date: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const getNewTokens = async (oAuth2Client: OAuth2Client): Promise<OAuth2Client | null> => {
|
|
27
|
-
try {
|
|
28
|
-
const authUrl = oAuth2Client.generateAuthUrl({
|
|
29
|
-
access_type: 'offline',
|
|
30
|
-
scope: SCOPES,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
console.log(chalk.cyan('🔐 Authorize this app by visiting this url:'), chalk.blue(authUrl));
|
|
34
|
-
|
|
35
|
-
const code = await getCodeServer();
|
|
36
|
-
|
|
37
|
-
const { tokens } = await oAuth2Client.getToken(code);
|
|
38
|
-
oAuth2Client.setCredentials(tokens);
|
|
39
|
-
|
|
40
|
-
// Store tokens
|
|
41
|
-
config.set('googleTokens', tokens);
|
|
42
|
-
|
|
43
|
-
return oAuth2Client;
|
|
44
|
-
} catch (error) {
|
|
45
|
-
console.error(chalk.red('❌ Error getting tokens:'), error);
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const authenticateGoogle = async (): Promise<OAuth2Client | null> => {
|
|
51
|
-
try {
|
|
52
|
-
// Check if credentials file exists
|
|
53
|
-
if (!existsSync(CREDENTIALS_PATH)) {
|
|
54
|
-
console.log(chalk.yellow('⚠️ Google Calendar credentials not found.'));
|
|
55
|
-
console.log(chalk.cyan('📝 Please follow these steps:'));
|
|
56
|
-
console.log(chalk.cyan('1. Go to https://console.cloud.google.com'));
|
|
57
|
-
console.log(chalk.cyan('2. Create a new project or select existing one'));
|
|
58
|
-
console.log(chalk.cyan('3. Enable Google Calendar API'));
|
|
59
|
-
console.log(chalk.cyan('4. Create OAuth 2.0 credentials'));
|
|
60
|
-
console.log(chalk.cyan('5. Download credentials.json and place it in the project root'));
|
|
61
|
-
console.log(chalk.cyan('6. Run: meetfy auth'));
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const credentials = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf-8'));
|
|
66
|
-
const {
|
|
67
|
-
client_secret: clientSecret,
|
|
68
|
-
client_id: clientId,
|
|
69
|
-
} = credentials.installed || credentials.web;
|
|
70
|
-
|
|
71
|
-
const oAuth2Client = new google.auth.OAuth2(
|
|
72
|
-
clientId,
|
|
73
|
-
clientSecret,
|
|
74
|
-
REDIRECT_URI,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Check if we have stored tokens
|
|
78
|
-
const storedTokens = config.get('googleTokens') as AuthTokens | undefined;
|
|
79
|
-
|
|
80
|
-
if (storedTokens) {
|
|
81
|
-
oAuth2Client.setCredentials(storedTokens);
|
|
82
|
-
|
|
83
|
-
// Check if token is expired
|
|
84
|
-
if (storedTokens.expiry_date && Date.now() > storedTokens.expiry_date) {
|
|
85
|
-
console.log(chalk.yellow('🔄 Token expired, refreshing...'));
|
|
86
|
-
const { credentials: newTokens } = await oAuth2Client.refreshAccessToken();
|
|
87
|
-
oAuth2Client.setCredentials(newTokens);
|
|
88
|
-
config.set('googleTokens', newTokens);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return oAuth2Client;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Get new tokens
|
|
95
|
-
return await getNewTokens(oAuth2Client);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
console.error(chalk.red('❌ Authentication error:'), error);
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
export const logoutGoogle = async () => {
|
|
103
|
-
config.clear();
|
|
104
|
-
};
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { google } from 'googleapis';
|
|
2
|
-
import { OAuth2Client } from 'google-auth-library';
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import ora from 'ora';
|
|
6
|
-
|
|
7
|
-
interface MeetingOptions {
|
|
8
|
-
title?: string;
|
|
9
|
-
description?: string;
|
|
10
|
-
participants?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface MeetingResult {
|
|
14
|
-
id: string;
|
|
15
|
-
hangoutLink: string;
|
|
16
|
-
startTime: string;
|
|
17
|
-
endTime: string;
|
|
18
|
-
title: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const getMeetingDetails = async (options: MeetingOptions) => {
|
|
22
|
-
const questions = [];
|
|
23
|
-
|
|
24
|
-
if (!options.title) {
|
|
25
|
-
questions.push({
|
|
26
|
-
type: 'input',
|
|
27
|
-
name: 'title',
|
|
28
|
-
message: 'Meeting title:',
|
|
29
|
-
default: 'Instant Meeting',
|
|
30
|
-
validate: (input: string) => {
|
|
31
|
-
if (!input.trim()) {
|
|
32
|
-
return 'Please enter a meeting title';
|
|
33
|
-
}
|
|
34
|
-
return true;
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (!options.description) {
|
|
40
|
-
questions.push({
|
|
41
|
-
type: 'input',
|
|
42
|
-
name: 'description',
|
|
43
|
-
message: 'Meeting description (optional):',
|
|
44
|
-
default: 'Instant meeting created via Meetfy CLI',
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!options.participants) {
|
|
49
|
-
questions.push({
|
|
50
|
-
type: 'input',
|
|
51
|
-
name: 'participants',
|
|
52
|
-
message: 'Participant emails (comma-separated, optional):',
|
|
53
|
-
default: '',
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
title: options.title || answers.title,
|
|
61
|
-
description: options.description || answers.description,
|
|
62
|
-
participants: (options.participants || answers.participants || '')
|
|
63
|
-
.split(',')
|
|
64
|
-
.filter((email: string) => email.trim() !== ''),
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const createInstantMeeting = async (
|
|
69
|
-
auth: OAuth2Client,
|
|
70
|
-
options: MeetingOptions,
|
|
71
|
-
): Promise<MeetingResult | null> => {
|
|
72
|
-
try {
|
|
73
|
-
const calendar = google.calendar({ version: 'v3', auth });
|
|
74
|
-
|
|
75
|
-
// Get meeting details from user if not provided
|
|
76
|
-
const meetingDetails = await getMeetingDetails(options);
|
|
77
|
-
|
|
78
|
-
// Calculate meeting times (30 minutes from now)
|
|
79
|
-
const now = new Date();
|
|
80
|
-
const startTime = new Date(now.getTime() + 5 * 60 * 1000); // Start in 5 minutes
|
|
81
|
-
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000); // 30 minutes duration
|
|
82
|
-
|
|
83
|
-
const spinner = ora('Creating meeting and reserving calendar...').start();
|
|
84
|
-
|
|
85
|
-
// Create calendar event
|
|
86
|
-
const event = {
|
|
87
|
-
summary: meetingDetails.title,
|
|
88
|
-
description: meetingDetails.description,
|
|
89
|
-
start: {
|
|
90
|
-
dateTime: startTime.toISOString(),
|
|
91
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
92
|
-
},
|
|
93
|
-
end: {
|
|
94
|
-
dateTime: endTime.toISOString(),
|
|
95
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
96
|
-
},
|
|
97
|
-
attendees: meetingDetails.participants.map((email: string) => ({ email: email.trim() })),
|
|
98
|
-
conferenceData: {
|
|
99
|
-
createRequest: {
|
|
100
|
-
requestId: `meetfy-${Date.now()}`,
|
|
101
|
-
conferenceSolutionKey: {
|
|
102
|
-
type: 'hangoutsMeet',
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
reminders: {
|
|
107
|
-
useDefault: false,
|
|
108
|
-
overrides: [
|
|
109
|
-
{ method: 'email', minutes: 10 },
|
|
110
|
-
{ method: 'popup', minutes: 5 },
|
|
111
|
-
],
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const response = await calendar.events.insert({
|
|
116
|
-
calendarId: 'primary',
|
|
117
|
-
requestBody: event,
|
|
118
|
-
conferenceDataVersion: 1,
|
|
119
|
-
sendUpdates: 'all',
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
spinner.succeed('Meeting created successfully!');
|
|
123
|
-
|
|
124
|
-
if (!response.data.id || !response.data.hangoutLink) {
|
|
125
|
-
throw new Error('Failed to create meeting with Google Meet link');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
id: response.data.id,
|
|
130
|
-
hangoutLink: response.data.hangoutLink,
|
|
131
|
-
startTime: startTime.toLocaleString(),
|
|
132
|
-
endTime: endTime.toLocaleString(),
|
|
133
|
-
title: meetingDetails.title,
|
|
134
|
-
};
|
|
135
|
-
} catch (error) {
|
|
136
|
-
console.error(chalk.red('❌ Error creating meeting:'), error);
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import http from 'node:http';
|
|
2
|
-
import express, { Request, Response } from 'express';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { PORT_NUMBER } from '../constants.js';
|
|
5
|
-
|
|
6
|
-
const server = express();
|
|
7
|
-
|
|
8
|
-
export const getCodeServer = async (): Promise<string> => new Promise<string>((resolve, reject) => {
|
|
9
|
-
let instance: http.Server;
|
|
10
|
-
|
|
11
|
-
server.get('/', (req: Request, res: Response) => {
|
|
12
|
-
console.log(req.query);
|
|
13
|
-
|
|
14
|
-
const { code } = req.query;
|
|
15
|
-
if (code) {
|
|
16
|
-
resolve(code as string);
|
|
17
|
-
res.send(`
|
|
18
|
-
<html>
|
|
19
|
-
<style>
|
|
20
|
-
body {
|
|
21
|
-
display: flex;
|
|
22
|
-
flex-direction: column;
|
|
23
|
-
align-items: center;
|
|
24
|
-
justify-content: center;
|
|
25
|
-
height: 100vh;
|
|
26
|
-
background-color: #f0f0f0;
|
|
27
|
-
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
28
|
-
}
|
|
29
|
-
h1 {
|
|
30
|
-
color: #000;
|
|
31
|
-
}
|
|
32
|
-
p {
|
|
33
|
-
color: #000;
|
|
34
|
-
}
|
|
35
|
-
</style>
|
|
36
|
-
<body>
|
|
37
|
-
<h1>Meetfy</h1>
|
|
38
|
-
<p>You can close this window now.</p>
|
|
39
|
-
</body>
|
|
40
|
-
</html>
|
|
41
|
-
`);
|
|
42
|
-
instance.close();
|
|
43
|
-
} else {
|
|
44
|
-
reject(new Error('No code found'));
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
instance = server.listen(PORT_NUMBER, (error) => {
|
|
49
|
-
console.log(chalk.cyan(`🔐 Waiting for code on port ${PORT_NUMBER}...`));
|
|
50
|
-
if (error) {
|
|
51
|
-
console.error(error);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
});
|
package/src/utils/cliUtils.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import boxen from 'boxen';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
|
|
4
|
-
export const showWelcomeMessage = (): void => {
|
|
5
|
-
console.log(boxen('Meetfy', {
|
|
6
|
-
padding: 1,
|
|
7
|
-
margin: 0,
|
|
8
|
-
width: 40,
|
|
9
|
-
title: 'Instant Meeting Creator',
|
|
10
|
-
titleAlignment: 'center',
|
|
11
|
-
textAlignment: 'center',
|
|
12
|
-
}));
|
|
13
|
-
console.log(chalk.gray('Create instant meetings and reserve time in Google Calendar\n'));
|
|
14
|
-
};
|
package/test.html
DELETED
|
File without changes
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "Node16",
|
|
5
|
-
"moduleResolution": "node16",
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"allowSyntheticDefaultImports": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"declaration": true,
|
|
14
|
-
"declarationMap": true,
|
|
15
|
-
"sourceMap": true,
|
|
16
|
-
"resolveJsonModule": true
|
|
17
|
-
},
|
|
18
|
-
"include": ["src/**/*"],
|
|
19
|
-
"exclude": ["node_modules", "dist"]
|
|
20
|
-
}
|