groupchat 0.0.2

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.
@@ -0,0 +1,244 @@
1
+ // src/auth/auth-manager.ts
2
+ import crypto from "crypto";
3
+ import http from "http";
4
+ import open from "open";
5
+
6
+ // src/auth/token-storage.ts
7
+ import keytar from "keytar";
8
+ var SERVICE_NAME = "groupchat";
9
+ var TOKEN_ACCOUNT = "auth-token";
10
+ var EXPIRY_ACCOUNT = "auth-token-expiry";
11
+ async function storeToken(token, expiresAt) {
12
+ await keytar.setPassword(SERVICE_NAME, TOKEN_ACCOUNT, token);
13
+ await keytar.setPassword(SERVICE_NAME, EXPIRY_ACCOUNT, expiresAt.toISOString());
14
+ }
15
+ async function getToken() {
16
+ const token = await keytar.getPassword(SERVICE_NAME, TOKEN_ACCOUNT);
17
+ const expiryStr = await keytar.getPassword(SERVICE_NAME, EXPIRY_ACCOUNT);
18
+ if (!token || !expiryStr) {
19
+ return null;
20
+ }
21
+ const expiresAt = new Date(expiryStr);
22
+ if (expiresAt <= /* @__PURE__ */ new Date()) {
23
+ await clearToken();
24
+ return null;
25
+ }
26
+ return { token, expiresAt };
27
+ }
28
+ async function clearToken() {
29
+ await keytar.deletePassword(SERVICE_NAME, TOKEN_ACCOUNT);
30
+ await keytar.deletePassword(SERVICE_NAME, EXPIRY_ACCOUNT);
31
+ }
32
+ async function hasValidToken() {
33
+ const stored = await getToken();
34
+ return stored !== null;
35
+ }
36
+
37
+ // src/lib/config.ts
38
+ import { config as loadEnv } from "dotenv";
39
+ if (process.env.NODE_ENV !== "production") {
40
+ loadEnv();
41
+ }
42
+ var DEFAULT_CONFIG = {
43
+ consoleUrl: "https://app.groupchatty.com",
44
+ wsUrl: "wss://api.groupchatty.com/socket"
45
+ };
46
+ function getConfig() {
47
+ return {
48
+ consoleUrl: process.env.GROUPCHAT_CONSOLE_URL || DEFAULT_CONFIG.consoleUrl,
49
+ wsUrl: process.env.GROUPCHAT_WS_URL || DEFAULT_CONFIG.wsUrl
50
+ };
51
+ }
52
+
53
+ // src/auth/auth-manager.ts
54
+ function generateState() {
55
+ return crypto.randomBytes(32).toString("hex");
56
+ }
57
+ async function findAvailablePort(startPort, endPort) {
58
+ for (let port = startPort; port <= endPort; port++) {
59
+ const available = await new Promise((resolve) => {
60
+ const server = http.createServer();
61
+ server.once("error", () => resolve(false));
62
+ server.once("listening", () => {
63
+ server.close();
64
+ resolve(true);
65
+ });
66
+ server.listen(port, "127.0.0.1");
67
+ });
68
+ if (available) {
69
+ return port;
70
+ }
71
+ }
72
+ throw new Error(`No available port found in range ${startPort}-${endPort}`);
73
+ }
74
+ var SUCCESS_HTML = `
75
+ <!DOCTYPE html>
76
+ <html>
77
+ <head>
78
+ <meta charset="utf-8" />
79
+ <title>Terminal Chat - Authentication Successful</title>
80
+ <style>
81
+ body {
82
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
83
+ background: #1F1F1F;
84
+ color: #CCCCCC;
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ min-height: 100vh;
89
+ margin: 0;
90
+ }
91
+ .container { text-align: center; padding: 2rem; }
92
+ .success { color: #4EC9B0; font-size: 1.5rem; margin-bottom: 1rem; }
93
+ .message { color: #9D9D9D; }
94
+ </style>
95
+ </head>
96
+ <body>
97
+ <div class="container">
98
+ <div class="success">&#10003; Authentication successful!</div>
99
+ <div class="message">You can close this window and return to the terminal.</div>
100
+ </div>
101
+ </body>
102
+ </html>
103
+ `;
104
+ var ERROR_HTML = (message) => `
105
+ <!DOCTYPE html>
106
+ <html>
107
+ <head>
108
+ <meta charset="utf-8" />
109
+ <title>Terminal Chat - Authentication Failed</title>
110
+ <style>
111
+ body {
112
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
113
+ background: #1F1F1F;
114
+ color: #CCCCCC;
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ min-height: 100vh;
119
+ margin: 0;
120
+ }
121
+ .container { text-align: center; padding: 2rem; }
122
+ .error { color: #F85149; font-size: 1.5rem; margin-bottom: 1rem; }
123
+ .message { color: #9D9D9D; }
124
+ </style>
125
+ </head>
126
+ <body>
127
+ <div class="container">
128
+ <div class="error">\u2717 Authentication failed</div>
129
+ <div class="message">${message}</div>
130
+ </div>
131
+ </body>
132
+ </html>
133
+ `;
134
+ function startAuthServer(port, expectedState, timeoutMs = 5 * 60 * 1e3) {
135
+ return new Promise((resolve, reject) => {
136
+ let settled = false;
137
+ const server = http.createServer((req, res) => {
138
+ if (settled) {
139
+ res.writeHead(400);
140
+ res.end();
141
+ return;
142
+ }
143
+ if (!req.url?.startsWith("/callback")) {
144
+ res.writeHead(404);
145
+ res.end();
146
+ return;
147
+ }
148
+ const url = new URL(req.url, `http://localhost:${port}`);
149
+ const token = url.searchParams.get("token");
150
+ const state = url.searchParams.get("state");
151
+ const expiresAt = url.searchParams.get("expiresAt");
152
+ if (state !== expectedState) {
153
+ res.writeHead(400, { "Content-Type": "text/html" });
154
+ res.end(ERROR_HTML("Invalid state parameter. Please try again."));
155
+ return;
156
+ }
157
+ if (!token) {
158
+ res.writeHead(400, { "Content-Type": "text/html" });
159
+ res.end(ERROR_HTML("No token received. Please try again."));
160
+ return;
161
+ }
162
+ settled = true;
163
+ res.writeHead(200, { "Content-Type": "text/html" });
164
+ res.end(SUCCESS_HTML);
165
+ server.close();
166
+ clearTimeout(timeout);
167
+ resolve({
168
+ token,
169
+ state,
170
+ expiresAt: expiresAt || new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString()
171
+ });
172
+ });
173
+ const timeout = setTimeout(() => {
174
+ if (!settled) {
175
+ settled = true;
176
+ server.close();
177
+ reject(new Error("Authentication timed out. Please try again."));
178
+ }
179
+ }, timeoutMs);
180
+ server.on("error", (err) => {
181
+ if (!settled) {
182
+ settled = true;
183
+ clearTimeout(timeout);
184
+ reject(err);
185
+ }
186
+ });
187
+ server.listen(port, "127.0.0.1");
188
+ });
189
+ }
190
+ async function login(onStatusChange) {
191
+ const config = getConfig();
192
+ const state = generateState();
193
+ onStatusChange?.("Starting authentication server...");
194
+ let port;
195
+ try {
196
+ port = await findAvailablePort(8080, 8099);
197
+ } catch (err) {
198
+ return {
199
+ success: false,
200
+ error: `Failed to find available port: ${err}`
201
+ };
202
+ }
203
+ const serverPromise = startAuthServer(port, state);
204
+ const callbackUrl = `http://localhost:${port}/callback`;
205
+ const authUrl = `${config.consoleUrl}/auth/cli?state=${state}&redirect_uri=${encodeURIComponent(callbackUrl)}`;
206
+ onStatusChange?.("Opening browser for authentication...");
207
+ try {
208
+ await open(authUrl);
209
+ } catch (err) {
210
+ return {
211
+ success: false,
212
+ error: `Failed to open browser: ${err}`
213
+ };
214
+ }
215
+ onStatusChange?.("Waiting for authentication...");
216
+ try {
217
+ const result = await serverPromise;
218
+ onStatusChange?.("Storing credentials...");
219
+ await storeToken(result.token, new Date(result.expiresAt));
220
+ return { success: true };
221
+ } catch (err) {
222
+ return {
223
+ success: false,
224
+ error: err instanceof Error ? err.message : String(err)
225
+ };
226
+ }
227
+ }
228
+ async function logout() {
229
+ await clearToken();
230
+ }
231
+ async function isAuthenticated() {
232
+ return hasValidToken();
233
+ }
234
+ async function getCurrentToken() {
235
+ return getToken();
236
+ }
237
+
238
+ export {
239
+ getConfig,
240
+ login,
241
+ logout,
242
+ isAuthenticated,
243
+ getCurrentToken
244
+ };
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-ZYY5PLDM.js";
3
+
4
+ // src/index.ts
5
+ import WebSocket from "ws";
6
+ import { program } from "commander";
7
+ import { render } from "ink";
8
+ import React2 from "react";
9
+
10
+ // src/lib/update-checker.ts
11
+ import { createRequire } from "module";
12
+ import { dirname, join } from "path";
13
+ import { fileURLToPath } from "url";
14
+ var __filename = fileURLToPath(import.meta.url);
15
+ var __dirname = dirname(__filename);
16
+ var require2 = createRequire(import.meta.url);
17
+ var packageJson = require2(join(__dirname, "../package.json"));
18
+ async function fetchLatestVersion(packageName) {
19
+ try {
20
+ const response = await fetch(
21
+ `https://registry.npmjs.org/${packageName}/latest`,
22
+ {
23
+ headers: {
24
+ Accept: "application/json"
25
+ },
26
+ signal: AbortSignal.timeout(5e3)
27
+ // 5 second timeout
28
+ }
29
+ );
30
+ if (!response.ok) {
31
+ return null;
32
+ }
33
+ const data = await response.json();
34
+ return typeof data.version === "string" ? data.version : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ function isNewerVersion(current, latest) {
40
+ const parseVersion = (v) => v.split(".").map((n) => parseInt(n, 10) || 0);
41
+ const currentParts = parseVersion(current);
42
+ const latestParts = parseVersion(latest);
43
+ for (let i = 0; i < 3; i++) {
44
+ const c = currentParts[i] || 0;
45
+ const l = latestParts[i] || 0;
46
+ if (l > c) return true;
47
+ if (l < c) return false;
48
+ }
49
+ return false;
50
+ }
51
+ async function checkForUpdate() {
52
+ const currentVersion = packageJson.version;
53
+ const packageName = packageJson.name;
54
+ const latestVersion = await fetchLatestVersion(packageName);
55
+ if (!latestVersion) {
56
+ return {
57
+ currentVersion,
58
+ latestVersion: currentVersion,
59
+ updateAvailable: false
60
+ };
61
+ }
62
+ return {
63
+ currentVersion,
64
+ latestVersion,
65
+ updateAvailable: isNewerVersion(currentVersion, latestVersion)
66
+ };
67
+ }
68
+ function getUpdateCommand() {
69
+ return `npm install -g ${packageJson.name}`;
70
+ }
71
+
72
+ // src/components/UpdatePrompt.tsx
73
+ import { useState } from "react";
74
+ import { Box, Text, useInput, useApp } from "ink";
75
+ import { execSync } from "child_process";
76
+ import { jsx, jsxs } from "react/jsx-runtime";
77
+ var options = [
78
+ { label: "Update now", value: "update" },
79
+ { label: "Skip", value: "skip" }
80
+ ];
81
+ function UpdatePrompt({ updateInfo, onComplete }) {
82
+ const [selectedIndex, setSelectedIndex] = useState(0);
83
+ const [isUpdating, setIsUpdating] = useState(false);
84
+ const [updateError, setUpdateError] = useState(null);
85
+ const { exit } = useApp();
86
+ useInput((input, key) => {
87
+ if (isUpdating) return;
88
+ if (key.upArrow || input === "k") {
89
+ setSelectedIndex((i) => Math.max(0, i - 1));
90
+ } else if (key.downArrow || input === "j") {
91
+ setSelectedIndex((i) => Math.min(options.length - 1, i + 1));
92
+ } else if (key.return) {
93
+ const selected = options[selectedIndex];
94
+ if (selected.value === "update") {
95
+ handleUpdate();
96
+ } else {
97
+ onComplete();
98
+ }
99
+ } else if (input === "1") {
100
+ handleUpdate();
101
+ } else if (input === "2") {
102
+ onComplete();
103
+ }
104
+ });
105
+ const handleUpdate = () => {
106
+ setIsUpdating(true);
107
+ const command = getUpdateCommand();
108
+ try {
109
+ execSync(command, { stdio: "inherit" });
110
+ console.log("\n\nUpdate complete! Please restart groupchat.\n");
111
+ exit();
112
+ } catch (err) {
113
+ setUpdateError(
114
+ `Update failed. Please run manually: ${command}`
115
+ );
116
+ setIsUpdating(false);
117
+ }
118
+ };
119
+ if (updateError) {
120
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
121
+ /* @__PURE__ */ jsx(Text, { color: "red", children: updateError }),
122
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press any key to continue..." })
123
+ ] });
124
+ }
125
+ if (isUpdating) {
126
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "cyan", children: "Updating..." }) });
127
+ }
128
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 1, children: [
129
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
130
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: " " }),
131
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
132
+ "Update available!",
133
+ " "
134
+ ] }),
135
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: updateInfo.currentVersion }),
136
+ /* @__PURE__ */ jsxs(Text, { children: [
137
+ " -",
138
+ ">",
139
+ " "
140
+ ] }),
141
+ /* @__PURE__ */ jsx(Text, { color: "green", children: updateInfo.latestVersion })
142
+ ] }),
143
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: options.map((option, index) => {
144
+ const isSelected = index === selectedIndex;
145
+ return /* @__PURE__ */ jsxs(Box, { children: [
146
+ /* @__PURE__ */ jsxs(Text, { color: isSelected ? "cyan" : void 0, children: [
147
+ isSelected ? ">" : " ",
148
+ " ",
149
+ index + 1,
150
+ ". ",
151
+ option.label
152
+ ] }),
153
+ option.value === "update" && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
154
+ " (runs `",
155
+ getUpdateCommand(),
156
+ "`)"
157
+ ] })
158
+ ] }, option.value);
159
+ }) }),
160
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press enter to continue" }) })
161
+ ] });
162
+ }
163
+
164
+ // package.json
165
+ var package_default = {
166
+ name: "groupchat",
167
+ version: "0.0.2",
168
+ description: "CLI chat client for Groupchat",
169
+ type: "module",
170
+ main: "./dist/index.js",
171
+ repository: {
172
+ type: "git",
173
+ url: "https://github.com/svapnil/groupchat-main"
174
+ },
175
+ publishConfig: {
176
+ access: "public"
177
+ },
178
+ bin: {
179
+ groupchat: "./dist/index.js"
180
+ },
181
+ files: [
182
+ "dist",
183
+ "README.md"
184
+ ],
185
+ scripts: {
186
+ build: "tsup src/index.ts --format esm --dts --clean",
187
+ dev: "tsup src/index.ts --format esm --watch",
188
+ "dev:bun": "bunx tsup src/index.ts --format esm --watch",
189
+ start: "NODE_ENV=production node dist/index.js",
190
+ "start:bun": "NODE_ENV=production bun dist/index.js",
191
+ typecheck: "tsc --noEmit",
192
+ prepublishOnly: "npm run typecheck && npm run build"
193
+ },
194
+ dependencies: {
195
+ chalk: "^5.3.0",
196
+ commander: "^12.1.0",
197
+ dotenv: "^17.2.3",
198
+ ink: "^5.0.1",
199
+ "ink-text-input": "^6.0.0",
200
+ keytar: "^7.9.0",
201
+ open: "^10.1.0",
202
+ phoenix: "^1.8.3",
203
+ react: "^18.3.1",
204
+ ws: "^8.18.0"
205
+ },
206
+ devDependencies: {
207
+ "@types/node": "^22.10.5",
208
+ "@types/react": "^18.3.18",
209
+ "@types/ws": "^8.18.1",
210
+ tsup: "^8.3.5",
211
+ typescript: "^5.7.2"
212
+ },
213
+ engines: {
214
+ node: ">=18"
215
+ },
216
+ keywords: [
217
+ "chat",
218
+ "cli",
219
+ "terminal",
220
+ "ink",
221
+ "tui"
222
+ ],
223
+ license: "MIT"
224
+ };
225
+
226
+ // src/index.ts
227
+ if (typeof globalThis.WebSocket === "undefined") {
228
+ globalThis.WebSocket = WebSocket;
229
+ }
230
+ async function showUpdatePrompt(updateInfo) {
231
+ return new Promise((resolve) => {
232
+ const { unmount, waitUntilExit } = render(
233
+ React2.createElement(UpdatePrompt, {
234
+ updateInfo,
235
+ onComplete: () => {
236
+ unmount();
237
+ resolve();
238
+ }
239
+ })
240
+ );
241
+ waitUntilExit().then(() => resolve());
242
+ });
243
+ }
244
+ async function startChat() {
245
+ if (!process.stdout.isTTY) {
246
+ console.error("Error: groupchat requires an interactive terminal.\n");
247
+ process.exit(1);
248
+ }
249
+ const updateInfo = await checkForUpdate();
250
+ if (updateInfo.updateAvailable) {
251
+ await showUpdatePrompt(updateInfo);
252
+ }
253
+ process.stdout.write("\x1B[2J\x1B[0f");
254
+ const { App } = await import("./App-LF2NVQZQ.js");
255
+ const { waitUntilExit } = render(React2.createElement(App), {
256
+ exitOnCtrlC: false
257
+ // We handle Ctrl+C manually
258
+ });
259
+ try {
260
+ await waitUntilExit();
261
+ } catch (err) {
262
+ }
263
+ }
264
+ program.name("groupchat").description("CLI chat client for Groupchat").version(package_default.version);
265
+ program.action(startChat);
266
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "groupchat",
3
+ "version": "0.0.2",
4
+ "description": "CLI chat client for Groupchat",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/svapnil/groupchat-main"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "bin": {
15
+ "groupchat": "./dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup src/index.ts --format esm --dts --clean",
23
+ "dev": "tsup src/index.ts --format esm --watch",
24
+ "dev:bun": "bunx tsup src/index.ts --format esm --watch",
25
+ "start": "NODE_ENV=production node dist/index.js",
26
+ "start:bun": "NODE_ENV=production bun dist/index.js",
27
+ "typecheck": "tsc --noEmit",
28
+ "prepublishOnly": "npm run typecheck && npm run build"
29
+ },
30
+ "dependencies": {
31
+ "chalk": "^5.3.0",
32
+ "commander": "^12.1.0",
33
+ "dotenv": "^17.2.3",
34
+ "ink": "^5.0.1",
35
+ "ink-text-input": "^6.0.0",
36
+ "keytar": "^7.9.0",
37
+ "open": "^10.1.0",
38
+ "phoenix": "^1.8.3",
39
+ "react": "^18.3.1",
40
+ "ws": "^8.18.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.10.5",
44
+ "@types/react": "^18.3.18",
45
+ "@types/ws": "^8.18.1",
46
+ "tsup": "^8.3.5",
47
+ "typescript": "^5.7.2"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "keywords": [
53
+ "chat",
54
+ "cli",
55
+ "terminal",
56
+ "ink",
57
+ "tui"
58
+ ],
59
+ "license": "MIT"
60
+ }