thinkncollab-cli 0.0.10 → 0.0.11
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/Readme.md +4 -1
- package/bin/index.js +102 -34
- package/branch/.tnc/.tncmeta.json +7 -0
- package/branch/init.js +47 -0
- package/branch/pull.js +124 -0
- package/package.json +1 -1
package/Readme.md
CHANGED
package/bin/index.js
CHANGED
|
@@ -8,24 +8,11 @@ import crypto from "crypto";
|
|
|
8
8
|
import FormData from "form-data";
|
|
9
9
|
|
|
10
10
|
const RC_FILE = path.join(os.homedir(), ".tncrc");
|
|
11
|
-
const
|
|
11
|
+
const VERSION_FILE = path.join(process.cwd(), ".tncversions");
|
|
12
|
+
const BASE_URL = "http://localhost:3001/rooms";
|
|
13
|
+
const CWD = process.cwd();
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
async function logout() {
|
|
15
|
-
try {
|
|
16
|
-
if (fs.existsSync(RC_FILE)) {
|
|
17
|
-
// Remove the .tncrc file safely
|
|
18
|
-
await fs.promises.rm(RC_FILE, { force: true });
|
|
19
|
-
console.log("✅ Logged out successfully. Local credentials removed.");
|
|
20
|
-
} else {
|
|
21
|
-
console.log("ℹ️ No active session found.");
|
|
22
|
-
}
|
|
23
|
-
} catch (err) {
|
|
24
|
-
console.error("❌ Error during logout:", err.message);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** ========== LOGIN ========== **/
|
|
15
|
+
/** ------------------ LOGIN ------------------ **/
|
|
29
16
|
async function login() {
|
|
30
17
|
const answers = await inquirer.prompt([
|
|
31
18
|
{ type: "input", name: "email", message: "Email:" },
|
|
@@ -47,6 +34,21 @@ async function login() {
|
|
|
47
34
|
}
|
|
48
35
|
}
|
|
49
36
|
|
|
37
|
+
/** ------------------ LOGOUT ------------------ **/
|
|
38
|
+
async function logout() {
|
|
39
|
+
try {
|
|
40
|
+
if (fs.existsSync(RC_FILE)) {
|
|
41
|
+
await fs.promises.rm(RC_FILE, { force: true });
|
|
42
|
+
console.log("✅ Logged out successfully. Local credentials removed.");
|
|
43
|
+
} else {
|
|
44
|
+
console.log("ℹ️ No active session found.");
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error("❌ Error during logout:", err.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** ------------------ TOKEN UTILS ------------------ **/
|
|
50
52
|
function readToken() {
|
|
51
53
|
if (!fs.existsSync(RC_FILE)) {
|
|
52
54
|
console.error("❌ Not logged in. Run 'tnc login' first.");
|
|
@@ -56,7 +58,7 @@ function readToken() {
|
|
|
56
58
|
return { token: data.token, email: data.email };
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
/**
|
|
61
|
+
/** ------------------ IGNORE HANDLING ------------------ **/
|
|
60
62
|
function loadIgnore(folderPath) {
|
|
61
63
|
const ignoreFile = path.join(folderPath, ".ignoretnc");
|
|
62
64
|
if (!fs.existsSync(ignoreFile)) return [];
|
|
@@ -80,7 +82,7 @@ function shouldIgnore(relativePath, ignoreList) {
|
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
/**
|
|
85
|
+
/** ------------------ SCAN FOLDER ------------------ **/
|
|
84
86
|
function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
|
|
85
87
|
const items = fs.readdirSync(folderPath, { withFileTypes: true });
|
|
86
88
|
const result = [];
|
|
@@ -97,14 +99,15 @@ function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
|
|
|
97
99
|
result.push({
|
|
98
100
|
name: item.name,
|
|
99
101
|
type: "folder",
|
|
100
|
-
children: scanFolder(fullPath, ignoreList, rootPath)
|
|
102
|
+
children: scanFolder(fullPath, ignoreList, rootPath),
|
|
103
|
+
path: relativePath
|
|
101
104
|
});
|
|
102
105
|
} else {
|
|
103
106
|
const stats = fs.statSync(fullPath);
|
|
104
107
|
result.push({
|
|
105
108
|
name: item.name,
|
|
106
109
|
type: "file",
|
|
107
|
-
path:
|
|
110
|
+
path: relativePath,
|
|
108
111
|
size: stats.size
|
|
109
112
|
});
|
|
110
113
|
}
|
|
@@ -112,7 +115,48 @@ function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
|
|
|
112
115
|
return result;
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
/**
|
|
118
|
+
/** ------------------ VERSIONING ------------------ **/
|
|
119
|
+
function loadVersions() {
|
|
120
|
+
if (!fs.existsSync(VERSION_FILE)) return {};
|
|
121
|
+
return JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function saveVersions(versionMap) {
|
|
125
|
+
fs.writeFileSync(VERSION_FILE, JSON.stringify(versionMap, null, 2));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function computeHashCLI(filePath) {
|
|
129
|
+
if (!fs.existsSync(filePath)) return null;
|
|
130
|
+
const stats = fs.statSync(filePath);
|
|
131
|
+
if (stats.isDirectory()) {
|
|
132
|
+
const items = fs.readdirSync(filePath);
|
|
133
|
+
const combined = items.map(name => computeHashCLI(path.join(filePath, name))).join("");
|
|
134
|
+
return crypto.createHash("sha256").update(filePath + combined).digest("hex");
|
|
135
|
+
} else {
|
|
136
|
+
const content = fs.readFileSync(filePath);
|
|
137
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function checkChanges(fileTree, versionMap, rootPath = CWD) {
|
|
142
|
+
return fileTree.map(item => {
|
|
143
|
+
const fullPath = path.join(rootPath, item.path || item.name);
|
|
144
|
+
const hash = computeHashCLI(fullPath);
|
|
145
|
+
const prevHash = versionMap[item.path || fullPath] || null;
|
|
146
|
+
const changed = hash !== prevHash;
|
|
147
|
+
|
|
148
|
+
let newItem = { ...item, changed, hash };
|
|
149
|
+
|
|
150
|
+
if (item.type === "folder" && item.children.length > 0) {
|
|
151
|
+
newItem.children = checkChanges(item.children, versionMap, rootPath);
|
|
152
|
+
newItem.changed = newItem.changed || newItem.children.some(c => c.changed);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return newItem;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** ------------------ CLOUDINARY UPLOAD ------------------ **/
|
|
116
160
|
async function uploadFileSigned(filePath, folder, roomId, token, email) {
|
|
117
161
|
const filename = path.basename(filePath);
|
|
118
162
|
|
|
@@ -152,26 +196,28 @@ async function uploadTree(fileTree, folderHex, roomId, token, email, parentPath
|
|
|
152
196
|
uploaded.push({
|
|
153
197
|
name: node.name,
|
|
154
198
|
type: "folder",
|
|
199
|
+
path: relativePath,
|
|
155
200
|
children
|
|
156
201
|
});
|
|
157
|
-
} else {
|
|
202
|
+
} else if (node.changed) {
|
|
158
203
|
const url = await uploadFileSigned(node.path, `tnc_uploads/${folderHex}`, roomId, token, email);
|
|
159
204
|
console.log(`📦 Uploaded: ${relativePath} → ${url}`);
|
|
160
|
-
|
|
161
205
|
uploaded.push({
|
|
162
206
|
name: node.name,
|
|
163
207
|
type: "file",
|
|
164
208
|
path: relativePath,
|
|
165
209
|
size: node.size,
|
|
166
|
-
url
|
|
210
|
+
url
|
|
167
211
|
});
|
|
212
|
+
} else {
|
|
213
|
+
uploaded.push(node); // unchanged, no upload
|
|
168
214
|
}
|
|
169
215
|
}
|
|
170
216
|
|
|
171
217
|
return uploaded;
|
|
172
218
|
}
|
|
173
219
|
|
|
174
|
-
/**
|
|
220
|
+
/** ------------------ PUSH FUNCTION ------------------ **/
|
|
175
221
|
async function push(roomId, targetPath) {
|
|
176
222
|
const { token, email } = readToken();
|
|
177
223
|
const stats = fs.statSync(targetPath);
|
|
@@ -185,7 +231,7 @@ async function push(roomId, targetPath) {
|
|
|
185
231
|
const relativePath = path.basename(targetPath);
|
|
186
232
|
content = shouldIgnore(relativePath, ignoreList)
|
|
187
233
|
? []
|
|
188
|
-
: [{ name: relativePath, type: "file", path:
|
|
234
|
+
: [{ name: relativePath, type: "file", path: relativePath, size: stats.size }];
|
|
189
235
|
}
|
|
190
236
|
|
|
191
237
|
if (!content.length) {
|
|
@@ -193,11 +239,20 @@ async function push(roomId, targetPath) {
|
|
|
193
239
|
return;
|
|
194
240
|
}
|
|
195
241
|
|
|
242
|
+
const previousVersions = loadVersions();
|
|
243
|
+
const contentWithChanges = checkChanges(content, previousVersions);
|
|
244
|
+
const hasChanges = contentWithChanges.some(item => item.changed || (item.children && item.children.some(c => c.changed)));
|
|
245
|
+
|
|
246
|
+
if (!hasChanges) {
|
|
247
|
+
console.log("ℹ️ No changes detected since last push.");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
196
251
|
try {
|
|
197
252
|
const folderHex = crypto.createHash("md5").update(path.basename(targetPath) + Date.now()).digest("hex");
|
|
198
253
|
|
|
199
254
|
console.log("🚀 Uploading to Cloudinary...");
|
|
200
|
-
const uploadedTree = await uploadTree(
|
|
255
|
+
const uploadedTree = await uploadTree(contentWithChanges, folderHex, roomId, token, email);
|
|
201
256
|
|
|
202
257
|
console.log("🗂️ Sending metadata to backend...");
|
|
203
258
|
await axios.post(
|
|
@@ -207,12 +262,24 @@ async function push(roomId, targetPath) {
|
|
|
207
262
|
);
|
|
208
263
|
|
|
209
264
|
console.log("✅ Upload complete! Metadata stored successfully.");
|
|
265
|
+
|
|
266
|
+
// Save version hashes
|
|
267
|
+
const flattenVersionMap = {};
|
|
268
|
+
const flatten = items => {
|
|
269
|
+
for (const item of items) {
|
|
270
|
+
flattenVersionMap[item.path || item.name] = item.hash;
|
|
271
|
+
if (item.children) flatten(item.children);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
flatten(contentWithChanges);
|
|
275
|
+
saveVersions(flattenVersionMap);
|
|
276
|
+
|
|
210
277
|
} catch (err) {
|
|
211
278
|
console.error("❌ Upload failed:", err.response?.data || err.message);
|
|
212
279
|
}
|
|
213
280
|
}
|
|
214
281
|
|
|
215
|
-
/**
|
|
282
|
+
/** ------------------ CLI HANDLER ------------------ **/
|
|
216
283
|
const args = process.argv.slice(2);
|
|
217
284
|
|
|
218
285
|
switch (args[0]) {
|
|
@@ -220,6 +287,10 @@ switch (args[0]) {
|
|
|
220
287
|
login();
|
|
221
288
|
break;
|
|
222
289
|
|
|
290
|
+
case "logout":
|
|
291
|
+
logout();
|
|
292
|
+
break;
|
|
293
|
+
|
|
223
294
|
case "push": {
|
|
224
295
|
const roomIndex = args.indexOf("--room");
|
|
225
296
|
if (roomIndex === -1 || !args[roomIndex + 1] || !args[roomIndex + 2]) {
|
|
@@ -231,14 +302,11 @@ switch (args[0]) {
|
|
|
231
302
|
push(roomId, targetPath);
|
|
232
303
|
break;
|
|
233
304
|
}
|
|
234
|
-
case "logout": {
|
|
235
|
-
logout();
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
305
|
|
|
239
306
|
default:
|
|
240
307
|
console.log("✅ TNC CLI ready!");
|
|
241
308
|
console.log("Commands:");
|
|
242
309
|
console.log(" tnc login");
|
|
243
310
|
console.log(" tnc push --room <roomId> <path>");
|
|
244
|
-
|
|
311
|
+
console.log(" tnc logout");
|
|
312
|
+
}
|
package/branch/init.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
const CWD = process.cwd();
|
|
7
|
+
|
|
8
|
+
async function projectInit() {
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const answer = await inquirer.prompt([
|
|
12
|
+
{type: "input", name: "projectName", message: "Enter Project Name:"},
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const HomeDir = os.homedir();
|
|
18
|
+
const data = fs.readFileSync(path.join(HomeDir, ".tncrc"));
|
|
19
|
+
const currentUser = JSON.parse(data).email;
|
|
20
|
+
// let us make the a .tncmeta.json file for keeping track of branch info
|
|
21
|
+
//.tncmeta.json should be in the a separate folder at the root of the project
|
|
22
|
+
|
|
23
|
+
const response = await axios.post("http://localhost:3001/cli/init", {
|
|
24
|
+
projectName : answer.projectName,
|
|
25
|
+
owner: currentUser
|
|
26
|
+
})
|
|
27
|
+
const projectId = response.data.project._id;
|
|
28
|
+
|
|
29
|
+
const tncFolder = fs.mkdirSync(path.join(CWD, ".tnc"), { recursive: true });
|
|
30
|
+
|
|
31
|
+
const metaFilePath = path.join(tncFolder, ".tncmeta.json");
|
|
32
|
+
|
|
33
|
+
fs.writeFileSync(metaFilePath, JSON.stringify({"projectId": projectId,
|
|
34
|
+
"projectName": answer.projectName,
|
|
35
|
+
"currentBranch": "main",
|
|
36
|
+
"lastCommit": null,
|
|
37
|
+
"files": {},
|
|
38
|
+
},
|
|
39
|
+
null,
|
|
40
|
+
2
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
console.log("✅ Project initialized successfully!");
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
projectInit();
|
package/branch/pull.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import { createUploader } from "./cloudUploader.js";
|
|
8
|
+
|
|
9
|
+
const BASE_URL = "http://localhost:3001";
|
|
10
|
+
const CWD = process.cwd();
|
|
11
|
+
|
|
12
|
+
function readToken() {
|
|
13
|
+
const homeDir = os.homedir();
|
|
14
|
+
return JSON.parse(fs.readFileSync(path.join(homeDir, ".tncrc")));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readMeta() {
|
|
18
|
+
const metaPath = path.join(CWD, ".tnc", ".tncmeta.json");
|
|
19
|
+
return fs.existsSync(metaPath) ? JSON.parse(fs.readFileSync(metaPath)) : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeMeta(meta) {
|
|
23
|
+
const metaPath = path.join(CWD, ".tnc", ".tncmeta.json");
|
|
24
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ========== IGNORE HANDLING ==========
|
|
28
|
+
function loadIgnore(folderPath) {
|
|
29
|
+
const ignoreFile = path.join(folderPath, ".ignoretnc");
|
|
30
|
+
if (!fs.existsSync(ignoreFile)) return [];
|
|
31
|
+
return fs.readFileSync(ignoreFile, "utf-8")
|
|
32
|
+
.split("\n")
|
|
33
|
+
.map(l => l.trim())
|
|
34
|
+
.filter(l => l && !l.startsWith("#"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function shouldIgnore(relativePath, ignoreList) {
|
|
38
|
+
relativePath = relativePath.replace(/\\/g, "/");
|
|
39
|
+
return ignoreList.some(pattern => {
|
|
40
|
+
pattern = pattern.replace(/\\/g, "/");
|
|
41
|
+
if (pattern.endsWith("/**")) {
|
|
42
|
+
const folder = pattern.slice(0, -3);
|
|
43
|
+
return relativePath === folder || relativePath.startsWith(folder + "/");
|
|
44
|
+
}
|
|
45
|
+
if (pattern.startsWith("*.")) return relativePath.endsWith(pattern.slice(1));
|
|
46
|
+
return relativePath === pattern;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ========== SCAN FOLDER ==========
|
|
51
|
+
function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
|
|
52
|
+
const items = fs.readdirSync(folderPath, { withFileTypes: true });
|
|
53
|
+
const result = [];
|
|
54
|
+
for (const item of items) {
|
|
55
|
+
const fullPath = path.join(folderPath, item.name);
|
|
56
|
+
const relativePath = path.relative(rootPath, fullPath).replace(/\\/g, "/");
|
|
57
|
+
if (shouldIgnore(relativePath, ignoreList)) continue;
|
|
58
|
+
|
|
59
|
+
if (item.isDirectory()) {
|
|
60
|
+
result.push({ name: item.name, type: "folder", children: scanFolder(fullPath, ignoreList, rootPath) });
|
|
61
|
+
} else {
|
|
62
|
+
const stats = fs.statSync(fullPath);
|
|
63
|
+
result.push({ name: item.name, type: "file", path: fullPath, size: stats.size });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ========== PUSH EVERYTHING ==========
|
|
70
|
+
export async function dpushAll() {
|
|
71
|
+
const { token, email } = readToken();
|
|
72
|
+
const meta = readMeta();
|
|
73
|
+
if (!meta) return console.log("❌ Project not initialized locally. Run init first.");
|
|
74
|
+
const { projectId, currentBranch } = meta;
|
|
75
|
+
|
|
76
|
+
const stats = fs.statSync(CWD);
|
|
77
|
+
const ignoreList = loadIgnore(CWD);
|
|
78
|
+
|
|
79
|
+
const content = scanFolder(CWD, ignoreList);
|
|
80
|
+
if (!content.length) return console.log("⚠️ Nothing to upload (all ignored).");
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const uploader = createUploader(BASE_URL, token, email);
|
|
84
|
+
const folderHex = crypto.createHash("md5").update(path.basename(CWD) + Date.now()).digest("hex");
|
|
85
|
+
|
|
86
|
+
console.log("🚀 Uploading entire project...");
|
|
87
|
+
const uploadedTree = await uploader.uploadTree(content, folderHex, projectId);
|
|
88
|
+
|
|
89
|
+
// Prepare metadata
|
|
90
|
+
const filesMetadata = [];
|
|
91
|
+
function flattenTree(nodes, parent = "") {
|
|
92
|
+
for (const node of nodes) {
|
|
93
|
+
const relPath = path.join(parent, node.name).replace(/\\/g, "/");
|
|
94
|
+
if (node.type === "file") {
|
|
95
|
+
const hash = crypto.createHash("md5").update(fs.readFileSync(path.join(CWD, relPath))).digest("hex");
|
|
96
|
+
filesMetadata.push({ filename: relPath, contentHash: hash, path: node.url, version: 0 });
|
|
97
|
+
} else if (node.type === "folder") {
|
|
98
|
+
flattenTree(node.children, relPath);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
flattenTree(uploadedTree);
|
|
103
|
+
|
|
104
|
+
console.log("🗂️ Sending metadata to backend...");
|
|
105
|
+
const pushRes = await axios.post(
|
|
106
|
+
`${BASE_URL}/cli/push`,
|
|
107
|
+
{ projectId, branchName: currentBranch, files: filesMetadata, author: email, message: "Full project push" },
|
|
108
|
+
{ headers: { authorization: `Bearer ${token}`, email } }
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
console.log("✅ Push successful:", pushRes.data);
|
|
112
|
+
|
|
113
|
+
// Update local meta
|
|
114
|
+
meta.lastCommit = pushRes.data.commitId;
|
|
115
|
+
for (const f of pushRes.data.files) meta.files[f.filename] = f.version;
|
|
116
|
+
writeMeta(meta);
|
|
117
|
+
console.log("📝 Local metadata updated.");
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error("❌ Push failed:", err.response?.data || err.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Call push all
|
|
124
|
+
dpushAll();
|