thinkncollab-cli 0.0.23 → 0.0.25

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