composter-cli 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/composter.js +2 -0
- package/package.json +26 -0
- package/src/commands/listCat.js +61 -0
- package/src/commands/login.js +71 -0
- package/src/commands/mkcat.js +71 -0
- package/src/commands/pull.js +141 -0
- package/src/commands/push.js +73 -0
- package/src/index.js +48 -0
- package/src/utils/crawler.js +129 -0
- package/src/utils/paths.js +13 -0
- package/src/utils/request.js +22 -0
- package/src/utils/session.js +16 -0
package/bin/composter.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "composter-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI tool to manage and sync React components to your personal component vault",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"composter": "./bin/composter.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["react", "components", "cli", "vault", "library", "shadcn"],
|
|
14
|
+
"author": "binit2-1",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/binit2-1/Composter"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^14.0.2",
|
|
22
|
+
"dotenv": "^16.4.5",
|
|
23
|
+
"inquirer": "^13.0.1",
|
|
24
|
+
"node-fetch": "^3.3.2"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { apiRequest } from "../utils/request.js";
|
|
2
|
+
import { loadSession } from "../utils/session.js";
|
|
3
|
+
|
|
4
|
+
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
|
+
|
|
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
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handle auth failure
|
|
24
|
+
if (res.status === 401) {
|
|
25
|
+
console.log("Session expired. Run composter login again.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Handle server errors
|
|
30
|
+
if (res.status >= 500) {
|
|
31
|
+
console.log("Server error. Try again later.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
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
|
+
|
|
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);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import fetch from "node-fetch";
|
|
3
|
+
import { saveSession } from "../utils/session.js";
|
|
4
|
+
import dotenv from "dotenv";
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
dotenv.config({ path: join(__dirname, '../../.env') });
|
|
11
|
+
|
|
12
|
+
const BASE_URL = `${process.env.BASE_URL || "https://composter.onrender.com/api"}/auth`;
|
|
13
|
+
|
|
14
|
+
export async function login() {
|
|
15
|
+
console.log("=== Composter Login ===");
|
|
16
|
+
|
|
17
|
+
const { email, password } = await inquirer.prompt([
|
|
18
|
+
{ type: "input", name: "email", message: "Email:" },
|
|
19
|
+
{ type: "password", name: "password", message: "Password:" }
|
|
20
|
+
]);
|
|
21
|
+
|
|
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);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
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
|
+
}
|
|
51
|
+
|
|
52
|
+
// Step 3 — Fetch JWT token
|
|
53
|
+
const tokenRes = await fetch(`${BASE_URL}/token`, {
|
|
54
|
+
method: "GET",
|
|
55
|
+
headers: { "Cookie": cookie }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let token = null;
|
|
59
|
+
if (tokenRes.ok) {
|
|
60
|
+
const json = await tokenRes.json();
|
|
61
|
+
token = json.token;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Step 4 — Save session + jwt locally
|
|
65
|
+
saveSession({
|
|
66
|
+
cookies: cookie,
|
|
67
|
+
jwt: token
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log("\nLogged in successfully!");
|
|
71
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { apiRequest } from "../utils/request.js";
|
|
2
|
+
import { loadSession } from "../utils/session.js";
|
|
3
|
+
|
|
4
|
+
export async function mkcat(categoryName) {
|
|
5
|
+
// Validate input
|
|
6
|
+
if (
|
|
7
|
+
!categoryName ||
|
|
8
|
+
categoryName.trim() === "" ||
|
|
9
|
+
categoryName.includes(" ") ||
|
|
10
|
+
categoryName.length > 10
|
|
11
|
+
) {
|
|
12
|
+
console.log(
|
|
13
|
+
"Invalid category name. It must be non-empty, without spaces, and ≤ 10 characters."
|
|
14
|
+
);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
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
|
+
const res = await apiRequest("/categories", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: JSON.stringify({ name: categoryName }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Parse JSON once
|
|
34
|
+
let body = null;
|
|
35
|
+
try {
|
|
36
|
+
body = await res.json();
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore if no JSON
|
|
39
|
+
}
|
|
40
|
+
|
|
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
|
+
if (res.ok) {
|
|
55
|
+
console.log(`Category '${categoryName}' created successfully!`);
|
|
56
|
+
console.log("ID:", body?.category?.id);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle other errors
|
|
61
|
+
const msg =
|
|
62
|
+
body?.error ||
|
|
63
|
+
body?.message ||
|
|
64
|
+
JSON.stringify(body) ||
|
|
65
|
+
`HTTP ${res.status}`;
|
|
66
|
+
|
|
67
|
+
console.log("Failed to create category:", msg);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.log("Network or unexpected error:", err.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { apiRequest } from "../utils/request.js";
|
|
2
|
+
import { loadSession } from "../utils/session.js";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
export async function pullComponent(category, title, targetDir) {
|
|
7
|
+
// 1. Validate Input
|
|
8
|
+
if (!category?.trim() || !title?.trim() || !targetDir?.trim()) {
|
|
9
|
+
console.log("❌ Category, title, and target directory are required.");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 2. Resolve Target Directory
|
|
14
|
+
// In multi-file mode, the target is usually a FOLDER, not a specific file.
|
|
15
|
+
const absoluteRoot = path.resolve(targetDir);
|
|
16
|
+
|
|
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}'...`);
|
|
26
|
+
|
|
27
|
+
const res = await apiRequest(`/components?category=${encodeURIComponent(category)}&title=${encodeURIComponent(title)}`, {
|
|
28
|
+
method: "GET",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Parse Body
|
|
33
|
+
let body = null;
|
|
34
|
+
try { body = await res.json(); } catch {}
|
|
35
|
+
|
|
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;
|
|
50
|
+
|
|
51
|
+
// --- STEP 4: PARSE FILES (Handle JSON vs String) ---
|
|
52
|
+
let filesMap = {};
|
|
53
|
+
try {
|
|
54
|
+
// Try to parse new multi-file format
|
|
55
|
+
filesMap = JSON.parse(component.code);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
// Fallback for old single-file components
|
|
58
|
+
// We'll create a default filename based on the title
|
|
59
|
+
let fileName = `${title}.jsx`;
|
|
60
|
+
// If the user pointed to a specific file (e.g. ./src/Button.js), use that name
|
|
61
|
+
if (path.extname(absoluteRoot)) {
|
|
62
|
+
fileName = path.basename(absoluteRoot);
|
|
63
|
+
}
|
|
64
|
+
filesMap[`/${fileName}`] = component.code;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- STEP 5: WRITE FILES TO DISK ---
|
|
68
|
+
console.log(`📦 Unpacking ${Object.keys(filesMap).length} file(s) into: ${absoluteRoot}`);
|
|
69
|
+
|
|
70
|
+
// Ensure the root target folder exists
|
|
71
|
+
if (!fs.existsSync(absoluteRoot)) {
|
|
72
|
+
fs.mkdirSync(absoluteRoot, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const createdFiles = [];
|
|
76
|
+
|
|
77
|
+
for (const [virtualPath, content] of Object.entries(filesMap)) {
|
|
78
|
+
// Remove leading slash (e.g. "/ui/Button.tsx" -> "ui/Button.tsx")
|
|
79
|
+
const relPath = virtualPath.startsWith('/') ? virtualPath.slice(1) : virtualPath;
|
|
80
|
+
|
|
81
|
+
// Construct full system path
|
|
82
|
+
const writePath = path.join(absoluteRoot, relPath);
|
|
83
|
+
const writeDir = path.dirname(writePath);
|
|
84
|
+
|
|
85
|
+
// Create sub-folders if needed
|
|
86
|
+
if (!fs.existsSync(writeDir)) {
|
|
87
|
+
fs.mkdirSync(writeDir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Write file
|
|
91
|
+
fs.writeFileSync(writePath, content, "utf-8");
|
|
92
|
+
createdFiles.push(relPath);
|
|
93
|
+
console.log(` + ${relPath}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- STEP 6: CHECK DEPENDENCIES ---
|
|
97
|
+
if (component.dependencies && Object.keys(component.dependencies).length > 0) {
|
|
98
|
+
checkDependencies(component.dependencies);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(`\n✅ Component '${title}' pulled successfully!`);
|
|
102
|
+
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.log("❌ Error pulling component:", err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Helper to check local package.json against required dependencies
|
|
110
|
+
*/
|
|
111
|
+
function checkDependencies(requiredDeps) {
|
|
112
|
+
const localPkgPath = path.resolve(process.cwd(), "package.json");
|
|
113
|
+
|
|
114
|
+
// If no package.json, we can't check, so just list them all
|
|
115
|
+
if (!fs.existsSync(localPkgPath)) {
|
|
116
|
+
console.log("\n⚠️ This component requires these packages:");
|
|
117
|
+
Object.entries(requiredDeps).forEach(([pkg, ver]) => console.log(` - ${pkg}@${ver}`));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const localPkg = JSON.parse(fs.readFileSync(localPkgPath, "utf-8"));
|
|
123
|
+
const installed = { ...localPkg.dependencies, ...localPkg.devDependencies };
|
|
124
|
+
const missing = [];
|
|
125
|
+
|
|
126
|
+
for (const [pkg, version] of Object.entries(requiredDeps)) {
|
|
127
|
+
if (!installed[pkg]) {
|
|
128
|
+
missing.push(`${pkg}@${version}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
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(" ")}`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log("\n✨ All dependencies are already installed.");
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
// Ignore JSON parse errors
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { apiRequest } from "../utils/request.js";
|
|
2
|
+
import { loadSession } from "../utils/session.js";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
// IMPORT THE NEW SPIDER
|
|
6
|
+
import { scanComponent } from "../utils/crawler.js";
|
|
7
|
+
|
|
8
|
+
export async function pushComponent(category, title, filepath) {
|
|
9
|
+
// 1. Validate Input
|
|
10
|
+
if (!category?.trim() || !title?.trim() || !filepath?.trim()) {
|
|
11
|
+
console.log("❌ Category, title, and filepath are required.");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 2. Validate Entry File
|
|
16
|
+
const absolutePath = path.resolve(filepath);
|
|
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");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 4. RUN THE CRAWLER
|
|
30
|
+
console.log(`Scanning ${path.basename(absolutePath)} and its dependencies...`);
|
|
31
|
+
|
|
32
|
+
const { files, dependencies } = scanComponent(absolutePath);
|
|
33
|
+
|
|
34
|
+
const fileCount = Object.keys(files).length;
|
|
35
|
+
const depCount = Object.keys(dependencies).length;
|
|
36
|
+
|
|
37
|
+
console.log(`📦 Bundled ${fileCount} file(s) and detected ${depCount} external package(s).`);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// 5. Send Request
|
|
41
|
+
// We send 'files' as a JSON string because your DB 'code' column is a String.
|
|
42
|
+
const res = await apiRequest("/components", {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
title,
|
|
47
|
+
category,
|
|
48
|
+
code: JSON.stringify(files),
|
|
49
|
+
dependencies: dependencies
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 6. Handle Response
|
|
54
|
+
let body = null;
|
|
55
|
+
try { body = await res.json(); } catch {}
|
|
56
|
+
|
|
57
|
+
if (res.status === 401) {
|
|
58
|
+
console.log("❌ Session expired. Run composter login again.");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (res.ok) {
|
|
63
|
+
console.log(`✅ Success! Component '${title}' pushed to '${category}'.`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
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
|
+
}
|
|
73
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { login } from "./commands/login.js";
|
|
5
|
+
import { mkcat } from "./commands/mkcat.js";
|
|
6
|
+
import { listCategories } from "./commands/listCat.js";
|
|
7
|
+
import { pushComponent } from "./commands/push.js";
|
|
8
|
+
import { pullComponent } from "./commands/pull.js";
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name("composter")
|
|
14
|
+
.description("CLI for Composter Platform")
|
|
15
|
+
.version("0.1.0");
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command("login")
|
|
19
|
+
.description("Log into your Composter account")
|
|
20
|
+
.action(login);
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command("mkcat <category-name>")
|
|
24
|
+
.description("Create a new category")
|
|
25
|
+
.action((categoryName) => mkcat(categoryName));
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command("ls")
|
|
29
|
+
.description("List categories")
|
|
30
|
+
.action(() => {
|
|
31
|
+
listCategories();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.command("push <category-name> <component-title> <file-path>")
|
|
36
|
+
.description("Push a new component")
|
|
37
|
+
.action((category, title, filepath) => {
|
|
38
|
+
pushComponent(category, title, filepath);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command("pull <category-name> <component-title> <file-path>")
|
|
43
|
+
.description("Pull a component")
|
|
44
|
+
.action((category, title, filepath) => {
|
|
45
|
+
pullComponent(category, title, filepath);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const IMPORT_REGEX = /(?:import|export)\s+(?:[\w*\s{},]*\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
5
|
+
|
|
6
|
+
// Helper: Find the nearest package.json starting from a file's directory
|
|
7
|
+
function findPackageRoot(startDir) {
|
|
8
|
+
let current = startDir;
|
|
9
|
+
const root = path.parse(current).root;
|
|
10
|
+
|
|
11
|
+
while (current !== root) {
|
|
12
|
+
if (fs.existsSync(path.join(current, "package.json"))) {
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
current = path.dirname(current);
|
|
16
|
+
}
|
|
17
|
+
// Fallback: If no package.json, use the directory of the entry file itself
|
|
18
|
+
return startDir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function scanComponent(entryFilePath) {
|
|
22
|
+
const absoluteEntry = path.resolve(entryFilePath);
|
|
23
|
+
|
|
24
|
+
// FIX: Anchor the root to the package.json folder
|
|
25
|
+
// This ensures paths are like "src/Button.jsx", not "frontend/src/Button.jsx"
|
|
26
|
+
const projectRoot = findPackageRoot(path.dirname(absoluteEntry));
|
|
27
|
+
|
|
28
|
+
const filesMap = {};
|
|
29
|
+
const npmDependencies = {};
|
|
30
|
+
const processed = new Set();
|
|
31
|
+
const queue = [absoluteEntry];
|
|
32
|
+
|
|
33
|
+
// Load package.json for versions
|
|
34
|
+
const localPkgPath = path.join(projectRoot, "package.json");
|
|
35
|
+
let localPkg = { dependencies: {}, devDependencies: {} };
|
|
36
|
+
try {
|
|
37
|
+
if (fs.existsSync(localPkgPath)) {
|
|
38
|
+
localPkg = JSON.parse(fs.readFileSync(localPkgPath, "utf-8"));
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {}
|
|
41
|
+
|
|
42
|
+
while (queue.length > 0) {
|
|
43
|
+
const fullPath = queue.shift();
|
|
44
|
+
if (processed.has(fullPath)) continue;
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(fullPath)) {
|
|
47
|
+
console.warn(`⚠️ Warning: File not found: ${fullPath}`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
52
|
+
processed.add(fullPath);
|
|
53
|
+
|
|
54
|
+
// --- CLEAN PATH GENERATION ---
|
|
55
|
+
// Calculate path relative to the PROJECT ROOT (package.json location)
|
|
56
|
+
let relativePath = path.relative(projectRoot, fullPath);
|
|
57
|
+
|
|
58
|
+
// Normalize slashes for Sandpack
|
|
59
|
+
relativePath = relativePath.split(path.sep).join("/");
|
|
60
|
+
|
|
61
|
+
// SANITY CHECK: If path still starts with "..", force it into a virtual root
|
|
62
|
+
// This handles cases where you import a file OUTSIDE your project root
|
|
63
|
+
if (relativePath.startsWith("..")) {
|
|
64
|
+
// Strip the dots: "../external/File.js" -> "/_external/File.js"
|
|
65
|
+
relativePath = relativePath.replace(/^(\.\.\/)+/, "_external/");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const virtualPath = `/${relativePath}`;
|
|
69
|
+
filesMap[virtualPath] = content;
|
|
70
|
+
|
|
71
|
+
// --- SCAN IMPORTS ---
|
|
72
|
+
let match;
|
|
73
|
+
IMPORT_REGEX.lastIndex = 0;
|
|
74
|
+
while ((match = IMPORT_REGEX.exec(content)) !== null) {
|
|
75
|
+
const importPath = match[1];
|
|
76
|
+
|
|
77
|
+
if (importPath.startsWith(".")) {
|
|
78
|
+
// CASE A: Relative Import (e.g. "./button")
|
|
79
|
+
const currentFileDir = path.dirname(fullPath);
|
|
80
|
+
const resolvedPath = resolveLocalImport(currentFileDir, importPath);
|
|
81
|
+
if (resolvedPath && !processed.has(resolvedPath)) {
|
|
82
|
+
queue.push(resolvedPath);
|
|
83
|
+
}
|
|
84
|
+
} else if (importPath.startsWith("@/")) {
|
|
85
|
+
// CASE B: Alias Import (e.g. "@/components/ui/button")
|
|
86
|
+
// We assume "@/" maps to "<projectRoot>/src/" (Standard Shadcn/Vite convention)
|
|
87
|
+
const pathInsideSrc = importPath.slice(2); // Remove "@/"
|
|
88
|
+
const srcDir = path.join(projectRoot, "src");
|
|
89
|
+
|
|
90
|
+
const resolvedPath = resolveLocalImport(srcDir, pathInsideSrc);
|
|
91
|
+
if (resolvedPath && !processed.has(resolvedPath)) {
|
|
92
|
+
queue.push(resolvedPath);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// CASE C: External Package (e.g. "lucide-react")
|
|
96
|
+
const pkgName = getPackageName(importPath);
|
|
97
|
+
const version =
|
|
98
|
+
localPkg.dependencies?.[pkgName] ||
|
|
99
|
+
localPkg.devDependencies?.[pkgName] ||
|
|
100
|
+
"latest";
|
|
101
|
+
npmDependencies[pkgName] = version;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { files: filesMap, dependencies: npmDependencies };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveLocalImport(dir, importPath) {
|
|
110
|
+
const extensions = [".tsx", ".ts", ".jsx", ".js", ".css"];
|
|
111
|
+
const base = path.join(dir, importPath);
|
|
112
|
+
if (fs.existsSync(base) && fs.statSync(base).isFile()) return base;
|
|
113
|
+
for (const ext of extensions) {
|
|
114
|
+
if (fs.existsSync(base + ext)) return base + ext;
|
|
115
|
+
}
|
|
116
|
+
for (const ext of extensions) {
|
|
117
|
+
const indexPath = path.join(base, `index${ext}`);
|
|
118
|
+
if (fs.existsSync(indexPath)) return indexPath;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getPackageName(importPath) {
|
|
124
|
+
if (importPath.startsWith("@")) {
|
|
125
|
+
const parts = importPath.split("/");
|
|
126
|
+
return `${parts[0]}/${parts[1]}`;
|
|
127
|
+
}
|
|
128
|
+
return importPath.split("/")[0];
|
|
129
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
|
|
5
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".config", "composter");
|
|
6
|
+
|
|
7
|
+
export function ensureConfigDir() {
|
|
8
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
9
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SESSION_PATH = path.join(CONFIG_DIR, "session.json");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
import { loadSession } from "./session.js";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
dotenv.config({ silent: true });
|
|
5
|
+
|
|
6
|
+
const BASE_URL = process.env.BASE_URL || "https://composter.onrender.com/api";
|
|
7
|
+
|
|
8
|
+
export async function apiRequest(path, options = {}) {
|
|
9
|
+
const session = loadSession();
|
|
10
|
+
const headers = options.headers || {};
|
|
11
|
+
|
|
12
|
+
if (session?.jwt) {
|
|
13
|
+
headers["Authorization"] = `Bearer ${session.jwt}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
17
|
+
...options,
|
|
18
|
+
headers,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return res;
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { SESSION_PATH, ensureConfigDir } from "./paths.js";
|
|
3
|
+
|
|
4
|
+
export function saveSession(sessionData) {
|
|
5
|
+
ensureConfigDir();
|
|
6
|
+
fs.writeFileSync(SESSION_PATH, JSON.stringify(sessionData, null, 2), "utf-8");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function loadSession() {
|
|
10
|
+
if (!fs.existsSync(SESSION_PATH)) return null;
|
|
11
|
+
return JSON.parse(fs.readFileSync(SESSION_PATH, "utf-8"));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function clearSession() {
|
|
15
|
+
if (fs.existsSync(SESSION_PATH)) fs.unlinkSync(SESSION_PATH);
|
|
16
|
+
}
|