composter-cli 1.0.14 → 1.0.15

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/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "composter-cli",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
+ "private": false,
4
5
  "type": "module",
5
6
  "description": "Your personal vault for React components. Push, pull, and sync reusable components across projects — like shadcn/ui but for YOUR code.",
6
7
  "main": "src/index.js",
@@ -36,8 +37,9 @@
36
37
  "node": ">=18.0.0"
37
38
  },
38
39
  "dependencies": {
40
+ "chalk": "^5.6.2",
39
41
  "commander": "^14.0.2",
40
- "dotenv": "^16.4.5",
42
+ "dotenv": "^17.2.3",
41
43
  "inquirer": "^13.0.1",
42
44
  "node-fetch": "^3.3.2"
43
45
  },
@@ -1,61 +1,33 @@
1
+ import chalk from "chalk";
1
2
  import { apiRequest } from "../utils/request.js";
2
- import { loadSession } from "../utils/session.js";
3
+ import { log } from "../utils/log.js";
3
4
 
4
5
  export async function listCategories() {
5
- const session = loadSession();
6
- if (!session || !session.jwt) {
7
- console.log("You must be logged in. Run: composter login");
8
- return;
9
- }
10
6
 
11
- try{
12
- const res = await apiRequest("/categories", {
13
- method: "GET",
14
- headers: { "Content-Type": "application/json" },
15
- });
16
- let body = null;
17
- try {
18
- body = await res.json();
19
- } catch {
20
- // Ignore if no JSON
21
- }
7
+ const res = await apiRequest("/categories", {
8
+ method: "GET",
9
+ headers: { "Content-Type": "application/json" },
10
+ });
22
11
 
23
- // Handle auth failure
24
- if (res.status === 401) {
25
- console.log("Session expired. Run composter login again.");
26
- return;
27
- }
12
+ let body = null;
28
13
 
29
- // Handle server errors
30
- if (res.status >= 500) {
31
- console.log("Server error. Try again later.");
32
- return;
33
- }
14
+ try {
15
+ body = await res.json();
16
+ } catch {
17
+ // Ignore if no JSON
18
+ }
34
19
 
35
- // Handle success
36
- if (res.ok) {
37
- const categories = body?.categories || [];
38
- if (categories.length === 0) {
39
- console.log("No categories found.");
40
- return;
41
- }
42
- categories.forEach((cat) => {
43
- //list them adjacent to each other with tab space between
44
- process.stdout.write(`${cat.name}\t\t`);
45
- });
46
- console.log();
47
- return;
48
- }
49
20
 
50
- // Handle other errors
51
- const errorMessage =
52
- (body && (body.message || body.error || JSON.stringify(body))) ||
53
- res.statusText ||
54
- `HTTP ${res.status}`;
55
- console.log("Error listing categories:", errorMessage);
56
- return;
57
- } catch (error) {
58
- console.log("Error fetching categories:", error);
21
+
22
+ const categories = body?.categories || [];
23
+ if (categories.length === 0) {
24
+ log.info("No categories found.");
59
25
  return;
60
26
  }
27
+ categories.forEach((cat) => {
28
+ //list them adjacent to each other with tab space between
29
+ process.stdout.write(chalk.cyan.bold(cat.name) + "\t\t");
30
+ });
31
+ console.log();
32
+ return;
61
33
  }
@@ -1,75 +1,70 @@
1
1
  import inquirer from "inquirer";
2
- import fetch from "node-fetch";
2
+ import { safeFetch } from "../utils/safeFetch.js";
3
3
  import { saveSession } from "../utils/session.js";
4
- import dotenv from "dotenv";
5
- import { fileURLToPath } from 'url';
6
- import { dirname, join } from 'path';
4
+ import { fileURLToPath } from "url";
5
+ import { dirname } from "path";
6
+ import { log } from "../utils/log.js";
7
+ import { handleFetchError } from "../utils/errorHandlers/fetchErrorHandler.js";
8
+ import chalk from "chalk";
9
+ import { composterLoginArtv2 } from "../constants/asciiArts.js";
7
10
 
8
11
  const __filename = fileURLToPath(import.meta.url);
9
12
  const __dirname = dirname(__filename);
10
- dotenv.config({ path: join(__dirname, '../../.env') });
11
13
 
12
- const BASE_URL = `${process.env.BASE_URL || "https://composter.onrender.com/api"}/auth`;
14
+ const BASE_URL = `${process.env.BASE_URL}/auth`;
13
15
 
14
16
  export async function login() {
15
- console.log("=== Composter Login ===");
17
+ console.log(chalk.bold.blue(composterLoginArtv2));
16
18
 
17
19
  const { email, password } = await inquirer.prompt([
18
20
  { type: "input", name: "email", message: "Email:" },
19
21
  { type: "password", name: "password", message: "Password:" }
20
22
  ]);
21
23
 
22
- // Step 1 — Sign in
23
- const res = await fetch(`${BASE_URL}/sign-in/email`, {
24
- method: "POST",
25
- headers: { "Content-Type": "application/json" },
26
- body: JSON.stringify({ email, password })
27
- });
28
-
29
- if (!res.ok) {
30
- // try to parse JSON error body, fall back to statusText
31
- let errBody = null;
32
- try {
33
- errBody = await res.json();
34
- } catch (e) {
35
- // body wasn't JSON or couldn't be parsed
36
- }
37
- const message =
38
- (errBody && (errBody.message || errBody.error || JSON.stringify(errBody))) ||
39
- res.statusText ||
40
- `HTTP ${res.status}`;
41
- console.log("\nLogin failed:", message);
24
+ const isValidEmail = /\S+@\S+\.\S+/.test(email);
25
+ if (!isValidEmail) {
26
+ log.error("Please enter a valid email address.");
42
27
  return;
43
28
  }
44
29
 
45
- // Step 2 — Extract session cookie
46
- const cookie = res.headers.get("set-cookie");
47
- if (!cookie) {
48
- console.log("Failed: No session cookie returned.");
49
- return;
50
- }
30
+ try {
31
+
32
+ // Step 1: Sign in with email and password to obtain session cookie
33
+ const res = await safeFetch(`${BASE_URL}/sign-in/email`, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify({ email, password })
37
+ });
51
38
 
52
- // Step 3 — Fetch JWT token
53
- const tokenRes = await fetch(`${BASE_URL}/token`, {
54
- method: "GET",
55
- headers: { "Cookie": cookie }
56
- });
39
+ const cookie = res.headers.get("set-cookie");
40
+ if (!cookie) {
41
+ log.error("Unable to retrieve session cookie. Login failed.");
42
+ return;
43
+ }
44
+
45
+ const tokenRes = await safeFetch(`${BASE_URL}/token`, {
46
+ method: "GET",
47
+ headers: { Cookie: cookie }
48
+ });
57
49
 
58
- let token = null;
59
- if (tokenRes.ok) {
60
50
  const json = await tokenRes.json();
61
- token = json.token;
62
- }
51
+ const token = json.token;
52
+
53
+ const expiresAt = new Date(
54
+ Date.now() + 30 * 24 * 60 * 60 * 1000
55
+ ).toISOString();
63
56
 
64
- // Step 4 — Save session + jwt locally with expiration
65
- const expiresAt = new Date(Date.now() + (30 * 24 * 60 * 60 * 1000)).toISOString(); // 30 days
66
- saveSession({
67
- cookies: cookie,
68
- jwt: token,
69
- createdAt: new Date().toISOString(),
70
- expiresAt: expiresAt
71
- });
57
+ saveSession({
58
+ cookies: cookie,
59
+ jwt: token,
60
+ createdAt: new Date().toISOString(),
61
+ expiresAt
62
+ });
63
+
64
+ log.success("You have successfully logged in");
65
+ log.info(`Session expires: ${expiresAt}`);
66
+ } catch (err) {
67
+ handleFetchError(err);
68
+ }
72
69
 
73
- console.log("\nLogged in successfully!");
74
- console.log(`Session expires: ${expiresAt}`);
75
70
  }
@@ -1,5 +1,5 @@
1
+ import { log } from "../utils/log.js";
1
2
  import { apiRequest } from "../utils/request.js";
2
- import { loadSession } from "../utils/session.js";
3
3
 
4
4
  export async function mkcat(categoryName) {
5
5
  // Validate input
@@ -9,21 +9,12 @@ export async function mkcat(categoryName) {
9
9
  categoryName.includes(" ") ||
10
10
  categoryName.length > 10
11
11
  ) {
12
- console.log(
13
- "Invalid category name. It must be non-empty, without spaces, and 10 characters."
12
+ log.warn(
13
+ "Invalid category name. It must be non-empty, without spaces, and at most 10 characters."
14
14
  );
15
- return;
15
+ process.exit(1);
16
16
  }
17
17
 
18
- // Check session
19
- const session = loadSession();
20
- if (!session || !session.jwt) {
21
- console.log("You must be logged in. Run: composter login");
22
- return;
23
- }
24
-
25
- try {
26
- // Send request
27
18
  const res = await apiRequest("/categories", {
28
19
  method: "POST",
29
20
  headers: { "Content-Type": "application/json" },
@@ -38,34 +29,16 @@ export async function mkcat(categoryName) {
38
29
  // Ignore if no JSON
39
30
  }
40
31
 
41
- // Handle auth failure
42
- if (res.status === 401) {
43
- console.log("Session expired. Run composter login again.");
44
- return;
45
- }
46
-
47
- // Handle server errors
48
- if (res.status >= 500) {
49
- console.log("Server error. Try again later.");
50
- return;
51
- }
52
-
53
- // Handle success
54
32
  if (res.ok) {
55
- console.log(`Category '${categoryName}' created successfully!`);
56
- console.log("ID:", body?.category?.id);
57
- return;
33
+ log.info(`Category '${categoryName}' created successfully!`);
34
+ process.exit(0);
58
35
  }
59
36
 
60
- // Handle other errors
37
+ // handling server sent errors, like duplicate category
61
38
  const msg =
62
- body?.error ||
63
- body?.message ||
64
- JSON.stringify(body) ||
39
+ (body && (body.error || body.message || JSON.stringify(body))) ||
65
40
  `HTTP ${res.status}`;
66
41
 
67
- console.log("Failed to create category:", msg);
68
- } catch (err) {
69
- console.log("Network or unexpected error:", err.message);
70
- }
42
+ log.error(`Failed to create category: ${msg}`);
43
+ process.exit(1);
71
44
  }
@@ -1,28 +1,22 @@
1
+ import chalk from "chalk";
2
+ import { log } from "../utils/log.js";
1
3
  import { apiRequest } from "../utils/request.js";
2
- import { loadSession } from "../utils/session.js";
3
4
  import fs from "fs";
4
5
  import path from "path";
5
6
 
6
7
  export async function pullComponent(category, title, targetDir) {
7
8
  // 1. Validate Input
9
+ // although commander ensures these are provided, we double-check here
8
10
  if (!category?.trim() || !title?.trim() || !targetDir?.trim()) {
9
- console.log("Category, title, and target directory are required.");
10
- return;
11
+ log.error("Category, title, and target directory are required.");
12
+ process.exit(1);
11
13
  }
12
14
 
13
15
  // 2. Resolve Target Directory
14
16
  // In multi-file mode, the target is usually a FOLDER, not a specific file.
15
17
  const absoluteRoot = path.resolve(targetDir);
16
18
 
17
- // 3. Check Session
18
- const session = loadSession();
19
- if (!session || !session.jwt) {
20
- console.log("❌ You must be logged in. Run: composter login");
21
- return;
22
- }
23
-
24
- try {
25
- console.log(`⏳ Fetching '${title}' from '${category}'...`);
19
+ log.info(`⏳ Fetching '${title}' from '${category}'...`);
26
20
 
27
21
  const res = await apiRequest(`/components?category=${encodeURIComponent(category)}&title=${encodeURIComponent(title)}`, {
28
22
  method: "GET",
@@ -33,22 +27,9 @@ export async function pullComponent(category, title, targetDir) {
33
27
  let body = null;
34
28
  try { body = await res.json(); } catch {}
35
29
 
36
- if (res.status === 401) {
37
- console.log("❌ Session expired. Run composter login again.");
38
- return;
39
- }
40
- if (res.status === 404) {
41
- console.log(`❌ Component '${title}' not found.`);
42
- return;
43
- }
44
- if (!res.ok) {
45
- console.log("❌ Server error:", body?.error || res.statusText);
46
- return;
47
- }
48
-
49
- const component = body.component;
30
+ const component = body.component ?? null;
50
31
 
51
- // --- STEP 4: PARSE FILES (Handle JSON vs String) ---
32
+ // PARSE FILES (Handle JSON vs String)
52
33
  let filesMap = {};
53
34
  try {
54
35
  // Try to parse new multi-file format
@@ -64,8 +45,8 @@ export async function pullComponent(category, title, targetDir) {
64
45
  filesMap[`/${fileName}`] = component.code;
65
46
  }
66
47
 
67
- // --- STEP 5: WRITE FILES TO DISK ---
68
- console.log(`📦 Unpacking ${Object.keys(filesMap).length} file(s) into: ${absoluteRoot}`);
48
+ // WRITE FILES TO DISK
49
+ log.info(`📦 Unpacking ${Object.keys(filesMap).length} file(s) into: ${absoluteRoot}`);
69
50
 
70
51
  // Ensure the root target folder exists
71
52
  if (!fs.existsSync(absoluteRoot)) {
@@ -90,19 +71,15 @@ export async function pullComponent(category, title, targetDir) {
90
71
  // Write file
91
72
  fs.writeFileSync(writePath, content, "utf-8");
92
73
  createdFiles.push(relPath);
93
- console.log(` + ${relPath}`);
74
+ console.log(chalk.cyan(` + ${relPath}`));
94
75
  }
95
76
 
96
- // --- STEP 6: CHECK DEPENDENCIES ---
77
+ // CHECK DEPENDENCIES
97
78
  if (component.dependencies && Object.keys(component.dependencies).length > 0) {
98
79
  checkDependencies(component.dependencies);
99
80
  }
100
81
 
101
- console.log(`\n✅ Component '${title}' pulled successfully!`);
102
-
103
- } catch (err) {
104
- console.log("❌ Error pulling component:", err);
105
- }
82
+ log.success(`Component '${title}' pulled successfully!`);
106
83
  }
107
84
 
108
85
  /**
@@ -113,8 +90,8 @@ function checkDependencies(requiredDeps) {
113
90
 
114
91
  // If no package.json, we can't check, so just list them all
115
92
  if (!fs.existsSync(localPkgPath)) {
116
- console.log("\n⚠️ This component requires these packages:");
117
- Object.entries(requiredDeps).forEach(([pkg, ver]) => console.log(` - ${pkg}@${ver}`));
93
+ log.warn("This component requires these packages:");
94
+ Object.entries(requiredDeps).forEach(([pkg, ver]) => log.warn(` - ${pkg}@${ver}`));
118
95
  return;
119
96
  }
120
97
 
@@ -130,10 +107,10 @@ function checkDependencies(requiredDeps) {
130
107
  }
131
108
 
132
109
  if (missing.length > 0) {
133
- console.log("\n⚠️ Missing Dependencies (Run this to fix):");
134
- console.log(` npm install ${missing.map(d => d.split('@')[0]).join(" ")}`);
110
+ log.warn("Missing Dependencies (Run this to fix):");
111
+ log.info(` npm install ${missing.map(d => d.split('@')[0]).join(" ")}`);
135
112
  } else {
136
- console.log("\n✨ All dependencies are already installed.");
113
+ log.info("All dependencies are already installed.");
137
114
  }
138
115
  } catch (e) {
139
116
  // Ignore JSON parse errors
@@ -1,42 +1,34 @@
1
1
  import { apiRequest } from "../utils/request.js";
2
- import { loadSession } from "../utils/session.js";
3
2
  import fs from "fs";
4
3
  import path from "path";
5
- // IMPORT THE NEW SPIDER
6
4
  import { scanComponent } from "../utils/crawler.js";
5
+ import { log } from "../utils/log.js";
7
6
 
8
7
  export async function pushComponent(category, title, filepath) {
9
8
  // 1. Validate Input
9
+ // although commander ensures these are provided, we double-check here
10
10
  if (!category?.trim() || !title?.trim() || !filepath?.trim()) {
11
- console.log("Category, title, and filepath are required.");
11
+ log.error("Category, title, and filepath are required.");
12
12
  return;
13
13
  }
14
14
 
15
15
  // 2. Validate Entry File
16
16
  const absolutePath = path.resolve(filepath);
17
17
  if (!fs.existsSync(absolutePath)) {
18
- console.log(`❌ File not found: ${absolutePath}`);
19
- return;
20
- }
21
-
22
- // 3. Check Session
23
- const session = loadSession();
24
- if (!session || !session.jwt) {
25
- console.log("❌ You must be logged in. Run: composter login");
18
+ log.error(`File not found: ${absolutePath}`);
26
19
  return;
27
20
  }
28
21
 
29
22
  // 4. RUN THE CRAWLER
30
- console.log(`Scanning ${path.basename(absolutePath)} and its dependencies...`);
23
+ log.info(`Scanning ${path.basename(absolutePath)} and its dependencies...`);
31
24
 
32
25
  const { files, dependencies } = scanComponent(absolutePath);
33
26
 
34
27
  const fileCount = Object.keys(files).length;
35
28
  const depCount = Object.keys(dependencies).length;
36
29
 
37
- console.log(`📦 Bundled ${fileCount} file(s) and detected ${depCount} external package(s).`);
30
+ log.info(`📦 Bundled ${fileCount} file(s) and detected ${depCount} external package(s).`);
38
31
 
39
- try {
40
32
  // 5. Send Request
41
33
  // We send 'files' as a JSON string because your DB 'code' column is a String.
42
34
  const res = await apiRequest("/components", {
@@ -54,20 +46,12 @@ export async function pushComponent(category, title, filepath) {
54
46
  let body = null;
55
47
  try { body = await res.json(); } catch {}
56
48
 
57
- if (res.status === 401) {
58
- console.log("❌ Session expired. Run composter login again.");
59
- return;
60
- }
61
-
62
49
  if (res.ok) {
63
- console.log(`✅ Success! Component '${title}' pushed to '${category}'.`);
50
+ log.success(`Success! Component '${title}' pushed to '${category}'.`);
64
51
  return;
65
52
  }
66
53
 
54
+ // handle client errors, e.g., 4xx (bad request, conflict, etc.)
67
55
  const errorMessage = body?.message || body?.error || res.statusText;
68
- console.log("❌ Error pushing component:", errorMessage);
69
-
70
- } catch (error) {
71
- console.log("❌ Network Error:", error.message);
72
- }
56
+ log.error(`Error pushing component: ${errorMessage}`);
73
57
  }
@@ -0,0 +1,21 @@
1
+ const composterLoginArt = `
2
+ █████████ ███████ ██████ ██████ ███████████ ███████ █████████ ███████████ ██████████ ███████████
3
+ ███░░░░░███ ███░░░░░███ ░░██████ ██████ ░░███░░░░░███ ███░░░░░███ ███░░░░░███░█░░░███░░░█░░███░░░░░█░░███░░░░░███
4
+ ███ ░░░ ███ ░░███ ░███░█████░███ ░███ ░███ ███ ░░███░███ ░░░ ░ ░███ ░ ░███ █ ░ ░███ ░███
5
+ ░███ ░███ ░███ ░███░░███ ░███ ░██████████ ░███ ░███░░█████████ ░███ ░██████ ░██████████
6
+ ░███ ░███ ░███ ░███ ░░░ ░███ ░███░░░░░░ ░███ ░███ ░░░░░░░░███ ░███ ░███░░█ ░███░░░░░███
7
+ ░░███ ███░░███ ███ ░███ ░███ ░███ ░░███ ███ ███ ░███ ░███ ░███ ░ █ ░███ ░███
8
+ ░░█████████ ░░░███████░ █████ █████ █████ ░░░███████░ ░░█████████ █████ ██████████ █████ █████
9
+ ░░░░░░░░░ ░░░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░░ ░░░░░░░░░ ░░░░░ ░░░░░░░░░░ ░░░░░ ░░░░░
10
+ `;
11
+
12
+ const composterLoginArtv2 = `
13
+ ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███████╗████████╗███████╗██████╗
14
+ ██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗██╔════╝╚══██╔══╝██╔════╝██╔══██╗
15
+ ██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║███████╗ ██║ █████╗ ██████╔╝
16
+ ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║╚════██║ ██║ ██╔══╝ ██╔══██╗
17
+ ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝███████║ ██║ ███████╗██║ ██║
18
+ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
19
+ `;
20
+
21
+ export { composterLoginArt, composterLoginArtv2 };
package/src/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
-
2
+ process.env.DOTENV_CONFIG_QUIET = "true";
3
+ process.env.DOTENVX_QUIET = "true";
4
+ import "dotenv/config";
3
5
  import { Command } from "commander";
4
6
  import { login } from "./commands/login.js";
5
7
  import { mkcat } from "./commands/mkcat.js";
@@ -8,6 +10,7 @@ import { pushComponent } from "./commands/push.js";
8
10
  import { pullComponent } from "./commands/pull.js";
9
11
  import { initMcp } from "./commands/init.js";
10
12
  import { createRequire } from "module";
13
+ import { log } from "./utils/log.js";
11
14
 
12
15
  const require = createRequire(import.meta.url);
13
16
  const packageJson = require("../package.json");
@@ -17,7 +20,17 @@ const program = new Command();
17
20
  program
18
21
  .name("composter")
19
22
  .description("CLI for Composter Platform")
20
- .version(packageJson.version);
23
+ .version(packageJson.version)
24
+ .configureOutput({
25
+ // Override the default error handling to use our custom handler
26
+ writeErr: (str) => {
27
+ if (str.includes("error:")) log.error(str.trim());
28
+ else log.info(str.trim());
29
+ },
30
+ writeOut: (str) => {
31
+ log.info(str.trim());
32
+ },
33
+ });
21
34
 
22
35
  program
23
36
  .command("login")
@@ -57,4 +70,23 @@ program
57
70
  pullComponent(category, title, filepath);
58
71
  });
59
72
 
73
+ process.on("SIGINT", () => {
74
+ process.stdout.write("\n");
75
+ process.exit(130);
76
+ });
77
+
78
+ process.on("unhandledRejection", (err) => {
79
+ // Ctrl+C during fetch / inquirer
80
+ if (
81
+ err?.name === "AbortError" ||
82
+ err?.name === "ExitPromptError"
83
+ ) {
84
+ log.info("\nOperation cancelled by user.\n");
85
+ process.exit(130);
86
+ }
87
+
88
+ console.error(err);
89
+ process.exit(1);
90
+ });
91
+
60
92
  program.parse(process.argv);
@@ -1,5 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { log } from "./log.js";
3
4
 
4
5
  const IMPORT_REGEX = /(?:import|export)\s+(?:[\w*\s{},]*\s+from\s+)?['"]([^'"]+)['"]/g;
5
6
 
@@ -37,14 +38,16 @@ export function scanComponent(entryFilePath) {
37
38
  if (fs.existsSync(localPkgPath)) {
38
39
  localPkg = JSON.parse(fs.readFileSync(localPkgPath, "utf-8"));
39
40
  }
40
- } catch (e) {}
41
+ } catch (e) {
42
+ log.warn("Could not read local package.json. The file may be corrupted")
43
+ }
41
44
 
42
45
  while (queue.length > 0) {
43
46
  const fullPath = queue.shift();
44
47
  if (processed.has(fullPath)) continue;
45
48
 
46
49
  if (!fs.existsSync(fullPath)) {
47
- console.warn(`⚠️ Warning: File not found: ${fullPath}`);
50
+ log.warn(`Warning: File not found - ${fullPath}`);
48
51
  continue;
49
52
  }
50
53
 
@@ -0,0 +1,36 @@
1
+ import { log } from "../log.js";
2
+
3
+ export function handleFetchError(err) {
4
+ switch (err.message) {
5
+ case "NETWORK_UNREACHABLE":
6
+ log.error("Cannot reach server. Check your internet or VPN connection.");
7
+ break;
8
+
9
+ case "SESSION_EXPIRED":
10
+ log.warn("Your session has expired. Please log in again.");
11
+ break;
12
+
13
+ case "NETWORK_TIMEOUT":
14
+ log.warn("Network request timed out. Please try again.");
15
+ break;
16
+
17
+ case "UNAUTHORIZED":
18
+ log.error("Invalid email or password.");
19
+ break;
20
+
21
+ case "NOT_FOUND":
22
+ log.error("Requested resource does not exist.");
23
+ break;
24
+
25
+ case "SERVER_ERROR":
26
+ log.error("Service temporarily unavailable. Try again later.");
27
+ break;
28
+
29
+ default:
30
+ log.error("An unexpected error occurred.");
31
+ }
32
+
33
+ if (process.env.DEBUG === "1" || process.env.DEBUG === "true") {
34
+ console.error(err);
35
+ }
36
+ }
@@ -0,0 +1,32 @@
1
+ import { log } from "../log.js";
2
+
3
+ export function handleSessionError(err) {
4
+ switch (err.message) {
5
+ case "FAILED_SESSION_SAVE":
6
+ log.error("Failed to save session data. Please try logging in again.");
7
+ break;
8
+
9
+ case "NO_SESSION":
10
+ log.warn("You need to log in first.");
11
+ log.info("Run: composter login");
12
+ break;
13
+
14
+ case "SESSION_FILE_CORRUPT":
15
+ log.warn("Session file is corrupt. Please log in again.");
16
+ break;
17
+
18
+ case "SESSION_INVALID":
19
+ log.warn("Session data is invalid. Please log in again.");
20
+ break;
21
+
22
+ case "SESSION_EXPIRED":
23
+ log.warn("Your session has expired. Please log in again.");
24
+ break;
25
+ default:
26
+ log.error("An unexpected error occurred.");
27
+ }
28
+
29
+ if (process.env.DEBUG === "1" || process.env.DEBUG === "true") {
30
+ console.error(err);
31
+ }
32
+ }
@@ -0,0 +1,8 @@
1
+ import chalk from "chalk";
2
+
3
+ export const log = {
4
+ info: (msg) => console.log(chalk.blue.bold(`${msg}`)),
5
+ success: (msg) => console.log(chalk.green.bold(`✔ ${msg}`)),
6
+ warn: (msg) => console.log(chalk.yellow.bold(`⚠ ${msg}`)),
7
+ error: (msg) => console.error(chalk.red.bold(`✖ ${msg}`)),
8
+ };
@@ -1,35 +1,44 @@
1
- import fetch from "node-fetch";
2
- import { loadSession, clearSession } from "./session.js";
3
- import dotenv from "dotenv";
4
- dotenv.config({ silent: true });
1
+ import { clearSession, loadSession } from "./session.js";
2
+ import { safeFetch } from "./safeFetch.js";
3
+ import { handleSessionError } from "./errorHandlers/sessionErrorHandler.js";
4
+ import { handleFetchError } from "./errorHandlers/fetchErrorHandler.js";
5
+ import { log } from "./log.js";
5
6
 
6
- const BASE_URL = process.env.BASE_URL || "https://composter.onrender.com/api";
7
+ const BASE_URL = process.env.BASE_URL;
7
8
 
8
9
  export async function apiRequest(path, options = {}) {
9
- const session = loadSession();
10
+
11
+ try {
12
+ const session = loadSession();
13
+ const headers = options.headers || {};
10
14
 
11
- if (!session) {
12
- console.error("Not authenticated. Please run 'composter login'");
13
- process.exit(1);
14
- }
15
+ if (session?.jwt) {
16
+ headers["Authorization"] = `Bearer ${session.jwt}`;
17
+ }
15
18
 
16
- const headers = options.headers || {};
17
-
18
- if (session?.jwt) {
19
- headers["Authorization"] = `Bearer ${session.jwt}`;
20
- }
21
-
22
- const res = await fetch(`${BASE_URL}${path}`, {
23
- ...options,
24
- headers,
25
- });
26
-
27
- // Handle 401 Unauthorized (expired/invalid session)
28
- if (res.status === 401) {
29
- console.error("Authentication failed. Please run 'composter login' again");
30
- clearSession();
31
- process.exit(1);
19
+ const res = await safeFetch(`${BASE_URL}${path}`, {
20
+ ...options,
21
+ headers,
22
+ });
23
+
24
+ return res;
25
+ } catch (error) {
26
+ if (error.type === "SESSION_ERROR") {
27
+ handleSessionError(error);
28
+ process.exit(1);
29
+ }
30
+ else {
31
+ // if we get an 401 error, it usually means the session is invalid or expired
32
+ // hence we clear the session and handle the error accordingly
33
+ if(error.type === "FETCH_ERROR" && error.message === "UNAUTHORIZED") {
34
+ clearSession();
35
+ log.warn("Session invalid or expired. Please log in again.");
36
+ log.info("Run: composter login");
37
+ process.exit(1);
38
+ }
39
+ handleFetchError(error);
40
+ process.exit(1);
41
+ }
32
42
  }
33
43
 
34
- return res;
35
44
  }
@@ -0,0 +1,49 @@
1
+ import fetch from "node-fetch";
2
+ import { FetchError } from "node-fetch";
3
+
4
+ export async function safeFetch(url, options = {}) {
5
+
6
+ let res;
7
+
8
+ const controller = new AbortController(); // to handle timeouts
9
+ const timeout = setTimeout(() => {
10
+ controller.abort();
11
+ }, 10000);
12
+
13
+ try {
14
+ res = await fetch(url, { ...options, signal: controller.signal });
15
+ } catch (err) {
16
+ if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND") {
17
+ throw new FetchError("NETWORK_UNREACHABLE", 'FETCH_ERROR');
18
+ }
19
+
20
+ if (err.name === "AbortError") {
21
+ throw new FetchError("NETWORK_TIMEOUT", 'FETCH_ERROR');
22
+ }
23
+
24
+ throw new FetchError("NETWORK_ERROR", 'FETCH_ERROR');
25
+ } finally {
26
+ clearTimeout(timeout);
27
+ }
28
+
29
+ if (!res.ok) {
30
+ if (res.status === 401 || res.status === 403) {
31
+ throw new FetchError("UNAUTHORIZED", 'FETCH_ERROR');
32
+ }
33
+
34
+ if (res.status === 404) {
35
+ throw new FetchError("NOT_FOUND", 'FETCH_ERROR');
36
+ }
37
+
38
+ if (res.status >= 500) {
39
+ throw new FetchError("SERVER_ERROR", 'FETCH_ERROR');
40
+ }
41
+
42
+ // Other HTTP errors will be handled by the caller
43
+ // these are mostly client errors (4xx) like 400, 409 etc.
44
+ return res;
45
+ }
46
+
47
+ return res;
48
+ }
49
+
@@ -1,30 +1,48 @@
1
1
  import fs from "fs";
2
2
  import { SESSION_PATH, ensureConfigDir } from "./paths.js";
3
3
 
4
+ class SessionError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.type = "SESSION_ERROR";
8
+ }
9
+ }
4
10
  export function saveSession(sessionData) {
5
- ensureConfigDir();
6
- fs.writeFileSync(SESSION_PATH, JSON.stringify(sessionData, null, 2), "utf-8");
11
+ try {
12
+ ensureConfigDir();
13
+ fs.writeFileSync(SESSION_PATH, JSON.stringify(sessionData, null, 2), "utf-8");
14
+ } catch (error) {
15
+ throw new SessionError("FAILED_SESSION_SAVE");
16
+ }
7
17
  }
8
18
 
9
19
  export function loadSession() {
10
- if (!fs.existsSync(SESSION_PATH)) return null;
20
+ if (!fs.existsSync(SESSION_PATH)) {
21
+ throw new SessionError("NO_SESSION");
22
+ }
11
23
 
24
+ let session;
25
+
26
+ // read and parse session file
12
27
  try {
13
- const session = JSON.parse(fs.readFileSync(SESSION_PATH, "utf-8"));
14
-
15
- // Check if session is expired
16
- if (session.expiresAt && new Date(session.expiresAt) < new Date()) {
17
- console.log("Session expired. Please run 'composter login' again.");
18
- clearSession();
19
- return null;
20
- }
21
-
22
- return session;
28
+ session = JSON.parse(fs.readFileSync(SESSION_PATH, "utf-8"));
23
29
  } catch (error) {
24
- console.error("Invalid session file. Please run 'composter login' again.");
25
30
  clearSession();
26
- return null;
31
+ throw new SessionError("SESSION_FILE_CORRUPT");
32
+ }
33
+
34
+ // validate session structure and if expiry is valid date
35
+ if (!session.expiresAt || isNaN(Date.parse(session.expiresAt))) {
36
+ clearSession();
37
+ throw new SessionError("SESSION_INVALID");
27
38
  }
39
+
40
+ // check if session is expired
41
+ if (new Date(session.expiresAt) < new Date()) {
42
+ clearSession();
43
+ throw new SessionError("SESSION_EXPIRED");
44
+ }
45
+ return session;
28
46
  }
29
47
 
30
48
  export function clearSession() {