thinkncollab-cli 0.0.21 → 0.0.23

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.
Files changed (2) hide show
  1. package/bin/index.js +137 -50
  2. package/package.json +1 -1
package/bin/index.js CHANGED
@@ -119,48 +119,86 @@ function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
119
119
  /** ------------------ VERSIONING ------------------ **/
120
120
  function loadVersions() {
121
121
  if (!fs.existsSync(VERSION_FILE)) return {};
122
- return JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
122
+ const data = JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
123
+
124
+ // Backward compatibility: if old format (just strings), convert to new format
125
+ const converted = {};
126
+ for (const [path, value] of Object.entries(data)) {
127
+ if (typeof value === 'string') {
128
+ converted[path] = { hash: value, url: '', version: 1 };
129
+ } else {
130
+ converted[path] = value;
131
+ }
132
+ }
133
+ return converted;
123
134
  }
124
135
 
125
136
  function saveVersions(versionMap) {
126
137
  fs.writeFileSync(VERSION_FILE, JSON.stringify(versionMap, null, 2));
127
138
  }
128
139
 
129
- function computeHashCLI(filePath) {
130
- if (!fs.existsSync(filePath)) return null;
131
-
132
- const stats = fs.statSync(filePath);
133
- if (stats.isDirectory()) {
134
- const items = fs.readdirSync(filePath);
135
- const combined = items.map(name => computeHashCLI(path.join(filePath, name)) || "").join("");
136
- return crypto.createHash("sha256").update(filePath + combined).digest("hex");
137
- } else {
138
- const content = fs.readFileSync(filePath);
139
- return crypto.createHash("sha256").update(content).digest("hex");
140
+ function computeFileHash(filePath) {
141
+ try {
142
+ if (!fs.existsSync(filePath)) return null;
143
+
144
+ const stats = fs.statSync(filePath);
145
+ if (stats.isDirectory()) {
146
+ // For folders: hash of sorted file names + their hashes
147
+ const items = fs.readdirSync(filePath).sort();
148
+ const childrenHashes = items.map(name =>
149
+ computeFileHash(path.join(filePath, name))
150
+ ).filter(Boolean).join('|');
151
+
152
+ return crypto.createHash("sha256")
153
+ .update(`folder:${filePath}|${childrenHashes}`)
154
+ .digest("hex");
155
+ } else {
156
+ // For files: hash of content + file metadata
157
+ const content = fs.readFileSync(filePath);
158
+ const stats = fs.statSync(filePath);
159
+
160
+ return crypto.createHash("sha256")
161
+ .update(content)
162
+ .update(`size:${stats.size}|mtime:${stats.mtimeMs}`)
163
+ .digest("hex");
164
+ }
165
+ } catch (err) {
166
+ console.error(`❌ Error computing hash for ${filePath}:`, err.message);
167
+ return null;
140
168
  }
141
169
  }
142
170
 
143
171
  function checkChanges(fileTree, versionMap, rootPath = CWD) {
144
172
  return fileTree.map(item => {
145
173
  const fullPath = path.join(rootPath, item.path || item.name);
174
+ const relativePath = item.path || item.name;
175
+
146
176
  let hash = null;
147
-
148
- if (fs.existsSync(fullPath)) {
149
- if (fs.statSync(fullPath).isDirectory()) {
150
- const children = fs.readdirSync(fullPath).map(name => computeHashCLI(path.join(fullPath, name)) || "").join("");
151
- hash = crypto.createHash("sha256").update(fullPath + children).digest("hex");
152
- } else {
153
- const content = fs.readFileSync(fullPath);
154
- hash = crypto.createHash("sha256").update(content).digest("hex");
177
+ let changed = true;
178
+
179
+ try {
180
+ if (fs.existsSync(fullPath)) {
181
+ hash = computeFileHash(fullPath);
182
+
183
+ // Check against previous version
184
+ const prevVersion = versionMap[relativePath];
185
+ if (prevVersion && prevVersion.hash === hash) {
186
+ changed = false;
187
+ }
155
188
  }
189
+ } catch (err) {
190
+ console.error(`❌ Error checking changes for ${relativePath}:`, err.message);
156
191
  }
157
192
 
158
- const prevHash = versionMap[item.path || fullPath] || null;
159
- const changed = hash !== prevHash;
160
-
161
- const newItem = { ...item, changed, hash };
193
+ const newItem = {
194
+ ...item,
195
+ changed,
196
+ hash,
197
+ path: relativePath // Ensure consistent path
198
+ };
162
199
 
163
- if (item.type === "folder" && item.children.length > 0) {
200
+ // Recursively check children for folders
201
+ if (item.type === "folder" && item.children && item.children.length > 0) {
164
202
  newItem.children = checkChanges(item.children, versionMap, rootPath);
165
203
  newItem.changed = newItem.changed || newItem.children.some(c => c.changed);
166
204
  }
@@ -198,22 +236,59 @@ async function uploadFileSigned(filePath, folder, roomId, token, email) {
198
236
  return cloudRes.data.secure_url;
199
237
  }
200
238
 
201
- async function uploadTree(fileTree, folderHex, roomId, token, email, parentPath = "") {
239
+ async function uploadTree(fileTree, folderHex, roomId, token, email, previousVersions, parentPath = "") {
202
240
  const uploaded = [];
203
241
 
204
242
  for (const node of fileTree) {
205
- const relativePath = path.join(parentPath, node.name).replace(/\\/g, "/");
243
+ const relativePath = node.path || path.join(parentPath, node.name).replace(/\\/g, "/");
206
244
 
207
245
  if (node.type === "folder") {
208
- const children = await uploadTree(node.children, folderHex, roomId, token, email, relativePath);
209
- const folderHash = crypto.createHash("sha256").update(relativePath + children.map(c => c.hash || "").join("")).digest("hex");
210
- uploaded.push({ ...node, children, hash: folderHash });
211
- } else if (node.changed && node.path) {
212
- const url = await uploadFileSigned(node.path, `tnc_uploads/${folderHex}`, roomId, token, email);
213
- console.log(`📦 Uploaded: ${relativePath} → ${url}`);
214
- uploaded.push({ ...node, url });
246
+ const children = await uploadTree(node.children, folderHex, roomId, token, email, previousVersions, relativePath);
247
+
248
+ uploaded.push({
249
+ ...node,
250
+ children,
251
+ hash: node.hash,
252
+ path: relativePath
253
+ });
215
254
  } else {
216
- uploaded.push(node); // unchanged, no upload
255
+ const prevFile = previousVersions[relativePath];
256
+
257
+ if (node.changed) {
258
+ try {
259
+ console.log(`📤 Uploading changed file: ${relativePath}`);
260
+ const url = await uploadFileSigned(node.path, `tnc_uploads/${folderHex}`, roomId, token, email);
261
+ console.log(`✅ Uploaded: ${relativePath}`);
262
+
263
+ uploaded.push({
264
+ ...node,
265
+ url,
266
+ hash: node.hash,
267
+ path: relativePath,
268
+ version: prevFile ? prevFile.version + 1 : 1
269
+ });
270
+ } catch (err) {
271
+ console.error(`❌ Failed to upload ${relativePath}:`, err.message);
272
+ // Fallback to previous version if upload fails
273
+ uploaded.push({
274
+ ...node,
275
+ url: prevFile?.url,
276
+ hash: node.hash,
277
+ path: relativePath,
278
+ version: prevFile?.version || 1
279
+ });
280
+ }
281
+ } else {
282
+ // Unchanged file - use previous URL and version
283
+ console.log(`📋 Using previous version for: ${relativePath}`);
284
+ uploaded.push({
285
+ ...node,
286
+ url: prevFile?.url,
287
+ hash: node.hash,
288
+ path: relativePath,
289
+ version: prevFile?.version || 1
290
+ });
291
+ }
217
292
  }
218
293
  }
219
294
 
@@ -230,10 +305,6 @@ async function push(roomId, targetPath) {
230
305
  }
231
306
  const meta = JSON.parse(fs.readFileSync(tncMetaPath, "utf-8"));
232
307
  const projectId = meta.projectId;
233
- if (!projectId) {
234
- console.error("❌ Project ID missing in metadata. Run 'tnc init' again.");
235
- process.exit(1);
236
- }
237
308
 
238
309
  const stats = fs.statSync(targetPath);
239
310
  const rootFolder = stats.isDirectory() ? targetPath : path.dirname(targetPath);
@@ -255,7 +326,16 @@ async function push(roomId, targetPath) {
255
326
  }
256
327
 
257
328
  const previousVersions = loadVersions();
329
+ console.log('📁 Previous versions:', Object.keys(previousVersions).length);
330
+
258
331
  const contentWithChanges = checkChanges(content, previousVersions);
332
+
333
+ // Debug: Show what changed
334
+ const changedFiles = contentWithChanges.flatMap(item =>
335
+ item.changed ? [item.path] : []
336
+ );
337
+ console.log('🔄 Changed files:', changedFiles.length, changedFiles);
338
+
259
339
  const hasChanges = contentWithChanges.some(item => item.changed || (item.children && item.children.some(c => c.changed)));
260
340
 
261
341
  if (!hasChanges) {
@@ -267,10 +347,9 @@ async function push(roomId, targetPath) {
267
347
  const folderHex = crypto.createHash("md5").update(path.basename(targetPath) + Date.now()).digest("hex");
268
348
 
269
349
  console.log("🚀 Uploading to Cloudinary...");
270
- const uploadedTree = await uploadTree(contentWithChanges, folderHex, roomId, token, email);
271
-
272
- console.log("🗂️ Sending metadata:", { folderId: folderHex, content: uploadedTree, uploadedBy: email, projectId });
350
+ const uploadedTree = await uploadTree(contentWithChanges, folderHex, roomId, token, email, previousVersions);
273
351
 
352
+ console.log("🗂️ Sending metadata...");
274
353
  await axios.post(
275
354
  `${BASE_URL}/${roomId}/upload`,
276
355
  { folderId: folderHex, content: uploadedTree, uploadedBy: email, projectId },
@@ -279,16 +358,24 @@ async function push(roomId, targetPath) {
279
358
 
280
359
  console.log("✅ Upload complete! Metadata stored successfully.");
281
360
 
282
- // Save version hashes
283
- const flattenVersionMap = {};
284
- const flatten = items => {
361
+ // Save version hashes WITH URLs
362
+ const newVersionMap = {};
363
+ const flattenAndStore = (items) => {
285
364
  for (const item of items) {
286
- flattenVersionMap[item.path || item.name] = item.hash;
287
- if (item.children) flatten(item.children);
365
+ if (item.type === "file") {
366
+ newVersionMap[item.path] = {
367
+ hash: item.hash,
368
+ url: item.url, // ✅ Store URL locally
369
+ version: item.version || 1
370
+ };
371
+ }
372
+ if (item.children) flattenAndStore(item.children);
288
373
  }
289
374
  };
290
- flatten(contentWithChanges);
291
- saveVersions(flattenVersionMap);
375
+ flattenAndStore(uploadedTree);
376
+ saveVersions(newVersionMap);
377
+
378
+ console.log(`💾 Saved ${Object.keys(newVersionMap).length} files to .tncversions`);
292
379
 
293
380
  } catch (err) {
294
381
  console.error("❌ Upload failed:", err.response?.data || err.message);
@@ -334,4 +421,4 @@ async function main() {
334
421
  }
335
422
  }
336
423
 
337
- main().catch(err => console.error(err));
424
+ main().catch(err => console.error(err));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thinkncollab-cli",
3
3
  "author": "Raman Singh",
4
- "version": "0.0.21",
4
+ "version": "0.0.23",
5
5
  "description": "CLI tool for ThinkNCollab",
6
6
  "main": "index.js",
7
7
  "bin": {