thinkncollab-cli 0.0.24 → 0.0.26

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
@@ -8,6 +8,7 @@ import crypto from "crypto";
8
8
  import FormData from "form-data";
9
9
  import projectInit from "../commands/init.js";
10
10
  import Status from "../commands/status.js";
11
+ import pull from "../commands/pull.js";
11
12
 
12
13
  const RC_FILE = path.join(os.homedir(), ".tncrc");
13
14
  const VERSION_FILE = path.join(process.cwd(), ".tncversions");
@@ -414,6 +415,22 @@ async function main() {
414
415
  case "init":
415
416
  await projectInit();
416
417
  break;
418
+
419
+ case "pull": {
420
+ const roomIndex = args.indexOf("--room");
421
+ const versionIndex = args.indexOf("--version");
422
+
423
+ if (roomIndex === -1 || !args[roomIndex + 1]) {
424
+ console.error("Usage: tnc pull --room <roomId> [--version <version>]");
425
+ process.exit(1);
426
+ }
427
+
428
+ const roomId = args[roomIndex + 1];
429
+ const version = versionIndex !== -1 && args[versionIndex + 1] ? args[versionIndex + 1] : "latest";
430
+
431
+ await pull(roomId, version);
432
+ break;
433
+ }
417
434
 
418
435
  default:
419
436
  console.log("✅ TNC CLI ready!");
package/commands/pull.js CHANGED
@@ -1,124 +1,213 @@
1
- #!/usr/bin/env node
2
1
  import fs from "fs";
3
2
  import path from "path";
4
- import crypto from "crypto";
5
3
  import axios from "axios";
4
+ import https from "https";
6
5
  import os from "os";
7
- import { createUploader } from "./cloudUploader.js";
8
6
 
9
- const BASE_URL = "http://localhost:3001";
7
+
8
+ const RC_FILE = path.join(os.homedir(), ".tncrc");
10
9
  const CWD = process.cwd();
11
10
 
11
+ /** ------------------ TOKEN UTILS ------------------ **/
12
12
  function readToken() {
13
- const homeDir = os.homedir();
14
- return JSON.parse(fs.readFileSync(path.join(homeDir, ".tncrc")));
13
+ if (!fs.existsSync(RC_FILE)) {
14
+ console.error("❌ Not logged in. Run 'tnc login' first.");
15
+ process.exit(1);
16
+ }
17
+ const data = JSON.parse(fs.readFileSync(RC_FILE));
18
+ return { token: data.token, email: data.email };
15
19
  }
16
20
 
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
+ /** ------------------ DOWNLOAD FILE ------------------ **/
22
+ async function downloadFile(url, filePath) {
23
+ return new Promise((resolve, reject) => {
24
+ const fileDir = path.dirname(filePath);
25
+
26
+ // Create directory if it doesn't exist
27
+ if (!fs.existsSync(fileDir)) {
28
+ fs.mkdirSync(fileDir, { recursive: true });
29
+ }
21
30
 
22
- function writeMeta(meta) {
23
- const metaPath = path.join(CWD, ".tnc", ".tncmeta.json");
24
- fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
25
- }
31
+ const file = fs.createWriteStream(filePath);
32
+
33
+ https.get(url, (response) => {
34
+ if (response.statusCode !== 200) {
35
+ reject(new Error(`Failed to download: ${response.statusCode}`));
36
+ return;
37
+ }
26
38
 
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
- }
39
+ response.pipe(file);
36
40
 
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;
41
+ file.on('finish', () => {
42
+ file.close();
43
+ resolve(filePath);
44
+ });
45
+
46
+ }).on('error', (err) => {
47
+ fs.unlink(filePath, () => {}); // Delete the file on error
48
+ reject(err);
49
+ });
47
50
  });
48
51
  }
49
52
 
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 });
53
+ /** ------------------ PROCESS FOLDER CONTENT ------------------ **/
54
+ async function processFolderContent(content, basePath = "") {
55
+ let downloadedCount = 0;
56
+ let skippedCount = 0;
57
+ let errorCount = 0;
58
+
59
+ for (const item of content) {
60
+ const itemPath = path.join(basePath, item.path || item.name);
61
+
62
+ if (item.type === "folder") {
63
+ // Create directory
64
+ if (!fs.existsSync(itemPath)) {
65
+ fs.mkdirSync(itemPath, { recursive: true });
66
+ console.log(`📁 Created directory: ${itemPath}`);
67
+ }
68
+
69
+ // Process children recursively
70
+ if (item.children && item.children.length > 0) {
71
+ const result = await processFolderContent(item.children, basePath);
72
+ downloadedCount += result.downloadedCount;
73
+ skippedCount += result.skippedCount;
74
+ errorCount += result.errorCount;
75
+ }
76
+ } else if (item.type === "file" && item.url) {
77
+ try {
78
+ // Check if file already exists and has same content
79
+ if (fs.existsSync(itemPath)) {
80
+ const existingStats = fs.statSync(itemPath);
81
+ if (existingStats.size === item.size) {
82
+ console.log(`✅ Already exists: ${itemPath}`);
83
+ skippedCount++;
84
+ continue;
85
+ }
86
+ }
87
+
88
+ console.log(`📥 Downloading: ${itemPath}`);
89
+ await downloadFile(item.url, itemPath);
90
+ console.log(`✅ Downloaded: ${itemPath}`);
91
+ downloadedCount++;
92
+ } catch (err) {
93
+ console.error(`❌ Failed to download ${itemPath}:`, err.message);
94
+ errorCount++;
95
+ }
96
+ } else if (item.type === "file" && !item.url) {
97
+ console.log(`⚠️ No URL available for: ${itemPath}`);
98
+ skippedCount++;
64
99
  }
65
100
  }
66
- return result;
101
+
102
+ return { downloadedCount, skippedCount, errorCount };
67
103
  }
68
104
 
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;
105
+ /** ------------------ PULL FUNCTION ------------------ **/
106
+ export default async function pull(roomId, version = "latest") {
107
+ try {
108
+ const { token, email } = readToken();
109
+
110
+ const tncMetaPath = path.join(process.cwd(), ".tnc", ".tncmeta.json");
111
+ if (!fs.existsSync(tncMetaPath)) {
112
+ console.error("❌ Project not initialized. Run 'tnc init' first.");
113
+ return;
114
+ }
75
115
 
76
- const stats = fs.statSync(CWD);
77
- const ignoreList = loadIgnore(CWD);
116
+ const meta = JSON.parse(fs.readFileSync(tncMetaPath, "utf-8"));
117
+ const projectId = meta.projectId;
78
118
 
79
- const content = scanFolder(CWD, ignoreList);
80
- if (!content.length) return console.log("⚠️ Nothing to upload (all ignored).");
119
+ console.log(`🚀 Pulling files from room ${roomId}...`);
120
+ console.log(`📋 Project: ${projectId}`);
121
+ console.log(`🎯 Version: ${version}`);
122
+ console.log("");
81
123
 
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
- }
124
+ // Get folder data from server
125
+ let url = `http://localhost:3001/folders/${roomId}/download`;
126
+ const params = { projectId };
127
+
128
+ if (version !== "latest") {
129
+ params.version = version;
130
+ }
131
+
132
+ const response = await axios.get(url, {
133
+ params,
134
+ headers: {
135
+ authorization: `Bearer ${token}`,
136
+ email: email
100
137
  }
138
+ });
139
+
140
+ const folderData = response.data;
141
+
142
+ if (!folderData || !folderData.rootContent) {
143
+ console.log("❌ No content found to download.");
144
+ return;
145
+ }
146
+
147
+ console.log(`📦 Found version ${folderData.version} with ${folderData.rootContent.length} items`);
148
+ console.log("");
149
+
150
+ // Create backup of current versions file if it exists
151
+ const versionsFile = path.join(process.cwd(), ".tncversions");
152
+ if (fs.existsSync(versionsFile)) {
153
+ const backupPath = path.join(process.cwd(), `.tncversions.backup.${Date.now()}`);
154
+ fs.copyFileSync(versionsFile, backupPath);
155
+ console.log(`💾 Backed up existing versions to: ${path.basename(backupPath)}`);
101
156
  }
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.");
157
+
158
+ // Process and download files
159
+ const result = await processFolderContent(folderData.rootContent, process.cwd());
160
+
161
+ console.log("");
162
+ console.log("📊 Download Summary:");
163
+ console.log("====================");
164
+ console.log(`✅ Downloaded: ${result.downloadedCount} files`);
165
+ console.log(`📋 Skipped: ${result.skippedCount} files (already exist)`);
166
+ console.log(`❌ Errors: ${result.errorCount} files`);
167
+
168
+ if (result.errorCount > 0) {
169
+ console.log("");
170
+ console.log("⚠️ Some files failed to download. Check the errors above.");
171
+ }
172
+
173
+ // Update local versions file with pulled data
174
+ const newVersionMap = {};
175
+ const flattenContent = (items) => {
176
+ for (const item of items) {
177
+ if (item.type === "file") {
178
+ newVersionMap[item.path] = {
179
+ hash: item.contentHash || item.hash,
180
+ url: item.url,
181
+ version: item.version || 1,
182
+ size: item.size || 0
183
+ };
184
+ }
185
+ if (item.children) {
186
+ flattenContent(item.children);
187
+ }
188
+ }
189
+ };
190
+
191
+ flattenContent(folderData.rootContent);
192
+ fs.writeFileSync(versionsFile, JSON.stringify(newVersionMap, null, 2));
193
+
194
+ console.log("");
195
+ console.log(`💾 Updated local versions file with ${Object.keys(newVersionMap).length} files`);
196
+ console.log(`🎉 Pull completed successfully!`);
197
+
118
198
  } catch (err) {
119
- console.error("❌ Push failed:", err.response?.data || err.message);
199
+ if (err.response) {
200
+ console.error("❌ Pull failed:", err.response.data.message || err.response.statusText);
201
+ } else {
202
+ console.error("❌ Pull failed:", err.message);
203
+ }
120
204
  }
121
205
  }
122
206
 
123
- // Call push all
124
- dpushAll();
207
+ /** ------------------ PULL WITH OPTIONS ------------------ **/
208
+ export async function pullWithOptions(roomId, options = {}) {
209
+ const version = options.version || "latest";
210
+ const targetPath = options.targetPath || process.cwd();
211
+
212
+ await pull(roomId, version, targetPath);
213
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thinkncollab-cli",
3
3
  "author": "Raman Singh",
4
- "version": "0.0.24",
4
+ "version": "0.0.26",
5
5
  "description": "CLI tool for ThinkNCollab",
6
6
  "main": "index.js",
7
7
  "bin": {