thinkncollab-cli 0.0.8
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/.ignoretnc +2 -0
- package/.vscode/geekload-types.d.ts +455 -0
- package/Readme.md +11 -0
- package/bin/index.js +225 -0
- package/bin/tnc.js +21 -0
- package/lib/api.js +28 -0
- package/lib/scanner.js +27 -0
- package/lib/uploader.js +5 -0
- package/package.json +18 -0
package/.ignoretnc
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
declare class GroupClass {
|
|
2
|
+
/**
|
|
3
|
+
* Execute test group
|
|
4
|
+
*
|
|
5
|
+
* @param scenario Name or identifier of scenario function
|
|
6
|
+
* @param users Amount of virtual users
|
|
7
|
+
* @param profile Duration string ('5s', '1m 30s' etc) or Profile object
|
|
8
|
+
*/
|
|
9
|
+
run(scenario: any, users: number, profile: any): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare class ConfigClass {
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configuration of project
|
|
18
|
+
*/
|
|
19
|
+
declare const config: ConfigClass;
|
|
20
|
+
|
|
21
|
+
declare class StageClass {
|
|
22
|
+
/**
|
|
23
|
+
* Execute test stage
|
|
24
|
+
*
|
|
25
|
+
* @param title Title of stage
|
|
26
|
+
* @param users Amount of virtual users
|
|
27
|
+
* @param groups Array of groups
|
|
28
|
+
*/
|
|
29
|
+
run(title: string, users: number, groups: GroupClass[]): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stage of test with set of Groups
|
|
34
|
+
*/
|
|
35
|
+
declare const Stage: StageClass;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Group of test with set of Scenarios
|
|
39
|
+
*/
|
|
40
|
+
declare const Group: GroupClass;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Class representing BaseHttpRequest.
|
|
44
|
+
*/
|
|
45
|
+
declare class BaseHttpRequest {
|
|
46
|
+
/**
|
|
47
|
+
* Create a http GET request.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} path - Relative path to target point.
|
|
50
|
+
* @returns {HttpRequest} - The created HttpRequest.
|
|
51
|
+
*/
|
|
52
|
+
get(path: string): HttpRequest;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a http PUT request.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} path - Relative path to target point.
|
|
58
|
+
* @returns {HttpRequest} - The created HttpRequest.
|
|
59
|
+
*/
|
|
60
|
+
put(path: string): HttpRequest;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a http POST request.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} path - Relative path to target point.
|
|
66
|
+
* @returns {HttpRequest} - The created HttpRequest.
|
|
67
|
+
*/
|
|
68
|
+
post(path: string): HttpRequest;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a http PATH request.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} path - Relative path to target point.
|
|
74
|
+
* @returns {HttpRequest} - The created HttpRequest.
|
|
75
|
+
*/
|
|
76
|
+
patch(path: string): HttpRequest;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a http DELETE request.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} path - Relative path to target point.
|
|
82
|
+
* @returns {HttpRequest} - The created HttpRequest.
|
|
83
|
+
*/
|
|
84
|
+
delete(path: string): HttpRequest;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a http OPTIONS request.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} path - Relative path to target point.
|
|
90
|
+
* @returns {HttpRequest} - The created HttpRequest.
|
|
91
|
+
*/
|
|
92
|
+
options(path: string): HttpRequest;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a http HEAD request.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} path - Relative path to target point.
|
|
98
|
+
* @returns {HttpRequest} - The created HttpRequest.
|
|
99
|
+
*/
|
|
100
|
+
head(path: string): HttpRequest;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Body of Request's response.
|
|
105
|
+
*/
|
|
106
|
+
declare class BodyClass {
|
|
107
|
+
asText: string;
|
|
108
|
+
asObject: object;
|
|
109
|
+
asBytes: number[]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Result of HttpRequest's execution.
|
|
114
|
+
*/
|
|
115
|
+
declare class HttpResult {
|
|
116
|
+
statusCode: number
|
|
117
|
+
body: BodyClass
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Class representing HttpRequest.
|
|
122
|
+
*/
|
|
123
|
+
declare class HttpRequest extends BaseHttpRequest {
|
|
124
|
+
/**
|
|
125
|
+
* Add header to request.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} name - Name of the header.
|
|
128
|
+
* @param {string} value - Value of the header.
|
|
129
|
+
* @returns {HttpRequest} - The modified HttpRequest.
|
|
130
|
+
*/
|
|
131
|
+
header(name: string, value: string): HttpRequest;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Add cookie to request.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} name - Name of the cookie.
|
|
137
|
+
* @param {string} value - Value of the cookie.
|
|
138
|
+
* @returns {HttpRequest} - The modified HttpRequest.
|
|
139
|
+
*/
|
|
140
|
+
cookie(name: string, value: string): HttpRequest;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Add query parameter to request url.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} name - Name of the query parameter.
|
|
146
|
+
* @param {string} value - Value of the query parameter.
|
|
147
|
+
* @returns {HttpRequest} - The modified HttpRequest.
|
|
148
|
+
*/
|
|
149
|
+
query(name: string, value: string): HttpRequest;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Set body value.
|
|
153
|
+
*
|
|
154
|
+
* @param {any} object - Body value.
|
|
155
|
+
* @returns {HttpRequest} - The modified HttpRequest.
|
|
156
|
+
*/
|
|
157
|
+
body(object: any): HttpRequest;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Execute request.
|
|
161
|
+
*
|
|
162
|
+
* @returns {HttpResult} - The HttpResult.
|
|
163
|
+
*/
|
|
164
|
+
sync(): HttpResult;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Execute request checks.
|
|
168
|
+
*
|
|
169
|
+
* @param {CheckExpression} check - Check object.
|
|
170
|
+
* @returns {HttpRequest} - The modified HttpRequest.
|
|
171
|
+
*/
|
|
172
|
+
then(check: CheckExpression): HttpRequest;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Execute request checks.
|
|
176
|
+
*
|
|
177
|
+
* @param {() => any} check - Check function.
|
|
178
|
+
* @returns {HttpRequest} - The modified HttpRequest.
|
|
179
|
+
*/
|
|
180
|
+
then(check: () => any): HttpRequest;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Class representing HttpConnection.
|
|
185
|
+
*/
|
|
186
|
+
declare class HttpConnection extends BaseHttpRequest {
|
|
187
|
+
/**
|
|
188
|
+
* Open connection to server.
|
|
189
|
+
*
|
|
190
|
+
* @returns {HttpConnection} - The HttpConnection.
|
|
191
|
+
*/
|
|
192
|
+
connect(): HttpConnection;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create a HTTP connection.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} url - Url of server.
|
|
199
|
+
* @param {Options} options - Optional options object.
|
|
200
|
+
* @returns {HttpConnection} - The HttpConnection.
|
|
201
|
+
*/
|
|
202
|
+
declare function http(url: string, options?: Options): HttpConnection;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Class representing Http2Connection.
|
|
206
|
+
*/
|
|
207
|
+
declare class Http2Connection extends HttpConnection {
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a HTTP2 connection.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} url - Url of server.
|
|
214
|
+
* @param {Options} options - Optional options object.
|
|
215
|
+
* @returns {Http2Connection} - The Http2Connection.
|
|
216
|
+
*/
|
|
217
|
+
declare function http2(url: string, options?: Options): Http2Connection;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Class representing WebSocketConnection.
|
|
221
|
+
*/
|
|
222
|
+
declare class WebSocketConnection {
|
|
223
|
+
/**
|
|
224
|
+
* Send text message.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} value - Text message.
|
|
227
|
+
*/
|
|
228
|
+
sendText(value: string): void;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Send array message.
|
|
232
|
+
*
|
|
233
|
+
* @param {byte[]} value - Byte array.
|
|
234
|
+
*/
|
|
235
|
+
sendArray(value: number[]): void;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Receive a text message.
|
|
239
|
+
*
|
|
240
|
+
* @returns {string} - The received text message.
|
|
241
|
+
*/
|
|
242
|
+
receiveText(): string;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Receive a byte array.
|
|
246
|
+
*
|
|
247
|
+
* @returns {byte[]} - The received byte array.
|
|
248
|
+
*/
|
|
249
|
+
receiveArray(): number[];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Create a WebSocket connection.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} url - Url of server.
|
|
256
|
+
* @param {Options} options - Optional options object.
|
|
257
|
+
* @returns {WebSocketConnection} - The WebSocketConnection.
|
|
258
|
+
*/
|
|
259
|
+
declare function websocket(url: string, options?: Options): WebSocketConnection;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create a Swagger connection.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} url - Url to Swagger definition.
|
|
265
|
+
* @param {Options} options - Optional options object.
|
|
266
|
+
* @returns {SwaggerConnection} - The SwaggerConnection.
|
|
267
|
+
*/
|
|
268
|
+
declare function swagger(url: string, options?: Options): SwaggerConnection;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Class representing SwaggerConnection.
|
|
272
|
+
*/
|
|
273
|
+
declare class SwaggerConnection {
|
|
274
|
+
/**
|
|
275
|
+
* Print loaded definition to console.
|
|
276
|
+
*/
|
|
277
|
+
show(): void;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Class representing Options.
|
|
282
|
+
*/
|
|
283
|
+
declare class Options {
|
|
284
|
+
cacheConnections: true;
|
|
285
|
+
connectionTimeout: '1m';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Change global options.
|
|
290
|
+
*
|
|
291
|
+
* @param {Options} value - Options object.
|
|
292
|
+
*/
|
|
293
|
+
declare function setOptions(value: Options): void;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Pause scenario execution.
|
|
297
|
+
*
|
|
298
|
+
* @param {string} minDuration - Minimum duration of pause ('5ms', '1s 150ms', etc).
|
|
299
|
+
* @param {string} maxDuration - Maximum duration of pause ('5ms', '1s 150ms', etc).
|
|
300
|
+
*/
|
|
301
|
+
declare function pause(minDuration: string, maxDuration: string): void;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Test log.
|
|
305
|
+
*/
|
|
306
|
+
declare const Log: LogClass;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Class representing LogClass.
|
|
310
|
+
*/
|
|
311
|
+
declare class LogClass {
|
|
312
|
+
/**
|
|
313
|
+
* Post message to test log.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} text - Text of the message.
|
|
316
|
+
*/
|
|
317
|
+
message(text: string): void;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Post warning message to test log.
|
|
321
|
+
*
|
|
322
|
+
* @param {string} text - Text of the message.
|
|
323
|
+
*/
|
|
324
|
+
warning(text: string): void;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Post error message to test log.
|
|
328
|
+
*
|
|
329
|
+
* @param {string} text - Text of the message.
|
|
330
|
+
*/
|
|
331
|
+
error(text: string): void;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Class representing CheckExpression.
|
|
336
|
+
*/
|
|
337
|
+
declare class CheckExpression {
|
|
338
|
+
/**
|
|
339
|
+
* Checking for equality of the current and reference values.
|
|
340
|
+
*
|
|
341
|
+
* @param {any} value - Reference value.
|
|
342
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
343
|
+
*/
|
|
344
|
+
equal(value: any): CheckExpression;
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Checking that the current value is greater than the reference value.
|
|
348
|
+
*
|
|
349
|
+
* @param {any} value - Reference value.
|
|
350
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
351
|
+
*/
|
|
352
|
+
great(value: any): CheckExpression;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Checking that the current value is less than the reference value.
|
|
356
|
+
*
|
|
357
|
+
* @param {any} value - Reference value.
|
|
358
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
359
|
+
*/
|
|
360
|
+
less(value: any): CheckExpression;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Checking that the current value contains the reference value.
|
|
364
|
+
*
|
|
365
|
+
* @param {any} value - Reference value.
|
|
366
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
367
|
+
*/
|
|
368
|
+
contains(value: any): CheckExpression;
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Checking that the reference value contains the current value.
|
|
372
|
+
*
|
|
373
|
+
* @param {any} value - Reference value.
|
|
374
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
375
|
+
*/
|
|
376
|
+
isContained(value: any): CheckExpression;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Checking that the reference value exists.
|
|
380
|
+
*
|
|
381
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
382
|
+
*/
|
|
383
|
+
exists(): CheckExpression;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Inverts the result of the check.
|
|
387
|
+
*
|
|
388
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
389
|
+
*/
|
|
390
|
+
not(): CheckExpression;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Setting a custom message to display a checking error.
|
|
394
|
+
*
|
|
395
|
+
* @param {string} text - Custom message text.
|
|
396
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
397
|
+
*/
|
|
398
|
+
message(text: string): CheckExpression;
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Saves the received value in the "Session" object with the specified name.
|
|
402
|
+
*
|
|
403
|
+
* @param {string} name - Value name.
|
|
404
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
405
|
+
*/
|
|
406
|
+
store(name: string): CheckExpression;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The source of the value of the response status code for its verification.
|
|
411
|
+
*
|
|
412
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
413
|
+
*/
|
|
414
|
+
declare function statusCode(): CheckExpression;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Selector for checking values obtained by Regular expression from body.
|
|
418
|
+
*
|
|
419
|
+
* @param {string | RegExp} expression - Regular expression.
|
|
420
|
+
* @param {number} group - Index of RegExp group. 0 by default.
|
|
421
|
+
* @param {number} index - Index of occurrence of expression. 0 by default.
|
|
422
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
423
|
+
*/
|
|
424
|
+
declare function regexp(expression: any, group: number, index: number): CheckExpression;
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Selector for checking values obtained by xPath expression from body.
|
|
428
|
+
*
|
|
429
|
+
* @param {string} expression - xPath expression.
|
|
430
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
431
|
+
*/
|
|
432
|
+
declare function xPath(expression: string): CheckExpression;
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Source of text for checking it.
|
|
436
|
+
*
|
|
437
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
438
|
+
*/
|
|
439
|
+
declare function text(): CheckExpression;
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Selector for checking cookies value obtained by name.
|
|
443
|
+
*
|
|
444
|
+
* @param {string} name - Name of cookie.
|
|
445
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
446
|
+
*/
|
|
447
|
+
declare function cookie(name: string): CheckExpression;
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Selector for checking headers value obtained by name.
|
|
451
|
+
*
|
|
452
|
+
* @param {string} name - Name of header.
|
|
453
|
+
* @returns {CheckExpression} - The CheckExpression.
|
|
454
|
+
*/
|
|
455
|
+
declare function header(name: string): CheckExpression;
|
package/Readme.md
ADDED
package/bin/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import FormData from "form-data";
|
|
9
|
+
|
|
10
|
+
const RC_FILE = path.join(os.homedir(), ".tncrc");
|
|
11
|
+
const BASE_URL = "http://localhost:3001/rooms";
|
|
12
|
+
|
|
13
|
+
/** ========== LOGIN ========== **/
|
|
14
|
+
async function login() {
|
|
15
|
+
const answers = await inquirer.prompt([
|
|
16
|
+
{ type: "input", name: "email", message: "Email:" },
|
|
17
|
+
{ type: "password", name: "password", message: "Password:" }
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
console.log("🔐 Logging in...");
|
|
22
|
+
const res = await axios.post("http://localhost:3001/login", {
|
|
23
|
+
email: answers.email,
|
|
24
|
+
password: answers.password
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const { token, email } = res.data;
|
|
28
|
+
fs.writeFileSync(RC_FILE, JSON.stringify({ token, email }, null, 2));
|
|
29
|
+
console.log(`✅ Login successful! Token saved to ${RC_FILE}`);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error("❌ Login failed:", err.response?.data?.message || err.message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readToken() {
|
|
36
|
+
if (!fs.existsSync(RC_FILE)) {
|
|
37
|
+
console.error("❌ Not logged in. Run 'tnc login' first.");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const data = JSON.parse(fs.readFileSync(RC_FILE));
|
|
41
|
+
return { token: data.token, email: data.email };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** ========== IGNORE HANDLING ========== **/
|
|
45
|
+
function loadIgnore(folderPath) {
|
|
46
|
+
const ignoreFile = path.join(folderPath, ".ignoretnc");
|
|
47
|
+
if (!fs.existsSync(ignoreFile)) return [];
|
|
48
|
+
return fs
|
|
49
|
+
.readFileSync(ignoreFile, "utf-8")
|
|
50
|
+
.split("\n")
|
|
51
|
+
.map(line => line.trim())
|
|
52
|
+
.filter(line => line && !line.startsWith("#"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function shouldIgnore(relativePath, ignoreList) {
|
|
56
|
+
return ignoreList.some(pattern => {
|
|
57
|
+
if (pattern.endsWith("/**")) {
|
|
58
|
+
const folder = pattern.slice(0, -3);
|
|
59
|
+
return relativePath === folder || relativePath.startsWith(folder + path.sep);
|
|
60
|
+
}
|
|
61
|
+
if (pattern.startsWith("*.")) {
|
|
62
|
+
return relativePath.endsWith(pattern.slice(1));
|
|
63
|
+
}
|
|
64
|
+
return relativePath === pattern;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** ========== SCAN FOLDER ========== **/
|
|
69
|
+
function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
|
|
70
|
+
const items = fs.readdirSync(folderPath, { withFileTypes: true });
|
|
71
|
+
const result = [];
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
const fullPath = path.join(folderPath, item.name);
|
|
74
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
75
|
+
|
|
76
|
+
if (shouldIgnore(relativePath, ignoreList)) {
|
|
77
|
+
console.log("⚠️ Ignored:", relativePath);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (item.isDirectory()) {
|
|
82
|
+
result.push({
|
|
83
|
+
name: item.name,
|
|
84
|
+
type: "folder",
|
|
85
|
+
children: scanFolder(fullPath, ignoreList, rootPath)
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
const stats = fs.statSync(fullPath);
|
|
89
|
+
result.push({
|
|
90
|
+
name: item.name,
|
|
91
|
+
type: "file",
|
|
92
|
+
path: fullPath,
|
|
93
|
+
size: stats.size
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** ========== CLOUDINARY UPLOAD (SIGNED) ========== **/
|
|
101
|
+
async function uploadFileSigned(filePath, folder, roomId, token, email) {
|
|
102
|
+
const filename = path.basename(filePath);
|
|
103
|
+
|
|
104
|
+
const sigRes = await axios.post(
|
|
105
|
+
`${BASE_URL}/${roomId}/get-upload-signature`,
|
|
106
|
+
{ filename, folder, roomId },
|
|
107
|
+
{ headers: { authorization: `Bearer ${token}`, email } }
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const { signature, timestamp, api_key, cloud_name } = sigRes.data;
|
|
111
|
+
|
|
112
|
+
const formData = new FormData();
|
|
113
|
+
formData.append("file", fs.createReadStream(filePath));
|
|
114
|
+
formData.append("folder", folder);
|
|
115
|
+
formData.append("public_id", filename);
|
|
116
|
+
formData.append("timestamp", timestamp);
|
|
117
|
+
formData.append("signature", signature);
|
|
118
|
+
formData.append("api_key", api_key);
|
|
119
|
+
|
|
120
|
+
const cloudRes = await axios.post(
|
|
121
|
+
`https://api.cloudinary.com/v1_1/${cloud_name}/auto/upload`,
|
|
122
|
+
formData,
|
|
123
|
+
{ headers: formData.getHeaders() }
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return cloudRes.data.secure_url;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function uploadTree(fileTree, folderHex, roomId, token, email, parentPath = "") {
|
|
130
|
+
const uploaded = [];
|
|
131
|
+
|
|
132
|
+
for (const node of fileTree) {
|
|
133
|
+
const relativePath = path.join(parentPath, node.name).replace(/\\/g, "/");
|
|
134
|
+
|
|
135
|
+
if (node.type === "folder") {
|
|
136
|
+
const children = await uploadTree(node.children, folderHex, roomId, token, email, relativePath);
|
|
137
|
+
uploaded.push({
|
|
138
|
+
name: node.name,
|
|
139
|
+
type: "folder",
|
|
140
|
+
children
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
const url = await uploadFileSigned(node.path, `tnc_uploads/${folderHex}`, roomId, token, email);
|
|
144
|
+
console.log(`📦 Uploaded: ${relativePath} → ${url}`);
|
|
145
|
+
|
|
146
|
+
uploaded.push({
|
|
147
|
+
name: node.name,
|
|
148
|
+
type: "file",
|
|
149
|
+
path: relativePath,
|
|
150
|
+
size: node.size,
|
|
151
|
+
url // ✅ send top-level URL now
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return uploaded;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** ========== PUSH FUNCTION ========== **/
|
|
160
|
+
async function push(roomId, targetPath) {
|
|
161
|
+
const { token, email } = readToken();
|
|
162
|
+
const stats = fs.statSync(targetPath);
|
|
163
|
+
const rootFolder = stats.isDirectory() ? targetPath : path.dirname(targetPath);
|
|
164
|
+
const ignoreList = loadIgnore(rootFolder);
|
|
165
|
+
|
|
166
|
+
let content;
|
|
167
|
+
if (stats.isDirectory()) {
|
|
168
|
+
content = scanFolder(targetPath, ignoreList);
|
|
169
|
+
} else {
|
|
170
|
+
const relativePath = path.basename(targetPath);
|
|
171
|
+
content = shouldIgnore(relativePath, ignoreList)
|
|
172
|
+
? []
|
|
173
|
+
: [{ name: relativePath, type: "file", path: targetPath, size: stats.size }];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!content.length) {
|
|
177
|
+
console.log("⚠️ Nothing to upload (all ignored).");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const folderHex = crypto.createHash("md5").update(path.basename(targetPath) + Date.now()).digest("hex");
|
|
183
|
+
|
|
184
|
+
console.log("🚀 Uploading to Cloudinary...");
|
|
185
|
+
const uploadedTree = await uploadTree(content, folderHex, roomId, token, email);
|
|
186
|
+
|
|
187
|
+
console.log("🗂️ Sending metadata to backend...");
|
|
188
|
+
await axios.post(
|
|
189
|
+
`${BASE_URL}/${roomId}/upload`,
|
|
190
|
+
{ folderId: folderHex, content: uploadedTree, uploadedBy: email },
|
|
191
|
+
{ headers: { authorization: `Bearer ${token}`, email } }
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
console.log("✅ Upload complete! Metadata stored successfully.");
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.error("❌ Upload failed:", err.response?.data || err.message);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** ========== CLI HANDLER ========== **/
|
|
201
|
+
const args = process.argv.slice(2);
|
|
202
|
+
|
|
203
|
+
switch (args[0]) {
|
|
204
|
+
case "login":
|
|
205
|
+
login();
|
|
206
|
+
break;
|
|
207
|
+
|
|
208
|
+
case "push": {
|
|
209
|
+
const roomIndex = args.indexOf("--room");
|
|
210
|
+
if (roomIndex === -1 || !args[roomIndex + 1] || !args[roomIndex + 2]) {
|
|
211
|
+
console.error("Usage: tnc push --room <roomId> <file-or-folder-path>");
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
const roomId = args[roomIndex + 1];
|
|
215
|
+
const targetPath = args[roomIndex + 2];
|
|
216
|
+
push(roomId, targetPath);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
default:
|
|
221
|
+
console.log("✅ TNC CLI ready!");
|
|
222
|
+
console.log("Commands:");
|
|
223
|
+
console.log(" tnc login");
|
|
224
|
+
console.log(" tnc push --room <roomId> <path>");
|
|
225
|
+
}
|
package/bin/tnc.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { pushToRoom } from "../lib/api.js";
|
|
4
|
+
|
|
5
|
+
program
|
|
6
|
+
.name("tnc")
|
|
7
|
+
.description("ThinkNCollab CLI tool")
|
|
8
|
+
.version("0.1.0");
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.command("push")
|
|
12
|
+
.option("--room <roomId>", "Room ID to push files")
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
if (!opts.room) {
|
|
15
|
+
console.error("❌ Please provide a room id using --room <id>");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
await pushToRoom(opts.room);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
program.parse();
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { scanDirectory } from "./scanner.js";
|
|
2
|
+
import { uploadFile } from "./uploader.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
|
|
6
|
+
export async function pushToRoom(roomId) {
|
|
7
|
+
const currentDir = process.cwd();
|
|
8
|
+
console.log(`📂 Scanning directory: ${currentDir}`);
|
|
9
|
+
|
|
10
|
+
const files = scanDirectory(currentDir);
|
|
11
|
+
|
|
12
|
+
const uploadedFiles = [];
|
|
13
|
+
for (let file of files) {
|
|
14
|
+
const url = await uploadFile(file);
|
|
15
|
+
uploadedFiles.push({
|
|
16
|
+
name: file.name,
|
|
17
|
+
path: file.path,
|
|
18
|
+
size: file.size,
|
|
19
|
+
url,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Instead of real backend, save JSON locally for now
|
|
24
|
+
const outputPath = path.join(currentDir, `tnc_snapshot_${roomId}.json`);
|
|
25
|
+
fs.writeFileSync(outputPath, JSON.stringify(uploadedFiles, null, 2));
|
|
26
|
+
|
|
27
|
+
console.log(`✅ Snapshot saved at ${outputPath}`);
|
|
28
|
+
}
|
package/lib/scanner.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export function scanDirectory(dirPath) {
|
|
5
|
+
const result = [];
|
|
6
|
+
|
|
7
|
+
function walk(dir) {
|
|
8
|
+
const files = fs.readdirSync(dir);
|
|
9
|
+
for (let file of files) {
|
|
10
|
+
const fullPath = path.join(dir, file);
|
|
11
|
+
const stat = fs.statSync(fullPath);
|
|
12
|
+
|
|
13
|
+
if (stat.isDirectory()) {
|
|
14
|
+
walk(fullPath);
|
|
15
|
+
} else {
|
|
16
|
+
result.push({
|
|
17
|
+
name: file,
|
|
18
|
+
path: fullPath,
|
|
19
|
+
size: stat.size,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
walk(dirPath);
|
|
26
|
+
return result;
|
|
27
|
+
}
|
package/lib/uploader.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "thinkncollab-cli",
|
|
3
|
+
"author": "Raman Singh",
|
|
4
|
+
"version": "0.0.8",
|
|
5
|
+
"description": "CLI tool for ThinkNCollab",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tnc-cli": "./bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/index.js"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"axios": "^1.12.2",
|
|
16
|
+
"inquirer": "^9.3.8"
|
|
17
|
+
}
|
|
18
|
+
}
|