composter-cli 1.0.14 → 1.0.16
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 +4 -2
- package/src/commands/listCat.js +22 -50
- package/src/commands/login.js +47 -52
- package/src/commands/mkcat.js +10 -37
- package/src/commands/pull.js +18 -41
- package/src/commands/push.js +9 -25
- package/src/constants/asciiArts.js +21 -0
- package/src/index.js +34 -2
- package/src/utils/crawler.js +5 -2
- package/src/utils/errorHandlers/fetchErrorHandler.js +36 -0
- package/src/utils/errorHandlers/sessionErrorHandler.js +32 -0
- package/src/utils/log.js +8 -0
- package/src/utils/request.js +36 -27
- package/src/utils/safeFetch.js +49 -0
- package/src/utils/session.js +33 -15
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "composter-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
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": "^
|
|
42
|
+
"dotenv": "^17.2.3",
|
|
41
43
|
"inquirer": "^13.0.1",
|
|
42
44
|
"node-fetch": "^3.3.2"
|
|
43
45
|
},
|
package/src/commands/listCat.js
CHANGED
|
@@ -1,61 +1,33 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
1
2
|
import { apiRequest } from "../utils/request.js";
|
|
2
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
24
|
-
if (res.status === 401) {
|
|
25
|
-
console.log("Session expired. Run composter login again.");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
12
|
+
let body = null;
|
|
28
13
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -1,75 +1,70 @@
|
|
|
1
1
|
import inquirer from "inquirer";
|
|
2
|
-
import
|
|
2
|
+
import { safeFetch } from "../utils/safeFetch.js";
|
|
3
3
|
import { saveSession } from "../utils/session.js";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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.
|
|
14
|
+
const BASE_URL = `${process.env.BASE_URL || "https://composter.vercel.app/api"}/auth`;
|
|
13
15
|
|
|
14
16
|
export async function login() {
|
|
15
|
-
console.log(
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
}
|
package/src/commands/mkcat.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
"Invalid category name. It must be non-empty, without spaces, and
|
|
12
|
+
log.warn(
|
|
13
|
+
"Invalid category name. It must be non-empty, without spaces, and at most 10 characters."
|
|
14
14
|
);
|
|
15
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
return;
|
|
33
|
+
log.info(`Category '${categoryName}' created successfully!`);
|
|
34
|
+
process.exit(0);
|
|
58
35
|
}
|
|
59
36
|
|
|
60
|
-
//
|
|
37
|
+
// handling server sent errors, like duplicate category
|
|
61
38
|
const msg =
|
|
62
|
-
body
|
|
63
|
-
body?.message ||
|
|
64
|
-
JSON.stringify(body) ||
|
|
39
|
+
(body && (body.error || body.message || JSON.stringify(body))) ||
|
|
65
40
|
`HTTP ${res.status}`;
|
|
66
41
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
console.log("Network or unexpected error:", err.message);
|
|
70
|
-
}
|
|
42
|
+
log.error(`Failed to create category: ${msg}`);
|
|
43
|
+
process.exit(1);
|
|
71
44
|
}
|
package/src/commands/pull.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
68
|
-
|
|
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
|
-
//
|
|
77
|
+
// CHECK DEPENDENCIES
|
|
97
78
|
if (component.dependencies && Object.keys(component.dependencies).length > 0) {
|
|
98
79
|
checkDependencies(component.dependencies);
|
|
99
80
|
}
|
|
100
81
|
|
|
101
|
-
|
|
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
|
-
|
|
117
|
-
Object.entries(requiredDeps).forEach(([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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
113
|
+
log.info("All dependencies are already installed.");
|
|
137
114
|
}
|
|
138
115
|
} catch (e) {
|
|
139
116
|
// Ignore JSON parse errors
|
package/src/commands/push.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/src/utils/crawler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/utils/log.js
ADDED
|
@@ -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
|
+
};
|
package/src/utils/request.js
CHANGED
|
@@ -1,35 +1,44 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
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.
|
|
7
|
+
const BASE_URL = process.env.BASE_URL || "https://composter.vercel.app/api";
|
|
7
8
|
|
|
8
9
|
export async function apiRequest(path, options = {}) {
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const session = loadSession();
|
|
13
|
+
const headers = options.headers || {};
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
+
if (session?.jwt) {
|
|
16
|
+
headers["Authorization"] = `Bearer ${session.jwt}`;
|
|
17
|
+
}
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
|
package/src/utils/session.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|