thinkncollab-cli 0.0.1-0.1

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 ADDED
@@ -0,0 +1,2 @@
1
+ /node_modules
2
+
@@ -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
@@ -0,0 +1,313 @@
1
+ # 🧠 ThinkNCollab CLI
2
+
3
+ A powerful command-line interface for seamless collaboration with **ThinkNCollab** — push files, manage rooms, and collaborate directly from your terminal.
4
+
5
+ ---
6
+
7
+ ## 🚀 Quick Start
8
+
9
+ ```bash
10
+ # Install the CLI globally
11
+ npm install -g @thinkncollab/tnc-cli
12
+
13
+ # Login to your ThinkNCollab account
14
+ tnc-cli login
15
+
16
+ # Push files to a room
17
+ tnc-cli push --room <roomId> <path>
18
+
19
+ # Logout
20
+ tnc-cli logout
21
+ ```
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 ADDED
@@ -0,0 +1,245 @@
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 = "https://thinkncollab.com/rooms";
12
+
13
+ // LOGOUT
14
+ async function logout() {
15
+ try {
16
+ if (fs.existsSync(RC_FILE)) {
17
+ // Remove the .tncrc file safely
18
+ await fs.promises.rm(RC_FILE, { force: true });
19
+ console.log("✅ Logged out successfully. Local credentials removed.");
20
+ } else {
21
+ console.log("ℹ️ No active session found.");
22
+ }
23
+ } catch (err) {
24
+ console.error("❌ Error during logout:", err.message);
25
+ }
26
+ }
27
+
28
+ /** ========== LOGIN ========== **/
29
+ async function login() {
30
+ const answers = await inquirer.prompt([
31
+ { type: "input", name: "email", message: "Email:" },
32
+ { type: "password", name: "password", message: "Password:" }
33
+ ]);
34
+
35
+ try {
36
+ console.log("🔐 Logging in...");
37
+ const res = await axios.post("https://thinkncollab.com/login", {
38
+ email: answers.email,
39
+ password: answers.password
40
+ });
41
+
42
+ const { token, email } = res.data;
43
+ fs.writeFileSync(RC_FILE, JSON.stringify({ token, email }, null, 2));
44
+ console.log(`✅ Login successful! Token saved to ${RC_FILE}`);
45
+ } catch (err) {
46
+ console.error("❌ Login failed:", err.response?.data?.message || err.message);
47
+ }
48
+ }
49
+
50
+ function readToken() {
51
+ if (!fs.existsSync(RC_FILE)) {
52
+ console.error("❌ Not logged in. Run 'tnc login' first.");
53
+ process.exit(1);
54
+ }
55
+ const data = JSON.parse(fs.readFileSync(RC_FILE));
56
+ return { token: data.token, email: data.email };
57
+ }
58
+
59
+ /** ========== IGNORE HANDLING ========== **/
60
+ function loadIgnore(folderPath) {
61
+ const ignoreFile = path.join(folderPath, ".ignoretnc");
62
+ if (!fs.existsSync(ignoreFile)) return [];
63
+ return fs
64
+ .readFileSync(ignoreFile, "utf-8")
65
+ .split("\n")
66
+ .map(line => line.trim())
67
+ .filter(line => line && !line.startsWith("#"));
68
+ }
69
+
70
+ function shouldIgnore(relativePath, ignoreList) {
71
+ return ignoreList.some(pattern => {
72
+ if (pattern.endsWith("/**")) {
73
+ const folder = pattern.slice(0, -3);
74
+ return relativePath === folder || relativePath.startsWith(folder + path.sep);
75
+ }
76
+ if (pattern.startsWith("*.")) {
77
+ return relativePath.endsWith(pattern.slice(1));
78
+ }
79
+ return relativePath === pattern;
80
+ });
81
+ }
82
+
83
+ /** ========== SCAN FOLDER ========== **/
84
+ function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
85
+ const items = fs.readdirSync(folderPath, { withFileTypes: true });
86
+ const result = [];
87
+ for (const item of items) {
88
+ const fullPath = path.join(folderPath, item.name);
89
+ const relativePath = path.relative(rootPath, fullPath);
90
+
91
+ if (shouldIgnore(relativePath, ignoreList)) {
92
+ console.log("⚠️ Ignored:", relativePath);
93
+ continue;
94
+ }
95
+
96
+ if (item.isDirectory()) {
97
+ result.push({
98
+ name: item.name,
99
+ type: "folder",
100
+ children: scanFolder(fullPath, ignoreList, rootPath)
101
+ });
102
+ } else {
103
+ const stats = fs.statSync(fullPath);
104
+ result.push({
105
+ name: item.name,
106
+ type: "file",
107
+ path: fullPath,
108
+ size: stats.size
109
+ });
110
+ }
111
+ }
112
+ return result;
113
+ }
114
+
115
+ /** ========== CLOUDINARY UPLOAD (SIGNED) ========== **/
116
+ async function uploadFileSigned(filePath, folder, roomId, token, email) {
117
+ const filename = path.basename(filePath);
118
+
119
+ const sigRes = await axios.post(
120
+ `${BASE_URL}/${roomId}/get-upload-signature`,
121
+ { filename, folder, roomId },
122
+ { headers: { authorization: `Bearer ${token}`, email } }
123
+ );
124
+
125
+ const { signature, timestamp, api_key, cloud_name } = sigRes.data;
126
+
127
+ const formData = new FormData();
128
+ formData.append("file", fs.createReadStream(filePath));
129
+ formData.append("folder", folder);
130
+ formData.append("public_id", filename);
131
+ formData.append("timestamp", timestamp);
132
+ formData.append("signature", signature);
133
+ formData.append("api_key", api_key);
134
+
135
+ const cloudRes = await axios.post(
136
+ `https://api.cloudinary.com/v1_1/${cloud_name}/auto/upload`,
137
+ formData,
138
+ { headers: formData.getHeaders() }
139
+ );
140
+
141
+ return cloudRes.data.secure_url;
142
+ }
143
+
144
+ async function uploadTree(fileTree, folderHex, roomId, token, email, parentPath = "") {
145
+ const uploaded = [];
146
+
147
+ for (const node of fileTree) {
148
+ const relativePath = path.join(parentPath, node.name).replace(/\\/g, "/");
149
+
150
+ if (node.type === "folder") {
151
+ const children = await uploadTree(node.children, folderHex, roomId, token, email, relativePath);
152
+ uploaded.push({
153
+ name: node.name,
154
+ type: "folder",
155
+ children
156
+ });
157
+ } else {
158
+ const url = await uploadFileSigned(node.path, `tnc_uploads/${folderHex}`, roomId, token, email);
159
+ console.log(`📦 Uploaded: ${relativePath} → ${url}`);
160
+
161
+ uploaded.push({
162
+ name: node.name,
163
+ type: "file",
164
+ path: relativePath,
165
+ size: node.size,
166
+ url // ✅ send top-level URL now
167
+ });
168
+ }
169
+ }
170
+
171
+ return uploaded;
172
+ }
173
+
174
+ /** ========== PUSH FUNCTION ========== **/
175
+ async function push(roomId, targetPath) {
176
+ const { token, email } = readToken();
177
+ const stats = fs.statSync(targetPath);
178
+ const rootFolder = stats.isDirectory() ? targetPath : path.dirname(targetPath);
179
+ const ignoreList = loadIgnore(rootFolder);
180
+
181
+ let content;
182
+ if (stats.isDirectory()) {
183
+ content = scanFolder(targetPath, ignoreList);
184
+ } else {
185
+ const relativePath = path.basename(targetPath);
186
+ content = shouldIgnore(relativePath, ignoreList)
187
+ ? []
188
+ : [{ name: relativePath, type: "file", path: targetPath, size: stats.size }];
189
+ }
190
+
191
+ if (!content.length) {
192
+ console.log("⚠️ Nothing to upload (all ignored).");
193
+ return;
194
+ }
195
+
196
+ try {
197
+ const folderHex = crypto.createHash("md5").update(path.basename(targetPath) + Date.now()).digest("hex");
198
+
199
+ console.log("🚀 Uploading to Cloudinary...");
200
+ const uploadedTree = await uploadTree(content, folderHex, roomId, token, email);
201
+
202
+ console.log("🗂️ Sending metadata to backend...");
203
+ await axios.post(
204
+ `${BASE_URL}/${roomId}/upload`,
205
+ { folderId: folderHex, content: uploadedTree, uploadedBy: email },
206
+ { headers: { authorization: `Bearer ${token}`, email } }
207
+ );
208
+
209
+ console.log("✅ Upload complete! Metadata stored successfully.");
210
+ } catch (err) {
211
+ console.error("❌ Upload failed:", err.response?.data || err.message);
212
+ }
213
+ }
214
+
215
+ /** ========== CLI HANDLER ========== **/
216
+ const args = process.argv.slice(2);
217
+
218
+ switch (args[0]) {
219
+ case "login":
220
+ login();
221
+ break;
222
+
223
+ case "push": {
224
+ const roomIndex = args.indexOf("--room");
225
+ if (roomIndex === -1 || !args[roomIndex + 1] || !args[roomIndex + 2]) {
226
+ console.error("Usage: tnc push --room <roomId> <file-or-folder-path>");
227
+ process.exit(1);
228
+ }
229
+ const roomId = args[roomIndex + 1];
230
+ const targetPath = args[roomIndex + 2];
231
+ push(roomId, targetPath);
232
+ break;
233
+ }
234
+ case "logout": {
235
+ logout();
236
+ break;
237
+ }
238
+
239
+ default:
240
+ console.log("✅ TNC CLI ready!");
241
+ console.log("Commands:");
242
+ console.log(" tnc login");
243
+ console.log(" tnc push --room <roomId> <path>");
244
+ console.log(" tnc logout");
245
+ }
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
+ }
@@ -0,0 +1,5 @@
1
+ export async function uploadFile(file) {
2
+ // Later: upload to S3/Cloudinary
3
+ // For now: just return a fake URL
4
+ return `https://fake-storage.com/${file.name}`;
5
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "thinkncollab-cli",
3
+ "author": "Raman Singh",
4
+ "version": "0.0.10.1",
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
+ }