thinkncollab-cli 0.0.22 → 0.0.24

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/index.js CHANGED
@@ -7,6 +7,7 @@ import path from "path";
7
7
  import crypto from "crypto";
8
8
  import FormData from "form-data";
9
9
  import projectInit from "../commands/init.js";
10
+ import Status from "../commands/status.js";
10
11
 
11
12
  const RC_FILE = path.join(os.homedir(), ".tncrc");
12
13
  const VERSION_FILE = path.join(process.cwd(), ".tncversions");
@@ -119,48 +120,86 @@ function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
119
120
  /** ------------------ VERSIONING ------------------ **/
120
121
  function loadVersions() {
121
122
  if (!fs.existsSync(VERSION_FILE)) return {};
122
- return JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
123
+ const data = JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
124
+
125
+ // Backward compatibility: if old format (just strings), convert to new format
126
+ const converted = {};
127
+ for (const [path, value] of Object.entries(data)) {
128
+ if (typeof value === 'string') {
129
+ converted[path] = { hash: value, url: '', version: 1 };
130
+ } else {
131
+ converted[path] = value;
132
+ }
133
+ }
134
+ return converted;
123
135
  }
124
136
 
125
137
  function saveVersions(versionMap) {
126
138
  fs.writeFileSync(VERSION_FILE, JSON.stringify(versionMap, null, 2));
127
139
  }
128
140
 
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");
141
+ function computeFileHash(filePath) {
142
+ try {
143
+ if (!fs.existsSync(filePath)) return null;
144
+
145
+ const stats = fs.statSync(filePath);
146
+ if (stats.isDirectory()) {
147
+ // For folders: hash of sorted file names + their hashes
148
+ const items = fs.readdirSync(filePath).sort();
149
+ const childrenHashes = items.map(name =>
150
+ computeFileHash(path.join(filePath, name))
151
+ ).filter(Boolean).join('|');
152
+
153
+ return crypto.createHash("sha256")
154
+ .update(`folder:${filePath}|${childrenHashes}`)
155
+ .digest("hex");
156
+ } else {
157
+ // For files: hash of content + file metadata
158
+ const content = fs.readFileSync(filePath);
159
+ const stats = fs.statSync(filePath);
160
+
161
+ return crypto.createHash("sha256")
162
+ .update(content)
163
+ .update(`size:${stats.size}|mtime:${stats.mtimeMs}`)
164
+ .digest("hex");
165
+ }
166
+ } catch (err) {
167
+ console.error(`❌ Error computing hash for ${filePath}:`, err.message);
168
+ return null;
140
169
  }
141
170
  }
142
171
 
143
172
  function checkChanges(fileTree, versionMap, rootPath = CWD) {
144
173
  return fileTree.map(item => {
145
174
  const fullPath = path.join(rootPath, item.path || item.name);
175
+ const relativePath = item.path || item.name;
176
+
146
177
  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");
178
+ let changed = true;
179
+
180
+ try {
181
+ if (fs.existsSync(fullPath)) {
182
+ hash = computeFileHash(fullPath);
183
+
184
+ // Check against previous version
185
+ const prevVersion = versionMap[relativePath];
186
+ if (prevVersion && prevVersion.hash === hash) {
187
+ changed = false;
188
+ }
155
189
  }
190
+ } catch (err) {
191
+ console.error(`❌ Error checking changes for ${relativePath}:`, err.message);
156
192
  }
157
193
 
158
- const prevHash = versionMap[item.path || fullPath] || null;
159
- const changed = hash !== prevHash;
160
-
161
- const newItem = { ...item, changed, hash };
194
+ const newItem = {
195
+ ...item,
196
+ changed,
197
+ hash,
198
+ path: relativePath // Ensure consistent path
199
+ };
162
200
 
163
- if (item.type === "folder" && item.children.length > 0) {
201
+ // Recursively check children for folders
202
+ if (item.type === "folder" && item.children && item.children.length > 0) {
164
203
  newItem.children = checkChanges(item.children, versionMap, rootPath);
165
204
  newItem.changed = newItem.changed || newItem.children.some(c => c.changed);
166
205
  }
@@ -202,41 +241,61 @@ async function uploadTree(fileTree, folderHex, roomId, token, email, previousVer
202
241
  const uploaded = [];
203
242
 
204
243
  for (const node of fileTree) {
205
- const relativePath = path.join(parentPath, node.name).replace(/\\/g, "/");
244
+ const relativePath = node.path || path.join(parentPath, node.name).replace(/\\/g, "/");
206
245
 
207
246
  if (node.type === "folder") {
208
247
  const children = await uploadTree(node.children, folderHex, roomId, token, email, previousVersions, 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
- try {
213
- const url = await uploadFileSigned(node.path, `tnc_uploads/${folderHex}`, roomId, token, email);
214
- console.log(`📦 Uploaded: ${relativePath} → ${url}`);
215
- uploaded.push({
216
- ...node,
217
- url,
218
- hash: node.hash
219
- });
220
- } catch (err) {
221
- console.error(`❌ Failed to upload ${relativePath}:`, err.message);
222
- uploaded.push({ ...node, hash: node.hash });
223
- }
224
- } else {
225
- // Unchanged file - use previous URL
226
- const prevFile = previousVersions[node.path];
248
+
227
249
  uploaded.push({
228
250
  ...node,
229
- url: prevFile?.url, // ✅ Use stored URL
251
+ children,
230
252
  hash: node.hash,
231
- version: prevFile?.version || 1
253
+ path: relativePath
232
254
  });
255
+ } else {
256
+ const prevFile = previousVersions[relativePath];
257
+
258
+ if (node.changed) {
259
+ try {
260
+ console.log(`📤 Uploading changed file: ${relativePath}`);
261
+ const url = await uploadFileSigned(node.path, `tnc_uploads/${folderHex}`, roomId, token, email);
262
+ console.log(`✅ Uploaded: ${relativePath}`);
263
+
264
+ uploaded.push({
265
+ ...node,
266
+ url,
267
+ hash: node.hash,
268
+ path: relativePath,
269
+ version: prevFile ? prevFile.version + 1 : 1
270
+ });
271
+ } catch (err) {
272
+ console.error(`❌ Failed to upload ${relativePath}:`, err.message);
273
+ // Fallback to previous version if upload fails
274
+ uploaded.push({
275
+ ...node,
276
+ url: prevFile?.url,
277
+ hash: node.hash,
278
+ path: relativePath,
279
+ version: prevFile?.version || 1
280
+ });
281
+ }
282
+ } else {
283
+ // Unchanged file - use previous URL and version
284
+ console.log(`📋 Using previous version for: ${relativePath}`);
285
+ uploaded.push({
286
+ ...node,
287
+ url: prevFile?.url,
288
+ hash: node.hash,
289
+ path: relativePath,
290
+ version: prevFile?.version || 1
291
+ });
292
+ }
233
293
  }
234
294
  }
235
295
 
236
296
  return uploaded;
237
297
  }
238
298
 
239
-
240
299
  /** ------------------ PUSH FUNCTION ------------------ **/
241
300
  async function push(roomId, targetPath) {
242
301
  const { token, email } = readToken();
@@ -268,7 +327,16 @@ async function push(roomId, targetPath) {
268
327
  }
269
328
 
270
329
  const previousVersions = loadVersions();
330
+ console.log('📁 Previous versions:', Object.keys(previousVersions).length);
331
+
271
332
  const contentWithChanges = checkChanges(content, previousVersions);
333
+
334
+ // Debug: Show what changed
335
+ const changedFiles = contentWithChanges.flatMap(item =>
336
+ item.changed ? [item.path] : []
337
+ );
338
+ console.log('🔄 Changed files:', changedFiles.length, changedFiles);
339
+
272
340
  const hasChanges = contentWithChanges.some(item => item.changed || (item.children && item.children.some(c => c.changed)));
273
341
 
274
342
  if (!hasChanges) {
@@ -308,6 +376,8 @@ async function push(roomId, targetPath) {
308
376
  flattenAndStore(uploadedTree);
309
377
  saveVersions(newVersionMap);
310
378
 
379
+ console.log(`💾 Saved ${Object.keys(newVersionMap).length} files to .tncversions`);
380
+
311
381
  } catch (err) {
312
382
  console.error("❌ Upload failed:", err.response?.data || err.message);
313
383
  }
@@ -337,6 +407,9 @@ async function main() {
337
407
  await push(roomId, targetPath);
338
408
  break;
339
409
  }
410
+ case "status":
411
+ await Status();
412
+ break;
340
413
 
341
414
  case "init":
342
415
  await projectInit();
@@ -352,4 +425,4 @@ async function main() {
352
425
  }
353
426
  }
354
427
 
355
- main().catch(err => console.error(err));
428
+ main().catch(err => console.error(err));
@@ -0,0 +1,284 @@
1
+
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import crypto from "crypto";
5
+
6
+ const VERSION_FILE = path.join(process.cwd(), ".tncversions");
7
+ const CWD = process.cwd();
8
+
9
+ /** ------------------ IGNORE HANDLING ------------------ **/
10
+ function loadIgnore(folderPath) {
11
+ const ignoreFile = path.join(folderPath, ".ignoretnc");
12
+ if (!fs.existsSync(ignoreFile)) return [];
13
+ return fs
14
+ .readFileSync(ignoreFile, "utf-8")
15
+ .split("\n")
16
+ .map(line => line.trim())
17
+ .filter(line => line && !line.startsWith("#"));
18
+ }
19
+
20
+ function shouldIgnore(relativePath, ignoreList) {
21
+ return ignoreList.some(pattern => {
22
+ if (pattern.endsWith("/**")) {
23
+ const folder = pattern.slice(0, -3);
24
+ return relativePath === folder || relativePath.startsWith(folder + path.sep);
25
+ }
26
+ if (pattern.startsWith("*.")) {
27
+ return relativePath.endsWith(pattern.slice(1));
28
+ }
29
+ return relativePath === pattern;
30
+ });
31
+ }
32
+
33
+ /** ------------------ SCAN FOLDER ------------------ **/
34
+ function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
35
+ const items = fs.readdirSync(folderPath, { withFileTypes: true });
36
+ const result = [];
37
+ for (const item of items) {
38
+ const fullPath = path.join(folderPath, item.name);
39
+ const relativePath = path.relative(rootPath, fullPath);
40
+
41
+ if (shouldIgnore(relativePath, ignoreList)) {
42
+ continue;
43
+ }
44
+
45
+ if (item.isDirectory()) {
46
+ result.push({
47
+ name: item.name,
48
+ type: "folder",
49
+ children: scanFolder(fullPath, ignoreList, rootPath),
50
+ path: relativePath
51
+ });
52
+ } else {
53
+ const stats = fs.statSync(fullPath);
54
+ result.push({
55
+ name: item.name,
56
+ type: "file",
57
+ path: relativePath,
58
+ size: stats.size
59
+ });
60
+ }
61
+ }
62
+ return result;
63
+ }
64
+
65
+ /** ------------------ VERSIONING ------------------ **/
66
+ function loadVersions() {
67
+ if (!fs.existsSync(VERSION_FILE)) return {};
68
+ const data = JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
69
+
70
+ // Backward compatibility: if old format (just strings), convert to new format
71
+ const converted = {};
72
+ for (const [path, value] of Object.entries(data)) {
73
+ if (typeof value === 'string') {
74
+ converted[path] = { hash: value, url: '', version: 1 };
75
+ } else {
76
+ converted[path] = value;
77
+ }
78
+ }
79
+ return converted;
80
+ }
81
+
82
+ function computeFileHash(filePath) {
83
+ try {
84
+ if (!fs.existsSync(filePath)) return null;
85
+
86
+ const stats = fs.statSync(filePath);
87
+ if (stats.isDirectory()) {
88
+ // For folders: hash of sorted file names + their hashes
89
+ const items = fs.readdirSync(filePath).sort();
90
+ const childrenHashes = items.map(name =>
91
+ computeFileHash(path.join(filePath, name))
92
+ ).filter(Boolean).join('|');
93
+
94
+ return crypto.createHash("sha256")
95
+ .update(`folder:${filePath}|${childrenHashes}`)
96
+ .digest("hex");
97
+ } else {
98
+ // For files: hash of content + file metadata
99
+ const content = fs.readFileSync(filePath);
100
+ const stats = fs.statSync(filePath);
101
+
102
+ return crypto.createHash("sha256")
103
+ .update(content)
104
+ .update(`size:${stats.size}|mtime:${stats.mtimeMs}`)
105
+ .digest("hex");
106
+ }
107
+ } catch (err) {
108
+ console.error(`❌ Error computing hash for ${filePath}:`, err.message);
109
+ return null;
110
+ }
111
+ }
112
+
113
+ function checkChanges(fileTree, versionMap, rootPath = CWD) {
114
+ return fileTree.map(item => {
115
+ const fullPath = path.join(rootPath, item.path || item.name);
116
+ const relativePath = item.path || item.name;
117
+
118
+ let hash = null;
119
+ let changed = true;
120
+
121
+ try {
122
+ if (fs.existsSync(fullPath)) {
123
+ hash = computeFileHash(fullPath);
124
+
125
+ // Check against previous version
126
+ const prevVersion = versionMap[relativePath];
127
+ if (prevVersion && prevVersion.hash === hash) {
128
+ changed = false;
129
+ }
130
+ }
131
+ } catch (err) {
132
+ console.error(`❌ Error checking changes for ${relativePath}:`, err.message);
133
+ }
134
+
135
+ const newItem = {
136
+ ...item,
137
+ changed,
138
+ hash,
139
+ path: relativePath
140
+ };
141
+
142
+ // Recursively check children for folders
143
+ if (item.type === "folder" && item.children && item.children.length > 0) {
144
+ newItem.children = checkChanges(item.children, versionMap, rootPath);
145
+ newItem.changed = newItem.changed || newItem.children.some(c => c.changed);
146
+ }
147
+
148
+ return newItem;
149
+ });
150
+ }
151
+
152
+ /** ------------------ STATUS FUNCTION ------------------ **/
153
+ export default async function status() {
154
+ try {
155
+ const tncMetaPath = path.join(process.cwd(), ".tnc", ".tncmeta.json");
156
+ if (!fs.existsSync(tncMetaPath)) {
157
+ console.error("❌ Project not initialized. Run 'tnc init' first.");
158
+ return;
159
+ }
160
+
161
+ const meta = JSON.parse(fs.readFileSync(tncMetaPath, "utf-8"));
162
+ const projectId = meta.projectId;
163
+
164
+ console.log("📊 TNC Project Status");
165
+ console.log("=====================");
166
+ console.log(`Project ID: ${projectId}`);
167
+ console.log(`Directory: ${process.cwd()}`);
168
+ console.log("");
169
+
170
+ // Check if versions file exists
171
+ if (!fs.existsSync(VERSION_FILE)) {
172
+ console.log("📭 No previous versions found. Run 'tnc push' first.");
173
+ return;
174
+ }
175
+
176
+ const previousVersions = loadVersions();
177
+ const totalFiles = Object.keys(previousVersions).length;
178
+
179
+ console.log(`📁 Total tracked files: ${totalFiles}`);
180
+ console.log("");
181
+
182
+ // Scan current directory for changes
183
+ const ignoreList = loadIgnore(process.cwd());
184
+ const currentContent = scanFolder(process.cwd(), ignoreList);
185
+ const contentWithChanges = checkChanges(currentContent, previousVersions);
186
+
187
+ // Analyze changes
188
+ let changedCount = 0;
189
+ let newCount = 0;
190
+ let unchangedCount = 0;
191
+ let ignoredCount = 0;
192
+
193
+ const flattenAndAnalyze = (items) => {
194
+ for (const item of items) {
195
+ if (item.type === "file") {
196
+ if (!previousVersions[item.path]) {
197
+ newCount++;
198
+ } else if (item.changed) {
199
+ changedCount++;
200
+ } else {
201
+ unchangedCount++;
202
+ }
203
+ }
204
+ if (item.children) {
205
+ flattenAndAnalyze(item.children);
206
+ }
207
+ }
208
+ };
209
+
210
+ flattenAndAnalyze(contentWithChanges);
211
+
212
+ console.log("🔄 Change Summary:");
213
+ console.log(` ✅ Unchanged files: ${unchangedCount}`);
214
+ console.log(` 📝 Modified files: ${changedCount}`);
215
+ console.log(` 🆕 New files: ${newCount}`);
216
+ console.log("");
217
+
218
+ // Show detailed changes
219
+ if (changedCount > 0 || newCount > 0) {
220
+ console.log("📋 Detailed Changes:");
221
+ console.log("-------------------");
222
+
223
+ const showChanges = (items, indent = "") => {
224
+ for (const item of items) {
225
+ if (item.type === "file") {
226
+ if (!previousVersions[item.path]) {
227
+ console.log(`${indent}🆕 ${item.path} (new)`);
228
+ } else if (item.changed) {
229
+ console.log(`${indent}📝 ${item.path} (modified)`);
230
+ }
231
+ } else if (item.type === "folder") {
232
+ const hasFileChanges = item.children && item.children.some(child =>
233
+ child.type === 'file' && (!previousVersions[child.path] || child.changed)
234
+ );
235
+
236
+ if (hasFileChanges) {
237
+ console.log(`${indent}📁 ${item.path}/`);
238
+ if (item.children) {
239
+ showChanges(item.children, indent + " ");
240
+ }
241
+ }
242
+ }
243
+ }
244
+ };
245
+
246
+ showChanges(contentWithChanges);
247
+ } else {
248
+ console.log("🎉 No changes detected. Everything is up to date!");
249
+ }
250
+
251
+ console.log("");
252
+
253
+ // Show version info
254
+ const versions = Object.values(previousVersions);
255
+ if (versions.length > 0) {
256
+ const maxVersion = Math.max(...versions.map(v => v.version || 1));
257
+ console.log(`🏷️ Latest version: ${maxVersion}`);
258
+
259
+ // Show file type breakdown
260
+ const extensions = {};
261
+ Object.keys(previousVersions).forEach(filePath => {
262
+ const ext = path.extname(filePath) || 'no extension';
263
+ extensions[ext] = (extensions[ext] || 0) + 1;
264
+ });
265
+
266
+ console.log("");
267
+ console.log("📄 File Types:");
268
+ Object.entries(extensions)
269
+ .sort(([,a], [,b]) => b - a)
270
+ .forEach(([ext, count]) => {
271
+ console.log(` ${ext || '(no ext)'}: ${count} files`);
272
+ });
273
+ }
274
+
275
+ // Show last push info if available
276
+ const versionFileStats = fs.statSync(VERSION_FILE);
277
+ console.log("");
278
+ console.log(`⏰ Last push: ${versionFileStats.mtime.toLocaleString()}`);
279
+
280
+ } catch (err) {
281
+ console.error("❌ Error checking status:", err.message);
282
+ }
283
+ }
284
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thinkncollab-cli",
3
3
  "author": "Raman Singh",
4
- "version": "0.0.22",
4
+ "version": "0.0.24",
5
5
  "description": "CLI tool for ThinkNCollab",
6
6
  "main": "index.js",
7
7
  "bin": {