thinkncollab-shell 0.0.10 → 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.
@@ -0,0 +1,524 @@
1
+ const cp = require('child_process');
2
+ const vm = require('vm');
3
+ const path = require('path');
4
+ const fs = require('fs-extra');
5
+ const chalk = require('chalk');
6
+ const { AsyncLocalStorage } = require('async_hooks');
7
+
8
+ const activeVUStore = new AsyncLocalStorage();
9
+
10
+ class CheckExpression {
11
+ constructor(target, args = []) {
12
+ this.target = target;
13
+ this.args = args;
14
+ this.ops = [];
15
+ this.invert = false;
16
+ this.msg = null;
17
+ this.storeName = null;
18
+ }
19
+ equal(value) { this.ops.push({ op: 'eq', value }); return this; }
20
+ great(value) { this.ops.push({ op: 'gt', value }); return this; }
21
+ less(value) { this.ops.push({ op: 'lt', value }); return this; }
22
+ contains(value) { this.ops.push({ op: 'contains', value }); return this; }
23
+ isContained(value) { this.ops.push({ op: 'isContained', value }); return this; }
24
+ exists() { this.ops.push({ op: 'exists' }); return this; }
25
+ not() { this.invert = !this.invert; return this; }
26
+ message(text) { this.msg = text; return this; }
27
+ store(name) { this.storeName = name; return this; }
28
+ }
29
+
30
+ function statusCode() { return new CheckExpression('status'); }
31
+ function text() { return new CheckExpression('text'); }
32
+ function regexp(expr, group = 0, index = 0) { return new CheckExpression('regex', [expr, group, index]); }
33
+ function cookie(name) { return new CheckExpression('cookie', [name]); }
34
+ function header(name) { return new CheckExpression('header', [name]); }
35
+ function xPath(expr) { return new CheckExpression('xpath', [expr]); }
36
+
37
+ class HttpResult {
38
+ constructor(statusCode, headers, body) {
39
+ this.statusCode = statusCode;
40
+ this.headers = headers;
41
+ this.body = {
42
+ asText: body,
43
+ asObject: (() => {
44
+ try { return JSON.parse(body); } catch { return {}; }
45
+ })(),
46
+ asBytes: Array.from(Buffer.from(body))
47
+ };
48
+ }
49
+ }
50
+
51
+ class HttpRequest {
52
+ constructor(method, url, path, conn) {
53
+ this.method = method;
54
+ this.url = url;
55
+ this.path = path;
56
+ this.conn = conn;
57
+ this.headersMap = {};
58
+ this.cookiesMap = {};
59
+ this.queryParams = {};
60
+ this.bodyData = null;
61
+ this.checks = [];
62
+ }
63
+
64
+ header(name, value) { this.headersMap[name] = value; return this; }
65
+ cookie(name, value) { this.cookiesMap[name] = value; return this; }
66
+ query(name, value) { this.queryParams[name] = value; return this; }
67
+ body(obj) { this.bodyData = obj; return this; }
68
+ then(check) {
69
+ if (typeof check === 'function' || check instanceof CheckExpression) {
70
+ this.checks.push(check);
71
+ }
72
+ return this;
73
+ }
74
+
75
+ sync() {
76
+ let fullUrl = this.url.replace(/\/$/, '') + '/' + this.path.replace(/^\//, '');
77
+ const qKeys = Object.keys(this.queryParams);
78
+ if (qKeys.length > 0) {
79
+ const qString = qKeys.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(this.queryParams[k])}`).join('&');
80
+ fullUrl += (fullUrl.includes('?') ? '&' : '?') + qString;
81
+ }
82
+
83
+ const args = ['-i', '-s', '-L', '-X', this.method];
84
+
85
+ Object.entries(this.headersMap).forEach(([k, v]) => {
86
+ args.push('-H', `${k}: ${v}`);
87
+ });
88
+
89
+ const cKeys = Object.keys(this.cookiesMap);
90
+ if (cKeys.length > 0) {
91
+ const cString = cKeys.map(k => `${k}=${this.cookiesMap[k]}`).join('; ');
92
+ args.push('-b', cString);
93
+ }
94
+
95
+ if (this.bodyData) {
96
+ const bodyStr = typeof this.bodyData === 'object' ? JSON.stringify(this.bodyData) : String(this.bodyData);
97
+ args.push('-d', bodyStr);
98
+ if (!this.headersMap['Content-Type'] && !this.headersMap['content-type']) {
99
+ args.push('-H', 'Content-Type: application/json');
100
+ }
101
+ }
102
+
103
+ args.push(fullUrl);
104
+
105
+ const startTime = Date.now();
106
+ const run = cp.spawnSync('curl', args, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
107
+ const latency = Date.now() - startTime;
108
+
109
+ if (run.error) {
110
+ throw new Error(`Failed to execute HTTP request: ${run.error.message}`);
111
+ }
112
+
113
+ const stdout = run.stdout || '';
114
+ const delimiter = stdout.includes('\r\n\r\n') ? '\r\n\r\n' : '\n\n';
115
+ const firstDelimIdx = stdout.indexOf(delimiter);
116
+
117
+ let headerSection = '';
118
+ let bodySection = '';
119
+
120
+ if (firstDelimIdx !== -1) {
121
+ headerSection = stdout.slice(0, firstDelimIdx);
122
+ bodySection = stdout.slice(firstDelimIdx + delimiter.length);
123
+ } else {
124
+ headerSection = stdout;
125
+ }
126
+
127
+ const lines = headerSection.split(/\r?\n/);
128
+ const statusLine = lines[0] || '';
129
+ const statusMatch = statusLine.match(/HTTP\/[\d\.]+\s+(\d+)/);
130
+ const statusCode = statusMatch ? parseInt(statusMatch[1]) : 0;
131
+
132
+ const headers = {};
133
+ for (let i = 1; i < lines.length; i++) {
134
+ const line = lines[i];
135
+ const colonIdx = line.indexOf(':');
136
+ if (colonIdx !== -1) {
137
+ const k = line.slice(0, colonIdx).trim().toLowerCase();
138
+ const v = line.slice(colonIdx + 1).trim();
139
+ headers[k] = v;
140
+ }
141
+ }
142
+
143
+ const result = new HttpResult(statusCode, headers, bodySection);
144
+
145
+ const store = activeVUStore.getStore();
146
+ if (store) {
147
+ store.stats.totalRequests++;
148
+ store.stats.latencies.push(latency);
149
+
150
+ let requestPassed = true;
151
+ const failures = [];
152
+
153
+ for (const check of this.checks) {
154
+ if (typeof check === 'function') {
155
+ try {
156
+ check(result);
157
+ } catch (err) {
158
+ requestPassed = false;
159
+ failures.push(err.message);
160
+ }
161
+ } else if (check instanceof CheckExpression) {
162
+ const checkPassed = evaluateCheck(check, result, store.session);
163
+ if (!checkPassed.passed) {
164
+ requestPassed = false;
165
+ failures.push(checkPassed.reason);
166
+ }
167
+ }
168
+ }
169
+
170
+ if (!requestPassed) {
171
+ store.stats.failedRequests++;
172
+ store.stats.failures.push({
173
+ url: fullUrl,
174
+ method: this.method,
175
+ failures
176
+ });
177
+ }
178
+ }
179
+
180
+ return result;
181
+ }
182
+ }
183
+
184
+ function evaluateCheck(check, result, session) {
185
+ let actualVal;
186
+
187
+ if (check.target === 'status') {
188
+ actualVal = result.statusCode;
189
+ } else if (check.target === 'text') {
190
+ actualVal = result.body.asText;
191
+ } else if (check.target === 'regex') {
192
+ const [pattern, group, index] = check.args;
193
+ const regex = new RegExp(pattern);
194
+ const match = result.body.asText.match(regex);
195
+ actualVal = match ? (match[group] || '') : '';
196
+ } else if (check.target === 'cookie') {
197
+ const [cookieName] = check.args;
198
+ const setCookie = result.headers['set-cookie'] || '';
199
+ const match = setCookie.match(new RegExp(`${cookieName}=([^;]+)`));
200
+ actualVal = match ? match[1] : undefined;
201
+ } else if (check.target === 'header') {
202
+ const [headerName] = check.args;
203
+ actualVal = result.headers[headerName.toLowerCase()];
204
+ } else if (check.target === 'xpath') {
205
+ const [expr] = check.args;
206
+ actualVal = result.body.asText.includes(expr) ? expr : '';
207
+ }
208
+
209
+ let passed = true;
210
+ let reason = '';
211
+
212
+ for (const opInfo of check.ops) {
213
+ const { op, value } = opInfo;
214
+ let opPass = false;
215
+
216
+ switch (op) {
217
+ case 'eq':
218
+ opPass = actualVal == value;
219
+ if (!opPass) reason = `Expected "${value}", got "${actualVal}"`;
220
+ break;
221
+ case 'gt':
222
+ opPass = Number(actualVal) > Number(value);
223
+ if (!opPass) reason = `Expected > ${value}, got ${actualVal}`;
224
+ break;
225
+ case 'lt':
226
+ opPass = Number(actualVal) < Number(value);
227
+ if (!opPass) reason = `Expected < ${value}, got ${actualVal}`;
228
+ break;
229
+ case 'contains':
230
+ opPass = String(actualVal).includes(String(value));
231
+ if (!opPass) reason = `Expected to contain "${value}", got "${actualVal}"`;
232
+ break;
233
+ case 'isContained':
234
+ opPass = String(value).includes(String(actualVal));
235
+ if (!opPass) reason = `Expected "${actualVal}" to be contained in "${value}"`;
236
+ break;
237
+ case 'exists':
238
+ opPass = actualVal !== undefined && actualVal !== null;
239
+ if (!opPass) reason = `Expected to exist, but was undefined/null`;
240
+ break;
241
+ }
242
+
243
+ if (check.invert) {
244
+ opPass = !opPass;
245
+ if (!opPass) reason = `Inverted check failed: ${reason}`;
246
+ }
247
+
248
+ if (!opPass) {
249
+ passed = false;
250
+ break;
251
+ }
252
+ }
253
+
254
+ if (passed && check.storeName) {
255
+ session[check.storeName] = actualVal;
256
+ }
257
+
258
+ if (!passed && check.msg) {
259
+ reason = check.msg;
260
+ }
261
+
262
+ return { passed, reason };
263
+ }
264
+
265
+ class HttpConnection {
266
+ constructor(url, options) {
267
+ this.url = url;
268
+ this.options = options;
269
+ }
270
+ connect() { return this; }
271
+ get(path) { return new HttpRequest('GET', this.url, path, this); }
272
+ put(path) { return new HttpRequest('PUT', this.url, path, this); }
273
+ post(path) { return new HttpRequest('POST', this.url, path, this); }
274
+ patch(path) { return new HttpRequest('PATCH', this.url, path, this); }
275
+ delete(path) { return new HttpRequest('DELETE', this.url, path, this); }
276
+ options(path) { return new HttpRequest('OPTIONS', this.url, path, this); }
277
+ head(path) { return new HttpRequest('HEAD', this.url, path, this); }
278
+ }
279
+
280
+ function http(url, options) { return new HttpConnection(url, options); }
281
+ function http2(url, options) { return new HttpConnection(url, options); }
282
+
283
+ function websocket(url, options) {
284
+ return {
285
+ connect() { return this; },
286
+ sendText(val) {},
287
+ sendArray(val) {},
288
+ receiveText() { return ''; },
289
+ receiveArray() { return []; }
290
+ };
291
+ }
292
+
293
+ function swagger(url, options) {
294
+ return {
295
+ show() { console.log('Swagger definition loaded from ' + url); }
296
+ };
297
+ }
298
+
299
+ class StageClass {
300
+ constructor(runner) {
301
+ this.runner = runner;
302
+ }
303
+ async run(title, users, groups) {
304
+ await this.runner.runStage(title, users, groups);
305
+ }
306
+ }
307
+
308
+ class GroupClass {
309
+ run(scenario, users, profile) {
310
+ return { scenario, users, profile };
311
+ }
312
+ }
313
+
314
+ class GeekLoadRunner {
315
+ constructor() {
316
+ this.stats = {
317
+ totalRequests: 0,
318
+ failedRequests: 0,
319
+ latencies: [],
320
+ failures: [],
321
+ errors: [],
322
+ stopped: false
323
+ };
324
+ this.results = null;
325
+ this.completionPromise = new Promise((resolve) => {
326
+ this.resolveCompletion = resolve;
327
+ });
328
+ this.stageStarted = false;
329
+ }
330
+
331
+ async pause(min, max) {
332
+ const parseDuration = (str) => {
333
+ const match = String(str).match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/);
334
+ if (!match) return 1000;
335
+ const val = parseFloat(match[1]);
336
+ const unit = match[2] || 'ms';
337
+ if (unit === 's') return val * 1000;
338
+ if (unit === 'm') return val * 60 * 1000;
339
+ if (unit === 'h') return val * 60 * 60 * 1000;
340
+ return val;
341
+ };
342
+ const minMs = parseDuration(min);
343
+ const maxMs = max ? parseDuration(max) : minMs;
344
+ const delay = minMs + Math.random() * (maxMs - minMs);
345
+
346
+ return new Promise(resolve => setTimeout(resolve, delay));
347
+ }
348
+
349
+ async runStage(title, users, groups) {
350
+ this.stageStarted = true;
351
+ console.log(chalk.cyan(`\n🚀 Starting Stage: "${title}"`));
352
+ console.log(chalk.dim(` Total Virtual Users: ${users}\n`));
353
+
354
+ const startTime = Date.now();
355
+ const vuPromises = [];
356
+
357
+ for (const group of groups) {
358
+ const { scenario, users: groupUsers, profile } = group;
359
+ const durationMs = this.parseDuration(profile);
360
+
361
+ console.log(chalk.dim(` Spawning Group scenario... [VUs: ${groupUsers}, Duration: ${profile}]`));
362
+
363
+ for (let i = 0; i < groupUsers; i++) {
364
+ vuPromises.push(this.runVU(scenario, durationMs, i + 1));
365
+ }
366
+ }
367
+
368
+ await Promise.all(vuPromises);
369
+
370
+ const endTime = Date.now();
371
+ const durationSec = (endTime - startTime) / 1000;
372
+
373
+ const latencies = this.stats.latencies.sort((a, b) => a - b);
374
+ const totalRequests = this.stats.totalRequests;
375
+ const failedRequests = this.stats.failedRequests;
376
+
377
+ const p95Idx = Math.floor(latencies.length * 0.95);
378
+ const p95 = latencies.length > 0 ? latencies[p95Idx] : 0;
379
+
380
+ const p99Idx = Math.floor(latencies.length * 0.99);
381
+ const p99 = latencies.length > 0 ? latencies[p99Idx] : 0;
382
+
383
+ const avg = latencies.length > 0
384
+ ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length)
385
+ : 0;
386
+
387
+ const rps = durationSec > 0 ? parseFloat((totalRequests / durationSec).toFixed(2)) : 0;
388
+ const failureRate = totalRequests > 0 ? parseFloat((failedRequests / totalRequests).toFixed(4)) : 0;
389
+
390
+ this.results = {
391
+ virtual_users: users,
392
+ duration_sec: parseFloat(durationSec.toFixed(2)),
393
+ p95_latency_ms: p95,
394
+ p99_latency_ms: p99,
395
+ avg_latency_ms: avg,
396
+ failure_rate: failureRate,
397
+ requests_per_sec: rps,
398
+ total_requests: totalRequests,
399
+ failed_requests: failedRequests,
400
+ failures: this.stats.failures,
401
+ errors: this.stats.errors
402
+ };
403
+
404
+ this.resolveCompletion(this.results);
405
+ }
406
+
407
+ parseDuration(str) {
408
+ const match = String(str).match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/);
409
+ if (!match) return 5000;
410
+ const val = parseFloat(match[1]);
411
+ const unit = match[2] || 'ms';
412
+ if (unit === 's') return val * 1000;
413
+ if (unit === 'm') return val * 60 * 1000;
414
+ if (unit === 'h') return val * 60 * 60 * 1000;
415
+ return val;
416
+ }
417
+
418
+ async runVU(scenarioFn, durationMs, vuId) {
419
+ const endTime = Date.now() + durationMs;
420
+ const session = {};
421
+
422
+ while (Date.now() < endTime && !this.stats.stopped) {
423
+ try {
424
+ await activeVUStore.run({ stats: this.stats, session, vuId }, async () => {
425
+ if (typeof scenarioFn === 'function') {
426
+ await scenarioFn();
427
+ } else {
428
+ throw new Error(`Scenario is not an executable function: ${scenarioFn}`);
429
+ }
430
+ });
431
+ } catch (err) {
432
+ this.stats.errors.push({
433
+ vuId,
434
+ time: Date.now(),
435
+ message: err.message
436
+ });
437
+ await new Promise(resolve => setTimeout(resolve, 500));
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ function transpile(code) {
444
+ let processed = code;
445
+
446
+ // 1. Convert standard function declarations to async:
447
+ processed = processed.replace(/(?<!async\s+)function\s+([\w\$]+)\s*\(/g, 'async function $1(');
448
+
449
+ // 2. Convert arrow functions to async:
450
+ processed = processed.replace(/(?<!async\s+)([\w\$]+|\([^\)]*\))\s*=>/g, 'async $1 =>');
451
+
452
+ // 3. Prepend "await " to calls ending in .sync()
453
+ processed = processed.replace(/(?<!await\s+)([\w\$\(\)\.\'\"\[\]\-]+)\.sync\(\)/g, 'await $1.sync()');
454
+
455
+ // 4. Prepend "await " to pause(...)
456
+ processed = processed.replace(/(?<!await\s+)pause\(/g, 'await pause(');
457
+
458
+ // Clean up double await
459
+ processed = processed.replace(/await\s+await\s+/g, 'await ');
460
+
461
+ return processed;
462
+ }
463
+
464
+ async function runLoadTest(scriptPath) {
465
+ if (!fs.existsSync(scriptPath)) {
466
+ throw new Error(`Load test script not found: ${scriptPath}`);
467
+ }
468
+
469
+ const scriptContent = fs.readFileSync(scriptPath, 'utf8');
470
+ const runner = new GeekLoadRunner();
471
+
472
+ const context = vm.createContext({
473
+ Stage: new StageClass(runner),
474
+ Group: new GroupClass(),
475
+ http,
476
+ http2,
477
+ websocket,
478
+ swagger,
479
+ pause: runner.pause.bind(runner),
480
+ statusCode,
481
+ text,
482
+ regexp,
483
+ cookie,
484
+ header,
485
+ xPath,
486
+ Log: {
487
+ message(txt) { console.log(chalk.dim(`[Log] ${txt}`)); },
488
+ warning(txt) { console.log(chalk.yellow(`[Warning] ${txt}`)); },
489
+ error(txt) { console.log(chalk.red(`[Error] ${txt}`)); }
490
+ },
491
+ config: {},
492
+ console: console,
493
+ globalThis: {}
494
+ });
495
+
496
+ const transpiledCode = transpile(scriptContent);
497
+ const wrappedCode = `
498
+ (async () => {
499
+ try {
500
+ ${transpiledCode}
501
+ } catch (err) {
502
+ globalThis.__geekload_err = err;
503
+ }
504
+ })()
505
+ `;
506
+
507
+ const script = new vm.Script(wrappedCode);
508
+ script.runInContext(context);
509
+
510
+ // Setup safety fallback timeout
511
+ setTimeout(() => {
512
+ if (!runner.stageStarted) {
513
+ const err = context.globalThis.__geekload_err || new Error('Script execution failed to call Stage.run');
514
+ runner.resolveCompletion(Promise.reject(err));
515
+ }
516
+ }, 1000);
517
+
518
+ return runner.completionPromise;
519
+ }
520
+
521
+ module.exports = {
522
+ runLoadTest,
523
+ transpile
524
+ };
@@ -36,6 +36,26 @@ function printNotification(data) {
36
36
  const ts = chalk.dim(`[${timestamp}]`);
37
37
 
38
38
  switch (data.type) {
39
+ case 'task':
40
+ beep();
41
+ console.log(`${ts} 📋 ${chalk.yellow('Task Assigned →')} ${chalk.white(data.message)}`);
42
+ break;
43
+ case 'task_reopened':
44
+ beep();
45
+ console.log(`${ts} 🔄 ${chalk.yellow('Task Reopened →')} ${chalk.white(data.message.replace(/^🔄\s*/, ''))}`);
46
+ break;
47
+ case 'task_completed':
48
+ beep();
49
+ if (data.message) {
50
+ console.log(`${ts} 🎉 ${chalk.green('Task Completed →')} ${chalk.white(data.message.replace(/^🎉\s*/, ''))}`);
51
+ } else {
52
+ console.log(`${ts} ✅ ${chalk.green(data.userName || 'Someone')} completed ${chalk.white(data.taskTitle)}`);
53
+ }
54
+ break;
55
+ case 'task_progress':
56
+ beep();
57
+ console.log(`${ts} ⏳ ${chalk.blue('Task Progress →')} ${chalk.white(data.message)}`);
58
+ break;
39
59
  case 'task_assigned':
40
60
  beep();
41
61
  console.log(`${ts} 📋 ${chalk.yellow('Task assigned →')} ${chalk.white(data.taskTitle)} ${chalk.dim('by')} ${chalk.cyan(data.assignedBy)}`);
@@ -43,10 +63,6 @@ function printNotification(data) {
43
63
  case 'task_started':
44
64
  console.log(`${ts} ⚡ ${chalk.green(data.userName)} started working on ${chalk.white(data.taskTitle)}`);
45
65
  break;
46
- case 'task_completed':
47
- beep();
48
- console.log(`${ts} ✅ ${chalk.green(data.userName)} completed ${chalk.white(data.taskTitle)}`);
49
- break;
50
66
  case 'message':
51
67
  beep();
52
68
  console.log(`${ts} 💬 ${chalk.cyan(data.from)}: ${data.text}`);
@@ -78,7 +94,11 @@ function printNotification(data) {
78
94
  process.stdout.write('\r' + ' '.repeat(40) + '\r');
79
95
  break;
80
96
  default:
81
- console.log(`${ts} ${chalk.white(JSON.stringify(data))}`);
97
+ if (data.message) {
98
+ console.log(`${ts} 📢 ${chalk.white(data.message)}`);
99
+ } else {
100
+ console.log(`${ts} ${chalk.white(JSON.stringify(data))}`);
101
+ }
82
102
  }
83
103
  }
84
104