@sap/eslint-plugin-cds 2.4.1 → 2.6.0

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +31 -2
  2. package/README.md +2 -1
  3. package/lib/api/index.js +13 -12
  4. package/lib/conf/all.js +22 -0
  5. package/lib/conf/index.js +22 -0
  6. package/lib/conf/recommended.js +19 -0
  7. package/lib/constants.js +19 -16
  8. package/lib/index.js +11 -33
  9. package/lib/parser.js +169 -10
  10. package/lib/rules/assoc2many-ambiguous-key.js +80 -96
  11. package/lib/rules/auth-no-empty-restrictions.js +23 -30
  12. package/lib/rules/auth-use-requires.js +25 -24
  13. package/lib/rules/auth-valid-restrict-grant.js +87 -49
  14. package/lib/rules/auth-valid-restrict-keys.js +30 -23
  15. package/lib/rules/auth-valid-restrict-to.js +97 -52
  16. package/lib/rules/auth-valid-restrict-where.js +52 -42
  17. package/lib/rules/extension-restrictions.js +69 -0
  18. package/lib/rules/index.js +27 -0
  19. package/lib/rules/latest-cds-version.js +23 -21
  20. package/lib/rules/min-node-version.js +25 -24
  21. package/lib/rules/no-db-keywords.js +23 -31
  22. package/lib/rules/no-dollar-prefixed-names.js +17 -14
  23. package/lib/rules/no-join-on-draft.js +27 -0
  24. package/lib/rules/require-2many-oncond.js +11 -16
  25. package/lib/rules/sql-cast-suggestion.js +16 -29
  26. package/lib/rules/start-elements-lowercase.js +42 -44
  27. package/lib/rules/start-entities-uppercase.js +29 -31
  28. package/lib/rules/valid-csv-header.js +65 -64
  29. package/lib/{api/lint.d.ts → types.d.ts} +5 -7
  30. package/lib/utils/Cache.js +33 -0
  31. package/lib/utils/Colors.js +9 -0
  32. package/lib/utils/createRule.js +317 -0
  33. package/lib/utils/findFuzzy.js +87 -0
  34. package/lib/utils/genDocs.js +345 -0
  35. package/lib/utils/getConfigPath.js +33 -0
  36. package/lib/utils/getConfiguredFileTypes.js +10 -0
  37. package/lib/utils/getFileExtensions.js +8 -0
  38. package/lib/utils/getProjectRootPath.js +25 -0
  39. package/lib/utils/isConfiguredFileType.js +20 -0
  40. package/lib/utils/rules.js +112 -1041
  41. package/lib/utils/runRuleTester.js +116 -0
  42. package/package.json +10 -4
  43. package/lib/processor.js +0 -50
  44. package/lib/rules/no-join-on-draft-enabled-entities.js +0 -40
  45. package/lib/utils/fuzzySearch.js +0 -94
  46. package/lib/utils/helpers.js +0 -94
  47. package/lib/utils/jsonc.js +0 -1
  48. package/lib/utils/model.js +0 -393
  49. package/lib/utils/ruleHelpers.js +0 -199
  50. package/lib/utils/ruleTester.js +0 -78
  51. package/lib/utils/validate.js +0 -36
@@ -0,0 +1,27 @@
1
+ module.exports = {
2
+ meta: {
3
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
4
+ docs: {
5
+ // eslint-disable-next-line quotes
6
+ description: "Draft-enabled entities shall not be used in views that make use of `JOIN`.",
7
+ recommended: true
8
+ },
9
+ type: 'suggestion',
10
+ model: 'inferred'
11
+ },
12
+ create: function (context) {
13
+ return { entity: checkNojoinDraftenabled }
14
+
15
+ function checkNojoinDraftenabled (e) {
16
+ if (e['@odata.draft.enabled']) {
17
+ if (e.query.SELECT.from.join) {
18
+ context.report({
19
+ message: 'Do not use draft-enabled entities in views that make use of `JOIN`.',
20
+ node: context.getNode(e),
21
+ file: e.$location.file
22
+ })
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
@@ -1,27 +1,22 @@
1
1
  module.exports = {
2
2
  meta: {
3
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
3
4
  docs: {
4
- description: `Foreign key information of a \`TO MANY\` relationship must be defined within the target and specified in an \`ON\` condition.`,
5
- category: "Model Validation",
6
- recommended: true,
7
- version: "2.1.0",
5
+ description: 'Foreign key information of a `TO MANY` relationship must be defined within the target and specified in an `ON` condition.',
6
+ recommended: true
8
7
  },
9
- severity: "error",
10
- type: "problem",
8
+ type: 'problem'
11
9
  },
12
10
  create: function (context) {
11
+ return { element: check2manyOncond }
13
12
 
14
- return { element: check_2many_oncond }
15
-
16
- function check_2many_oncond(e) {
13
+ function check2manyOncond (e) {
17
14
  if (e.is2many && !e.on && typeof e.target === 'string') {
18
- const loc = context.cds.getLocation(e.name, e);
19
- return {
15
+ context.report({
20
16
  message: `You must provide an \`ON\` condition for \`TO MANY\` relationship '${e.name}'.`,
21
- loc,
22
- file: e.parent.$location.file,
23
- };
17
+ node: context.getNode(e)
18
+ })
24
19
  }
25
20
  }
26
- },
27
- };
21
+ }
22
+ }
@@ -1,52 +1,39 @@
1
1
  /* eslint-disable no-undef */
2
2
  module.exports = {
3
3
  meta: {
4
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
4
5
  docs: {
5
- description: "Should make suggestions for possible missing SQL casts.",
6
- category: "Model Validation",
7
- recommended: true,
8
- version: "1.0.8",
6
+ description: 'Should make suggestions for possible missing SQL casts.',
7
+ recommended: true
9
8
  },
10
- severity: "warn",
11
- type: "suggestion",
9
+ type: 'suggestion',
12
10
  hasSuggestions: true,
13
11
  messages: {
14
- missingSQLCast: "Potential issue - Missing SQL cast for column expression?",
15
- },
12
+ missingSQLCast: 'Potential issue - Missing SQL cast for column expression?'
13
+ }
16
14
  },
17
15
  create: function (context) {
18
- return { view: check_sql_cast };
16
+ return { view: checkSqlCast }
19
17
 
20
- function check_sql_cast(v) {
21
- const reports = [];
18
+ function checkSqlCast (v) {
22
19
  if (v.query && v.query.SET) {
23
20
  for (const { SELECT } of v.query.SET.args) {
24
21
  // Only in UNION cases?
25
22
  for (const each of SELECT.columns || []) {
26
- const { xpr, cast, $location: location } = each;
23
+ const { xpr, cast } = each
27
24
  if (cast && xpr) {
28
25
  if (xpr[0].xpr && xpr[0].cast) {
29
- continue;
26
+ continue
30
27
  } else {
31
- if (context.sourcecode.lines[location.line - 1]) {
32
- const endCol = context.sourcecode.lines[location.line - 1].length;
33
- const loc = {
34
- start: { line: location.line, column: location.col - 1 },
35
- end: { line: location.line, column: endCol },
36
- };
37
- reports.push({
38
- messageId: "missingSQLCast",
39
- loc,
40
- file: location.file,
41
- });
42
- }
28
+ context.report({
29
+ messageId: 'missingSQLCast',
30
+ node: context.getNode(v)
31
+ })
43
32
  }
44
33
  }
45
34
  }
46
35
  }
47
36
  }
48
- if (reports.length > 0 ) return reports;
49
37
  }
50
-
51
- },
52
- };
38
+ }
39
+ }
@@ -1,61 +1,59 @@
1
1
  module.exports = {
2
2
  meta: {
3
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
3
4
  docs: {
4
- description: "Regular element names should start with lowercase letters.",
5
- category: "Model Validation",
6
- version: "1.0.4",
5
+ description: 'Regular element names should start with lowercase letters.'
7
6
  },
8
- type: "suggestion",
7
+ type: 'suggestion',
9
8
  hasSuggestions: true,
10
9
  messages: {
11
10
  startLowercase: "Element name '{{entityName}}.{{elementName}}' should start with a lowercase letter.",
12
- fixLowercase: "Start element name with a lowercase letter.",
11
+ fixLowercase: 'Start element name with a lowercase letter.'
13
12
  },
14
- fixable: "code",
13
+ fixable: 'code'
15
14
  },
16
15
  create: function (context) {
17
- const { cds, filePath, sourcecode } = context;
16
+ const sourcecode = context.getSourceCode()
18
17
 
19
18
  return {
20
- element: check_start_lowercase,
21
- };
19
+ element: checkStartLowercase
20
+ }
22
21
 
23
- function check_start_lowercase(e) {
24
- if (!filePath.endsWith(".cds")) return;
25
- const elementName = e.name;
26
- const entityName = e.parent.name;
27
- if (elementName && !(entityName.startsWith("localized") || entityName.endsWith("texts"))) {
28
- if (elementName.charAt(0) !== elementName.charAt(0).toLowerCase() && !["ID"].includes(elementName)) {
29
- if (e.$location && e.$location.file) {
30
- const file = e.$location.file;
31
- const loc = cds.getLocation(elementName, e);
32
- const fix = (fixer, source = sourcecode) => {
33
- const elementNameSanitized = elementName.charAt(0).toLowerCase() + elementName.slice(1);
34
- const rangeEnd = source.getIndexFromLoc({
35
- line: loc.end.line,
36
- column: loc.end.column,
37
- });
38
- const rangeBeg = rangeEnd ? rangeEnd - elementNameSanitized.length : 0;
39
- return fixer.replaceTextRange([rangeBeg, rangeEnd], elementNameSanitized);
40
- };
41
- return {
42
- messageId: "startLowercase",
43
- loc,
44
- file,
45
- data: {
46
- entityName,
47
- elementName,
48
- },
49
- suggest: [
50
- {
51
- messageId: "fixLowercase",
52
- fix,
53
- },
54
- ],
55
- };
22
+ function checkStartLowercase (e) {
23
+ const elementName = e.name
24
+ const entityName = e.parent.name
25
+ if (elementName && !(entityName.startsWith('localized') || entityName.endsWith('texts'))) {
26
+ if (elementName.charAt(0) !== elementName.charAt(0).toLowerCase() && !['ID'].includes(elementName)) {
27
+ if (e.$location && e.$location.file) {
28
+ const file = e.$location.file
29
+ const loc = context.getLocation(elementName, e)
30
+ const fix = (fixer, source = sourcecode) => {
31
+ const elementNameSanitized = elementName.charAt(0).toLowerCase() + elementName.slice(1)
32
+ const rangeEnd = source.getIndexFromLoc({
33
+ line: loc.end.line,
34
+ column: loc.end.column
35
+ })
36
+ const rangeBeg = rangeEnd ? rangeEnd - elementNameSanitized.length : 0
37
+ return fixer.replaceTextRange([rangeBeg, rangeEnd], elementNameSanitized)
56
38
  }
39
+ context.report({
40
+ messageId: 'startLowercase',
41
+ loc,
42
+ file,
43
+ data: {
44
+ entityName,
45
+ elementName
46
+ },
47
+ suggest: [
48
+ {
49
+ messageId: 'fixLowercase',
50
+ fix
51
+ }
52
+ ]
53
+ })
54
+ }
57
55
  }
58
56
  }
59
57
  }
60
- },
61
- };
58
+ }
59
+ }
@@ -1,55 +1,53 @@
1
- const { splitEntityName } = require("../utils/ruleHelpers");
1
+ const { splitDefName } = require('../utils/rules')
2
2
 
3
3
  module.exports = {
4
4
  meta: {
5
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
5
6
  docs: {
6
- description: "Regular entity names should start with uppercase letters.",
7
- category: "Model Validation",
8
- version: "1.0.4",
7
+ description: 'Regular entity names should start with uppercase letters.'
9
8
  },
10
- type: "suggestion",
9
+ type: 'suggestion',
11
10
  hasSuggestions: true,
12
11
  messages: {
13
12
  startUppercase: "Entity name '{{entityName}}' should start with an uppercase letter.",
14
- fixUppercase: "Start entity name with an uppercase letter.",
13
+ fixUppercase: 'Start entity name with an uppercase letter.'
15
14
  },
16
- fixable: "code",
15
+ fixable: 'code'
17
16
  },
18
17
  create: function (context) {
19
- const { cds, filePath, sourcecode } = context;
18
+ const sourcecode = context.getSourceCode()
20
19
 
21
- return { entity: check_starts_uppercase };
20
+ return { entity: checkStartsUppercase }
22
21
 
23
- function check_starts_uppercase(e) {
24
- if (!filePath.endsWith(".cds")) return;
25
- const entityName = splitEntityName(e).entity;
22
+ function checkStartsUppercase (e) {
23
+ const entityName = splitDefName(e).name
26
24
  if (entityName.charAt(0) !== entityName.charAt(0).toUpperCase()) {
27
25
  if (e.$location && e.$location.file) {
28
- const file = e.$location.file;
29
- const loc = cds.getLocation(entityName, e);
30
- const fix = (fixer, source = sourcecode) => {
31
- const entityNameSanitized = entityName.charAt(0).toUpperCase() + entityName.slice(1);
32
- const rangeEnd = source.getIndexFromLoc({
26
+ const file = e.$location.file
27
+ const loc = context.getLocation(entityName, e)
28
+ const fix = (fixer) => {
29
+ const entityNameSanitized = entityName.charAt(0).toUpperCase() + entityName.slice(1)
30
+ const rangeEnd = sourcecode.getIndexFromLoc({
33
31
  line: loc.end.line,
34
- column: loc.end.column,
35
- });
36
- const rangeBeg = rangeEnd ? rangeEnd - entityNameSanitized.length : 0;
37
- return fixer.replaceTextRange([rangeBeg, rangeEnd], entityNameSanitized);
38
- };
39
- return {
40
- messageId: "startUppercase",
32
+ column: loc.end.column
33
+ })
34
+ const rangeBeg = rangeEnd ? rangeEnd - entityNameSanitized.length : 0
35
+ return fixer.replaceTextRange([rangeBeg, rangeEnd], entityNameSanitized)
36
+ }
37
+ context.report({
38
+ messageId: 'startUppercase',
41
39
  loc,
42
40
  file,
43
41
  data: { entityName },
44
42
  suggest: [
45
43
  {
46
- messageId: "fixUppercase",
47
- fix,
48
- },
49
- ],
50
- };
44
+ messageId: 'fixUppercase',
45
+ fix
46
+ }
47
+ ]
48
+ })
51
49
  }
52
50
  }
53
51
  }
54
- },
55
- };
52
+ }
53
+ }
@@ -1,100 +1,101 @@
1
- const {basename, extname} = require('path')
2
- const findFuzzy = require('../utils/fuzzySearch')
1
+ const cds = require('@sap/cds')
2
+ const { basename, extname } = require('path')
3
+ const findFuzzy = require('../utils/findFuzzy')
3
4
  const SEP = '[,;\t]'
4
5
  const EOL = '\\r?\\n'
5
6
 
6
7
  module.exports = {
7
8
  meta: {
9
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
8
10
  docs: {
9
- description: `CSV files for entities must refer to valid element names.`,
10
- category: "Model Validation",
11
- recommended: true,
12
- version: "2.3.0",
11
+ description: 'CSV files for entities must refer to valid element names.',
12
+ category: 'Model Validation',
13
+ recommended: true
13
14
  },
14
- severity: "warn",
15
- type: "problem",
15
+ severity: 'warn',
16
+ type: 'problem',
16
17
  hasSuggestions: true,
17
18
  messages: {
18
- InvalidColumn: `Invalid column '{{column}}'. Did you mean '{{candidates}}'?`,
19
- ReplaceColumnWith: `Replace '{{column}}' with '{{candidates}}'`
20
- }
19
+ InvalidColumn: "Invalid column '{{column}}'. Did you mean '{{candidates}}'?",
20
+ ReplaceColumnWith: "Replace '{{column}}' with '{{candidates}}'"
21
+ },
22
+ model: 'inferred'
21
23
  },
22
24
  create: function (context) {
25
+ return checkValidHeaders
23
26
 
24
- return { all: check_valid_headers }
27
+ function checkValidHeaders () {
28
+ const filePath = context.getFilename()
29
+ const sourcecode = context.getSourceCode()
30
+ const code = sourcecode.getText()
25
31
 
26
- function check_valid_headers() {
27
- const reports = [];
28
- const {cds, code, filePath, sourcecode} = context;
32
+ let model = context.getModel()
33
+ if (!filePath.endsWith('.csv')) return
34
+ if (!model) return
29
35
 
30
- if (!filePath.endsWith('.csv')) return
31
- if (!cds.model) return
32
- let { env, model } = cds;
33
- model = cds.compile.for.sql(model, { names:env.sql.names, messages: [] } )
36
+ model = cds.compile.for.sql(model, { names: cds.env.sql.names, messages: [] })
34
37
 
35
- const filename = basename(filePath)
36
- const entityName = filename.replace(/-/g,'.').slice(0, -extname(filename).length)
37
- const entity = _entity4(entityName, model);
38
- if (!entity) return
38
+ const filename = basename(filePath)
39
+ const entityName = filename.replace(/-/g, '.').slice(0, -extname(filename).length)
40
+ const entity = _entity4(entityName, model)
41
+ if (!entity) return
39
42
 
40
- const elements = Object.values(entity.elements)
41
- .filter (e => !!e['@cds.persistence.name'])
42
- .map (e => e['@cds.persistence.name'].toUpperCase())
43
+ const elements = Object.values(entity.elements)
44
+ .filter((e) => !!e['@cds.persistence.name'])
45
+ .map((e) => e['@cds.persistence.name'].toUpperCase())
43
46
 
44
- const [ cols ] = cds.parse.csv(code);
45
- const missing = cols.filter (col => !elements.includes(col.toUpperCase()))
46
- missing.forEach(miss => {
47
- const index = _findInCode (miss, code)
48
- const loc = sourcecode.getLocFromIndex(index)
49
- const candidates = findFuzzy(miss, Object.keys(entity.elements).sort())
50
- const suggest = candidates.map(cand => { return {
51
- messageId: 'ReplaceColumnWith',
52
- data: {column: miss, candidates:cand},
53
- fix: (fixer) => fixer.replaceTextRange([index, index+miss.length], cand)
54
- }})
55
- reports.push({
56
- messageId: 'InvalidColumn',
57
- data: {column: miss, candidates},
58
- loc: {start: loc, end: {line: loc.line, column: loc.column+miss.length}},
59
- file: filePath,
60
- suggest
61
- })
62
- })
63
- return reports
47
+ const [cols] = cds.parse.csv(code)
48
+ const missing = cols.filter((col) => !elements.includes(col.toUpperCase()))
49
+ for (const miss of missing) {
50
+ const index = _findInCode(miss, code)
51
+ const loc = sourcecode.getLocFromIndex(index)
52
+ const candidates = findFuzzy(miss, Object.keys(entity.elements).sort())
53
+ const suggest = candidates.map((cand) => {
54
+ return {
55
+ messageId: 'ReplaceColumnWith',
56
+ data: { column: miss, candidates: cand },
57
+ fix: (fixer) => fixer.replaceTextRange([index, index + miss.length], cand)
58
+ }
59
+ })
60
+ context.report({
61
+ messageId: 'InvalidColumn',
62
+ data: { column: miss, candidates },
63
+ loc: { start: loc, end: { line: loc.line, column: loc.column + miss.length } },
64
+ file: filePath,
65
+ suggest
66
+ })
67
+ }
64
68
  }
65
-
66
69
  }
67
-
68
70
  }
69
71
 
70
72
  function _findInCode (miss, code) {
71
73
  // middle
72
- let match = new RegExp(SEP+miss+SEP).exec(code)
73
- if (match) return match.index+1
74
+ let match = new RegExp(SEP + miss + SEP).exec(code)
75
+ if (match) return match.index + 1
74
76
  // end of line
75
- match = new RegExp(SEP+miss+EOL).exec(code)
76
- if (match) return match.index+1
77
+ match = new RegExp(SEP + miss + EOL).exec(code)
78
+ if (match) return match.index + 1
77
79
  // start of doc
78
- match = new RegExp('^'+miss+SEP).exec(code)
79
- if (match) return match.index
80
+ match = new RegExp('^' + miss + SEP).exec(code)
81
+ if (match) return match.index
80
82
  // somewhere (fallback)
81
83
  return code.indexOf(miss)
82
84
  }
83
85
 
84
86
  function _entity4 (name, csn) {
85
- let entity = csn.definitions [name]
87
+ const entity = csn.definitions[name]
86
88
  if (!entity) {
87
- if (/(.+)[._]texts_?/.test (name)) { // 'Books.texts', 'Books.texts_de'
88
- const base = csn.definitions [RegExp.$1]
89
- return base && _entity4 (base.elements.texts.target, csn)
90
- }
91
- else return
89
+ if (/(.+)[._]texts_?/.test(name)) {
90
+ // 'Books.texts', 'Books.texts_de'
91
+ const base = csn.definitions[RegExp.$1]
92
+ return base && _entity4(base.elements.texts.target, csn)
93
+ } else return
92
94
  }
93
95
  // we also support simple views if they have no projection
94
- const p = entity.query && entity.query.SELECT || entity.projection
96
+ const p = (entity.query && entity.query.SELECT) || entity.projection
95
97
  if (p && !p.columns && p.from.ref && p.from.ref.length === 1) {
96
- if (csn.definitions [p.from.ref[0]]) return entity
98
+ if (csn.definitions[p.from.ref[0]]) return entity
97
99
  }
98
- return entity.name ? entity : { name, __proto__:entity }
100
+ return entity.name ? entity : { name, __proto__: entity }
99
101
  }
100
-
@@ -3,26 +3,24 @@ import { Linter, Rule, RuleTester, SourceCode } from "eslint";
3
3
 
4
4
  export interface CDSRuleContext extends Rule.RuleContext {
5
5
  cds: any;
6
- configPath: string;
6
+ rootPath: string;
7
7
  code: string;
8
8
  filePath: string;
9
9
  options: [];
10
10
  id: string;
11
11
  sourcecode: SourceCode;
12
+ getModel: function;
12
13
  report: (CDSRuleReport) => void;
13
14
  err: Error;
14
15
  }
15
16
 
16
- export interface CDSRuleSpec {
17
- meta: CDSRuleMetaData,
17
+ export interface Rule {
18
+ meta: RuleMetaData,
18
19
  create: (context: CDSRuleContext) => void;
19
20
  }
20
21
 
21
22
  export interface CDSRuleMetaData extends Rule.RuleMetaData {
22
- docs: Rule.RuleMetaData['docs'] & {
23
- version: string;
24
- };
25
- severity?: Linter.RuleLevel;
23
+ model?: "parsed" | "inferred" | "none";
26
24
  }
27
25
 
28
26
  export type CDSRuleReport = Rule.ReportDescriptor & {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Simple cache to store model and any cds calls made in the rule creation
3
+ * api to modify the model
4
+ */
5
+ const cache = new Map()
6
+
7
+ module.exports = {
8
+ has (key) {
9
+ return cache.has(key)
10
+ },
11
+ set (key, value) {
12
+ return cache.set(key, [value, Date.now()])
13
+ },
14
+ get (key) {
15
+ return cache.get(key) ? cache.get(key)[0] : undefined
16
+ },
17
+ dump () {
18
+ const dump = {}
19
+ for (const [key, value] of cache.entries()) {
20
+ const timestamp = new Date(value[1])
21
+ dump[key] = { key, value: JSON.stringify(value[0]), timestamp }
22
+ }
23
+ return dump
24
+ },
25
+ remove (key) {
26
+ if (cache.has(key)) {
27
+ cache.delete(key)
28
+ }
29
+ },
30
+ clear () {
31
+ cache.clear()
32
+ }
33
+ }
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ reset: '\x1b[0m', // Default
3
+ bold: '\x1b[1m', // Bold/Bright
4
+ link: '\x1b[4m', // underline
5
+ red: '\x1b[91m', // Bright Foreground Red
6
+ green: '\x1b[32m', // Foreground Green
7
+ blue: '\x1b[34m', // Foreground Blue
8
+ orange: '\x1b[38;2;255;140;0m' // darker orange, works with bright and dark background
9
+ }