@probelabs/visor 0.1.42 → 0.1.44

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/README.md CHANGED
@@ -366,7 +366,8 @@ Learn more: [docs/http.md](docs/http.md)
366
366
 
367
367
  Mix providers (`ai`, `http`, `http_client`, `log`, `command`, `claude-code`) or add your own.
368
368
 
369
- Learn more: [docs/pluggable.md](docs/pluggable.md)
369
+ - **Command Provider**: Execute shell commands with templating and security - [docs/command-provider.md](docs/command-provider.md)
370
+ - **Custom Providers**: Build your own providers - [docs/pluggable.md](docs/pluggable.md)
370
371
 
371
372
  ## šŸŽÆ GitHub Action Reference
372
373
 
@@ -1 +1 @@
1
- {"version":3,"file":"","sourceRoot":"","sources":["file:///home/runner/work/visor/visor/src/cli-main.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA0Q1C"}
1
+ {"version":3,"file":"","sourceRoot":"","sources":["file:///home/runner/work/visor/visor/src/cli-main.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAmS1C"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"","sourceRoot":"","sources":["file:///home/runner/work/visor/visor/src/cli.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAA2B,MAAM,aAAa,CAAC;AAIlE;;GAEG;AACH,qBAAa,GAAG;IACd,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,WAAW,CAA4E;IAC/F,OAAO,CAAC,YAAY,CAA0D;;IAO9E;;OAEG;IACH,OAAO,CAAC,YAAY;IAwCpB;;OAEG;IACH,OAAO,CAAC,aAAa,CAEnB;IAEF;;OAEG;IACI,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU;IAkI5C;;OAEG;IACH,OAAO,CAAC,eAAe;IAkCvB;;OAEG;IACI,WAAW,IAAI,MAAM;IAsC5B;;OAEG;IACI,UAAU,IAAI,MAAM;IAa3B;;OAEG;IACI,eAAe,IAAI,MAAM;IAiBhC;;OAEG;IACI,QAAQ,IAAI,IAAI;IAIvB;;OAEG;IACI,WAAW,IAAI,IAAI;CAG3B"}
1
+ {"version":3,"file":"","sourceRoot":"","sources":["file:///home/runner/work/visor/visor/src/cli.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAA2B,MAAM,aAAa,CAAC;AAIlE;;GAEG;AACH,qBAAa,GAAG;IACd,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,WAAW,CAA4E;IAC/F,OAAO,CAAC,YAAY,CAA0D;;IAO9E;;OAEG;IACH,OAAO,CAAC,YAAY;IAwCpB;;OAEG;IACH,OAAO,CAAC,aAAa,CAEnB;IAEF;;OAEG;IACI,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU;IAkI5C;;OAEG;IACH,OAAO,CAAC,eAAe;IAkCvB;;OAEG;IACI,WAAW,IAAI,MAAM;IAsC5B;;OAEG;IACI,UAAU,IAAI,MAAM;IAoC3B;;OAEG;IACI,eAAe,IAAI,MAAM;IAiBhC;;OAEG;IACI,QAAQ,IAAI,IAAI;IAIvB;;OAEG;IACI,WAAW,IAAI,IAAI;CAG3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"","sourceRoot":"","sources":["file:///home/runner/work/visor/visor/src/config.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,WAAW,EAOX,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,eAAe,CASrB;IACF,OAAO,CAAC,kBAAkB,CASxB;IACF,OAAO,CAAC,kBAAkB,CAAgE;IAC1F,OAAO,CAAC,mBAAmB,CAAkD;IAE7E;;OAEG;IACU,UAAU,CACrB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,WAAW,CAAC;IA6EvB;;OAEG;IACU,iBAAiB,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,WAAW,CAAC;IAyBrF;;OAEG;YACW,qBAAqB;IAiBnC;;OAEG;IACU,gBAAgB,IAAI,OAAO,CAAC,WAAW,CAAC;IAerD;;OAEG;IACI,wBAAwB,IAAI,WAAW,GAAG,IAAI;IAoDrD;;OAEG;IACH,OAAO,CAAC,eAAe;IAuBvB;;OAEG;IACI,mBAAmB,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,UAAU,EAAE,UAAU,GAAG,YAAY;IAqB9F;;OAEG;IACU,0BAA0B,IAAI,OAAO,CAAC;QACjD,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,oBAAoB,EAAE,oBAAoB,CAAC;KAC5C,CAAC;IA2BF;;OAEG;IACH,OAAO,CAAC,cAAc;IAqEtB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuJ3B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA6DzB;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA6EhC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA+B5B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAY9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;CA6B1B"}
1
+ {"version":3,"file":"","sourceRoot":"","sources":["file:///home/runner/work/visor/visor/src/config.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,WAAW,EAOX,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,eAAe,CASrB;IACF,OAAO,CAAC,kBAAkB,CASxB;IACF,OAAO,CAAC,kBAAkB,CAAgE;IAC1F,OAAO,CAAC,mBAAmB,CAAkD;IAE7E;;OAEG;IACU,UAAU,CACrB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,WAAW,CAAC;IA2FvB;;OAEG;IACU,iBAAiB,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,WAAW,CAAC;IAyBrF;;OAEG;YACW,qBAAqB;IAiBnC;;OAEG;IACU,gBAAgB,IAAI,OAAO,CAAC,WAAW,CAAC;IAerD;;OAEG;IACI,wBAAwB,IAAI,WAAW,GAAG,IAAI;IAoDrD;;OAEG;IACH,OAAO,CAAC,eAAe;IAuBvB;;OAEG;IACI,mBAAmB,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,UAAU,EAAE,UAAU,GAAG,YAAY;IAqB9F;;OAEG;IACU,0BAA0B,IAAI,OAAO,CAAC;QACjD,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,oBAAoB,EAAE,oBAAoB,CAAC;KAC5C,CAAC;IA2BF;;OAEG;IACH,OAAO,CAAC,cAAc;IAkEtB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IA0J3B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA6DzB;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA6EhC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA+B5B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAY9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;CA6B1B"}
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ process.env.VISOR_VERSION = '0.1.44';
2
3
  /******/ (() => { // webpackBootstrap
3
4
  /******/ var __webpack_modules__ = ({
4
5
 
@@ -96907,15 +96908,36 @@ async function main() {
96907
96908
  try {
96908
96909
  config = await configManager.loadConfig(options.configPath);
96909
96910
  }
96910
- catch {
96911
- console.error(`āš ļø Warning: Configuration file not found: ${options.configPath}`);
96912
- console.error('Falling back to default configuration');
96913
- config = await configManager
96914
- .findAndLoadConfig()
96915
- .catch(() => configManager.getDefaultConfig());
96911
+ catch (error) {
96912
+ // Show the actual error message, not just assume "file not found"
96913
+ if (error instanceof Error) {
96914
+ console.error(`āŒ Error loading configuration from ${options.configPath}:`);
96915
+ console.error(` ${error.message}`);
96916
+ // Provide helpful hints based on the error type
96917
+ if (error.message.includes('not found')) {
96918
+ console.error('\nšŸ’” Hint: Check that the file path is correct and the file exists.');
96919
+ console.error(' You can use an absolute path: --config $(pwd)/.visor.yaml');
96920
+ }
96921
+ else if (error.message.includes('Invalid YAML')) {
96922
+ console.error('\nšŸ’” Hint: Check your YAML syntax. You can validate it at https://www.yamllint.com/');
96923
+ }
96924
+ else if (error.message.includes('extends')) {
96925
+ console.error('\nšŸ’” Hint: Check that extended configuration files exist and are accessible.');
96926
+ }
96927
+ else if (error.message.includes('permission')) {
96928
+ console.error('\nšŸ’” Hint: Check file permissions. The file must be readable.');
96929
+ }
96930
+ }
96931
+ else {
96932
+ console.error(`āŒ Error loading configuration: ${error}`);
96933
+ }
96934
+ // Exit with error when explicit config path fails
96935
+ console.error('\nšŸ›‘ Exiting: Cannot proceed when specified configuration file fails to load.');
96936
+ process.exit(1);
96916
96937
  }
96917
96938
  }
96918
96939
  else {
96940
+ // Auto-discovery mode - fallback to defaults is OK
96919
96941
  config = await configManager
96920
96942
  .findAndLoadConfig()
96921
96943
  .catch(() => configManager.getDefaultConfig());
@@ -97367,17 +97389,38 @@ class CLI {
97367
97389
  * Get version from package.json
97368
97390
  */
97369
97391
  getVersion() {
97392
+ // First, try environment variable (set during build/publish)
97393
+ if (process.env.VISOR_VERSION) {
97394
+ return process.env.VISOR_VERSION;
97395
+ }
97370
97396
  try {
97371
- const packageJsonPath = path.join(__dirname, '../../package.json');
97372
- if (fs.existsSync(packageJsonPath)) {
97373
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
97374
- return packageJson.version || '1.0.0';
97397
+ // Try multiple possible locations for package.json
97398
+ const possiblePaths = [
97399
+ path.join(__dirname, '../../package.json'), // Development
97400
+ path.join(__dirname, '../package.json'), // Alternate bundled
97401
+ path.join(process.cwd(), 'package.json'), // Current directory
97402
+ '/snapshot/visor/package.json', // Vercel pkg snapshot
97403
+ ];
97404
+ for (const packageJsonPath of possiblePaths) {
97405
+ if (fs.existsSync(packageJsonPath)) {
97406
+ try {
97407
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
97408
+ // Only use if it's the visor package
97409
+ if (packageJson.name === '@probelabs/visor' && packageJson.version) {
97410
+ return packageJson.version;
97411
+ }
97412
+ }
97413
+ catch {
97414
+ // Try next path
97415
+ }
97416
+ }
97375
97417
  }
97376
97418
  }
97377
97419
  catch {
97378
- // Fallback to default version
97420
+ // Continue to fallback
97379
97421
  }
97380
- return '1.0.0';
97422
+ // Fallback to the actual current version in package.json
97423
+ return '0.1.42';
97381
97424
  }
97382
97425
  /**
97383
97426
  * Get examples text for help
@@ -97611,10 +97654,22 @@ class ConfigManager {
97611
97654
  }
97612
97655
  catch (error) {
97613
97656
  if (error instanceof Error) {
97614
- if (error.message.includes('not found') || error.message.includes('Invalid YAML')) {
97657
+ // Pass through detailed error messages unchanged
97658
+ if (error.message.includes('not found') ||
97659
+ error.message.includes('Invalid YAML') ||
97660
+ error.message.includes('extends') ||
97661
+ error.message.includes('EACCES') ||
97662
+ error.message.includes('EISDIR')) {
97615
97663
  throw error;
97616
97664
  }
97617
- throw new Error(`Failed to read configuration file: ${error.message}`);
97665
+ // Add more context for generic errors
97666
+ if (error.message.includes('ENOENT')) {
97667
+ throw new Error(`Configuration file not found: ${configPath}`);
97668
+ }
97669
+ if (error.message.includes('EPERM')) {
97670
+ throw new Error(`Permission denied reading configuration file: ${configPath}`);
97671
+ }
97672
+ throw new Error(`Failed to read configuration file ${configPath}: ${error.message}`);
97618
97673
  }
97619
97674
  throw error;
97620
97675
  }
@@ -97821,10 +97876,7 @@ class ConfigManager {
97821
97876
  if (!checkConfig.type) {
97822
97877
  checkConfig.type = 'ai';
97823
97878
  }
97824
- // Default 'on' to ['manual'] if not specified
97825
- if (!checkConfig.on) {
97826
- checkConfig.on = ['manual'];
97827
- }
97879
+ // 'on' field is optional - if not specified, check can run on any event
97828
97880
  this.validateCheckConfig(checkName, checkConfig, errors);
97829
97881
  }
97830
97882
  }
@@ -97926,21 +97978,24 @@ class ConfigManager {
97926
97978
  });
97927
97979
  }
97928
97980
  }
97929
- if (!checkConfig.on || !Array.isArray(checkConfig.on)) {
97930
- errors.push({
97931
- field: `checks.${checkName}.on`,
97932
- message: `Invalid check configuration for "${checkName}": missing or invalid 'on' field`,
97933
- });
97934
- }
97935
- else {
97936
- // Validate event triggers
97937
- for (const event of checkConfig.on) {
97938
- if (!this.validEventTriggers.includes(event)) {
97939
- errors.push({
97940
- field: `checks.${checkName}.on`,
97941
- message: `Invalid event "${event}". Must be one of: ${this.validEventTriggers.join(', ')}`,
97942
- value: event,
97943
- });
97981
+ // 'on' field is optional - if not specified, check can be triggered by any event
97982
+ if (checkConfig.on) {
97983
+ if (!Array.isArray(checkConfig.on)) {
97984
+ errors.push({
97985
+ field: `checks.${checkName}.on`,
97986
+ message: `Invalid check configuration for "${checkName}": 'on' field must be an array`,
97987
+ });
97988
+ }
97989
+ else {
97990
+ // Validate event triggers
97991
+ for (const event of checkConfig.on) {
97992
+ if (!this.validEventTriggers.includes(event)) {
97993
+ errors.push({
97994
+ field: `checks.${checkName}.on`,
97995
+ message: `Invalid event "${event}". Must be one of: ${this.validEventTriggers.join(', ')}`,
97996
+ value: event,
97997
+ });
97998
+ }
97944
97999
  }
97945
98000
  }
97946
98001
  }
@@ -100183,8 +100238,8 @@ async function handleEvent(octokit, inputs, eventName, context, config) {
100183
100238
  const eventChecks = [];
100184
100239
  for (const [checkName, checkConfig] of Object.entries(config.checks || {})) {
100185
100240
  // Check if this check should run for this event
100186
- const checkEvents = checkConfig.on || ['pr_opened', 'pr_updated'];
100187
- if (checkEvents.includes(eventType)) {
100241
+ // If 'on' is not specified, the check can run on any event
100242
+ if (!checkConfig.on || checkConfig.on.includes(eventType)) {
100188
100243
  eventChecks.push(checkName);
100189
100244
  }
100190
100245
  }
@@ -100553,13 +100608,16 @@ async function handleIssueComment(octokit, owner, repo, context, inputs, actionC
100553
100608
  if (!config?.checks?.[checkId])
100554
100609
  return false;
100555
100610
  const checkConfig = config.checks[checkId];
100556
- const checkEvents = checkConfig.on || ['pr_opened', 'pr_updated'];
100611
+ // If 'on' is not specified, the check can run on any event
100612
+ if (!checkConfig.on) {
100613
+ return true;
100614
+ }
100557
100615
  // For issue comments, only run checks that are configured for issue_comment events
100558
100616
  if (!isPullRequest) {
100559
- return checkEvents.includes('issue_comment');
100617
+ return checkConfig.on.includes('issue_comment');
100560
100618
  }
100561
100619
  // For PR comments, run checks configured for PR events or issue_comment
100562
- return checkEvents.includes('pr_updated') || checkEvents.includes('issue_comment');
100620
+ return checkConfig.on.includes('pr_updated') || checkConfig.on.includes('issue_comment');
100563
100621
  });
100564
100622
  if (filteredCheckIds.length === 0) {
100565
100623
  console.log(`No checks configured to run for ${isPullRequest ? 'PR' : 'issue'} comments`);
@@ -103583,9 +103641,12 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
103583
103641
  const { exec } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(35317)));
103584
103642
  const { promisify } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(39023)));
103585
103643
  const execAsync = promisify(exec);
103644
+ // Get timeout from config (in seconds) or use default (60 seconds)
103645
+ const timeoutSeconds = config.timeout || 60;
103646
+ const timeoutMs = timeoutSeconds * 1000;
103586
103647
  const { stdout, stderr } = await execAsync(renderedCommand, {
103587
103648
  env: scriptEnv,
103588
- timeout: 60000, // 60 second timeout
103649
+ timeout: timeoutMs,
103589
103650
  maxBuffer: 10 * 1024 * 1024, // 10MB buffer
103590
103651
  });
103591
103652
  if (stderr && process.env.DEBUG) {
@@ -103688,7 +103749,18 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
103688
103749
  return safeVars;
103689
103750
  }
103690
103751
  getSupportedConfigKeys() {
103691
- return ['type', 'exec', 'transform', 'env', 'depends_on', 'on', 'if', 'group', 'forEach'];
103752
+ return [
103753
+ 'type',
103754
+ 'exec',
103755
+ 'transform',
103756
+ 'env',
103757
+ 'timeout',
103758
+ 'depends_on',
103759
+ 'on',
103760
+ 'if',
103761
+ 'group',
103762
+ 'forEach',
103763
+ ];
103692
103764
  }
103693
103765
  async isAvailable() {
103694
103766
  // Command provider is always available as long as we can execute commands
@@ -1 +1 @@
1
- {"version":3,"file":"command-check-provider.d.ts","sourceRoot":"","sources":["file:///home/runner/work/visor/visor/src/providers/command-check-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAChF,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,aAAa;IACrD,OAAO,CAAC,MAAM,CAAS;;IAWvB,OAAO,IAAI,MAAM;IAIjB,cAAc,IAAI,MAAM;IAIlB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAejD,OAAO,CACX,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,mBAAmB,EAC3B,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC7C,OAAO,CAAC,aAAa,CAAC;IA0HzB,OAAO,CAAC,kBAAkB;IAqB1B,OAAO,CAAC,2BAA2B;IAgBnC,sBAAsB,IAAI,MAAM,EAAE;IAI5B,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAKrC,eAAe,IAAI,MAAM,EAAE;CAO5B"}
1
+ {"version":3,"file":"command-check-provider.d.ts","sourceRoot":"","sources":["file:///home/runner/work/visor/visor/src/providers/command-check-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAChF,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,aAAa;IACrD,OAAO,CAAC,MAAM,CAAS;;IAWvB,OAAO,IAAI,MAAM;IAIjB,cAAc,IAAI,MAAM;IAIlB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAejD,OAAO,CACX,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,mBAAmB,EAC3B,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC7C,OAAO,CAAC,aAAa,CAAC;IA8HzB,OAAO,CAAC,kBAAkB;IAqB1B,OAAO,CAAC,2BAA2B;IAgBnC,sBAAsB,IAAI,MAAM,EAAE;IAe5B,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAKrC,eAAe,IAAI,MAAM,EAAE;CAO5B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/visor",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "visor": "./dist/index.js"
@@ -17,7 +17,7 @@
17
17
  "registry": "https://registry.npmjs.org/"
18
18
  },
19
19
  "scripts": {
20
- "build": "ncc build src/index.ts -o dist && cp -r defaults dist/ && echo '#!/usr/bin/env node' | cat - dist/index.js > temp && mv temp dist/index.js && chmod +x dist/index.js",
20
+ "build": "ncc build src/index.ts -o dist && cp -r defaults dist/ && node scripts/inject-version.js && echo '#!/usr/bin/env node' | cat - dist/index.js > temp && mv temp dist/index.js && chmod +x dist/index.js",
21
21
  "test": "jest",
22
22
  "prepublishOnly": "npm run build",
23
23
  "test:watch": "jest --watch",