thinkncollab-cli 0.0.10 → 0.0.12

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