meetfy 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -41
  2. package/dist/index.js +304 -657
  3. package/package.json +17 -27
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,236 +1,176 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
 
4
- // src/index.tsx
5
- import { render } from "ink";
4
+ // src/cli.ts
6
5
  import { Command } from "commander";
6
+ import chalk2 from "chalk";
7
7
 
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
8
+ // src/auth.ts
9
+ import http from "node:http";
41
10
  import { google } from "googleapis";
42
- import { readFileSync, existsSync } from "fs";
43
- import { join } from "path";
44
11
 
45
- // src/services/configService.ts
12
+ // src/config.ts
46
13
  import Conf from "conf";
47
- var config = new Conf({
14
+ var conf = new Conf({
48
15
  projectName: "meetfy",
49
16
  configName: "meetfy-config"
50
17
  });
51
- var configService_default = config;
52
-
53
- // src/services/webServer.ts
54
- import express from "express";
55
-
56
- // src/constants.ts
57
- var PORT_NUMBER = 3434;
18
+ function getConfig(key) {
19
+ return conf.get(key);
20
+ }
21
+ function setConfig(key, value) {
22
+ conf.set(key, value);
23
+ }
24
+ function clearConfig() {
25
+ conf.clear();
26
+ }
58
27
 
59
- // src/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;
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);
54
+ }
55
+ } catch {
97
56
  }
98
- onListening?.();
99
- });
100
- });
101
-
102
- // src/services/authService.ts
103
- var SCOPES = [
104
- "https://www.googleapis.com/auth/calendar",
105
- "https://www.googleapis.com/auth/calendar.events"
106
- ];
107
- var CREDENTIALS_PATH = join(process.cwd(), "credentials.json");
108
- var REDIRECT_URI = `http://localhost:${PORT_NUMBER}`;
109
- var authenticateGoogle = async () => {
57
+ }
58
+ return makeClient(clientId, tokens);
59
+ }
60
+ async function authenticate() {
61
+ const client = await getClient();
62
+ if (client) return { type: "ok", client };
63
+ const forward = `http://localhost:${REDIRECT_PORT}`;
110
64
  try {
111
- if (!existsSync(CREDENTIALS_PATH)) {
112
- return { type: "no_credentials" };
65
+ const res = await fetch(`${WORKER_URL}/auth/url?forward=${encodeURIComponent(forward)}`);
66
+ if (!res.ok) {
67
+ const err = await res.json().catch(() => ({}));
68
+ return { type: "error", message: err.error ?? "Failed to get auth URL" };
113
69
  }
114
- const 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);
70
+ const { authUrl } = await res.json();
71
+ return {
72
+ type: "need_code",
73
+ authUrl,
74
+ waitForTokens: () => waitForTokensThenSave(REDIRECT_PORT)
75
+ };
76
+ } catch (e) {
77
+ return { type: "error", message: e.message ?? "Auth service unreachable" };
78
+ }
79
+ }
80
+ function waitForTokensThenSave(port) {
81
+ return new Promise((resolve, reject) => {
82
+ const server = http.createServer({ maxHeaderSize: 64 * 1024 }, (req, res) => {
83
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
84
+ const raw = url.searchParams.get("tokens");
85
+ if (!raw) {
86
+ res.writeHead(400, { "Content-Type": "text/plain" }).end("Missing tokens");
87
+ return;
131
88
  }
132
- return { type: "ok", client: oAuth2Client };
133
- }
134
- const authUrl = oAuth2Client.generateAuthUrl({
135
- access_type: "offline",
136
- scope: SCOPES
137
- });
138
- const fetchToken = async (callbacks) => {
139
89
  try {
140
- const 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;
90
+ const json2 = Buffer.from(raw, "base64").toString("utf-8");
91
+ const { client_id: clientId, ...tokens } = JSON.parse(json2);
92
+ if (!clientId || !tokens.access_token) throw new Error("Incomplete payload");
93
+ res.writeHead(200, { "Content-Type": "text/html", Connection: "close" }).end(HTML_OK);
94
+ setConfig("googleTokens", tokens);
95
+ setConfig("googleClientId", clientId);
96
+ resolve(makeClient(clientId, tokens));
97
+ server.close();
145
98
  } catch {
146
- return null;
99
+ res.writeHead(400, { "Content-Type": "text/plain", Connection: "close" }).end("Invalid tokens");
100
+ reject(new Error("Invalid tokens"));
101
+ server.close();
147
102
  }
148
- };
149
- 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
- };
103
+ });
104
+ server.on("error", reject);
105
+ server.listen(port);
106
+ });
107
+ }
108
+ function logout() {
109
+ clearConfig();
110
+ }
157
111
 
158
- // src/services/meetingService.ts
112
+ // src/calendar.ts
159
113
  import { google as google2 } from "googleapis";
160
- var createInstantMeeting = async (auth, options) => {
114
+ async function createMeeting(client, input) {
161
115
  try {
162
- const calendar = google2.calendar({ version: "v3", auth });
116
+ const calendar = google2.calendar({ version: "v3", auth: client });
163
117
  const now = /* @__PURE__ */ new Date();
164
118
  const startTime = new Date(now.getTime() + 5 * 60 * 1e3);
165
119
  const endTime = new Date(startTime.getTime() + 30 * 60 * 1e3);
166
- const 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"
120
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
121
+ const res = await calendar.events.insert({
122
+ calendarId: "primary",
123
+ requestBody: {
124
+ summary: input.title,
125
+ description: input.description,
126
+ start: { dateTime: startTime.toISOString(), timeZone: tz },
127
+ end: { dateTime: endTime.toISOString(), timeZone: tz },
128
+ attendees: input.participants.map((email) => ({ email: email.trim() })),
129
+ conferenceData: {
130
+ createRequest: {
131
+ requestId: `meetfy-${Date.now()}`,
132
+ conferenceSolutionKey: { type: "hangoutsMeet" }
183
133
  }
134
+ },
135
+ reminders: {
136
+ useDefault: false,
137
+ overrides: [
138
+ { method: "email", minutes: 10 },
139
+ { method: "popup", minutes: 5 }
140
+ ]
184
141
  }
185
142
  },
186
- reminders: {
187
- useDefault: false,
188
- overrides: [
189
- { method: "email", minutes: 10 },
190
- { method: "popup", minutes: 5 }
191
- ]
192
- }
193
- };
194
- const response = await calendar.events.insert({
195
- calendarId: "primary",
196
- requestBody: event,
197
143
  conferenceDataVersion: 1,
198
144
  sendUpdates: "all"
199
145
  });
200
- if (!response.data.id || !response.data.hangoutLink) {
201
- throw new Error("Failed to create meeting with Google Meet link");
202
- }
146
+ if (!res.data.id || !res.data.hangoutLink) return null;
203
147
  return {
204
- id: response.data.id,
205
- hangoutLink: response.data.hangoutLink,
148
+ id: res.data.id,
149
+ title: input.title,
206
150
  startTime: startTime.toLocaleString(),
207
151
  endTime: endTime.toLocaleString(),
208
- title: options.title
152
+ hangoutLink: res.data.hangoutLink
209
153
  };
210
154
  } catch {
211
155
  return null;
212
156
  }
213
- };
214
- var getNextMeeting = async (auth) => {
157
+ }
158
+ async function getNextMeeting(client) {
215
159
  try {
216
- const calendar = google2.calendar({ version: "v3", auth });
160
+ const calendar = google2.calendar({ version: "v3", auth: client });
217
161
  const now = (/* @__PURE__ */ new Date()).toISOString();
218
- const response = await calendar.events.list({
162
+ const res = await calendar.events.list({
219
163
  calendarId: "primary",
220
164
  timeMin: now,
221
165
  singleEvents: true,
222
166
  orderBy: "startTime",
223
167
  maxResults: 1
224
168
  });
225
- const event = response.data.items?.[0];
226
- if (!event) {
227
- return null;
228
- }
169
+ const event = res.data.items?.[0];
170
+ if (!event) return null;
229
171
  const start = event.start?.dateTime || event.start?.date;
230
172
  const end = event.end?.dateTime || event.end?.date;
231
- if (!start || !end) {
232
- return null;
233
- }
173
+ if (!start || !end) return null;
234
174
  return {
235
175
  id: event.id ?? "",
236
176
  title: event.summary ?? "Untitled",
@@ -242,496 +182,203 @@ var getNextMeeting = async (auth) => {
242
182
  } catch {
243
183
  return null;
244
184
  }
245
- };
185
+ }
246
186
 
247
- // src/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";
187
+ // src/format.ts
188
+ import chalk from "chalk";
189
+ function welcome() {
190
+ return [
191
+ "",
192
+ chalk.cyan.bold(" Meetfy"),
193
+ chalk.dim(" Instant Meeting Creator \u2014 reserve time in Google Calendar"),
194
+ ""
195
+ ].join("\n");
255
196
  }
256
- function 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;
197
+ function meeting(meeting2) {
198
+ const lines = [
199
+ chalk.cyan.bold(` ${meeting2.title}`),
200
+ chalk.dim(` \u{1F550} ${meeting2.startTime} \u2013 ${meeting2.endTime}`)
201
+ ];
202
+ if (meeting2.hangoutLink) lines.push(chalk.blue(` \u{1F517} ${meeting2.hangoutLink}`));
203
+ if (meeting2.location) lines.push(chalk.dim(` \u{1F4CD} ${meeting2.location}`));
204
+ return lines.join("\n");
394
205
  }
395
-
396
- // 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";
405
-
406
- // src/utils/openAuthUrl.ts
407
- async function copyAndOpenUrl(url) {
408
- const results = [];
409
- try {
410
- const { default: clipboardy } = await import("clipboardy");
411
- await clipboardy.write(url);
412
- results.push("URL copied to clipboard");
413
- } catch {
414
- results.push("Could not copy to clipboard");
415
- }
416
- try {
417
- const open = (await import("open")).default;
418
- await open(url);
419
- results.push("Opening in browser...");
420
- } catch {
421
- results.push("Could not open browser");
422
- }
423
- return results.join(". ");
206
+ function authNeedCode(authUrl) {
207
+ return [
208
+ chalk.cyan("\u{1F510} Authorize this app by visiting this URL:"),
209
+ chalk.blue(authUrl),
210
+ "",
211
+ chalk.green("Press Enter to copy URL and open in browser.")
212
+ ].join("\n");
424
213
  }
425
-
426
- // 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
- ] });
545
- }
546
- return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text3, { color: "red", children: [
547
- "\u274C ",
548
- errorMessage
549
- ] }) });
214
+ function authWaiting() {
215
+ return chalk.dim("\u23F3 Waiting for code on port 3434...");
216
+ }
217
+ function authSuccess() {
218
+ return [
219
+ chalk.green("\u2705 Authentication successful!"),
220
+ "",
221
+ chalk.dim("Available commands:"),
222
+ chalk.cyan(" meetfy create") + chalk.dim(" Create an instant meeting (30 min)"),
223
+ chalk.cyan(" meetfy next") + chalk.dim(" Show your next meeting"),
224
+ chalk.cyan(" meetfy logout") + chalk.dim(" Log out from Google")
225
+ ].join("\n");
226
+ }
227
+ function createSuccess(meeting2) {
228
+ return [
229
+ chalk.green("\u2705 Meeting created successfully!"),
230
+ chalk.cyan(`\u{1F4C5} ${meeting2.title}`),
231
+ chalk.blue(`\u{1F517} ${meeting2.hangoutLink}`),
232
+ chalk.dim(`\u23F0 ${meeting2.startTime} \u2013 ${meeting2.endTime}`)
233
+ ].join("\n");
234
+ }
235
+ function logoutSuccess() {
236
+ return chalk.green("\u2705 Logged out successfully!");
237
+ }
238
+ function noMeetings() {
239
+ return chalk.yellow("\u{1F4ED} No upcoming meetings found.");
240
+ }
241
+ function nextMeetingTitle() {
242
+ return chalk.cyan("\u{1F4C5} Next meeting:\n");
243
+ }
244
+ function authErrorJson(result) {
245
+ return result.type === "error" ? result.message : "auth_required";
550
246
  }
551
247
 
552
- // src/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!" }) });
248
+ // src/prompts.ts
249
+ import * as readline from "node:readline";
250
+ function createRl() {
251
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
252
+ }
253
+ function question(rl, prompt, defaultVal = "") {
254
+ return new Promise((resolve) => {
255
+ const p = defaultVal ? `${prompt} (${defaultVal}): ` : `${prompt}: `;
256
+ rl.question(p, (answer) => resolve(answer.trim() || defaultVal.trim()));
257
+ });
258
+ }
259
+ function closeRl(rl) {
260
+ rl.close();
564
261
  }
565
262
 
566
- // src/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." }) });
263
+ // src/browser.ts
264
+ import open from "open";
265
+ import clipboardy from "clipboardy";
266
+ function copyAndOpenUrl(url) {
267
+ clipboardy.write(url).catch(() => {
268
+ });
269
+ open(url).catch(() => {
270
+ });
638
271
  }
639
272
 
640
- // src/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");
644
- function outputJson(obj) {
273
+ // src/cli.ts
274
+ function json(obj) {
645
275
  console.log(JSON.stringify(obj, null, 0));
646
276
  }
647
- function 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);
277
+ function runCli() {
278
+ const program = new Command();
279
+ program.name("meetfy").description("CLI tool for creating instant meetings and reserving time in Google Calendar").version("1.0.0").option("--json", "Output result as JSON");
280
+ program.command("create").description("Create an instant meeting and reserve 30 minutes in your Google Calendar").option("-t, --title <title>", "Meeting title").option("-d, --description <description>", "Meeting description").option("-p, --participants <emails>", "Comma-separated list of participant emails").action(async (opts) => {
281
+ const useJson = program.opts().json;
282
+ if (useJson) {
283
+ const auth = await authenticate();
284
+ if (auth.type !== "ok") {
285
+ json({ success: false, error: authErrorJson(auth) });
286
+ process.exit(1);
287
+ }
288
+ const title2 = opts.title?.trim() || "Instant Meeting";
289
+ const description2 = opts.description?.trim() || "Instant meeting created via Meetfy CLI";
290
+ const participants2 = (opts.participants ?? "").split(",").map((e) => e.trim()).filter(Boolean);
291
+ const result2 = await createMeeting(auth.client, { title: title2, description: description2, participants: participants2 });
292
+ if (result2) json({ success: true, meeting: result2 });
293
+ else json({ success: false, error: "Failed to create meeting" });
294
+ process.exit(result2 ? 0 : 1);
658
295
  }
659
- 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
- });
667
- if (meeting) {
668
- outputJson({ success: true, meeting });
669
- } else {
670
- outputJson({ success: false, error: "Failed to create meeting" });
296
+ console.log(welcome());
297
+ const rl = createRl();
298
+ const title = opts.title?.trim() || await question(rl, "Meeting title", "Instant Meeting");
299
+ const description = opts.description?.trim() || await question(rl, "Meeting description", "Instant meeting created via Meetfy CLI");
300
+ const participantsStr = opts.participants ?? await question(rl, "Participant emails (comma-separated)", "");
301
+ closeRl(rl);
302
+ const client = await getClient();
303
+ if (!client) {
304
+ console.error(chalk2.red("\u274C Not authenticated. Run meetfy auth first."));
671
305
  process.exit(1);
672
306
  }
673
- 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
682
- }
683
- ) })
684
- );
685
- await waitUntilExit();
686
- });
687
- program.command("auth").description("Authenticate with Google Calendar").action(async () => {
688
- if (program.opts().json) {
689
- const authResult = await authenticateGoogle();
690
- if (authResult.type === "ok") {
691
- outputJson({ success: true });
692
- } else if (authResult.type === "need_code") {
693
- outputJson({ success: false, authRequired: true, authUrl: authResult.authUrl });
307
+ const participants = participantsStr.split(",").map((e) => e.trim()).filter(Boolean);
308
+ console.log(chalk2.dim("\n\u23F3 Creating meeting..."));
309
+ const result = await createMeeting(client, { title, description, participants });
310
+ if (result) console.log("\n" + createSuccess(result));
311
+ else {
312
+ console.error(chalk2.red("\n\u274C Failed to create meeting. Run meetfy auth if needed."));
694
313
  process.exit(1);
695
- } else if (authResult.type === "no_credentials") {
696
- outputJson({ success: false, error: "no_credentials" });
314
+ }
315
+ });
316
+ program.command("auth").description("Authenticate with Google Calendar").action(async () => {
317
+ const useJson = program.opts().json;
318
+ const auth = await authenticate();
319
+ if (useJson) {
320
+ if (auth.type === "ok") json({ success: true });
321
+ else if (auth.type === "need_code") json({ success: false, authRequired: true, authUrl: auth.authUrl });
322
+ else json({ success: false, error: auth.message });
323
+ process.exit(auth.type === "ok" ? 0 : 1);
324
+ }
325
+ console.log(welcome());
326
+ if (auth.type === "ok") {
327
+ console.log(authSuccess());
328
+ process.exit(0);
329
+ }
330
+ if (auth.type === "error") {
331
+ console.error(chalk2.red("\u274C"), auth.message);
697
332
  process.exit(1);
698
- } else {
699
- outputJson({ success: false, error: authResult.message });
333
+ }
334
+ const tokensPromise = auth.waitForTokens();
335
+ console.log(authNeedCode(auth.authUrl));
336
+ console.log(authWaiting());
337
+ const rl = createRl();
338
+ await new Promise((r) => rl.once("line", r));
339
+ closeRl(rl);
340
+ copyAndOpenUrl(auth.authUrl);
341
+ try {
342
+ await tokensPromise;
343
+ console.log("\n" + authSuccess());
344
+ } catch {
345
+ console.error(chalk2.red("\n\u274C Failed to get token."));
700
346
  process.exit(1);
701
347
  }
702
- 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) });
348
+ process.exit(0);
349
+ });
350
+ program.command("logout").description("Logout from Google Calendar").action(async () => {
351
+ logout();
352
+ if (program.opts().json) json({ success: true });
353
+ else console.log(logoutSuccess());
354
+ });
355
+ program.command("next").description("Show your next scheduled meeting").action(async () => {
356
+ const useJson = program.opts().json;
357
+ const client = await getClient();
358
+ if (useJson) {
359
+ if (!client) {
360
+ json({ success: false, error: "auth_required" });
361
+ process.exit(1);
362
+ }
363
+ const result2 = await getNextMeeting(client);
364
+ json({ success: true, meeting: result2 ?? null });
365
+ return;
366
+ }
367
+ console.log(welcome());
368
+ if (!client) {
369
+ console.error(chalk2.red("\u274C Not authenticated. Run meetfy auth first."));
726
370
  process.exit(1);
727
371
  }
728
- const 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();
372
+ const result = await getNextMeeting(client);
373
+ if (!result) {
374
+ console.log(noMeetings());
375
+ return;
376
+ }
377
+ console.log(nextMeetingTitle());
378
+ console.log(meeting(result));
379
+ });
380
+ program.parse();
381
+ }
382
+
383
+ // src/index.ts
384
+ runCli();
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "meetfy",
3
- "version": "1.0.2",
4
- "packageManager": "pnpm@10.31.0",
3
+ "version": "1.0.4",
5
4
  "description": "CLI tool for creating instant meetings and reserving time in Google Calendar",
6
5
  "main": "dist/index.js",
7
6
  "type": "module",
@@ -13,13 +12,14 @@
13
12
  "README.md"
14
13
  ],
15
14
  "scripts": {
16
- "start": "tsx src/index.tsx",
17
- "build": "node build.mjs",
15
+ "start": "tsx src/index.ts",
16
+ "build": "tsx scripts/build.ts",
18
17
  "prepublishOnly": "pnpm run build",
19
- "lint": "eslint src/**/*.{ts,tsx}"
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,36 +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
- "ink": "^5.0.0",
40
- "ink-spinner": "^5.0.0",
41
- "ink-text-input": "^6.0.0",
42
- "open": "^10.2.0",
43
- "react": "^18.3.1",
44
- "tsx": "^4.20.3",
45
- "typescript": "5.5"
32
+ "chalk": "5.6.2",
33
+ "clipboardy": "5.3.1",
34
+ "commander": "14.0.3",
35
+ "conf": "15.1.0",
36
+ "google-auth-library": "10.6.1",
37
+ "googleapis": "171.4.0",
38
+ "open": "11.0.0",
39
+ "typescript": "5.9.3"
46
40
  },
47
41
  "devDependencies": {
48
- "@types/express": "^5.0.3",
49
- "@types/node": "^20.10.0",
50
- "@types/react": "^18.3.0",
42
+ "@types/node": "25.4.0",
51
43
  "@typescript-eslint/eslint-plugin": "^7.18.0",
52
44
  "@typescript-eslint/parser": "^7.18.0",
53
- "esbuild": "^0.25.8",
45
+ "esbuild": "0.27.3",
54
46
  "eslint": "^8.57.1",
55
47
  "eslint-config-airbnb": "^19.0.4",
56
48
  "eslint-config-airbnb-typescript": "^18.0.0",
57
49
  "eslint-plugin-import": "^2.32.0",
58
- "eslint-plugin-jsx-a11y": "^6.10.2",
59
- "eslint-plugin-react": "^7.37.5",
60
- "eslint-plugin-react-hooks": "^4.6.2"
50
+ "tsx": "4.21.0"
61
51
  }
62
52
  }