cheatchat 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/chat.js +3 -0
- package/package.json +34 -0
- package/src/auth/login.js +48 -0
- package/src/auth/logout.js +19 -0
- package/src/auth/signup.js +65 -0
- package/src/auth/whoami.js +35 -0
- package/src/commands/connect.js +205 -0
- package/src/commands/help.js +39 -0
- package/src/commands/login.js +5 -0
- package/src/commands/logout.js +5 -0
- package/src/commands/signup.js +5 -0
- package/src/config/config.js +71 -0
- package/src/index.js +77 -0
- package/src/services/api.js +67 -0
- package/src/services/socket.js +79 -0
- package/src/ui/banner.js +58 -0
- package/src/ui/color.js +34 -0
- package/src/ui/printer.js +119 -0
- package/src/ui/spinner.js +12 -0
- package/src/ui/tables.js +20 -0
- package/src/utils/constants.js +17 -0
- package/src/utils/conversation.js +7 -0
- package/src/utils/logger.js +18 -0
- package/src/utils/validator.js +31 -0
- package/src/websocket/connect.js +41 -0
- package/src/websocket/disconnect.js +13 -0
- package/src/websocket/receiver.js +26 -0
- package/src/websocket/sender.js +25 -0
package/bin/chat.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cheatchat",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"bin": {
|
|
6
|
+
"chat": "./bin/chat.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/ramoliyaYug/cheatChat.git"
|
|
16
|
+
},
|
|
17
|
+
"author": "yug",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/ramoliyaYug/cheatChat/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/ramoliyaYug/cheatChat#readme",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"axios": "^1.18.1",
|
|
25
|
+
"boxen": "^8.0.1",
|
|
26
|
+
"chalk": "^5.6.2",
|
|
27
|
+
"cli-table3": "^0.6.5",
|
|
28
|
+
"commander": "^15.0.0",
|
|
29
|
+
"dotenv": "^17.4.2",
|
|
30
|
+
"inquirer": "^14.0.2",
|
|
31
|
+
"ora": "^9.4.1",
|
|
32
|
+
"ws": "^8.21.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import { login as apiLogin } from "../services/api.js";
|
|
3
|
+
import { saveAuth } from "../config/config.js";
|
|
4
|
+
import { createSpinner } from "../ui/spinner.js";
|
|
5
|
+
import { printSuccess, printError, printBlank } from "../ui/printer.js";
|
|
6
|
+
import { brand, muted, whiteBold } from "../ui/color.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interactive login flow.
|
|
10
|
+
*/
|
|
11
|
+
export async function loginFlow() {
|
|
12
|
+
printBlank();
|
|
13
|
+
console.log(` ${whiteBold("Login to CheatChat")}`);
|
|
14
|
+
printBlank();
|
|
15
|
+
|
|
16
|
+
const answers = await inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: "input",
|
|
19
|
+
name: "username",
|
|
20
|
+
message: brand("Username:"),
|
|
21
|
+
validate: (input) => input.trim().length > 0 || "Username is required",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: "password",
|
|
25
|
+
name: "password",
|
|
26
|
+
message: brand("Password:"),
|
|
27
|
+
mask: "•",
|
|
28
|
+
validate: (input) => input.length > 0 || "Password is required",
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const spinner = createSpinner("Authenticating...");
|
|
33
|
+
spinner.start();
|
|
34
|
+
|
|
35
|
+
const result = await apiLogin(answers.username, answers.password);
|
|
36
|
+
|
|
37
|
+
if (result.success) {
|
|
38
|
+
saveAuth(result.username, result.token);
|
|
39
|
+
spinner.succeed(`Welcome back, ${brand(result.username)}!`);
|
|
40
|
+
printBlank();
|
|
41
|
+
console.log(` ${muted("Run")} ${brand("chat connect")} ${muted("to start chatting")}`);
|
|
42
|
+
} else {
|
|
43
|
+
spinner.fail("Login failed");
|
|
44
|
+
printError(result.error || "Invalid username or password");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
printBlank();
|
|
48
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { clearAuth, loadAuth } from "../config/config.js";
|
|
2
|
+
import { printSuccess, printWarning, printBlank } from "../ui/printer.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Logout — clear stored credentials.
|
|
6
|
+
*/
|
|
7
|
+
export function logoutFlow() {
|
|
8
|
+
printBlank();
|
|
9
|
+
|
|
10
|
+
const auth = loadAuth();
|
|
11
|
+
if (!auth) {
|
|
12
|
+
printWarning("You are not logged in.");
|
|
13
|
+
} else {
|
|
14
|
+
clearAuth();
|
|
15
|
+
printSuccess(`Logged out. Goodbye, ${auth.username}!`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
printBlank();
|
|
19
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import { signup as apiSignup } from "../services/api.js";
|
|
3
|
+
import { saveAuth } from "../config/config.js";
|
|
4
|
+
import { createSpinner } from "../ui/spinner.js";
|
|
5
|
+
import { printSuccess, printError, printBlank } from "../ui/printer.js";
|
|
6
|
+
import { validateUsername, validatePassword } from "../utils/validator.js";
|
|
7
|
+
import { brand, muted, whiteBold } from "../ui/color.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Interactive signup flow.
|
|
11
|
+
*/
|
|
12
|
+
export async function signupFlow() {
|
|
13
|
+
printBlank();
|
|
14
|
+
console.log(` ${whiteBold("Create your CheatChat account")}`);
|
|
15
|
+
printBlank();
|
|
16
|
+
|
|
17
|
+
const answers = await inquirer.prompt([
|
|
18
|
+
{
|
|
19
|
+
type: "input",
|
|
20
|
+
name: "username",
|
|
21
|
+
message: brand("Username:"),
|
|
22
|
+
validate: (input) => {
|
|
23
|
+
const { valid, message } = validateUsername(input);
|
|
24
|
+
return valid || message;
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: "password",
|
|
29
|
+
name: "password",
|
|
30
|
+
message: brand("Password:"),
|
|
31
|
+
mask: "•",
|
|
32
|
+
validate: (input) => {
|
|
33
|
+
const { valid, message } = validatePassword(input);
|
|
34
|
+
return valid || message;
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "password",
|
|
39
|
+
name: "confirmPassword",
|
|
40
|
+
message: brand("Confirm Password:"),
|
|
41
|
+
mask: "•",
|
|
42
|
+
validate: (input, answers) => {
|
|
43
|
+
if (input !== answers.password) return "Passwords do not match";
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const spinner = createSpinner("Creating your account...");
|
|
50
|
+
spinner.start();
|
|
51
|
+
|
|
52
|
+
const result = await apiSignup(answers.username, answers.password);
|
|
53
|
+
|
|
54
|
+
if (result.success) {
|
|
55
|
+
saveAuth(result.username, result.token);
|
|
56
|
+
spinner.succeed(`Account created! Welcome, ${brand(result.username)}`);
|
|
57
|
+
printBlank();
|
|
58
|
+
console.log(` ${muted("Run")} ${brand("chat connect")} ${muted("to start chatting")}`);
|
|
59
|
+
} else {
|
|
60
|
+
spinner.fail("Signup failed");
|
|
61
|
+
printError(result.error || "Unknown error");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
printBlank();
|
|
65
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { loadAuth } from "../config/config.js";
|
|
2
|
+
import { getMe } from "../services/api.js";
|
|
3
|
+
import { createSpinner } from "../ui/spinner.js";
|
|
4
|
+
import { printError, printWarning, printBlank } from "../ui/printer.js";
|
|
5
|
+
import { printUserTable } from "../ui/tables.js";
|
|
6
|
+
import { brand, whiteBold } from "../ui/color.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Display current user info.
|
|
10
|
+
*/
|
|
11
|
+
export async function whoamiFlow() {
|
|
12
|
+
printBlank();
|
|
13
|
+
|
|
14
|
+
const auth = loadAuth();
|
|
15
|
+
if (!auth) {
|
|
16
|
+
printWarning("You are not logged in. Run: chat login");
|
|
17
|
+
printBlank();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const spinner = createSpinner("Fetching profile...");
|
|
22
|
+
spinner.start();
|
|
23
|
+
|
|
24
|
+
const result = await getMe(auth.token);
|
|
25
|
+
|
|
26
|
+
if (result.success) {
|
|
27
|
+
spinner.succeed(`Logged in as ${brand(result.user.username)}`);
|
|
28
|
+
printUserTable(result.user);
|
|
29
|
+
} else {
|
|
30
|
+
spinner.fail("Failed to fetch profile");
|
|
31
|
+
printError(result.error || "Session may have expired. Try: chat login");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
printBlank();
|
|
35
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import { loadAuth } from "../config/config.js";
|
|
3
|
+
import { connectWebSocket } from "../websocket/connect.js";
|
|
4
|
+
import { disconnectWebSocket } from "../websocket/disconnect.js";
|
|
5
|
+
import { handleIncomingMessage } from "../websocket/receiver.js";
|
|
6
|
+
import { sendMessage } from "../websocket/sender.js";
|
|
7
|
+
import { showChatHeader } from "../ui/banner.js";
|
|
8
|
+
import {
|
|
9
|
+
printChatHelp,
|
|
10
|
+
printModeSwitch,
|
|
11
|
+
printError,
|
|
12
|
+
printWarning,
|
|
13
|
+
printSystem,
|
|
14
|
+
printBlank,
|
|
15
|
+
} from "../ui/printer.js";
|
|
16
|
+
import { brand, accent, muted, prompt as promptColor } from "../ui/color.js";
|
|
17
|
+
import { CHAT_MODE } from "../utils/constants.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Main connect command — enters interactive chat mode.
|
|
21
|
+
*/
|
|
22
|
+
export async function connectCommand() {
|
|
23
|
+
printBlank();
|
|
24
|
+
|
|
25
|
+
// ─── Check Authentication ────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const auth = loadAuth();
|
|
28
|
+
if (!auth) {
|
|
29
|
+
printWarning("You are not logged in.");
|
|
30
|
+
console.log(` ${muted("Run")} ${brand("chat login")} ${muted("or")} ${brand("chat signup")} ${muted("first")}`);
|
|
31
|
+
printBlank();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { username, token } = auth;
|
|
36
|
+
|
|
37
|
+
// ─── Chat State ──────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
let chatMode = CHAT_MODE.GLOBAL;
|
|
40
|
+
let dmTarget = null;
|
|
41
|
+
let ws = null;
|
|
42
|
+
let rl = null;
|
|
43
|
+
let isExiting = false;
|
|
44
|
+
|
|
45
|
+
// ─── Prompt Helper ───────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function getPromptString() {
|
|
48
|
+
if (chatMode === CHAT_MODE.GLOBAL) {
|
|
49
|
+
return ` ${accent("🌍")} ${promptColor("›")} `;
|
|
50
|
+
}
|
|
51
|
+
return ` ${accent("🔒")} ${brand(dmTarget)} ${promptColor("›")} `;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function refreshPrompt() {
|
|
55
|
+
if (rl) {
|
|
56
|
+
rl.setPrompt(getPromptString());
|
|
57
|
+
rl.prompt();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Handle Incoming Messages ────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function onMessage(data) {
|
|
64
|
+
// Clear the current prompt line before printing message
|
|
65
|
+
readline.clearLine(process.stdout, 0);
|
|
66
|
+
readline.cursorTo(process.stdout, 0);
|
|
67
|
+
|
|
68
|
+
handleIncomingMessage(data, username);
|
|
69
|
+
|
|
70
|
+
// Re-display the prompt
|
|
71
|
+
refreshPrompt();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Handle Connection Close ─────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function onClose(code, reason) {
|
|
77
|
+
if (!isExiting) {
|
|
78
|
+
printBlank();
|
|
79
|
+
printSystem("Connection lost. You have been disconnected.");
|
|
80
|
+
printBlank();
|
|
81
|
+
cleanup();
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Cleanup ─────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function cleanup() {
|
|
89
|
+
isExiting = true;
|
|
90
|
+
if (rl) {
|
|
91
|
+
rl.close();
|
|
92
|
+
rl = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Connect ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
ws = await connectWebSocket(token, onMessage, onClose);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
printError("Could not connect. Check your credentials or try: chat login");
|
|
102
|
+
printBlank();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Show Chat UI ────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
printBlank();
|
|
109
|
+
showChatHeader(username, chatMode, dmTarget);
|
|
110
|
+
printBlank();
|
|
111
|
+
printSystem("You are in Global Chat. Type a message or /help for commands.");
|
|
112
|
+
printBlank();
|
|
113
|
+
|
|
114
|
+
// ─── Start Interactive Readline ──────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
rl = readline.createInterface({
|
|
117
|
+
input: process.stdin,
|
|
118
|
+
output: process.stdout,
|
|
119
|
+
prompt: getPromptString(),
|
|
120
|
+
terminal: true,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
rl.prompt();
|
|
124
|
+
|
|
125
|
+
rl.on("line", (input) => {
|
|
126
|
+
const line = input.trim();
|
|
127
|
+
|
|
128
|
+
if (!line) {
|
|
129
|
+
rl.prompt();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Chat Commands ─────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
if (line.startsWith("/")) {
|
|
136
|
+
const parts = line.split(/\s+/);
|
|
137
|
+
const cmd = parts[0].toLowerCase();
|
|
138
|
+
|
|
139
|
+
switch (cmd) {
|
|
140
|
+
case "/global":
|
|
141
|
+
chatMode = CHAT_MODE.GLOBAL;
|
|
142
|
+
dmTarget = null;
|
|
143
|
+
printModeSwitch("global");
|
|
144
|
+
refreshPrompt();
|
|
145
|
+
return;
|
|
146
|
+
|
|
147
|
+
case "/dm": {
|
|
148
|
+
const target = parts[1];
|
|
149
|
+
if (!target) {
|
|
150
|
+
printError("Usage: /dm <username>");
|
|
151
|
+
rl.prompt();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (target === username) {
|
|
155
|
+
printError("You cannot DM yourself.");
|
|
156
|
+
rl.prompt();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
chatMode = CHAT_MODE.PRIVATE;
|
|
160
|
+
dmTarget = target;
|
|
161
|
+
printModeSwitch("private", target);
|
|
162
|
+
refreshPrompt();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case "/help":
|
|
167
|
+
printChatHelp();
|
|
168
|
+
rl.prompt();
|
|
169
|
+
return;
|
|
170
|
+
|
|
171
|
+
case "/exit":
|
|
172
|
+
disconnectWebSocket(ws);
|
|
173
|
+
cleanup();
|
|
174
|
+
process.exit(0);
|
|
175
|
+
return;
|
|
176
|
+
|
|
177
|
+
default:
|
|
178
|
+
printError(`Unknown command: ${cmd}. Type /help for available commands.`);
|
|
179
|
+
rl.prompt();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Send Message ──────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
sendMessage(ws, chatMode, line, dmTarget);
|
|
187
|
+
rl.prompt();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
rl.on("close", () => {
|
|
191
|
+
if (!isExiting) {
|
|
192
|
+
disconnectWebSocket(ws);
|
|
193
|
+
cleanup();
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ─── Handle process signals ──────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
process.on("SIGINT", () => {
|
|
201
|
+
disconnectWebSocket(ws);
|
|
202
|
+
cleanup();
|
|
203
|
+
process.exit(0);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { brand, muted, accent, whiteBold } from "../ui/color.js";
|
|
2
|
+
import { printBlank } from "../ui/printer.js";
|
|
3
|
+
import { APP_NAME } from "../utils/constants.js";
|
|
4
|
+
|
|
5
|
+
export function helpCommand() {
|
|
6
|
+
printBlank();
|
|
7
|
+
console.log(` ${whiteBold(`${APP_NAME} — Command Reference`)}`);
|
|
8
|
+
printBlank();
|
|
9
|
+
|
|
10
|
+
const commands = [
|
|
11
|
+
["chat signup", "Create a new account"],
|
|
12
|
+
["chat login", "Login to your account"],
|
|
13
|
+
["chat logout", "Logout and clear credentials"],
|
|
14
|
+
["chat whoami", "Show current user info"],
|
|
15
|
+
["chat connect", "Connect to chat"],
|
|
16
|
+
["chat help", "Show this help menu"],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (const [cmd, desc] of commands) {
|
|
20
|
+
console.log(` ${brand(cmd.padEnd(20))} ${muted(desc)}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
printBlank();
|
|
24
|
+
console.log(` ${whiteBold("Inside Chat:")}`);
|
|
25
|
+
printBlank();
|
|
26
|
+
|
|
27
|
+
const chatCommands = [
|
|
28
|
+
["/global", "Switch to global chat"],
|
|
29
|
+
["/dm <username>", "Switch to private messaging"],
|
|
30
|
+
["/help", "Show chat commands"],
|
|
31
|
+
["/exit", "Disconnect and exit"],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const [cmd, desc] of chatCommands) {
|
|
35
|
+
console.log(` ${accent(cmd.padEnd(20))} ${muted(desc)}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
printBlank();
|
|
39
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
// ─── Config Path ─────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), ".cheatchat");
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
9
|
+
|
|
10
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
function ensureConfigDir() {
|
|
13
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
14
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readConfig() {
|
|
19
|
+
ensureConfigDir();
|
|
20
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeConfig(data) {
|
|
32
|
+
ensureConfigDir();
|
|
33
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Save authentication credentials to disk.
|
|
40
|
+
*/
|
|
41
|
+
export function saveAuth(username, token) {
|
|
42
|
+
writeConfig({ username, token });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load stored authentication credentials.
|
|
47
|
+
* Returns { username, token } or null if not logged in.
|
|
48
|
+
*/
|
|
49
|
+
export function loadAuth() {
|
|
50
|
+
const config = readConfig();
|
|
51
|
+
if (config.username && config.token) {
|
|
52
|
+
return { username: config.username, token: config.token };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Remove stored authentication credentials (logout).
|
|
59
|
+
*/
|
|
60
|
+
export function clearAuth() {
|
|
61
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
62
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if the user is currently authenticated.
|
|
68
|
+
*/
|
|
69
|
+
export function isLoggedIn() {
|
|
70
|
+
return loadAuth() !== null;
|
|
71
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { showBanner } from "./ui/banner.js";
|
|
3
|
+
import { signupCommand } from "./commands/signup.js";
|
|
4
|
+
import { loginCommand } from "./commands/login.js";
|
|
5
|
+
import { logoutCommand } from "./commands/logout.js";
|
|
6
|
+
import { helpCommand } from "./commands/help.js";
|
|
7
|
+
import { connectCommand } from "./commands/connect.js";
|
|
8
|
+
import { whoamiFlow } from "./auth/whoami.js";
|
|
9
|
+
import { APP_NAME, APP_VERSION, APP_DESCRIPTION } from "./utils/constants.js";
|
|
10
|
+
|
|
11
|
+
// ─── CLI Program ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name("chat")
|
|
17
|
+
.version(APP_VERSION)
|
|
18
|
+
.description(APP_DESCRIPTION);
|
|
19
|
+
|
|
20
|
+
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command("signup")
|
|
24
|
+
.description("Create a new CheatChat account")
|
|
25
|
+
.action(async () => {
|
|
26
|
+
showBanner();
|
|
27
|
+
await signupCommand();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command("login")
|
|
32
|
+
.description("Login to your CheatChat account")
|
|
33
|
+
.action(async () => {
|
|
34
|
+
showBanner();
|
|
35
|
+
await loginCommand();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command("logout")
|
|
40
|
+
.description("Logout and clear stored credentials")
|
|
41
|
+
.action(() => {
|
|
42
|
+
showBanner();
|
|
43
|
+
logoutCommand();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
program
|
|
47
|
+
.command("whoami")
|
|
48
|
+
.description("Show current logged-in user")
|
|
49
|
+
.action(async () => {
|
|
50
|
+
showBanner();
|
|
51
|
+
await whoamiFlow();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
program
|
|
55
|
+
.command("connect")
|
|
56
|
+
.description("Connect to CheatChat and start messaging")
|
|
57
|
+
.action(async () => {
|
|
58
|
+
showBanner();
|
|
59
|
+
await connectCommand();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
program
|
|
63
|
+
.command("help")
|
|
64
|
+
.description("Show all available commands")
|
|
65
|
+
.action(() => {
|
|
66
|
+
showBanner();
|
|
67
|
+
helpCommand();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ─── Default (no command) ────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
if (process.argv.length <= 2) {
|
|
73
|
+
showBanner();
|
|
74
|
+
helpCommand();
|
|
75
|
+
} else {
|
|
76
|
+
program.parse(process.argv);
|
|
77
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { REST_API_URL } from "../utils/constants.js";
|
|
3
|
+
import { logError } from "../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
// ─── Axios Instance ──────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const api = axios.create({
|
|
8
|
+
baseURL: REST_API_URL,
|
|
9
|
+
timeout: 15000,
|
|
10
|
+
headers: { "Content-Type": "application/json" },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// ─── Auth Endpoints ──────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register a new user.
|
|
17
|
+
* Returns { success, username, token, message, error }
|
|
18
|
+
*/
|
|
19
|
+
export async function signup(username, password) {
|
|
20
|
+
try {
|
|
21
|
+
const { data } = await api.post("/auth/signup", { username, password });
|
|
22
|
+
return data;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return extractError(err);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Authenticate an existing user.
|
|
30
|
+
* Returns { success, username, token, message, error }
|
|
31
|
+
*/
|
|
32
|
+
export async function login(username, password) {
|
|
33
|
+
try {
|
|
34
|
+
const { data } = await api.post("/auth/login", { username, password });
|
|
35
|
+
return data;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return extractError(err);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the current authenticated user's profile.
|
|
43
|
+
* Returns { success, user, error }
|
|
44
|
+
*/
|
|
45
|
+
export async function getMe(token) {
|
|
46
|
+
try {
|
|
47
|
+
const { data } = await api.get("/auth/me", {
|
|
48
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
49
|
+
});
|
|
50
|
+
return data;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return extractError(err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Error Extraction ────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function extractError(err) {
|
|
59
|
+
if (err.response?.data) {
|
|
60
|
+
return err.response.data;
|
|
61
|
+
}
|
|
62
|
+
logError("api", err);
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
error: err.message || "Network error — please check your connection",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { WS_API_URL } from "../utils/constants.js";
|
|
3
|
+
import { logError, debug } from "../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create and return a WebSocket connection.
|
|
7
|
+
* @param {string} token - JWT auth token
|
|
8
|
+
* @param {object} handlers - { onOpen, onMessage, onClose, onError }
|
|
9
|
+
* @returns {WebSocket}
|
|
10
|
+
*/
|
|
11
|
+
export function createSocket(token, handlers = {}) {
|
|
12
|
+
const url = `${WS_API_URL}?token=${token}`;
|
|
13
|
+
debug("Connecting to WebSocket:", url);
|
|
14
|
+
|
|
15
|
+
const ws = new WebSocket(url);
|
|
16
|
+
|
|
17
|
+
ws.on("open", () => {
|
|
18
|
+
debug("WebSocket connected");
|
|
19
|
+
handlers.onOpen?.();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
ws.on("message", (raw) => {
|
|
23
|
+
try {
|
|
24
|
+
const data = JSON.parse(raw.toString());
|
|
25
|
+
debug("WS message:", data);
|
|
26
|
+
handlers.onMessage?.(data);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
logError("ws-parse", err);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
ws.on("close", (code, reason) => {
|
|
33
|
+
debug("WebSocket closed:", code, reason?.toString());
|
|
34
|
+
handlers.onClose?.(code, reason?.toString());
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
ws.on("error", (err) => {
|
|
38
|
+
logError("ws-error", err);
|
|
39
|
+
handlers.onError?.(err);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return ws;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Send a global message through the WebSocket.
|
|
47
|
+
*/
|
|
48
|
+
export function sendGlobalMessage(ws, text) {
|
|
49
|
+
if (ws.readyState !== WebSocket.OPEN) return false;
|
|
50
|
+
ws.send(JSON.stringify({
|
|
51
|
+
action: "message",
|
|
52
|
+
type: "global",
|
|
53
|
+
text,
|
|
54
|
+
}));
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Send a private message through the WebSocket.
|
|
60
|
+
*/
|
|
61
|
+
export function sendPrivateMessage(ws, to, text) {
|
|
62
|
+
if (ws.readyState !== WebSocket.OPEN) return false;
|
|
63
|
+
ws.send(JSON.stringify({
|
|
64
|
+
action: "message",
|
|
65
|
+
type: "private",
|
|
66
|
+
to,
|
|
67
|
+
text,
|
|
68
|
+
}));
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Close the WebSocket connection gracefully.
|
|
74
|
+
*/
|
|
75
|
+
export function closeSocket(ws) {
|
|
76
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
77
|
+
ws.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/ui/banner.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import boxen from "boxen";
|
|
2
|
+
import { brand, accent, muted, whiteBold } from "./color.js";
|
|
3
|
+
import { APP_NAME, APP_VERSION, APP_DESCRIPTION } from "../utils/constants.js";
|
|
4
|
+
|
|
5
|
+
// ─── ASCII Art Logo ──────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const LOGO = `
|
|
8
|
+
██████╗██╗ ██╗███████╗ █████╗ ████████╗
|
|
9
|
+
██╔════╝██║ ██║██╔════╝██╔══██╗╚══██╔══╝
|
|
10
|
+
██║ ███████║█████╗ ███████║ ██║
|
|
11
|
+
██║ ██╔══██║██╔══╝ ██╔══██║ ██║
|
|
12
|
+
╚██████╗██║ ██║███████╗██║ ██║ ██║
|
|
13
|
+
╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝
|
|
14
|
+
██████╗██╗ ██╗ █████╗ ████████╗
|
|
15
|
+
██╔════╝██║ ██║██╔══██╗╚══██╔══╝
|
|
16
|
+
██║ ███████║███████║ ██║
|
|
17
|
+
██║ ██╔══██║██╔══██║ ██║
|
|
18
|
+
╚██████╗██║ ██║██║ ██║ ██║
|
|
19
|
+
╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝`;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Display the welcome banner.
|
|
23
|
+
*/
|
|
24
|
+
export function showBanner() {
|
|
25
|
+
const logo = brand(LOGO);
|
|
26
|
+
const tagline = `\n ${whiteBold(APP_NAME)} ${muted(`v${APP_VERSION}`)}`;
|
|
27
|
+
const desc = ` ${accent(APP_DESCRIPTION)}`;
|
|
28
|
+
|
|
29
|
+
const box = boxen(`${logo}\n${tagline}\n${desc}`, {
|
|
30
|
+
padding: { top: 0, bottom: 1, left: 2, right: 2 },
|
|
31
|
+
borderColor: "#6C63FF",
|
|
32
|
+
borderStyle: "round",
|
|
33
|
+
dimBorder: false,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
console.log(box);
|
|
37
|
+
console.log();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Display a compact header for chat mode.
|
|
42
|
+
*/
|
|
43
|
+
export function showChatHeader(username, mode, target) {
|
|
44
|
+
const modeLabel = mode === "global"
|
|
45
|
+
? accent("🌍 Global Chat")
|
|
46
|
+
: accent(`🔒 DM → ${whiteBold(target)}`);
|
|
47
|
+
|
|
48
|
+
const box = boxen(
|
|
49
|
+
`${whiteBold(APP_NAME)} ${muted("•")} ${muted("logged in as")} ${brand(username)} ${muted("•")} ${modeLabel}`,
|
|
50
|
+
{
|
|
51
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
52
|
+
borderColor: "#6C63FF",
|
|
53
|
+
borderStyle: "round",
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
console.log(box);
|
|
58
|
+
}
|
package/src/ui/color.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
// ─── Brand Colors ────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export const brand = chalk.hex("#6C63FF"); // Primary purple
|
|
6
|
+
export const brandBold = chalk.hex("#6C63FF").bold;
|
|
7
|
+
export const accent = chalk.hex("#00D9FF"); // Cyan accent
|
|
8
|
+
export const accentBold = chalk.hex("#00D9FF").bold;
|
|
9
|
+
export const success = chalk.hex("#00E676"); // Green
|
|
10
|
+
export const warning = chalk.hex("#FFD600"); // Yellow
|
|
11
|
+
export const danger = chalk.hex("#FF5252"); // Red
|
|
12
|
+
export const muted = chalk.hex("#888888"); // Gray
|
|
13
|
+
export const subtle = chalk.hex("#555555"); // Darker gray
|
|
14
|
+
export const white = chalk.white;
|
|
15
|
+
export const whiteBold = chalk.white.bold;
|
|
16
|
+
export const dim = chalk.dim;
|
|
17
|
+
|
|
18
|
+
// ─── Message Colors ─────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export const globalTag = chalk.bgHex("#6C63FF").white.bold;
|
|
21
|
+
export const privateTag = chalk.bgHex("#FF6B6B").white.bold;
|
|
22
|
+
export const systemTag = chalk.bgHex("#FFD600").black.bold;
|
|
23
|
+
export const errorTag = chalk.bgHex("#FF5252").white.bold;
|
|
24
|
+
|
|
25
|
+
export const senderSelf = chalk.hex("#00E676").bold;
|
|
26
|
+
export const senderOther = chalk.hex("#00D9FF").bold;
|
|
27
|
+
export const timestamp = chalk.hex("#666666");
|
|
28
|
+
export const messageText = chalk.white;
|
|
29
|
+
|
|
30
|
+
// ─── UI Elements ─────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export const separator = chalk.hex("#333333");
|
|
33
|
+
export const prompt = chalk.hex("#6C63FF").bold;
|
|
34
|
+
export const inputArrow = chalk.hex("#6C63FF")("›");
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
globalTag,
|
|
3
|
+
privateTag,
|
|
4
|
+
systemTag,
|
|
5
|
+
errorTag,
|
|
6
|
+
senderSelf,
|
|
7
|
+
senderOther,
|
|
8
|
+
timestamp as tsColor,
|
|
9
|
+
messageText,
|
|
10
|
+
muted,
|
|
11
|
+
success,
|
|
12
|
+
danger,
|
|
13
|
+
warning,
|
|
14
|
+
accent,
|
|
15
|
+
brand,
|
|
16
|
+
whiteBold,
|
|
17
|
+
dim,
|
|
18
|
+
} from "./color.js";
|
|
19
|
+
|
|
20
|
+
// ─── Formatters ──────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function formatTime(ts) {
|
|
23
|
+
const d = ts ? new Date(ts) : new Date();
|
|
24
|
+
return d.toLocaleTimeString("en-US", {
|
|
25
|
+
hour: "2-digit",
|
|
26
|
+
minute: "2-digit",
|
|
27
|
+
hour12: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Message Printers ────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Print a global chat message.
|
|
35
|
+
*/
|
|
36
|
+
export function printGlobalMessage(sender, text, ts, currentUser) {
|
|
37
|
+
const time = tsColor(formatTime(ts));
|
|
38
|
+
const tag = globalTag(" GLOBAL ");
|
|
39
|
+
const name = sender === currentUser ? senderSelf(sender) : senderOther(sender);
|
|
40
|
+
console.log(` ${tag} ${time} ${name}: ${messageText(text)}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Print a private/DM message.
|
|
45
|
+
*/
|
|
46
|
+
export function printPrivateMessage(sender, text, ts, currentUser) {
|
|
47
|
+
const time = tsColor(formatTime(ts));
|
|
48
|
+
const tag = privateTag(" DM ");
|
|
49
|
+
const name = sender === currentUser ? senderSelf("You") : senderOther(sender);
|
|
50
|
+
console.log(` ${tag} ${time} ${name}: ${messageText(text)}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Print a system notification.
|
|
55
|
+
*/
|
|
56
|
+
export function printSystem(text) {
|
|
57
|
+
console.log(` ${systemTag(" SYS ")} ${muted(text)}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Print an error message.
|
|
62
|
+
*/
|
|
63
|
+
export function printError(text) {
|
|
64
|
+
console.log(` ${errorTag(" ERR ")} ${danger(text)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Print a success message.
|
|
69
|
+
*/
|
|
70
|
+
export function printSuccess(text) {
|
|
71
|
+
console.log(` ${success("✓")} ${success(text)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Print a warning.
|
|
76
|
+
*/
|
|
77
|
+
export function printWarning(text) {
|
|
78
|
+
console.log(` ${warning("⚠")} ${warning(text)}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Print info.
|
|
83
|
+
*/
|
|
84
|
+
export function printInfo(text) {
|
|
85
|
+
console.log(` ${accent("ℹ")} ${accent(text)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Print the in-chat help menu.
|
|
90
|
+
*/
|
|
91
|
+
export function printChatHelp() {
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(` ${whiteBold("Chat Commands:")}`);
|
|
94
|
+
console.log(` ${brand("/global")} ${muted("Switch to global chat")}`);
|
|
95
|
+
console.log(` ${brand("/dm <username>")} ${muted("Switch to private messaging")}`);
|
|
96
|
+
console.log(` ${brand("/help")} ${muted("Show this help menu")}`);
|
|
97
|
+
console.log(` ${brand("/exit")} ${muted("Disconnect and exit")}`);
|
|
98
|
+
console.log();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Print current chat mode indicator.
|
|
103
|
+
*/
|
|
104
|
+
export function printModeSwitch(mode, target) {
|
|
105
|
+
console.log();
|
|
106
|
+
if (mode === "global") {
|
|
107
|
+
printSystem(`Switched to ${accent("🌍 Global Chat")} — messages go to everyone`);
|
|
108
|
+
} else {
|
|
109
|
+
printSystem(`Switched to ${accent(`🔒 DM → ${whiteBold(target)}`)} — messages are private`);
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Print a blank line.
|
|
116
|
+
*/
|
|
117
|
+
export function printBlank() {
|
|
118
|
+
console.log();
|
|
119
|
+
}
|
package/src/ui/tables.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { muted } from "./color.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Print a simple table for user profile info.
|
|
5
|
+
*/
|
|
6
|
+
export function printUserTable(user) {
|
|
7
|
+
const rows = [
|
|
8
|
+
["Username", user.username],
|
|
9
|
+
["Created", new Date(user.createdAt).toLocaleString()],
|
|
10
|
+
["Last Seen", new Date(user.lastSeen).toLocaleString()],
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const maxKey = Math.max(...rows.map((r) => r[0].length));
|
|
14
|
+
|
|
15
|
+
console.log();
|
|
16
|
+
for (const [key, value] of rows) {
|
|
17
|
+
console.log(` ${muted(key.padEnd(maxKey + 2))} ${value}`);
|
|
18
|
+
}
|
|
19
|
+
console.log();
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ─── API Configuration ───────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export const REST_API_URL = "https://bwkwmpi8ek.execute-api.ap-south-1.amazonaws.com/dev";
|
|
4
|
+
export const WS_API_URL = "wss://lhyew4zkm2.execute-api.ap-south-1.amazonaws.com/dev/";
|
|
5
|
+
|
|
6
|
+
// ─── App Metadata ────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export const APP_NAME = "CheatChat";
|
|
9
|
+
export const APP_VERSION = "1.0.0";
|
|
10
|
+
export const APP_DESCRIPTION = "A minimal terminal chat app powered by AWS.";
|
|
11
|
+
|
|
12
|
+
// ─── Chat Modes ──────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export const CHAT_MODE = {
|
|
15
|
+
GLOBAL: "global",
|
|
16
|
+
PRIVATE: "private",
|
|
17
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { muted } from "../ui/color.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple logger for debug output (disabled by default).
|
|
5
|
+
*/
|
|
6
|
+
const DEBUG = process.env.CHEATCHAT_DEBUG === "true";
|
|
7
|
+
|
|
8
|
+
export function debug(...args) {
|
|
9
|
+
if (DEBUG) {
|
|
10
|
+
console.log(muted("[DEBUG]"), ...args);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function logError(context, err) {
|
|
15
|
+
if (DEBUG) {
|
|
16
|
+
console.error(muted(`[ERROR:${context}]`), err);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a username string.
|
|
3
|
+
* Returns { valid, message }
|
|
4
|
+
*/
|
|
5
|
+
export function validateUsername(input) {
|
|
6
|
+
const trimmed = input.trim();
|
|
7
|
+
if (trimmed.length < 3) {
|
|
8
|
+
return { valid: false, message: "Username must be at least 3 characters" };
|
|
9
|
+
}
|
|
10
|
+
if (trimmed.length > 20) {
|
|
11
|
+
return { valid: false, message: "Username must be at most 20 characters" };
|
|
12
|
+
}
|
|
13
|
+
if (!/^[a-zA-Z0-9_]+$/.test(trimmed)) {
|
|
14
|
+
return { valid: false, message: "Username can only contain letters, numbers, and underscores" };
|
|
15
|
+
}
|
|
16
|
+
return { valid: true, message: null };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate a password string.
|
|
21
|
+
* Returns { valid, message }
|
|
22
|
+
*/
|
|
23
|
+
export function validatePassword(input) {
|
|
24
|
+
if (input.length < 6) {
|
|
25
|
+
return { valid: false, message: "Password must be at least 6 characters" };
|
|
26
|
+
}
|
|
27
|
+
if (input.length > 128) {
|
|
28
|
+
return { valid: false, message: "Password must be at most 128 characters" };
|
|
29
|
+
}
|
|
30
|
+
return { valid: true, message: null };
|
|
31
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createSocket } from "../services/socket.js";
|
|
2
|
+
import { createSpinner } from "../ui/spinner.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Establish a WebSocket connection and return a promise that resolves
|
|
6
|
+
* with the connected WebSocket, or rejects on failure.
|
|
7
|
+
* @param {string} token - JWT auth token
|
|
8
|
+
* @param {function} onMessage - Handler for incoming messages
|
|
9
|
+
* @param {function} onClose - Handler for connection close
|
|
10
|
+
* @returns {Promise<WebSocket>}
|
|
11
|
+
*/
|
|
12
|
+
export function connectWebSocket(token, onMessage, onClose) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const spinner = createSpinner("Connecting to CheatChat...");
|
|
15
|
+
spinner.start();
|
|
16
|
+
|
|
17
|
+
const ws = createSocket(token, {
|
|
18
|
+
onOpen: () => {
|
|
19
|
+
spinner.succeed("Connected to CheatChat!");
|
|
20
|
+
resolve(ws);
|
|
21
|
+
},
|
|
22
|
+
onMessage,
|
|
23
|
+
onClose: (code, reason) => {
|
|
24
|
+
onClose?.(code, reason);
|
|
25
|
+
},
|
|
26
|
+
onError: (err) => {
|
|
27
|
+
spinner.fail("Connection failed");
|
|
28
|
+
reject(err);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Timeout after 10 seconds
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
if (ws.readyState !== 1) {
|
|
35
|
+
spinner.fail("Connection timed out");
|
|
36
|
+
ws.close();
|
|
37
|
+
reject(new Error("Connection timed out"));
|
|
38
|
+
}
|
|
39
|
+
}, 10000);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { closeSocket } from "../services/socket.js";
|
|
2
|
+
import { printSystem, printBlank } from "../ui/printer.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Gracefully disconnect from the WebSocket.
|
|
6
|
+
*/
|
|
7
|
+
export function disconnectWebSocket(ws) {
|
|
8
|
+
printBlank();
|
|
9
|
+
printSystem("Disconnecting...");
|
|
10
|
+
closeSocket(ws);
|
|
11
|
+
printSystem("Goodbye! 👋");
|
|
12
|
+
printBlank();
|
|
13
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { printGlobalMessage, printPrivateMessage, printError, printSystem } from "../ui/printer.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handle incoming WebSocket messages and dispatch to the appropriate printer.
|
|
5
|
+
* @param {object} data - Parsed JSON message from the server
|
|
6
|
+
* @param {string} currentUser - The local user's username
|
|
7
|
+
*/
|
|
8
|
+
export function handleIncomingMessage(data, currentUser) {
|
|
9
|
+
switch (data.event) {
|
|
10
|
+
case "global_message":
|
|
11
|
+
printGlobalMessage(data.sender, data.text, data.timestamp, currentUser);
|
|
12
|
+
break;
|
|
13
|
+
|
|
14
|
+
case "private_message":
|
|
15
|
+
printPrivateMessage(data.sender, data.text, data.timestamp, currentUser);
|
|
16
|
+
break;
|
|
17
|
+
|
|
18
|
+
case "error":
|
|
19
|
+
printError(data.message || "Unknown server error");
|
|
20
|
+
break;
|
|
21
|
+
|
|
22
|
+
default:
|
|
23
|
+
printSystem(`Unknown event: ${data.event}`);
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { sendGlobalMessage, sendPrivateMessage } from "../services/socket.js";
|
|
2
|
+
import { printError } from "../ui/printer.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Send a message based on the current chat mode.
|
|
6
|
+
* @param {WebSocket} ws - Active WebSocket connection
|
|
7
|
+
* @param {string} mode - "global" or "private"
|
|
8
|
+
* @param {string} text - Message text
|
|
9
|
+
* @param {string} [dmTarget] - Target username for private messages
|
|
10
|
+
*/
|
|
11
|
+
export function sendMessage(ws, mode, text, dmTarget) {
|
|
12
|
+
if (!text.trim()) return;
|
|
13
|
+
|
|
14
|
+
if (mode === "global") {
|
|
15
|
+
const ok = sendGlobalMessage(ws, text);
|
|
16
|
+
if (!ok) printError("Not connected. Message not sent.");
|
|
17
|
+
} else if (mode === "private") {
|
|
18
|
+
if (!dmTarget) {
|
|
19
|
+
printError("No DM target set. Use /dm <username>");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const ok = sendPrivateMessage(ws, dmTarget, text);
|
|
23
|
+
if (!ok) printError("Not connected. Message not sent.");
|
|
24
|
+
}
|
|
25
|
+
}
|