meetfy 1.0.2 → 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.
Files changed (2) hide show
  1. package/dist/index.js +403 -680
  2. package/package.json +4 -12
package/dist/index.js CHANGED
@@ -1,737 +1,460 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
 
4
- // src/index.tsx
5
- import { render } from "ink";
4
+ // src/adapters/cli/index.ts
6
5
  import { Command } from "commander";
7
6
 
8
- // src/components/App.tsx
9
- import { Box as Box2 } from "ink";
10
-
11
- // src/components/Welcome.tsx
12
- import { Box, Text } from "ink";
13
- import { jsx, jsxs } from "react/jsx-runtime";
14
- function Welcome() {
15
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
16
- /* @__PURE__ */ jsx(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "center", children: [
17
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Meetfy" }),
18
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Instant Meeting Creator" })
19
- ] }) }),
20
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Create instant meetings and reserve time in Google Calendar" }),
21
- /* @__PURE__ */ jsx(Text, { children: "\n" })
22
- ] });
23
- }
24
-
25
- // src/components/App.tsx
26
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
27
- function App({ children }) {
28
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
29
- /* @__PURE__ */ jsx2(Welcome, {}),
30
- children
31
- ] });
32
- }
33
-
34
- // src/components/CreateMeetingView.tsx
35
- import { useState, useCallback } from "react";
36
- import { Box as Box3, Text as Text2, useApp } from "ink";
37
- import TextInput from "ink-text-input";
38
- import Spinner from "ink-spinner";
39
-
40
- // src/services/authService.ts
7
+ // src/adapters/google/auth-adapter.ts
41
8
  import { google } from "googleapis";
42
- import { readFileSync, existsSync } from "fs";
43
- import { join } from "path";
9
+ import { readFileSync, existsSync } from "node:fs";
10
+ import { join } from "node:path";
44
11
 
45
- // src/services/configService.ts
12
+ // src/infrastructure/config.ts
46
13
  import Conf from "conf";
47
- var config = new Conf({
48
- projectName: "meetfy",
49
- configName: "meetfy-config"
50
- });
51
- var configService_default = config;
14
+ function createConfig() {
15
+ return new Conf({
16
+ projectName: "meetfy",
17
+ configName: "meetfy-config"
18
+ });
19
+ }
52
20
 
53
- // src/services/webServer.ts
21
+ // src/infrastructure/web-server.ts
54
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
+ }
55
56
 
56
- // src/constants.ts
57
- var PORT_NUMBER = 3434;
58
-
59
- // src/services/webServer.ts
60
- var server = express();
61
- var getCodeServer = async (onListening) => new Promise((resolve, reject) => {
62
- let instance;
63
- server.get("/", (req, res) => {
64
- const { code } = req.query;
65
- if (code) {
66
- resolve(code);
67
- res.send(`
68
- <html>
69
- <style>
70
- body {
71
- display: flex;
72
- flex-direction: column;
73
- align-items: center;
74
- justify-content: center;
75
- height: 100vh;
76
- background-color: #f0f0f0;
77
- font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
78
- }
79
- h1 { color: #000; }
80
- p { color: #000; }
81
- </style>
82
- <body>
83
- <h1>Meetfy</h1>
84
- <p>You can close this window now.</p>
85
- </body>
86
- </html>
87
- `);
88
- instance.close();
89
- } else {
90
- reject(new Error("No code found"));
91
- }
92
- });
93
- instance = server.listen(PORT_NUMBER, (error) => {
94
- if (error) {
95
- reject(error);
96
- return;
97
- }
98
- onListening?.();
99
- });
100
- });
101
-
102
- // src/services/authService.ts
57
+ // src/adapters/google/auth-adapter.ts
103
58
  var SCOPES = [
104
59
  "https://www.googleapis.com/auth/calendar",
105
60
  "https://www.googleapis.com/auth/calendar.events"
106
61
  ];
107
62
  var CREDENTIALS_PATH = join(process.cwd(), "credentials.json");
108
- var REDIRECT_URI = `http://localhost:${PORT_NUMBER}`;
109
- var authenticateGoogle = async () => {
110
- try {
111
- if (!existsSync(CREDENTIALS_PATH)) {
112
- return { type: "no_credentials" };
113
- }
114
- const credentials = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf-8"));
115
- const {
116
- client_secret: clientSecret,
117
- client_id: clientId
118
- } = credentials.installed || credentials.web;
119
- const oAuth2Client = new google.auth.OAuth2(
120
- clientId,
121
- clientSecret,
122
- REDIRECT_URI
123
- );
124
- const storedTokens = configService_default.get("googleTokens");
125
- if (storedTokens) {
126
- oAuth2Client.setCredentials(storedTokens);
127
- if (storedTokens.expiry_date && Date.now() > storedTokens.expiry_date) {
128
- const { credentials: newTokens } = await oAuth2Client.refreshAccessToken();
129
- oAuth2Client.setCredentials(newTokens);
130
- configService_default.set("googleTokens", newTokens);
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" };
131
108
  }
132
- return { type: "ok", client: oAuth2Client };
109
+ },
110
+ async logout() {
111
+ config.clear();
133
112
  }
134
- const authUrl = oAuth2Client.generateAuthUrl({
135
- access_type: "offline",
136
- scope: SCOPES
137
- });
138
- const fetchToken = async (callbacks) => {
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) {
139
121
  try {
140
- const code = await getCodeServer(callbacks.onWaiting);
141
- const { tokens } = await oAuth2Client.getToken(code);
142
- oAuth2Client.setCredentials(tokens);
143
- configService_default.set("googleTokens", tokens);
144
- return oAuth2Client;
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
+ };
145
161
  } catch {
146
162
  return null;
147
163
  }
148
- };
149
- return { type: "need_code", authUrl, fetchToken };
150
- } catch {
151
- return { type: "error", message: "Authentication failed" };
152
- }
153
- };
154
- var logoutGoogle = async () => {
155
- configService_default.clear();
156
- };
157
-
158
- // src/services/meetingService.ts
159
- import { google as google2 } from "googleapis";
160
- var createInstantMeeting = async (auth, options) => {
161
- try {
162
- const calendar = google2.calendar({ version: "v3", auth });
163
- const now = /* @__PURE__ */ new Date();
164
- const startTime = new Date(now.getTime() + 5 * 60 * 1e3);
165
- const endTime = new Date(startTime.getTime() + 30 * 60 * 1e3);
166
- const event = {
167
- summary: options.title,
168
- description: options.description,
169
- start: {
170
- dateTime: startTime.toISOString(),
171
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
172
- },
173
- end: {
174
- dateTime: endTime.toISOString(),
175
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
176
- },
177
- attendees: options.participants.map((email) => ({ email: email.trim() })),
178
- conferenceData: {
179
- createRequest: {
180
- requestId: `meetfy-${Date.now()}`,
181
- conferenceSolutionKey: {
182
- type: "hangoutsMeet"
183
- }
184
- }
185
- },
186
- reminders: {
187
- useDefault: false,
188
- overrides: [
189
- { method: "email", minutes: 10 },
190
- { method: "popup", minutes: 5 }
191
- ]
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
192
  }
193
- };
194
- const response = await calendar.events.insert({
195
- calendarId: "primary",
196
- requestBody: event,
197
- conferenceDataVersion: 1,
198
- sendUpdates: "all"
199
- });
200
- if (!response.data.id || !response.data.hangoutLink) {
201
- throw new Error("Failed to create meeting with Google Meet link");
202
- }
203
- return {
204
- id: response.data.id,
205
- hangoutLink: response.data.hangoutLink,
206
- startTime: startTime.toLocaleString(),
207
- endTime: endTime.toLocaleString(),
208
- title: options.title
209
- };
210
- } catch {
211
- return null;
212
- }
213
- };
214
- var getNextMeeting = async (auth) => {
215
- try {
216
- const calendar = google2.calendar({ version: "v3", auth });
217
- const now = (/* @__PURE__ */ new Date()).toISOString();
218
- const response = await calendar.events.list({
219
- calendarId: "primary",
220
- timeMin: now,
221
- singleEvents: true,
222
- orderBy: "startTime",
223
- maxResults: 1
224
- });
225
- const event = response.data.items?.[0];
226
- if (!event) {
227
- return null;
228
- }
229
- const start = event.start?.dateTime || event.start?.date;
230
- const end = event.end?.dateTime || event.end?.date;
231
- if (!start || !end) {
232
- return null;
233
193
  }
234
- return {
235
- id: event.id ?? "",
236
- title: event.summary ?? "Untitled",
237
- startTime: new Date(start).toLocaleString(),
238
- endTime: new Date(end).toLocaleString(),
239
- hangoutLink: event.hangoutLink ?? void 0,
240
- location: event.location ?? void 0
241
- };
242
- } catch {
243
- return null;
244
- }
245
- };
194
+ };
195
+ }
246
196
 
247
- // src/components/CreateMeetingView.tsx
248
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
249
- function getInitialStep(props) {
250
- const { initialTitle, initialDescription, initialParticipants } = props;
251
- if (!initialTitle) return "title";
252
- if (initialDescription === void 0) return "description";
253
- if (initialParticipants === void 0) return "participants";
254
- return "creating";
197
+ // src/use-cases/authenticate.ts
198
+ function makeAuthenticate(authGateway) {
199
+ return async function authenticate() {
200
+ return authGateway.authenticate();
201
+ };
255
202
  }
256
- function CreateMeetingView({
257
- initialTitle = "",
258
- initialDescription,
259
- initialParticipants
260
- }) {
261
- const { exit } = useApp();
262
- const [step, setStep] = useState(() => getInitialStep({
263
- initialTitle,
264
- initialDescription,
265
- initialParticipants
266
- }));
267
- const [title, setTitle] = useState(initialTitle || "Instant Meeting");
268
- const [description, setDescription] = useState(initialDescription ?? "Instant meeting created via Meetfy CLI");
269
- const [participants, setParticipants] = useState(initialParticipants ?? "");
270
- const [meeting, setMeeting] = useState(null);
271
- const [errorMessage, setErrorMessage] = useState("");
272
- const handleTitleSubmit = useCallback(() => {
273
- if (!title.trim()) return;
274
- if (initialDescription !== void 0) setStep("participants");
275
- else setStep("description");
276
- }, [title, initialDescription]);
277
- const handleDescriptionSubmit = useCallback(() => {
278
- if (initialParticipants !== void 0) setStep("creating");
279
- else setStep("participants");
280
- }, [initialParticipants]);
281
- const handleParticipantsSubmit = useCallback(() => {
282
- setStep("creating");
283
- }, []);
284
- useEffect(() => {
285
- if (step !== "creating") return;
286
- let cancelled = false;
287
- (async () => {
288
- const authResult = await authenticateGoogle();
289
- if (cancelled) return;
290
- if (authResult.type !== "ok") {
291
- setStep("no_auth");
292
- return;
293
- }
294
- const options = {
295
- title: title.trim(),
296
- description: description.trim(),
297
- participants: participants.split(",").map((e) => e.trim()).filter(Boolean)
298
- };
299
- const result = await createInstantMeeting(authResult.client, options);
300
- if (cancelled) return;
301
- if (result) {
302
- setMeeting(result);
303
- setStep("success");
304
- setTimeout(() => exit(), 3e3);
305
- } else {
306
- setStep("error");
307
- setErrorMessage("Failed to create meeting");
308
- }
309
- })();
310
- return () => {
311
- cancelled = true;
312
- };
313
- }, [step, title, description, participants, exit]);
314
- if (step === "title") {
315
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
316
- /* @__PURE__ */ jsx3(Box3, { marginRight: 1, children: /* @__PURE__ */ jsx3(Text2, { children: "Meeting title:" }) }),
317
- /* @__PURE__ */ jsx3(
318
- TextInput,
319
- {
320
- value: title,
321
- onChange: setTitle,
322
- onSubmit: handleTitleSubmit,
323
- placeholder: "Instant Meeting"
324
- }
325
- )
326
- ] });
327
- }
328
- if (step === "description") {
329
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
330
- /* @__PURE__ */ jsx3(Box3, { marginRight: 1, children: /* @__PURE__ */ jsx3(Text2, { children: "Meeting description (optional):" }) }),
331
- /* @__PURE__ */ jsx3(
332
- TextInput,
333
- {
334
- value: description,
335
- onChange: setDescription,
336
- onSubmit: handleDescriptionSubmit,
337
- placeholder: "Instant meeting created via Meetfy CLI"
338
- }
339
- )
340
- ] });
341
- }
342
- if (step === "participants") {
343
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
344
- /* @__PURE__ */ jsx3(Box3, { marginRight: 1, children: /* @__PURE__ */ jsx3(Text2, { children: "Participant emails (comma-separated, optional):" }) }),
345
- /* @__PURE__ */ jsx3(
346
- TextInput,
347
- {
348
- value: participants,
349
- onChange: setParticipants,
350
- onSubmit: handleParticipantsSubmit,
351
- placeholder: ""
352
- }
353
- )
354
- ] });
355
- }
356
- if (step === "creating") {
357
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text2, { color: "gray", children: [
358
- /* @__PURE__ */ jsx3(Spinner, { type: "dots" }),
359
- " Creating meeting and reserving calendar..."
360
- ] }) });
361
- }
362
- if (step === "success" && meeting) {
363
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
364
- /* @__PURE__ */ jsx3(Text2, { color: "green", children: "\u2705 Meeting created successfully!" }),
365
- /* @__PURE__ */ jsxs3(Text2, { color: "cyan", children: [
366
- "\u{1F4C5} Meeting ID: ",
367
- meeting.id
368
- ] }),
369
- /* @__PURE__ */ jsxs3(Text2, { color: "cyan", children: [
370
- "\u{1F517} Join URL: ",
371
- meeting.hangoutLink
372
- ] }),
373
- /* @__PURE__ */ jsx3(Text2, { color: "cyan", children: "\u23F0 Duration: 30 minutes" }),
374
- /* @__PURE__ */ jsxs3(Text2, { color: "cyan", children: [
375
- "\u{1F4C5} Start Time: ",
376
- meeting.startTime
377
- ] })
378
- ] });
379
- }
380
- if (step === "no_auth") {
381
- return /* @__PURE__ */ jsxs3(Box3, { children: [
382
- /* @__PURE__ */ jsx3(Text2, { color: "red", children: "\u274C Authentication failed. Please run " }),
383
- /* @__PURE__ */ jsx3(Text2, { color: "cyan", children: "meetfy auth" }),
384
- /* @__PURE__ */ jsx3(Text2, { color: "red", children: " first." })
385
- ] });
386
- }
387
- if (step === "error") {
388
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text2, { color: "red", children: [
389
- "\u274C ",
390
- errorMessage
391
- ] }) });
392
- }
393
- return null;
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";
394
285
  }
395
286
 
396
- // src/components/AuthView.tsx
397
- import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
398
- import {
399
- Box as Box4,
400
- Text as Text3,
401
- useApp as useApp2,
402
- useInput
403
- } from "ink";
404
- import Spinner2 from "ink-spinner";
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
+ }
405
303
 
406
- // src/utils/openAuthUrl.ts
304
+ // src/adapters/cli/browser.ts
407
305
  async function copyAndOpenUrl(url) {
408
- const results = [];
306
+ const parts = [];
409
307
  try {
410
308
  const { default: clipboardy } = await import("clipboardy");
411
309
  await clipboardy.write(url);
412
- results.push("URL copied to clipboard");
310
+ parts.push("URL copied to clipboard");
413
311
  } catch {
414
- results.push("Could not copy to clipboard");
312
+ parts.push("Could not copy");
415
313
  }
416
314
  try {
417
315
  const open = (await import("open")).default;
418
316
  await open(url);
419
- results.push("Opening in browser...");
317
+ parts.push("Opening in browser...");
420
318
  } catch {
421
- results.push("Could not open browser");
422
- }
423
- return results.join(". ");
424
- }
425
-
426
- // src/components/AuthView.tsx
427
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
428
- function AuthView() {
429
- const { exit } = useApp2();
430
- const [status, setStatus] = useState2("loading");
431
- const [authUrl, setAuthUrl] = useState2("");
432
- const [errorMessage, setErrorMessage] = useState2("");
433
- const [actionMessage, setActionMessage] = useState2(null);
434
- const handleCopyAndOpen = useCallback2(async () => {
435
- if (!authUrl) return;
436
- setActionMessage(null);
437
- const msg = await copyAndOpenUrl(authUrl);
438
- setActionMessage(msg);
439
- }, [authUrl]);
440
- useInput(
441
- (input, key) => {
442
- if (key.return && (status === "need_code" || status === "waiting")) {
443
- handleCopyAndOpen();
444
- }
445
- },
446
- { isActive: status === "need_code" || status === "waiting" }
447
- );
448
- useEffect2(() => {
449
- let cancelled = false;
450
- (async () => {
451
- const result = await authenticateGoogle();
452
- if (cancelled) return;
453
- if (result.type === "ok") {
454
- setStatus("success");
455
- setTimeout(() => exit(), 2500);
456
- return;
457
- }
458
- if (result.type === "no_credentials") {
459
- setStatus("no_credentials");
460
- return;
461
- }
462
- if (result.type === "need_code") {
463
- setAuthUrl(result.authUrl);
464
- setStatus("need_code");
465
- const client = await result.fetchToken({
466
- onWaiting: () => {
467
- if (!cancelled) setStatus("waiting");
468
- }
469
- });
470
- if (cancelled) return;
471
- if (client) {
472
- setStatus("success");
473
- setTimeout(() => exit(), 2500);
474
- } else {
475
- setStatus("error");
476
- setErrorMessage("Failed to get token");
477
- }
478
- return;
479
- }
480
- setStatus("error");
481
- setErrorMessage(result.message);
482
- })();
483
- return () => {
484
- cancelled = true;
485
- };
486
- }, [exit]);
487
- if (status === "loading") {
488
- return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text3, { color: "gray", children: [
489
- /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
490
- " Checking authentication..."
491
- ] }) });
492
- }
493
- if (status === "no_credentials") {
494
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
495
- /* @__PURE__ */ jsx4(Text3, { color: "yellow", children: "\u26A0\uFE0F Google Calendar credentials not found." }),
496
- /* @__PURE__ */ jsx4(Text3, { color: "cyan", children: "\u{1F4DD} Please follow these steps:" }),
497
- /* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 1. Go to https://console.cloud.google.com" }),
498
- /* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 2. Create a new project or select existing one" }),
499
- /* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 3. Enable Google Calendar API" }),
500
- /* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 4. Create OAuth 2.0 credentials (Desktop app)" }),
501
- /* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 5. Add Authorized redirect URI: " }),
502
- /* @__PURE__ */ jsxs4(Text3, { color: "yellow", children: [
503
- " ",
504
- REDIRECT_URI
505
- ] }),
506
- /* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 6. Download credentials.json and place it in the project root" }),
507
- /* @__PURE__ */ jsx4(Text3, { color: "cyan", children: " 7. Run: meetfy auth" })
508
- ] });
509
- }
510
- if (status === "need_code" || status === "waiting") {
511
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
512
- /* @__PURE__ */ jsx4(Text3, { color: "cyan", children: "\u{1F510} Authorize this app by visiting this URL:" }),
513
- /* @__PURE__ */ jsx4(Text3, { color: "blue", children: authUrl }),
514
- /* @__PURE__ */ jsx4(Text3, { children: "\n" }),
515
- /* @__PURE__ */ jsx4(Text3, { color: "green", children: "Press Enter to copy URL and open in browser" }),
516
- actionMessage && /* @__PURE__ */ jsxs4(Text3, { color: "gray", children: [
517
- " ",
518
- actionMessage
519
- ] }),
520
- /* @__PURE__ */ jsx4(Text3, { children: "\n" }),
521
- /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: 'If you see "redirect_uri_mismatch", add this exact URI in Google Cloud Console:' }),
522
- /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: " Credentials \u2192 your OAuth 2.0 Client ID \u2192 Authorized redirect URIs" }),
523
- /* @__PURE__ */ jsxs4(Text3, { color: "yellow", children: [
524
- " ",
525
- REDIRECT_URI
526
- ] }),
527
- /* @__PURE__ */ jsx4(Text3, { children: "\n" }),
528
- status === "waiting" && /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text3, { color: "gray", children: [
529
- /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
530
- " Waiting for code on port 3434..."
531
- ] }) })
532
- ] });
533
- }
534
- if (status === "success") {
535
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
536
- /* @__PURE__ */ jsx4(Text3, { color: "green", children: "\u2705 Authentication successful!" }),
537
- /* @__PURE__ */ jsx4(Text3, { children: "\n" }),
538
- /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "Available commands:" }),
539
- /* @__PURE__ */ jsx4(Text3, { children: " meetfy create Create an instant meeting (30 min)" }),
540
- /* @__PURE__ */ jsx4(Text3, { children: " meetfy next Show your next meeting" }),
541
- /* @__PURE__ */ jsx4(Text3, { children: " meetfy logout Log out from Google" }),
542
- /* @__PURE__ */ jsx4(Text3, { children: "\n" }),
543
- /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "Exiting..." })
544
- ] });
319
+ parts.push("Could not open browser");
545
320
  }
546
- return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text3, { color: "red", children: [
547
- "\u274C ",
548
- errorMessage
549
- ] }) });
321
+ return parts.join(". ");
550
322
  }
551
323
 
552
- // src/components/LogoutView.tsx
553
- import { useEffect as useEffect3 } from "react";
554
- import { Box as Box5, Text as Text4, useApp as useApp3 } from "ink";
555
- import { jsx as jsx5 } from "react/jsx-runtime";
556
- function LogoutView() {
557
- const { exit } = useApp3();
558
- useEffect3(() => {
559
- logoutGoogle().then(() => {
560
- setTimeout(() => exit(), 800);
561
- });
562
- }, [exit]);
563
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text4, { color: "green", children: "\u2705 Logged out successfully!" }) });
564
- }
565
-
566
- // src/components/NextMeetingView.tsx
567
- import { useState as useState3, useEffect as useEffect4 } from "react";
568
- import { Box as Box6, Text as Text5, useApp as useApp4 } from "ink";
569
- import Spinner3 from "ink-spinner";
570
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
571
- function NextMeetingView() {
572
- const { exit } = useApp4();
573
- const [status, setStatus] = useState3("loading");
574
- const [meeting, setMeeting] = useState3(null);
575
- useEffect4(() => {
576
- let cancelled = false;
577
- (async () => {
578
- const authResult = await authenticateGoogle();
579
- if (cancelled) return;
580
- if (authResult.type !== "ok") {
581
- setStatus("no_auth");
582
- return;
583
- }
584
- const next = await getNextMeeting(authResult.client);
585
- if (cancelled) return;
586
- if (!next) {
587
- setStatus("no_meeting");
588
- return;
589
- }
590
- setMeeting(next);
591
- setStatus("success");
592
- })();
593
- return () => {
594
- cancelled = true;
595
- };
596
- }, []);
597
- if (status === "loading") {
598
- return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
599
- /* @__PURE__ */ jsx6(Spinner3, { type: "dots" }),
600
- " Loading next meeting..."
601
- ] }) });
602
- }
603
- if (status === "no_auth") {
604
- return /* @__PURE__ */ jsxs5(Box6, { children: [
605
- /* @__PURE__ */ jsx6(Text5, { color: "red", children: "\u274C Authentication failed. Please run " }),
606
- /* @__PURE__ */ jsx6(Text5, { color: "cyan", children: "meetfy auth" }),
607
- /* @__PURE__ */ jsx6(Text5, { color: "red", children: " first." })
608
- ] });
609
- }
610
- if (status === "no_meeting") {
611
- return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text5, { color: "yellow", children: "\u{1F4ED} No upcoming meetings found." }) });
612
- }
613
- if (meeting) {
614
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
615
- /* @__PURE__ */ jsx6(Text5, { bold: true, color: "green", children: "\u{1F4C5} Next meeting" }),
616
- /* @__PURE__ */ jsx6(Text5, { children: "\n" }),
617
- /* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
618
- " ",
619
- meeting.title
620
- ] }),
621
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
622
- " \u{1F550} ",
623
- meeting.startTime,
624
- " \u2013 ",
625
- meeting.endTime
626
- ] }),
627
- meeting.hangoutLink && /* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
628
- " \u{1F517} ",
629
- meeting.hangoutLink
630
- ] }),
631
- meeting.location && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
632
- " \u{1F4CD} ",
633
- meeting.location
634
- ] })
635
- ] });
636
- }
637
- return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text5, { color: "red", children: "\u274C Error fetching next meeting." }) });
638
- }
639
-
640
- // src/index.tsx
641
- import { jsx as jsx7 } from "react/jsx-runtime";
642
- var program = new Command();
643
- program.name("meetfy").description("CLI tool for creating instant meetings and reserving time in Google Calendar").version("1.0.0").option("--json", "Output result as JSON");
324
+ // src/adapters/cli/index.ts
325
+ var REDIRECT_PORT = 3434;
644
326
  function outputJson(obj) {
645
327
  console.log(JSON.stringify(obj, null, 0));
646
328
  }
647
- function authErrorForJson(result) {
648
- if (result.type === "no_credentials") return "no_credentials";
649
- if (result.type === "error") return result.message ?? "unknown";
650
- return "auth_required";
651
- }
652
- 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 (options) => {
653
- if (program.opts().json) {
654
- const authResult = await authenticateGoogle();
655
- if (authResult.type !== "ok") {
656
- outputJson({ success: false, error: authErrorForJson(authResult) });
657
- process.exit(1);
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;
658
357
  }
659
- const title = options.title?.trim() || "Instant Meeting";
660
- const description = options.description?.trim() || "Instant meeting created via Meetfy CLI";
661
- const participants = (options.participants ?? "").split(",").map((e) => e.trim()).filter(Boolean);
662
- const meeting = await createInstantMeeting(authResult.client, {
663
- title,
664
- description,
665
- participants
666
- });
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 });
667
367
  if (meeting) {
668
- outputJson({ success: true, 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}`);
669
372
  } else {
670
- outputJson({ success: false, error: "Failed to create meeting" });
373
+ console.error("\n\u274C Failed to create meeting. Run meetfy auth if needed.");
671
374
  process.exit(1);
672
375
  }
673
- return;
674
- }
675
- const { waitUntilExit } = render(
676
- /* @__PURE__ */ jsx7(App, { children: /* @__PURE__ */ jsx7(
677
- CreateMeetingView,
678
- {
679
- initialTitle: options.title,
680
- initialDescription: options.description,
681
- initialParticipants: options.participants
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);
682
392
  }
683
- ) })
684
- );
685
- await waitUntilExit();
686
- });
687
- program.command("auth").description("Authenticate with Google Calendar").action(async () => {
688
- if (program.opts().json) {
689
- const authResult = await authenticateGoogle();
690
- if (authResult.type === "ok") {
691
- outputJson({ success: true });
692
- } else if (authResult.type === "need_code") {
693
- outputJson({ success: false, authRequired: true, authUrl: authResult.authUrl });
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());
694
402
  process.exit(1);
695
- } else if (authResult.type === "no_credentials") {
696
- outputJson({ success: false, error: "no_credentials" });
403
+ }
404
+ if (auth.type === "error") {
405
+ console.error("\u274C", auth.message);
697
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());
698
422
  } else {
699
- outputJson({ success: false, error: authResult.message });
423
+ console.error("\n\u274C Failed to get token.");
700
424
  process.exit(1);
701
425
  }
702
- return;
703
- }
704
- const { waitUntilExit } = render(
705
- /* @__PURE__ */ jsx7(App, { children: /* @__PURE__ */ jsx7(AuthView, {}) })
706
- );
707
- await waitUntilExit();
708
- process.exit(0);
709
- });
710
- program.command("logout").description("Logout from Google Calendar").action(async () => {
711
- if (program.opts().json) {
712
- await logoutGoogle();
713
- outputJson({ success: true });
714
- return;
715
- }
716
- const { waitUntilExit } = render(
717
- /* @__PURE__ */ jsx7(App, { children: /* @__PURE__ */ jsx7(LogoutView, {}) })
718
- );
719
- await waitUntilExit();
720
- });
721
- program.command("next").description("Show your next scheduled meeting").action(async () => {
722
- if (program.opts().json) {
723
- const authResult = await authenticateGoogle();
724
- if (authResult.type !== "ok") {
725
- outputJson({ success: false, error: authErrorForJson(authResult) });
726
- process.exit(1);
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!");
727
434
  }
728
- const meeting = await getNextMeeting(authResult.client);
729
- outputJson({ success: true, meeting: meeting ?? null });
730
- return;
731
- }
732
- const { waitUntilExit } = render(
733
- /* @__PURE__ */ jsx7(App, { children: /* @__PURE__ */ jsx7(NextMeetingView, {}) })
734
- );
735
- await waitUntilExit();
736
- });
737
- program.parse();
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,6 +1,6 @@
1
1
  {
2
2
  "name": "meetfy",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "packageManager": "pnpm@10.31.0",
5
5
  "description": "CLI tool for creating instant meetings and reserving time in Google Calendar",
6
6
  "main": "dist/index.js",
@@ -13,10 +13,10 @@
13
13
  "README.md"
14
14
  ],
15
15
  "scripts": {
16
- "start": "tsx src/index.tsx",
16
+ "start": "tsx src/index.ts",
17
17
  "build": "node build.mjs",
18
18
  "prepublishOnly": "pnpm run build",
19
- "lint": "eslint src/**/*.{ts,tsx}"
19
+ "lint": "eslint src/**/*.ts"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=18"
@@ -36,27 +36,19 @@
36
36
  "express": "^5.1.0",
37
37
  "google-auth-library": "^9.0.0",
38
38
  "googleapis": "^128.0.0",
39
- "ink": "^5.0.0",
40
- "ink-spinner": "^5.0.0",
41
- "ink-text-input": "^6.0.0",
42
39
  "open": "^10.2.0",
43
- "react": "^18.3.1",
44
40
  "tsx": "^4.20.3",
45
41
  "typescript": "5.5"
46
42
  },
47
43
  "devDependencies": {
48
44
  "@types/express": "^5.0.3",
49
45
  "@types/node": "^20.10.0",
50
- "@types/react": "^18.3.0",
51
46
  "@typescript-eslint/eslint-plugin": "^7.18.0",
52
47
  "@typescript-eslint/parser": "^7.18.0",
53
48
  "esbuild": "^0.25.8",
54
49
  "eslint": "^8.57.1",
55
50
  "eslint-config-airbnb": "^19.0.4",
56
51
  "eslint-config-airbnb-typescript": "^18.0.0",
57
- "eslint-plugin-import": "^2.32.0",
58
- "eslint-plugin-jsx-a11y": "^6.10.2",
59
- "eslint-plugin-react": "^7.37.5",
60
- "eslint-plugin-react-hooks": "^4.6.2"
52
+ "eslint-plugin-import": "^2.32.0"
61
53
  }
62
54
  }