thinkncollab-cli 0.0.77 → 0.0.78
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 +31 -384
- package/commands/branch.js +17 -14
- package/commands/connect.js +59 -7
- package/commands/init.js +8 -2
- package/commands/merge.js +0 -0
- package/commands/myTask.js +7 -3
- package/commands/pull.js +3 -5
- package/commands/push.js +465 -0
- package/commands/sendInvite.js +21 -30
- package/commands/taskCompletion.js +67 -0
- package/lib/getVerify.js +23 -0
- package/package.json +4 -3
- package/.vscode/geekload-types.d.ts +0 -455
- package/bin/tnc.js +0 -21
- package/lib/api.js +0 -28
- package/lib/scanner.js +0 -27
- package/lib/uploader.js +0 -5
package/commands/pull.js
CHANGED
|
@@ -19,17 +19,15 @@ function readToken() {
|
|
|
19
19
|
return { token: data.token, email: data.email };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** ------------------ DOWNLOAD FILE ------------------ **/
|
|
22
|
+
/** ------------------ DOWNLOAD FILE ------------------ **/
|
|
23
23
|
async function downloadFile(url, filePath) {
|
|
24
24
|
return new Promise((resolve, reject) => {
|
|
25
25
|
const fileDir = path.dirname(filePath);
|
|
26
26
|
if (!fs.existsSync(fileDir)) fs.mkdirSync(fileDir, { recursive: true });
|
|
27
|
-
|
|
28
27
|
const file = fs.createWriteStream(filePath);
|
|
29
28
|
const client = url.startsWith("https") ? https : http;
|
|
30
29
|
|
|
31
|
-
client
|
|
32
|
-
.get(url, (response) => {
|
|
30
|
+
client.get(url, (response) => {
|
|
33
31
|
if (response.statusCode !== 200) {
|
|
34
32
|
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
35
33
|
return;
|
|
@@ -47,7 +45,7 @@ async function downloadFile(url, filePath) {
|
|
|
47
45
|
});
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
/**------------------PROCESS FOLDER CONTENT------------------**/
|
|
51
49
|
async function processFolderContent(content, basePath = "") {
|
|
52
50
|
let downloadedCount = 0;
|
|
53
51
|
let skippedCount = 0;
|
package/commands/push.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import FormData from 'form-data';
|
|
8
|
+
import machine from 'node-machine-id';
|
|
9
|
+
const RC_FILE = path.join(os.homedir(), '.tncrc');
|
|
10
|
+
const VERSION_FILE = path.join(process.cwd(), '.tncversions');
|
|
11
|
+
const BASE_URL = 'http://localhost:3001/rooms';
|
|
12
|
+
const CWD = process.cwd();
|
|
13
|
+
/** ------------------ READ TOKEN ------------------ **/
|
|
14
|
+
|
|
15
|
+
function readToken() {
|
|
16
|
+
if (!fs.existsSync(RC_FILE)) {
|
|
17
|
+
console.error("❌ Not logged in. Run 'tnc login' first.");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const data = JSON.parse(fs.readFileSync(RC_FILE));
|
|
21
|
+
return { token: data.token, email: data.email };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** ------------------ IGNORE HANDLING ------------------ **/
|
|
25
|
+
function loadIgnore(folderPath) {
|
|
26
|
+
const ignoreFile = path.join(folderPath, ".ignoretnc");
|
|
27
|
+
if (!fs.existsSync(ignoreFile)) return [];
|
|
28
|
+
return fs
|
|
29
|
+
.readFileSync(ignoreFile, "utf-8")
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map(line => line.trim())
|
|
32
|
+
.filter(line => line && !line.startsWith("#"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function shouldIgnore(relativePath, ignoreList) {
|
|
36
|
+
return ignoreList.some(pattern => {
|
|
37
|
+
if (pattern.endsWith("/**")) {
|
|
38
|
+
const folder = pattern.slice(0, -3);
|
|
39
|
+
return relativePath === folder || relativePath.startsWith(folder + path.sep);
|
|
40
|
+
}
|
|
41
|
+
if (pattern.startsWith("*.")) {
|
|
42
|
+
return relativePath.endsWith(pattern.slice(1));
|
|
43
|
+
}
|
|
44
|
+
return relativePath === pattern;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** ------------------ SCAN FOLDER ------------------ **/
|
|
49
|
+
function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
|
|
50
|
+
const items = fs.readdirSync(folderPath, { withFileTypes: true });
|
|
51
|
+
const result = [];
|
|
52
|
+
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
const fullPath = path.join(folderPath, item.name);
|
|
55
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
56
|
+
|
|
57
|
+
if (shouldIgnore(relativePath, ignoreList)) {
|
|
58
|
+
console.log("⚠️ Ignored:", relativePath);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (item.isDirectory()) {
|
|
63
|
+
result.push({
|
|
64
|
+
name: item.name,
|
|
65
|
+
type: "folder",
|
|
66
|
+
children: scanFolder(fullPath, ignoreList, rootPath),
|
|
67
|
+
path: item.name
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
const stats = fs.statSync(fullPath);
|
|
71
|
+
result.push({
|
|
72
|
+
name: item.name,
|
|
73
|
+
type: "file",
|
|
74
|
+
path: relativePath,
|
|
75
|
+
size: stats.size
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** ------------------ VERSIONING ------------------ **/
|
|
83
|
+
function loadVersions() {
|
|
84
|
+
if (!fs.existsSync(VERSION_FILE)) return {};
|
|
85
|
+
const data = JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
|
|
86
|
+
|
|
87
|
+
// Backward compatibility: if old format (just strings), convert to new format
|
|
88
|
+
const converted = {};
|
|
89
|
+
for (const [path, value] of Object.entries(data)) {
|
|
90
|
+
if (typeof value === 'string') {
|
|
91
|
+
converted[path] = { hash: value, url: '', version: 1 };
|
|
92
|
+
} else {
|
|
93
|
+
converted[path] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return converted;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function saveVersions(versionMap) {
|
|
100
|
+
fs.writeFileSync(VERSION_FILE, JSON.stringify(versionMap, null, 2));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function computeFileHash(filePath) {
|
|
104
|
+
try {
|
|
105
|
+
if (!fs.existsSync(filePath)) return null;
|
|
106
|
+
|
|
107
|
+
const stats = fs.statSync(filePath);
|
|
108
|
+
if (stats.isDirectory()) {
|
|
109
|
+
// For folders: hash of sorted file names + their hashes
|
|
110
|
+
const items = fs.readdirSync(filePath).sort();
|
|
111
|
+
const childrenHashes = items.map(name =>
|
|
112
|
+
computeFileHash(path.join(filePath, name))
|
|
113
|
+
).filter(Boolean).join('|');
|
|
114
|
+
|
|
115
|
+
return crypto.createHash("sha256")
|
|
116
|
+
.update(`folder:${filePath}|${childrenHashes}`)
|
|
117
|
+
.digest("hex");
|
|
118
|
+
} else {
|
|
119
|
+
// For files: hash of content + file metadata
|
|
120
|
+
const content = fs.readFileSync(filePath);
|
|
121
|
+
const stats = fs.statSync(filePath);
|
|
122
|
+
|
|
123
|
+
return crypto.createHash("sha256")
|
|
124
|
+
.update(content)
|
|
125
|
+
.update(`size:${stats.size}|mtime:${stats.mtimeMs}`)
|
|
126
|
+
.digest("hex");
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error(`❌ Error computing hash for ${filePath}:`, err.message);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function checkChanges(fileTree, versionMap, rootPath = CWD) {
|
|
135
|
+
return fileTree.map(item => {
|
|
136
|
+
const fullPath = path.join(rootPath, item.path || item.name);
|
|
137
|
+
const relativePath = item.path || item.name;
|
|
138
|
+
|
|
139
|
+
let hash = null;
|
|
140
|
+
let changed = true;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
if (fs.existsSync(fullPath)) {
|
|
144
|
+
hash = computeFileHash(fullPath);
|
|
145
|
+
|
|
146
|
+
// Check against previous version
|
|
147
|
+
const prevVersion = versionMap[relativePath];
|
|
148
|
+
if (prevVersion && prevVersion.hash === hash) {
|
|
149
|
+
changed = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error(` Error checking changes for ${relativePath}:`, err.message);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const newItem = {
|
|
157
|
+
...item,
|
|
158
|
+
changed,
|
|
159
|
+
hash,
|
|
160
|
+
path: relativePath // Ensure consistent path
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Recursively check children for folders
|
|
164
|
+
if (item.type === "folder" && item.children && item.children.length > 0) {
|
|
165
|
+
newItem.children = checkChanges(item.children, versionMap, rootPath);
|
|
166
|
+
newItem.changed = newItem.changed || newItem.children.some(c => c.changed);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return newItem;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** ------------------ MERGE CONFLICT HANDLING ------------------ **/
|
|
174
|
+
async function checkMergeConflicts(roomId, branch, localFiles) {
|
|
175
|
+
try {
|
|
176
|
+
console.log('🔍 Checking for merge conflicts...');
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
const cloudFiles = await getCloudFiles(roomId, branch);
|
|
180
|
+
const conflicts = [];
|
|
181
|
+
|
|
182
|
+
for (const localFile of localFiles) {
|
|
183
|
+
const cloudFile = cloudFiles.find(f => f.path === localFile.path);
|
|
184
|
+
|
|
185
|
+
if (cloudFile && localFile.hash !== cloudFile.hash) {
|
|
186
|
+
|
|
187
|
+
conflicts.push({
|
|
188
|
+
file: localFile.path,
|
|
189
|
+
message: `File changed by another user in cloud`
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return conflicts;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.log(' Could not check conflicts:', error.message);
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function getCloudFiles(roomId, branch) {
|
|
202
|
+
|
|
203
|
+
const { token, email } = readToken();
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const response = await axios.get(
|
|
207
|
+
`${BASE_URL}/${roomId}/files`,
|
|
208
|
+
{
|
|
209
|
+
params: { branch },
|
|
210
|
+
headers: { authorization: `Bearer ${token}`, email }
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
return response.data.files || [];
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.log('❌ Could not fetch cloud files:', error.message);
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function handleConflicts(conflicts) {
|
|
221
|
+
if (conflicts.length === 0) return true;
|
|
222
|
+
|
|
223
|
+
console.log('\n MERGE CONFLICTS DETECTED:');
|
|
224
|
+
conflicts.forEach(conflict => {
|
|
225
|
+
console.log(`📄 ${conflict.file} - ${conflict.message}`);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
console.log('\n✅ Auto-resolving: Using your local version for all conflicts');
|
|
229
|
+
console.log('💡 Note: Other users\' changes will be overwritten');
|
|
230
|
+
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** ------------------ CLOUDINARY UPLOAD ------------------ **/
|
|
235
|
+
async function uploadFileSigned(filePath, folder, roomId, token, email) {
|
|
236
|
+
const filename = path.basename(filePath);
|
|
237
|
+
|
|
238
|
+
const sigRes = await axios.post(
|
|
239
|
+
`${BASE_URL}/${roomId}/get-upload-signature`,
|
|
240
|
+
{ filename, folder, roomId },
|
|
241
|
+
{ headers: { authorization: `Bearer ${token}`, email } }
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const { signature, timestamp, api_key, cloud_name } = sigRes.data;
|
|
245
|
+
|
|
246
|
+
const formData = new FormData();
|
|
247
|
+
formData.append("file", fs.createReadStream(filePath));
|
|
248
|
+
formData.append("folder", folder);
|
|
249
|
+
formData.append("public_id", filename);
|
|
250
|
+
formData.append("timestamp", timestamp);
|
|
251
|
+
formData.append("signature", signature);
|
|
252
|
+
formData.append("api_key", api_key);
|
|
253
|
+
|
|
254
|
+
const cloudRes = await axios.post(
|
|
255
|
+
`https://api.cloudinary.com/v1_1/${cloud_name}/auto/upload`,
|
|
256
|
+
formData,
|
|
257
|
+
{ headers: formData.getHeaders() }
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
return cloudRes.data.secure_url;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** ------------------ UPLOAD TREE - FIXED PATH CONSTRUCTION ------------------ **/
|
|
264
|
+
async function uploadTree(fileTree, folderHex, roomId, token, email, previousVersions, parentPath = "") {
|
|
265
|
+
const uploaded = [];
|
|
266
|
+
|
|
267
|
+
for (const node of fileTree) {
|
|
268
|
+
|
|
269
|
+
let relativePath;
|
|
270
|
+
if (parentPath) {
|
|
271
|
+
relativePath = path.join(parentPath, node.name).replace(/\\/g, "/");
|
|
272
|
+
} else {
|
|
273
|
+
relativePath = node.path || node.name;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (node.type === "folder") {
|
|
277
|
+
const children = await uploadTree(node.children, folderHex, roomId, token, email, previousVersions, relativePath);
|
|
278
|
+
|
|
279
|
+
uploaded.push({
|
|
280
|
+
...node,
|
|
281
|
+
children,
|
|
282
|
+
hash: node.hash,
|
|
283
|
+
path: relativePath
|
|
284
|
+
});
|
|
285
|
+
} else {
|
|
286
|
+
const prevFile = previousVersions[relativePath];
|
|
287
|
+
|
|
288
|
+
if (node.changed) {
|
|
289
|
+
try {
|
|
290
|
+
console.log(` Uploading changed file: ${relativePath}`);
|
|
291
|
+
const url = await uploadFileSigned(path.join(CWD, node.path), `tnc_uploads/${folderHex}`, roomId, token, email);
|
|
292
|
+
console.log(` Uploaded: ${relativePath}`);
|
|
293
|
+
|
|
294
|
+
uploaded.push({
|
|
295
|
+
...node,
|
|
296
|
+
url,
|
|
297
|
+
hash: node.hash,
|
|
298
|
+
path: relativePath,
|
|
299
|
+
version: prevFile ? prevFile.version + 1 : 1
|
|
300
|
+
});
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.error(` Failed to upload ${relativePath}:`, err.message);
|
|
303
|
+
|
|
304
|
+
uploaded.push({
|
|
305
|
+
...node,
|
|
306
|
+
url: prevFile?.url,
|
|
307
|
+
hash: node.hash,
|
|
308
|
+
path: relativePath,
|
|
309
|
+
version: prevFile?.version || 1
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
|
|
314
|
+
console.log(` Using previous version for: ${relativePath}`);
|
|
315
|
+
uploaded.push({
|
|
316
|
+
...node,
|
|
317
|
+
url: prevFile?.url,
|
|
318
|
+
hash: node.hash,
|
|
319
|
+
path: relativePath,
|
|
320
|
+
version: prevFile?.version || 1
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return uploaded;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** ------------------ PUSH FUNCTION WITH MERGE CONFLICT CHECK ------------------ **/
|
|
330
|
+
async function push(roomId, targetPath) {
|
|
331
|
+
const { token, email } = readToken();
|
|
332
|
+
const tncMetaPath = path.join(process.cwd(), ".tnc", ".tncmeta.json");
|
|
333
|
+
if (!fs.existsSync(tncMetaPath)) {
|
|
334
|
+
console.error(" Project not initialized. Run 'tnc-cli init' first.");
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
const meta = JSON.parse(fs.readFileSync(tncMetaPath, "utf-8"));
|
|
338
|
+
const projectId = meta.projectId;
|
|
339
|
+
const branch = meta.branch;
|
|
340
|
+
const branchId = meta.branchId;
|
|
341
|
+
const tncPushInfo = path.join(process.cwd(), ".tnc", ".tncpush.json");
|
|
342
|
+
|
|
343
|
+
const lastFolderId = JSON.parse(fs.readFileSync(tncPushInfo, "utf-8")).folderId;
|
|
344
|
+
const stats = fs.statSync(targetPath);
|
|
345
|
+
const rootFolder = stats.isDirectory() ? targetPath : path.dirname(targetPath);
|
|
346
|
+
const ignoreList = loadIgnore(rootFolder);
|
|
347
|
+
let content;
|
|
348
|
+
if (stats.isDirectory()) {
|
|
349
|
+
content = scanFolder(targetPath, ignoreList);
|
|
350
|
+
} else {
|
|
351
|
+
const relativePath = path.basename(targetPath);
|
|
352
|
+
content = shouldIgnore(relativePath, ignoreList)
|
|
353
|
+
? []
|
|
354
|
+
: [{ name: relativePath, type: "file", path: relativePath, size: stats.size }];
|
|
355
|
+
}
|
|
356
|
+
if (!content.length) {
|
|
357
|
+
console.log(" Nothing to upload (all ignored).");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const previousVersions = loadVersions();
|
|
361
|
+
console.log(' Previous versions:', Object.keys(previousVersions).length);
|
|
362
|
+
|
|
363
|
+
const contentWithChanges = checkChanges(content, previousVersions);
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
const changedFiles = contentWithChanges.flatMap(item =>
|
|
367
|
+
item.changed ? [item.path] : []
|
|
368
|
+
);
|
|
369
|
+
console.log('Changed files:', changedFiles.length, changedFiles);
|
|
370
|
+
|
|
371
|
+
const hasChanges = contentWithChanges.some(item => item.changed || (item.children && item.children.some(c => c.changed)));
|
|
372
|
+
|
|
373
|
+
if (!hasChanges) {
|
|
374
|
+
console.log(" No changes detected since last push.");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
const localFiles = getLocalFilesList(contentWithChanges);
|
|
380
|
+
const conflicts = await checkMergeConflicts(roomId, branch, localFiles);
|
|
381
|
+
|
|
382
|
+
if (conflicts.length > 0) {
|
|
383
|
+
const shouldContinue = await handleConflicts(conflicts);
|
|
384
|
+
if (!shouldContinue) {
|
|
385
|
+
console.error('Push cancelled due to conflicts');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const folderHex = crypto.createHash("md5").update(path.basename(targetPath) + Date.now()).digest("hex");
|
|
392
|
+
console.log(" Uploading to Cloudinary...");
|
|
393
|
+
const uploadedTree = await uploadTree(contentWithChanges, folderHex, roomId, token, email, previousVersions);
|
|
394
|
+
console.log(" Sending metadata...");
|
|
395
|
+
const res = await axios.post(
|
|
396
|
+
`${BASE_URL}/${roomId}/upload`,
|
|
397
|
+
{ folderId: folderHex, content: uploadedTree, uploadedBy: email, projectId, latestFolderId: lastFolderId, branch: branch, branchId: branchId },
|
|
398
|
+
{ headers: { authorization: `Bearer ${token}`, email } }
|
|
399
|
+
);
|
|
400
|
+
console.log(" Upload complete! Metadata stored successfully.");
|
|
401
|
+
const newVersionMap = {};
|
|
402
|
+
const flattenAndStore = (items) => {
|
|
403
|
+
for (const item of items) {
|
|
404
|
+
if (item.type === "file") {
|
|
405
|
+
newVersionMap[item.path] = {
|
|
406
|
+
hash: item.hash,
|
|
407
|
+
url: item.url,
|
|
408
|
+
version: item.version || 1
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (item.children) flattenAndStore(item.children);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
flattenAndStore(uploadedTree);
|
|
415
|
+
saveVersions(newVersionMap);
|
|
416
|
+
|
|
417
|
+
const versionNumbers = Object.values(newVersionMap).map(f => f.version || 1);
|
|
418
|
+
const latestVersion = versionNumbers.length > 0 ? Math.max(...versionNumbers) : 1;
|
|
419
|
+
const newPushRecord = {
|
|
420
|
+
version: latestVersion,
|
|
421
|
+
pushedAt: new Date().toISOString(),
|
|
422
|
+
roomId: roomId,
|
|
423
|
+
pushedBy: email,
|
|
424
|
+
projectId: projectId,
|
|
425
|
+
folderId: res.data.folderId,
|
|
426
|
+
branch: res.data.branch,
|
|
427
|
+
branchId: res.data.branchId
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const pushFilePath = path.join(CWD, ".tnc", ".tncpush.json");
|
|
431
|
+
fs.writeFileSync(
|
|
432
|
+
pushFilePath,
|
|
433
|
+
JSON.stringify(newPushRecord, null, 2)
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
console.log(` Saved ${Object.keys(newVersionMap).length} files to .tncversions`);
|
|
437
|
+
|
|
438
|
+
} catch (err) {
|
|
439
|
+
console.error(" Upload failed:", err.response?.data || err.message);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function getLocalFilesList(contentWithChanges) {
|
|
444
|
+
const files = [];
|
|
445
|
+
|
|
446
|
+
function extractFiles(items) {
|
|
447
|
+
for (const item of items) {
|
|
448
|
+
if (item.type === "file") {
|
|
449
|
+
files.push({
|
|
450
|
+
path: item.path,
|
|
451
|
+
hash: item.hash,
|
|
452
|
+
content: fs.readFileSync(path.join(CWD, item.path), 'utf-8')
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (item.children) {
|
|
456
|
+
extractFiles(item.children);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
extractFiles(contentWithChanges);
|
|
462
|
+
return files;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export default push;
|
package/commands/sendInvite.js
CHANGED
|
@@ -4,17 +4,13 @@ import axios from "axios";
|
|
|
4
4
|
import os from "os";
|
|
5
5
|
|
|
6
6
|
const homeDir = os.homedir();
|
|
7
|
-
const url = "
|
|
8
|
-
|
|
9
|
-
// Get saved email from ~/.tncrc
|
|
7
|
+
const url = "https://thinkncollab.com/cli/invite";
|
|
10
8
|
async function getEmail() {
|
|
11
9
|
const rcFile = path.join(homeDir, ".tncrc");
|
|
12
|
-
|
|
13
10
|
if (!fs.existsSync(rcFile)) {
|
|
14
|
-
console.log("
|
|
11
|
+
console.log(" Please login first!");
|
|
15
12
|
process.exit(1);
|
|
16
13
|
}
|
|
17
|
-
|
|
18
14
|
const content = fs.readFileSync(rcFile, "utf-8");
|
|
19
15
|
const email = JSON.parse(content).email;
|
|
20
16
|
return email;
|
|
@@ -22,40 +18,35 @@ async function getEmail() {
|
|
|
22
18
|
async function getToken() {
|
|
23
19
|
const rcFile = path.join(homeDir, '.tncrc');
|
|
24
20
|
if(!fs.readFileSync(rcFile)){
|
|
25
|
-
console.log("
|
|
21
|
+
console.log(" Please login first! ")
|
|
26
22
|
}
|
|
27
23
|
const content = fs.readFileSync(rcFile, 'utf-8');
|
|
28
24
|
const token = JSON.parse(content).token;
|
|
29
25
|
return token;
|
|
30
|
-
|
|
31
|
-
|
|
32
26
|
}
|
|
33
|
-
|
|
34
|
-
// Fetch tasks for a given room
|
|
35
27
|
async function sendInvite(inviteeEmail) {
|
|
36
28
|
try {
|
|
37
29
|
const email = await getEmail();
|
|
38
30
|
const token = await getToken();
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
31
|
+
const CWD = process.cwd();
|
|
32
|
+
const metaDataPath = path.join(".tnc", '.tncmeta.json');
|
|
33
|
+
const metaData = JSON.parse(fs.readFileSync(path.join(CWD, metaDataPath), 'utf-8'));
|
|
34
|
+
const roomId = metaData.roomId;
|
|
35
|
+
console.log(email, token, roomId, inviteeEmail);
|
|
36
|
+
const res = await axios({
|
|
37
|
+
method: 'post',
|
|
38
|
+
url: `${url}/${roomId}`,
|
|
39
|
+
params: {
|
|
40
|
+
email,
|
|
41
|
+
token,
|
|
42
|
+
inviteeEmail,
|
|
43
|
+
roomId
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
console.log(" Invitation sent successfully to", inviteeEmail);
|
|
47
|
+
console.log("Invite Link:", res.data.invitationLink);
|
|
56
48
|
} catch (error) {
|
|
57
|
-
console.error("
|
|
49
|
+
console.error(" Error while generating the invitation link:", error.response?.data || error.message);
|
|
58
50
|
}
|
|
59
51
|
}
|
|
60
|
-
|
|
61
52
|
export default sendInvite;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import machine from 'node-machine-id';
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const CWD = process.cwd();
|
|
11
|
+
|
|
12
|
+
const tncrcPath = path.join(os.homedir(), '.tncrc');
|
|
13
|
+
const tncMetaPath = path.join(CWD, '.tnc', '.tncmeta.json');
|
|
14
|
+
|
|
15
|
+
async function taskCompletion() {
|
|
16
|
+
try {
|
|
17
|
+
if (!fs.existsSync(tncrcPath)) {
|
|
18
|
+
console.error("❌ You are not logged in. Run 'tnc login' first.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(tncMetaPath)) {
|
|
23
|
+
console.error("❌ This directory is not initialized. Run 'tnc init' first.");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const tncrcData = fs.readFileSync(tncrcPath, "utf-8");
|
|
28
|
+
const { email, token } = JSON.parse(tncrcData);
|
|
29
|
+
|
|
30
|
+
const tncMetaData = fs.readFileSync(tncMetaPath, "utf-8");
|
|
31
|
+
const { projectId } = JSON.parse(tncMetaData);
|
|
32
|
+
|
|
33
|
+
const { taskId } = await inquirer.prompt([
|
|
34
|
+
{
|
|
35
|
+
type: "input",
|
|
36
|
+
name: "taskId",
|
|
37
|
+
message: "Enter Task ID to mark as complete:",
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const trimmedTaskId = taskId.trim();
|
|
42
|
+
|
|
43
|
+
if (!trimmedTaskId) {
|
|
44
|
+
console.error("❌ Task ID cannot be empty.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const response = await axios.post(
|
|
49
|
+
`https://thinkncollab.com/cli/taskcomplete/${trimmedTaskId}`,
|
|
50
|
+
{
|
|
51
|
+
email,
|
|
52
|
+
token,
|
|
53
|
+
machineId: machine.machineIdSync(),
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
console.log("✅ Task marked as complete:", response.data.message);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err.response) {
|
|
60
|
+
console.error("❌ Error:", err.response.data.error);
|
|
61
|
+
} else {
|
|
62
|
+
console.error("❌ An unexpected error occurred:", err.message);
|
|
63
|
+
}
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export default taskCompletion;
|
package/lib/getVerify.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os from 'os'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const RC_FILE = os.homedir();
|
|
6
|
+
|
|
7
|
+
async function getVerify() {
|
|
8
|
+
const tncfilepath = path.join(RC_FILE, ".tncrc");
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(tncfilepath)) {
|
|
11
|
+
throw new Error("Configuration file not found. Please login first.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const doc = await fs.readFileSync(tncfilepath, "utf-8");
|
|
15
|
+
const obj = JSON.parse(doc);
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
return obj;
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default getVerify;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkncollab-cli",
|
|
3
3
|
"author": "Raman Singh",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.78",
|
|
5
5
|
"description": "CLI tool for ThinkNCollab",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"axios": "^1.12.2",
|
|
16
16
|
"chalk": "^5.6.2",
|
|
17
|
-
"inquirer": "^9.3.8"
|
|
17
|
+
"inquirer": "^9.3.8",
|
|
18
|
+
"node-machine-id": "^1.1.12"
|
|
18
19
|
}
|
|
19
|
-
}
|
|
20
|
+
}
|