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 CHANGED
@@ -14,7 +14,10 @@ npm install -g @thinkncollab/tnc-cli
14
14
  tnc-cli login
15
15
 
16
16
  # Push files to a room
17
- tnc-cli push --room 64a1b2c3d4e5f6a1b2c3d4e5 ./project-files/
17
+ tnc-cli push --room <roomId> <path>
18
+
19
+ # Logout
20
+ tnc-cli logout
18
21
  ```
19
22
 
20
23
  ---
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 BASE_URL = "https://thinkncollab.com/rooms";
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
- // LOGOUT
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
- /** ========== IGNORE HANDLING ========== **/
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
- /** ========== SCAN FOLDER ========== **/
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: fullPath,
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
- /** ========== CLOUDINARY UPLOAD (SIGNED) ========== **/
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 // ✅ send top-level URL now
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
- /** ========== PUSH FUNCTION ========== **/
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: targetPath, size: stats.size }];
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(content, folderHex, roomId, token, email);
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
- /** ========== CLI HANDLER ========== **/
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
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "projectId": "68f9a4687d7da88440b2e0eb",
3
+ "projectName": "my project",
4
+ "currentBranch": "main",
5
+ "lastCommit": null,
6
+ "files": {}
7
+ }
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();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thinkncollab-cli",
3
3
  "author": "Raman Singh",
4
- "version": "0.0.10",
4
+ "version": "0.0.11",
5
5
  "description": "CLI tool for ThinkNCollab",
6
6
  "main": "index.js",
7
7
  "bin": {