thinkncollab-cli 0.0.9 β†’ 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
@@ -1,11 +1,313 @@
1
- "tnc-cli login" for login to get access for CLI to push files
1
+ # 🧠 ThinkNCollab CLI
2
2
 
3
- "tnc-cli push --room <roomId> <file-or-folder-path>" to push from the root directory.
3
+ A powerful command-line interface for seamless collaboration with **ThinkNCollab** β€” push files, manage rooms, and collaborate directly from your terminal.
4
4
 
5
- .ignoretnc files for ignorng the files...
5
+ ---
6
6
 
7
+ ## πŸš€ Quick Start
7
8
 
9
+ ```bash
10
+ # Install the CLI globally
11
+ npm install -g @thinkncollab/tnc-cli
8
12
 
13
+ # Login to your ThinkNCollab account
14
+ tnc-cli login
9
15
 
16
+ # Push files to a room
17
+ tnc-cli push --room <roomId> <path>
10
18
 
19
+ # Logout
20
+ tnc-cli logout
21
+ ```
11
22
 
23
+ ---
24
+
25
+ ## πŸ” Authentication
26
+
27
+ ### **Login Command**
28
+
29
+ Authenticate with your ThinkNCollab account to enable CLI access:
30
+
31
+ ```bash
32
+ tnc-cli login
33
+ ```
34
+
35
+ ### **What Happens During Login**
36
+
37
+ - Opens a secure browser window to ThinkNCollab’s authentication page
38
+ - Completes OAuth2 authentication flow
39
+ - Creates an encrypted `.tncrc` configuration file in your home directory
40
+ - Stores secure tokens for future CLI sessions
41
+
42
+ ### **Manual Authentication**
43
+
44
+ ```bash
45
+ tnc-cli login --token YOUR_AUTH_TOKEN
46
+ ```
47
+
48
+ ### **Verify Authentication**
49
+
50
+ ```bash
51
+ tnc-cli whoami
52
+ ```
53
+
54
+ ### **Logout**
55
+
56
+ Clear stored credentials:
57
+
58
+ ```bash
59
+ tnc-cli logout
60
+ ```
61
+
62
+ ---
63
+
64
+ ## πŸ“¦ File Operations
65
+
66
+ ### **Push Command**
67
+
68
+ Push files or directories to ThinkNCollab rooms:
69
+
70
+ ```bash
71
+ tnc-cli push --room <roomId> <path>
72
+ ```
73
+
74
+ #### **Syntax**
75
+
76
+ ```bash
77
+ tnc-cli push --room ROOM_ID PATH [ADDITIONAL_PATHS...]
78
+ ```
79
+
80
+ #### **Examples**
81
+
82
+ | Action | Command |
83
+ |--------|----------|
84
+ | Push a single file | `tnc-cli push --room 64a1b2c3d4e5f6a1b2c3d4e5 document.pdf` |
85
+ | Push entire folder | `tnc-cli push --room 64a1b2c3d4e5f6a1b2c3d4e5 ./src/` |
86
+ | Push multiple items | `tnc-cli push --room 64a1b2c3d4e5f6a1b2c3d4e5 file1.js assets/ components/` |
87
+ | Push current directory | `tnc-cli push --room 64a1b2c3d4e5f6a1b2c3d4e5 .` |
88
+
89
+ #### **Options**
90
+
91
+ | Option | Short | Description |
92
+ |---------|--------|-------------|
93
+ | `--room` | `-r` | **Required:** Target room ID |
94
+ | `--message` | `-m` | Commit message describing changes |
95
+ | `--force` | `-f` | Force push (overwrite conflicts) |
96
+ | `--dry-run` | β€” | Preview files before pushing |
97
+ | `--exclude` | β€” | Additional patterns to exclude |
98
+
99
+ ---
100
+
101
+ ## 🧩 Room Management
102
+
103
+ | Command | Description |
104
+ |----------|--------------|
105
+ | `tnc-cli rooms list` | List accessible rooms |
106
+ | `tnc-cli rooms info <id>` | Show details for a specific room |
107
+
108
+ ---
109
+
110
+ ## 🚫 File Ignoring
111
+
112
+ Use a `.ignoretnc` file in your project root to exclude files/folders during push.
113
+
114
+ ### **Example `.ignoretnc`**
115
+
116
+ ```text
117
+ # Dependencies
118
+ node_modules/
119
+ vendor/
120
+ bower_components/
121
+
122
+ # Build outputs
123
+ /dist
124
+ /build
125
+ /.next
126
+ /out
127
+
128
+ # Environment
129
+ .env
130
+ .env.local
131
+ .env.production
132
+ .env.development
133
+
134
+ # Logs
135
+ *.log
136
+ npm-debug.log*
137
+ yarn-debug.log*
138
+
139
+ # Temporary / OS
140
+ *.tmp
141
+ .DS_Store
142
+ Thumbs.db
143
+
144
+ # IDE
145
+ .vscode/
146
+ .idea/
147
+
148
+ # Test
149
+ *.test.js
150
+ *.spec.js
151
+ /coverage/
152
+
153
+ # Large assets
154
+ *.psd
155
+ *.ai
156
+ *.sketch
157
+ ```
158
+
159
+ ### **Pattern Rules**
160
+
161
+ | Type | Example | Description |
162
+ |------|----------|-------------|
163
+ | Directory | `dist/` | Ignore whole directory |
164
+ | File Extension | `*.log` | Ignore all `.log` files |
165
+ | Specific File | `secret.env` | Ignore single file |
166
+ | Wildcard | `test-*.js` | Match name patterns |
167
+ | Negation | `!keep.js` | Include despite other rules |
168
+ | Comment | `# comment` | Ignored by parser |
169
+
170
+ ---
171
+
172
+ ## βš™οΈ Configuration
173
+
174
+ After login, an encrypted `.tncrc` file is created in your home directory.
175
+
176
+ ### **Example `.tncrc`**
177
+
178
+ ```json
179
+ {
180
+ "user": {
181
+ "id": "encrypted_user_id",
182
+ "email": "encrypted_email",
183
+ "name": "encrypted_display_name"
184
+ },
185
+ "auth": {
186
+ "token": "encrypted_jwt_token",
187
+ "refreshToken": "encrypted_refresh_token",
188
+ "expires": "2025-12-31T23:59:59Z"
189
+ },
190
+ "workspace": {
191
+ "id": "encrypted_workspace_id",
192
+ "name": "encrypted_workspace_name"
193
+ },
194
+ "settings": {
195
+ "defaultRoom": "optional_default_room_id",
196
+ "autoSync": false
197
+ }
198
+ }
199
+ ```
200
+
201
+ ### **Environment Variables**
202
+
203
+ ```bash
204
+ export TNC_API_TOKEN="your_api_token"
205
+ export TNC_API_URL="https://api.thinkncollab.com"
206
+ export TNC_DEFAULT_ROOM="your_default_room_id"
207
+ ```
208
+
209
+ ---
210
+
211
+ ## ⚑ Advanced Usage
212
+
213
+ ### **Batch Push**
214
+
215
+ ```bash
216
+ tnc-cli push --room room1,room2,room3 ./shared-assets/
217
+ ```
218
+
219
+ ### **CI/CD Integration**
220
+
221
+ ```bash
222
+ tnc-cli login --token $TNC_DEPLOY_TOKEN
223
+ tnc-cli push --room $PRODUCTION_ROOM ./dist/ --message "Build ${CI_COMMIT_SHA}"
224
+ ```
225
+
226
+ ### **Watch for Changes (Experimental)**
227
+
228
+ ```bash
229
+ tnc-cli watch --room 64a1b2c3d4e5f6a1b2c3d4e5 ./src/
230
+ ```
231
+
232
+ ---
233
+
234
+ ## 🧰 Troubleshooting
235
+
236
+ ### **Authentication Issues**
237
+
238
+ ```bash
239
+ tnc-cli logout
240
+ tnc-cli login
241
+ ```
242
+
243
+ - Ensure valid token and room access
244
+ - Token may need refresh or rotation
245
+
246
+ ### **Permission Errors**
247
+
248
+ - Confirm write access to target room
249
+ - Check if the room ID is active
250
+
251
+ ### **File Size Limits**
252
+
253
+ | Type | Limit |
254
+ |------|--------|
255
+ | Individual File | 100 MB |
256
+ | Total Push | 1 GB |
257
+
258
+ ### **Debug Mode**
259
+
260
+ Enable detailed logs:
261
+
262
+ ```bash
263
+ tnc-cli --debug push --room 64a1b2c3d4e5f6a1b2c3d4e5 ./path/
264
+ ```
265
+
266
+ ---
267
+
268
+ ## πŸ”’ Security Guidelines
269
+
270
+ - **Never share** your `.tncrc` file β€” it stores encrypted tokens
271
+ - **Never commit** `.tncrc` to Git or any version control
272
+ - Use `.ignoretnc` to exclude sensitive files
273
+ - Rotate API tokens regularly
274
+ - Validate room access before pushing confidential data
275
+
276
+ ---
277
+
278
+ ## πŸ’‘ Best Practices
279
+
280
+ - Use environment variables for automated environments
281
+ - Review `.ignoretnc` before each push
282
+ - Run `--dry-run` to preview changes
283
+ - Monitor push logs for unexpected files
284
+
285
+ ---
286
+
287
+ ## 🧭 Command Reference
288
+
289
+ | Command | Description |
290
+ |----------|-------------|
291
+ | `tnc-cli login` | Authenticate with ThinkNCollab |
292
+ | `tnc-cli logout` | Clear credentials |
293
+ | `tnc-cli whoami` | Show current user info |
294
+ | `tnc-cli push --room <id> <path>` | Push files/folders to a room |
295
+ | `tnc-cli rooms list` | List all accessible rooms |
296
+ | `tnc-cli rooms info <id>` | Show room details |
297
+ | `tnc-cli --version` | Show CLI version |
298
+ | `tnc-cli --help` | Show help information |
299
+
300
+ ---
301
+
302
+ ## 🧩 Resources & Support
303
+
304
+ - πŸ“˜ **Documentation:** [docs.thinkncollab.com/cli](https://docs.thinkncollab.com/cli)
305
+ - πŸ™ **GitHub Issues:** [ThinkNCollab Repository](https://github.com/thinkncollab)
306
+ - βœ‰οΈ **Email:** support@thinkncollab.com
307
+ - πŸ’¬ **Community:** Join our [ThinkNCollab Discord](https://discord.gg/thinkncollab)
308
+
309
+ ---
310
+
311
+ ## πŸ“„ License
312
+
313
+ MIT License – see `LICENSE` file for details.
package/bin/index.js CHANGED
@@ -8,9 +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
- /** ========== LOGIN ========== **/
15
+ /** ------------------ LOGIN ------------------ **/
14
16
  async function login() {
15
17
  const answers = await inquirer.prompt([
16
18
  { type: "input", name: "email", message: "Email:" },
@@ -32,6 +34,21 @@ async function login() {
32
34
  }
33
35
  }
34
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 ------------------ **/
35
52
  function readToken() {
36
53
  if (!fs.existsSync(RC_FILE)) {
37
54
  console.error("❌ Not logged in. Run 'tnc login' first.");
@@ -41,7 +58,7 @@ function readToken() {
41
58
  return { token: data.token, email: data.email };
42
59
  }
43
60
 
44
- /** ========== IGNORE HANDLING ========== **/
61
+ /** ------------------ IGNORE HANDLING ------------------ **/
45
62
  function loadIgnore(folderPath) {
46
63
  const ignoreFile = path.join(folderPath, ".ignoretnc");
47
64
  if (!fs.existsSync(ignoreFile)) return [];
@@ -65,7 +82,7 @@ function shouldIgnore(relativePath, ignoreList) {
65
82
  });
66
83
  }
67
84
 
68
- /** ========== SCAN FOLDER ========== **/
85
+ /** ------------------ SCAN FOLDER ------------------ **/
69
86
  function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
70
87
  const items = fs.readdirSync(folderPath, { withFileTypes: true });
71
88
  const result = [];
@@ -82,14 +99,15 @@ function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
82
99
  result.push({
83
100
  name: item.name,
84
101
  type: "folder",
85
- children: scanFolder(fullPath, ignoreList, rootPath)
102
+ children: scanFolder(fullPath, ignoreList, rootPath),
103
+ path: relativePath
86
104
  });
87
105
  } else {
88
106
  const stats = fs.statSync(fullPath);
89
107
  result.push({
90
108
  name: item.name,
91
109
  type: "file",
92
- path: fullPath,
110
+ path: relativePath,
93
111
  size: stats.size
94
112
  });
95
113
  }
@@ -97,7 +115,48 @@ function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
97
115
  return result;
98
116
  }
99
117
 
100
- /** ========== 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 ------------------ **/
101
160
  async function uploadFileSigned(filePath, folder, roomId, token, email) {
102
161
  const filename = path.basename(filePath);
103
162
 
@@ -137,26 +196,28 @@ async function uploadTree(fileTree, folderHex, roomId, token, email, parentPath
137
196
  uploaded.push({
138
197
  name: node.name,
139
198
  type: "folder",
199
+ path: relativePath,
140
200
  children
141
201
  });
142
- } else {
202
+ } else if (node.changed) {
143
203
  const url = await uploadFileSigned(node.path, `tnc_uploads/${folderHex}`, roomId, token, email);
144
204
  console.log(`πŸ“¦ Uploaded: ${relativePath} β†’ ${url}`);
145
-
146
205
  uploaded.push({
147
206
  name: node.name,
148
207
  type: "file",
149
208
  path: relativePath,
150
209
  size: node.size,
151
- url // βœ… send top-level URL now
210
+ url
152
211
  });
212
+ } else {
213
+ uploaded.push(node); // unchanged, no upload
153
214
  }
154
215
  }
155
216
 
156
217
  return uploaded;
157
218
  }
158
219
 
159
- /** ========== PUSH FUNCTION ========== **/
220
+ /** ------------------ PUSH FUNCTION ------------------ **/
160
221
  async function push(roomId, targetPath) {
161
222
  const { token, email } = readToken();
162
223
  const stats = fs.statSync(targetPath);
@@ -170,7 +231,7 @@ async function push(roomId, targetPath) {
170
231
  const relativePath = path.basename(targetPath);
171
232
  content = shouldIgnore(relativePath, ignoreList)
172
233
  ? []
173
- : [{ name: relativePath, type: "file", path: targetPath, size: stats.size }];
234
+ : [{ name: relativePath, type: "file", path: relativePath, size: stats.size }];
174
235
  }
175
236
 
176
237
  if (!content.length) {
@@ -178,11 +239,20 @@ async function push(roomId, targetPath) {
178
239
  return;
179
240
  }
180
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
+
181
251
  try {
182
252
  const folderHex = crypto.createHash("md5").update(path.basename(targetPath) + Date.now()).digest("hex");
183
253
 
184
254
  console.log("πŸš€ Uploading to Cloudinary...");
185
- const uploadedTree = await uploadTree(content, folderHex, roomId, token, email);
255
+ const uploadedTree = await uploadTree(contentWithChanges, folderHex, roomId, token, email);
186
256
 
187
257
  console.log("πŸ—‚οΈ Sending metadata to backend...");
188
258
  await axios.post(
@@ -192,12 +262,24 @@ async function push(roomId, targetPath) {
192
262
  );
193
263
 
194
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
+
195
277
  } catch (err) {
196
278
  console.error("❌ Upload failed:", err.response?.data || err.message);
197
279
  }
198
280
  }
199
281
 
200
- /** ========== CLI HANDLER ========== **/
282
+ /** ------------------ CLI HANDLER ------------------ **/
201
283
  const args = process.argv.slice(2);
202
284
 
203
285
  switch (args[0]) {
@@ -205,6 +287,10 @@ switch (args[0]) {
205
287
  login();
206
288
  break;
207
289
 
290
+ case "logout":
291
+ logout();
292
+ break;
293
+
208
294
  case "push": {
209
295
  const roomIndex = args.indexOf("--room");
210
296
  if (roomIndex === -1 || !args[roomIndex + 1] || !args[roomIndex + 2]) {
@@ -222,4 +308,5 @@ switch (args[0]) {
222
308
  console.log("Commands:");
223
309
  console.log(" tnc login");
224
310
  console.log(" tnc push --room <roomId> <path>");
311
+ console.log(" tnc logout");
225
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.9",
4
+ "version": "0.0.11",
5
5
  "description": "CLI tool for ThinkNCollab",
6
6
  "main": "index.js",
7
7
  "bin": {