thinkncollab-shell 0.0.12 → 0.0.14

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,177 @@ 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
+ };
402
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);
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
+ case 'tnc-build': {
501
+ const passed = result.exitCode === 0;
502
+ return {
503
+ passed,
504
+ reason: passed ? 'TNC Local CI pipeline passed' : 'TNC Local CI pipeline failed'
505
+ };
506
+ }
507
+
508
+ default:
509
+ return { passed: false, reason: `Unknown test type: ${type}` };
403
510
  }
511
+
512
+ return {
513
+ passed: failures.length === 0,
514
+ reason: failures.length === 0 ? 'All checks passed' : failures.join(' | '),
515
+ failures,
516
+ };
517
+ } catch (err) {
518
+ return { passed: false, reason: `Judge error: ${err.message}` };
404
519
  }
520
+ }
405
521
 
406
- if (type === 'browser') {
407
- if (expect?.element && !result.elementExists)
408
- failures.push(`Element "${expect.element}" not found on page`);
522
+ function judgeWithConditions(conditions, actual) {
523
+ const failures = [];
409
524
 
410
- if (expect?.text && result.elementText && !result.elementText.includes(expect.text))
411
- failures.push(`Element text: expected "${expect.text}", got "${result.elementText}"`);
525
+ for (const [key, ops] of Object.entries(conditions)) {
526
+ const actualVal = actual[key];
412
527
 
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}"`);
528
+ if (actualVal === undefined) {
529
+ failures.push(`Missing key: "${key}"`);
530
+ continue;
531
+ }
532
+
533
+ for (const [op, expected] of Object.entries(ops)) {
534
+ let pass = false;
535
+ switch (op) {
536
+ case 'gte': pass = actualVal >= expected; break;
537
+ case 'lte': pass = actualVal <= expected; break;
538
+ case 'gt': pass = actualVal > expected; break;
539
+ case 'lt': pass = actualVal < expected; break;
540
+ case 'eq': pass = actualVal === expected; break;
541
+ case 'contains': pass = String(actualVal).includes(String(expected)); break;
542
+ case 'regex': pass = new RegExp(expected).test(String(actualVal)); break;
543
+ default: pass = false;
544
+ }
545
+ if (!pass) {
546
+ failures.push(`"${key}" failed: ${actualVal} ${op} ${expected}`);
547
+ }
417
548
  }
418
549
  }
419
550
 
420
551
  return {
421
- passed: failures.length === 0,
422
- reason: failures.length === 0 ? 'All checks passed' : failures.join(' | '),
423
- failures,
552
+ passed: failures.length === 0,
553
+ reason: failures.length === 0
554
+ ? `All conditions passed`
555
+ : `Failed conditions: ${failures.join('; ')}`,
556
+ diff: failures.length > 0 ? { conditions, actual } : undefined
424
557
  };
425
558
  }
426
559
 
@@ -525,6 +658,246 @@ const authConfig = { ...boardAuth, ...(testConfig.auth || {}) };
525
658
  if (result.elementText)
526
659
  console.log(chalk.dim(` → Text : "${result.elementText.trim().slice(0, 60)}"`));
527
660
 
661
+ } else if (testConfig.type === 'exitCode' || testConfig.type === 'stdout' || testConfig.type === 'regex') {
662
+ const cmd = testConfig.command || testConfig.script;
663
+ if (!cmd) {
664
+ throw new Error(`testConfig type "${testConfig.type}" requires a "command" or "script" to execute.`);
665
+ }
666
+ console.log(chalk.dim(` → Executing local command: "${cmd}"`));
667
+ const runRes = await runLocalCommand(cmd);
668
+ result = {
669
+ exitCode: runRes.exitCode,
670
+ stdout: runRes.stdout,
671
+ stderr: runRes.stderr
672
+ };
673
+ console.log(chalk.dim(` → Exit Code: ${result.exitCode}`));
674
+ if (result.stdout) console.log(chalk.dim(` → Stdout : ${result.stdout.trim().slice(0, 80)}`));
675
+
676
+ } else if (testConfig.type === 'json') {
677
+ const cmd = testConfig.command || testConfig.script;
678
+ if (cmd) {
679
+ console.log(chalk.dim(` → Executing local command: "${cmd}"`));
680
+ const runRes = await runLocalCommand(cmd);
681
+ let parsedJson = {};
682
+ try {
683
+ parsedJson = JSON.parse(runRes.stdout.trim());
684
+ } catch (e) {
685
+ throw new Error(`Command executed but stdout was not valid JSON: ${runRes.stdout}`);
686
+ }
687
+ result = {
688
+ exitCode: runRes.exitCode,
689
+ stdout: runRes.stdout,
690
+ stderr: runRes.stderr,
691
+ json: parsedJson,
692
+ output: parsedJson
693
+ };
694
+ } else {
695
+ throw new Error('testConfig type "json" requires a "command" or "script" to output JSON.');
696
+ }
697
+
698
+ } else if (testConfig.type === 'loadtest' || testConfig.type === 'geekload') {
699
+ const script = testConfig.script || testConfig.command;
700
+ if (!script) {
701
+ throw new Error(`testConfig type "${testConfig.type}" requires a "script" or "command" specifying the load test script path.`);
702
+ }
703
+ const scriptPath = path.resolve(process.cwd(), script);
704
+ console.log(chalk.dim(` → Running GeekLoad test script: ${script}`));
705
+ const { runLoadTest } = require('../../core/geekload');
706
+ const loadtestResult = await runLoadTest(scriptPath);
707
+
708
+ const Table = require('cli-table3');
709
+ const table = new Table({
710
+ head: [chalk.cyan('Metric'), chalk.cyan('Value')],
711
+ colWidths: [25, 20]
712
+ });
713
+ table.push(
714
+ ['Virtual Users', loadtestResult.virtual_users],
715
+ ['Duration (sec)', loadtestResult.duration_sec],
716
+ ['Total Requests', loadtestResult.total_requests],
717
+ ['Failed Requests', loadtestResult.failed_requests],
718
+ ['Failure Rate', `${(loadtestResult.failure_rate * 100).toFixed(2)}%`],
719
+ ['p95 Latency (ms)', loadtestResult.p95_latency_ms]
720
+ );
721
+ console.log('\n' + table.toString() + '\n');
722
+
723
+ result = {
724
+ exitCode: loadtestResult.failed_requests > 0 ? 1 : 0,
725
+ output: loadtestResult,
726
+ json: loadtestResult
727
+ };
728
+
729
+ } else if (testConfig.type === 'tnc-build') {
730
+ const fs = require('fs');
731
+ const path = require('path');
732
+ const { spawn } = require('child_process');
733
+
734
+ const projectRoot = process.cwd();
735
+ const workflowsDir = path.join(projectRoot, '.tnc/workflows');
736
+
737
+ if (!fs.existsSync(workflowsDir)) {
738
+ throw new Error('No .tnc/workflows directory found in project root.');
739
+ }
740
+
741
+ const files = fs.readdirSync(workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
742
+ if (files.length === 0) {
743
+ throw new Error('No workflow files found in .tnc/workflows/');
744
+ }
745
+
746
+ const workflowFile = files[0];
747
+ console.log(chalk.dim(` → Found workflow file: ${workflowFile}`));
748
+ const content = fs.readFileSync(path.join(workflowsDir, workflowFile), 'utf8');
749
+
750
+ // Parse steps
751
+ const lines = content.split('\n');
752
+ let workflowName = workflowFile;
753
+ let steps = [];
754
+ let currentStepName = '';
755
+
756
+ for (let i = 0; i < lines.length; i++) {
757
+ const line = lines[i].trim();
758
+ if (line.startsWith('name:')) {
759
+ workflowName = line.substring(5).trim().replace(/['"]/g, '');
760
+ continue;
761
+ }
762
+ if (line.startsWith('-')) {
763
+ const cleaned = line.substring(1).trim();
764
+ if (cleaned.startsWith('name:')) {
765
+ currentStepName = cleaned.substring(5).trim().replace(/['"]/g, '');
766
+ } else if (cleaned.startsWith('run:')) {
767
+ let runCmd = cleaned.substring(4).trim();
768
+ if ((runCmd.startsWith('"') && runCmd.endsWith('"')) || (runCmd.startsWith("'") && runCmd.endsWith("'"))) {
769
+ runCmd = runCmd.substring(1, runCmd.length - 1);
770
+ }
771
+ if (runCmd === '|') {
772
+ let block = [];
773
+ let j = i + 1;
774
+ while (j < lines.length) {
775
+ const next = lines[j];
776
+ const match = next.match(/^(\s+)(.*)/);
777
+ if (match && match[1].length > 4) {
778
+ block.push(next.trim());
779
+ j++;
780
+ } else {
781
+ break;
782
+ }
783
+ }
784
+ runCmd = block.join('\n');
785
+ i = j - 1;
786
+ }
787
+ steps.push({
788
+ name: currentStepName || `run: ${runCmd.substring(0, 30)}`,
789
+ run: runCmd
790
+ });
791
+ currentStepName = '';
792
+ }
793
+ } else if (line.startsWith('name:')) {
794
+ currentStepName = line.substring(5).trim().replace(/['"]/g, '');
795
+ } else if (line.startsWith('run:')) {
796
+ let runCmd = line.substring(4).trim();
797
+ if ((runCmd.startsWith('"') && runCmd.endsWith('"')) || (runCmd.startsWith("'") && runCmd.endsWith("'"))) {
798
+ runCmd = runCmd.substring(1, runCmd.length - 1);
799
+ }
800
+ if (runCmd === '|') {
801
+ let block = [];
802
+ let j = i + 1;
803
+ while (j < lines.length) {
804
+ const next = lines[j];
805
+ const match = next.match(/^(\s+)(.*)/);
806
+ if (match && match[1].length > 4) {
807
+ block.push(next.trim());
808
+ j++;
809
+ } else {
810
+ break;
811
+ }
812
+ }
813
+ runCmd = block.join('\n');
814
+ i = j - 1;
815
+ }
816
+ steps.push({
817
+ name: currentStepName || `run: ${runCmd.substring(0, 30)}`,
818
+ run: runCmd
819
+ });
820
+ currentStepName = '';
821
+ }
822
+ }
823
+
824
+ if (steps.length === 0) {
825
+ throw new Error('No steps found in the workflow file.');
826
+ }
827
+
828
+ console.log(chalk.cyan(`\n🚀 [TNC Actions] Starting Local Workflow: "${workflowName}"\n`));
829
+
830
+ const stepResults = [];
831
+ let overallPassed = true;
832
+ let logsBuffer = '';
833
+
834
+ for (let s = 0; s < steps.length; s++) {
835
+ const step = steps[s];
836
+ console.log(chalk.cyan(`⚙️ [Step ${s+1}/${steps.length}] "${step.name}"`));
837
+ console.log(chalk.gray(`$ ${step.run}`));
838
+
839
+ // Run command shell execution
840
+ const exitCode = await new Promise((resolve) => {
841
+ const shellCmd = process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
842
+ const shellArgs = process.platform === 'win32' ? ['/s', '/c', step.run] : ['-c', step.run];
843
+
844
+ const proc = spawn(shellCmd, shellArgs, {
845
+ cwd: projectRoot,
846
+ env: { ...process.env, PATH: process.env.PATH, PUPPETEER_SKIP_DOWNLOAD: 'true' }
847
+ });
848
+
849
+ proc.stdout.on('data', (chunk) => {
850
+ const chunkStr = chunk.toString();
851
+ process.stdout.write(chunkStr);
852
+ logsBuffer += chunkStr;
853
+ });
854
+
855
+ proc.stderr.on('data', (chunk) => {
856
+ const chunkStr = chunk.toString();
857
+ process.stderr.write(chalk.red(chunkStr));
858
+ logsBuffer += chunkStr;
859
+ });
860
+
861
+ proc.on('close', (code) => {
862
+ resolve(code);
863
+ });
864
+ });
865
+
866
+ const stepPassed = (exitCode === 0);
867
+ stepResults.push({
868
+ name: step.name,
869
+ command: step.run,
870
+ exitCode,
871
+ passed: stepPassed
872
+ });
873
+
874
+ if (!stepPassed) {
875
+ overallPassed = false;
876
+ console.log(chalk.red(`\n❌ Step "${step.name}" failed with exit code: ${exitCode}. Aborting.\n`));
877
+ break;
878
+ } else {
879
+ console.log(chalk.green(`\n✅ Step "${step.name}" passed successfully.\n`));
880
+ }
881
+ }
882
+
883
+ result = {
884
+ exitCode: overallPassed ? 0 : 1,
885
+ stdout: logsBuffer,
886
+ stderr: '',
887
+ output: {
888
+ workflow: workflowName,
889
+ status: overallPassed ? 'passed' : 'failed',
890
+ stepsCount: steps.length,
891
+ stepsRun: stepResults
892
+ },
893
+ json: {
894
+ workflow: workflowName,
895
+ status: overallPassed ? 'passed' : 'failed',
896
+ stepsCount: steps.length,
897
+ stepsRun: stepResults
898
+ }
899
+ };
900
+
528
901
  } else {
529
902
  console.log(chalk.red(`❌ Unsupported test type: ${testConfig.type}`));
530
903
  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
  };