thinkncollab-shell 0.0.12 → 0.0.13

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.
@@ -2,6 +2,8 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const crypto = require('crypto');
5
+ const cp = require('child_process');
6
+ const path = require('path');
5
7
 
6
8
  // ─── Helpers ──────────────────────────────────────────────────────────────────
7
9
 
@@ -381,46 +383,169 @@ async function runBrowserTest(testConfig, testUser, boardAuth = {}) {
381
383
  }
382
384
  }
383
385
 
386
+ // Helper to run a local command and capture exitCode & stdout/stderr
387
+ function runLocalCommand(command) {
388
+ return new Promise((resolve) => {
389
+ cp.exec(command, (error, stdout, stderr) => {
390
+ const exitCode = error ? (error.code || 1) : 0;
391
+ resolve({
392
+ exitCode,
393
+ stdout: stdout.toString(),
394
+ stderr: stderr.toString()
395
+ });
396
+ });
397
+ });
398
+ }
399
+
384
400
  // ─── Local Judge ──────────────────────────────────────────────────────────────
385
401
  function judgeLocally(testConfig, result) {
386
- const { type, expect } = testConfig;
402
+ const { type, value, condition, expect } = testConfig;
387
403
  const failures = [];
388
404
 
389
- if (type === 'http') {
390
- if (expect?.status !== undefined && result.status !== expect.status)
391
- failures.push(`Status: expected ${expect.status}, got ${result.status}`);
392
-
393
- if (expect?.body) {
394
- for (const [key, condition] of Object.entries(expect.body)) {
395
- const actual = getNestedValue(result.body, key);
396
- if (condition?.exists && (actual === undefined || actual === null))
397
- failures.push(`body.${key}: expected to exist`);
398
- else if (condition?.eq !== undefined && actual !== condition.eq)
399
- failures.push(`body.${key}: expected "${condition.eq}", got "${actual}"`);
400
- else if (condition?.contains !== undefined && !String(actual).includes(String(condition.contains)))
401
- failures.push(`body.${key}: expected to contain "${condition.contains}"`);
405
+ try {
406
+ switch (type) {
407
+ case 'exitCode': {
408
+ const passed = result.exitCode === value;
409
+ return {
410
+ passed,
411
+ reason: passed
412
+ ? `Exit code matched: ${value}`
413
+ : `Expected exit code ${value}, got ${result.exitCode}`
414
+ };
415
+ }
416
+
417
+ case 'stdout': {
418
+ const actualOut = (result.stdout || '').trim();
419
+ const expectedOut = (value || '').trim();
420
+ const passed = actualOut === expectedOut;
421
+ return {
422
+ passed,
423
+ reason: passed ? `Output matched` : `Output mismatch`,
424
+ diff: passed ? undefined : { expected: expectedOut, actual: actualOut }
425
+ };
426
+ }
427
+
428
+ case 'regex': {
429
+ const pattern = new RegExp(value);
430
+ const target = result.stdout || result.output || '';
431
+ const passed = pattern.test(target);
432
+ return {
433
+ passed,
434
+ reason: passed ? `Regex matched: ${value}` : `Regex did not match: ${value}`
435
+ };
436
+ }
437
+
438
+ case 'http': {
439
+ if (expect?.status !== undefined && result.status !== expect.status) {
440
+ failures.push(`Status: expected ${expect.status}, got ${result.status}`);
441
+ }
442
+ if (expect?.body) {
443
+ for (const [key, condition] of Object.entries(expect.body)) {
444
+ const actualVal = getNestedValue(result.body || {}, key);
445
+ if (condition?.exists) {
446
+ if (actualVal === undefined || actualVal === null)
447
+ failures.push(`body.${key}: expected to exist`);
448
+ } else if (condition?.eq !== undefined) {
449
+ if (actualVal !== condition.eq)
450
+ failures.push(`body.${key}: expected "${condition.eq}", got "${actualVal}"`);
451
+ } else if (condition?.contains !== undefined) {
452
+ if (!String(actualVal).includes(String(condition.contains)))
453
+ failures.push(`body.${key}: expected to contain "${condition.contains}"`);
454
+ }
455
+ }
456
+ }
457
+ break;
458
+ }
459
+
460
+ case 'json': {
461
+ const actualData = result.output || result.json || {};
462
+ if (condition) {
463
+ return judgeWithConditions(condition, actualData);
464
+ } else {
465
+ const passed = JSON.stringify(value) === JSON.stringify(actualData);
466
+ return {
467
+ passed,
468
+ reason: passed ? `JSON matched` : `JSON mismatch`,
469
+ diff: passed ? undefined : { expected: value, actual: actualData }
470
+ };
471
+ }
472
+ }
473
+ case 'loadtest':
474
+ case 'geekload': {
475
+ const actualData = result.output || result.json || {};
476
+ const cond = condition || expect?.condition || expect || {};
477
+ return judgeWithConditions(cond, actualData);
402
478
  }
479
+
480
+ case 'browser': {
481
+ if (expect?.element && !result.elementExists) {
482
+ failures.push(`Element "${expect.element}" not found on page`);
483
+ }
484
+ if (expect?.text && result.elementText) {
485
+ if (!result.elementText.includes(expect.text))
486
+ failures.push(`Element text: expected to contain "${expect.text}", got "${result.elementText}"`);
487
+ }
488
+ if (expect?.redirectUrl) {
489
+ try {
490
+ const actualPath = new URL(result.url).pathname;
491
+ if (!actualPath.includes(expect.redirectUrl))
492
+ failures.push(`URL: expected "${expect.redirectUrl}", got "${actualPath}"`);
493
+ } catch {
494
+ failures.push(`Invalid URL in result: "${result.url}"`);
495
+ }
496
+ }
497
+ break;
498
+ }
499
+
500
+ default:
501
+ return { passed: false, reason: `Unknown test type: ${type}` };
403
502
  }
503
+
504
+ return {
505
+ passed: failures.length === 0,
506
+ reason: failures.length === 0 ? 'All checks passed' : failures.join(' | '),
507
+ failures,
508
+ };
509
+ } catch (err) {
510
+ return { passed: false, reason: `Judge error: ${err.message}` };
404
511
  }
512
+ }
405
513
 
406
- if (type === 'browser') {
407
- if (expect?.element && !result.elementExists)
408
- failures.push(`Element "${expect.element}" not found on page`);
514
+ function judgeWithConditions(conditions, actual) {
515
+ const failures = [];
516
+
517
+ for (const [key, ops] of Object.entries(conditions)) {
518
+ const actualVal = actual[key];
409
519
 
410
- if (expect?.text && result.elementText && !result.elementText.includes(expect.text))
411
- failures.push(`Element text: expected "${expect.text}", got "${result.elementText}"`);
520
+ if (actualVal === undefined) {
521
+ failures.push(`Missing key: "${key}"`);
522
+ continue;
523
+ }
412
524
 
413
- if (expect?.redirectUrl) {
414
- const actualPath = new URL(result.url).pathname;
415
- if (!actualPath.includes(expect.redirectUrl))
416
- failures.push(`URL: expected "${expect.redirectUrl}", got "${actualPath}"`);
525
+ for (const [op, expected] of Object.entries(ops)) {
526
+ let pass = false;
527
+ switch (op) {
528
+ case 'gte': pass = actualVal >= expected; break;
529
+ case 'lte': pass = actualVal <= expected; break;
530
+ case 'gt': pass = actualVal > expected; break;
531
+ case 'lt': pass = actualVal < expected; break;
532
+ case 'eq': pass = actualVal === expected; break;
533
+ case 'contains': pass = String(actualVal).includes(String(expected)); break;
534
+ case 'regex': pass = new RegExp(expected).test(String(actualVal)); break;
535
+ default: pass = false;
536
+ }
537
+ if (!pass) {
538
+ failures.push(`"${key}" failed: ${actualVal} ${op} ${expected}`);
539
+ }
417
540
  }
418
541
  }
419
542
 
420
543
  return {
421
- passed: failures.length === 0,
422
- reason: failures.length === 0 ? 'All checks passed' : failures.join(' | '),
423
- failures,
544
+ passed: failures.length === 0,
545
+ reason: failures.length === 0
546
+ ? `All conditions passed`
547
+ : `Failed conditions: ${failures.join('; ')}`,
548
+ diff: failures.length > 0 ? { conditions, actual } : undefined
424
549
  };
425
550
  }
426
551
 
@@ -525,6 +650,74 @@ const authConfig = { ...boardAuth, ...(testConfig.auth || {}) };
525
650
  if (result.elementText)
526
651
  console.log(chalk.dim(` → Text : "${result.elementText.trim().slice(0, 60)}"`));
527
652
 
653
+ } else if (testConfig.type === 'exitCode' || testConfig.type === 'stdout' || testConfig.type === 'regex') {
654
+ const cmd = testConfig.command || testConfig.script;
655
+ if (!cmd) {
656
+ throw new Error(`testConfig type "${testConfig.type}" requires a "command" or "script" to execute.`);
657
+ }
658
+ console.log(chalk.dim(` → Executing local command: "${cmd}"`));
659
+ const runRes = await runLocalCommand(cmd);
660
+ result = {
661
+ exitCode: runRes.exitCode,
662
+ stdout: runRes.stdout,
663
+ stderr: runRes.stderr
664
+ };
665
+ console.log(chalk.dim(` → Exit Code: ${result.exitCode}`));
666
+ if (result.stdout) console.log(chalk.dim(` → Stdout : ${result.stdout.trim().slice(0, 80)}`));
667
+
668
+ } else if (testConfig.type === 'json') {
669
+ const cmd = testConfig.command || testConfig.script;
670
+ if (cmd) {
671
+ console.log(chalk.dim(` → Executing local command: "${cmd}"`));
672
+ const runRes = await runLocalCommand(cmd);
673
+ let parsedJson = {};
674
+ try {
675
+ parsedJson = JSON.parse(runRes.stdout.trim());
676
+ } catch (e) {
677
+ throw new Error(`Command executed but stdout was not valid JSON: ${runRes.stdout}`);
678
+ }
679
+ result = {
680
+ exitCode: runRes.exitCode,
681
+ stdout: runRes.stdout,
682
+ stderr: runRes.stderr,
683
+ json: parsedJson,
684
+ output: parsedJson
685
+ };
686
+ } else {
687
+ throw new Error('testConfig type "json" requires a "command" or "script" to output JSON.');
688
+ }
689
+
690
+ } else if (testConfig.type === 'loadtest' || testConfig.type === 'geekload') {
691
+ const script = testConfig.script || testConfig.command;
692
+ if (!script) {
693
+ throw new Error(`testConfig type "${testConfig.type}" requires a "script" or "command" specifying the load test script path.`);
694
+ }
695
+ const scriptPath = path.resolve(process.cwd(), script);
696
+ console.log(chalk.dim(` → Running GeekLoad test script: ${script}`));
697
+ const { runLoadTest } = require('../../core/geekload');
698
+ const loadtestResult = await runLoadTest(scriptPath);
699
+
700
+ const Table = require('cli-table3');
701
+ const table = new Table({
702
+ head: [chalk.cyan('Metric'), chalk.cyan('Value')],
703
+ colWidths: [25, 20]
704
+ });
705
+ table.push(
706
+ ['Virtual Users', loadtestResult.virtual_users],
707
+ ['Duration (sec)', loadtestResult.duration_sec],
708
+ ['Total Requests', loadtestResult.total_requests],
709
+ ['Failed Requests', loadtestResult.failed_requests],
710
+ ['Failure Rate', `${(loadtestResult.failure_rate * 100).toFixed(2)}%`],
711
+ ['p95 Latency (ms)', loadtestResult.p95_latency_ms]
712
+ );
713
+ console.log('\n' + table.toString() + '\n');
714
+
715
+ result = {
716
+ exitCode: loadtestResult.failed_requests > 0 ? 1 : 0,
717
+ output: loadtestResult,
718
+ json: loadtestResult
719
+ };
720
+
528
721
  } else {
529
722
  console.log(chalk.red(`❌ Unsupported test type: ${testConfig.type}`));
530
723
  return;
@@ -0,0 +1,73 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const { runLoadTest } = require('../../core/geekload');
5
+ const Table = require('cli-table3');
6
+
7
+ module.exports = {
8
+ name: 'loadtest',
9
+ description: 'Run a local load test script using GeekLoad',
10
+ aliases: ['geekload'],
11
+ requiresAuth: false,
12
+
13
+ async execute(args, shell) {
14
+ if (!args[0]) {
15
+ console.log(chalk.red('Usage: loadtest <script-path>'));
16
+ return;
17
+ }
18
+
19
+ const scriptPath = path.resolve(process.cwd(), args[0]);
20
+ if (!fs.existsSync(scriptPath)) {
21
+ console.log(chalk.red(`❌ Script not found: ${args[0]}`));
22
+ return;
23
+ }
24
+
25
+ console.log(chalk.cyan(`\n📋 Loading test script: ${args[0]}`));
26
+
27
+ try {
28
+ const results = await runLoadTest(scriptPath);
29
+
30
+ const table = new Table({
31
+ head: [chalk.green('Metric'), chalk.green('Value')],
32
+ colWidths: [25, 20]
33
+ });
34
+
35
+ table.push(
36
+ ['Virtual Users', results.virtual_users],
37
+ ['Duration (sec)', results.duration_sec],
38
+ ['Total Requests', results.total_requests],
39
+ ['Failed Requests', results.failed_requests],
40
+ ['Requests/sec (RPS)', results.requests_per_sec],
41
+ ['Failure Rate', `${(results.failure_rate * 100).toFixed(2)}%`],
42
+ ['Average Latency (ms)', results.avg_latency_ms],
43
+ ['p95 Latency (ms)', results.p95_latency_ms],
44
+ ['p99 Latency (ms)', results.p99_latency_ms]
45
+ );
46
+
47
+ console.log(table.toString());
48
+
49
+ if (results.failures.length > 0) {
50
+ console.log(chalk.yellow(`\n⚠️ Recorded Failures (${results.failures.length}):`));
51
+ results.failures.slice(0, 5).forEach((f, idx) => {
52
+ console.log(chalk.dim(` ${idx + 1}. [${f.method}] ${f.url}`));
53
+ f.failures.forEach(msg => {
54
+ console.log(chalk.red(` - ${msg}`));
55
+ });
56
+ });
57
+ if (results.failures.length > 5) {
58
+ console.log(chalk.dim(` ... and ${results.failures.length - 5} more failures`));
59
+ }
60
+ }
61
+
62
+ if (results.errors.length > 0) {
63
+ console.log(chalk.red(`\n❌ Execution Errors (${results.errors.length}):`));
64
+ results.errors.slice(0, 5).forEach((e, idx) => {
65
+ console.log(chalk.red(` VU ${e.vuId} Error: ${e.message}`));
66
+ });
67
+ }
68
+
69
+ } catch (err) {
70
+ console.log(chalk.red(`❌ Load test execution failed: ${err.message}`));
71
+ }
72
+ }
73
+ };
@@ -2,60 +2,71 @@
2
2
  const chalk = require('chalk');
3
3
 
4
4
  const STATUS_MAP = {
5
- accept: 'accepted',
6
- start: 'inprogress',
7
- complete: 'completed',
8
- reject: 'rejected',
9
- done: 'completed',
5
+ accept: 'accepted',
6
+ start: 'inprogress',
7
+ complete: 'completed',
8
+ reject: 'rejected',
9
+ done: 'completed',
10
+ 'task-status': null, // requires explicit arg
10
11
  };
11
12
 
12
13
  module.exports = {
13
- name: 'task-status',
14
- description: 'Update your task status',
15
- aliases: ['accept', 'start', 'complete', 'reject', 'done'],
16
- requiresAuth: true,
17
- requiresRoom: true,
18
-
19
- async execute(args, shell) {
20
- const roomId = shell.ws.getCurrentRoom();
21
- const taskId = args[0];
22
- // invoked as 'accept <id>', 'start <id>', etc.
23
- const verb = shell._lastCommand || 'task-status';
24
- const status = STATUS_MAP[verb] || args[1];
25
-
26
- if (!taskId) {
27
- console.log(chalk.yellow(' Usage: accept <taskId> | start <taskId> | complete <taskId>'));
28
- return;
29
- }
30
- if (!status) {
31
- console.log(chalk.yellow(' Usage: task-status <taskId> <accepted|inprogress|completed|rejected>'));
32
- return;
33
- }
34
-
35
- const statusColors = {
36
- accepted: chalk.green,
37
- inprogress: chalk.blue,
38
- completed: chalk.green,
39
- rejected: chalk.red,
40
- };
41
- const col = statusColors[status] || chalk.white;
42
-
43
- try {
44
- const result = await shell.api._request(
45
- 'POST',
46
- `/thinknsh/${roomId}/tasks/${taskId}/status`,
47
- { status }
48
- );
49
-
50
- console.log(col(`\n ✓ Task "${result.taskTitle}" → ${status}`));
51
- if (result.allCompleted) {
52
- console.log(chalk.green(' 🎉 All assignees done — task fully completed!\n'));
53
- } else {
54
- console.log('');
55
- }
56
-
57
- } catch (err) {
58
- console.log(chalk.red(`\n ❌ ${err.message}\n`));
59
- }
14
+ name: 'task-status',
15
+ description: 'Update your task status',
16
+ aliases: ['accept', 'start', 'complete', 'reject', 'done'],
17
+ requiresAuth: true,
18
+ requiresRoom: true,
19
+
20
+ async execute(args, shell, invokedAs) {
21
+ const roomId = shell.ws.getCurrentRoom(); // ← top pe, ek baar
22
+ const taskId = args[0];
23
+ const verb = invokedAs || shell._lastCommand || 'task-status';
24
+ const status = STATUS_MAP[verb] ?? args[1];
25
+
26
+ if (!taskId) {
27
+ console.log(chalk.yellow(' Usage: accept <taskId> | start <taskId> | complete <taskId>'));
28
+ return;
29
+ }
30
+ if (!status) {
31
+ console.log(chalk.yellow(' Usage: task-status <taskId> <accepted|inprogress|completed|rejected>'));
32
+ return;
33
+ }
34
+
35
+ try {
36
+ const result = await shell.api._request(
37
+ 'POST',
38
+ `/thinknsh/${roomId}/tasks/${taskId}/status`,
39
+ { status }
40
+ );
41
+
42
+ const statusColors = {
43
+ accepted: chalk.green,
44
+ inprogress: chalk.blue,
45
+ completed: chalk.green,
46
+ rejected: chalk.red,
47
+ };
48
+
49
+ const col = statusColors[status] || chalk.white;
50
+ console.log(col(`\n ✓ Task "${result.taskTitle}" → ${status}`));
51
+
52
+ if (result.allCompleted) {
53
+ console.log(chalk.green(' 🎉 All assignees done — task fully completed!\n'));
54
+ } else {
55
+ console.log('');
56
+ }
57
+
58
+ // Socket emit
59
+ if (shell.ws.isConnected?.() && shell.ws.socket) {
60
+ shell.ws.socket.emit('task:' + (status === 'inprogress' ? 'started' : status), {
61
+ roomId,
62
+ taskId,
63
+ taskTitle: result.taskTitle,
64
+ userId: shell.api.getUser()?._id,
65
+ });
60
66
  }
67
+
68
+ } catch (err) {
69
+ console.log(chalk.red(`\n ❌ ${err.message}\n`));
70
+ }
71
+ }
61
72
  };
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  src/commands/tasks/task-info.js — View task details + testConfig + webhook URL
3
- Usage: task-info <taskId> OR info <taskId>
3
+ Usage: task-info <taskId> OR info <taskId> load
4
4
  */
5
5
  const chalk = require('chalk');
6
6
 
@@ -55,6 +55,13 @@ console.log(` ${chalk.dim('id ')} ${chalk.dim(task._id)}${task.taskID ? c
55
55
  task.description.split('\n').forEach(l => console.log(` ${chalk.white(l)}`));
56
56
  }
57
57
 
58
+ if (task.referenceLinks && task.referenceLinks.length > 0) {
59
+ console.log(chalk.cyan('\n ─── Supported Links ─────────────────────────────'));
60
+ task.referenceLinks.forEach(link => {
61
+ console.log(` ${chalk.dim(link.title.padEnd(12))} ${chalk.blue(link.url)}`);
62
+ });
63
+ }
64
+
58
65
  // ── Assignees ─────────────────────────────────────────────────────────────
59
66
  if (task.assignedTo?.length) {
60
67
  console.log(chalk.dim('\n assignees:'));
@@ -63,7 +63,7 @@ async function cmdStart(shell) {
63
63
  console.log(chalk.green(`\n✅ Terminal sharing started`));
64
64
  console.log(chalk.dim(` Session : ${data.sessionId}`));
65
65
  console.log(chalk.dim(` Room : ${roomId}`));
66
- console.log(chalk.dim(` URL : /terminal/${data.sessionId}`));
66
+ console.log(chalk.dim(` URL : https://thinkncollab.com/terminal/${data.sessionId}`));
67
67
  console.log(chalk.dim(`\n Room members can now view your terminal.`));
68
68
  console.log(chalk.dim(` Use "share grant <userId>" to give write access.\n`));
69
69