color-capable 1.0.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 (4) hide show
  1. package/README.md +66 -0
  2. package/index.js +255 -0
  3. package/package.json +63 -0
  4. package/test.js +429 -0
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # color-capable
2
+
3
+ > Detect whether a terminal supports color
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install color-capable
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ const supportsColor = require("color-capable")
15
+
16
+ if (supportsColor.stdout) {
17
+ console.log("Terminal stdout supports color")
18
+ }
19
+
20
+ if (supportsColor.stdout.has256) {
21
+ console.log("Terminal stdout supports 256 colors")
22
+ }
23
+
24
+ if (supportsColor.stderr.has16m) {
25
+ console.log("Terminal stderr supports 16 million colors (truecolor)")
26
+ }
27
+ ```
28
+
29
+ ## API
30
+
31
+ Returns an `object` with a `stdout` and `stderr` property for testing either streams. Each property is an `Object`, or `false` if color is not supported.
32
+
33
+ The `stdout`/`stderr` objects specifies a level of support for color through a `.level` property and a corresponding flag:
34
+
35
+ - `.level = 1` and `.hasBasic = true`: Basic color support (16 colors)
36
+ - `.level = 2` and `.has256 = true`: 256 color support
37
+ - `.level = 3` and `.has16m = true`: Truecolor support (16 million colors)
38
+
39
+ ### Custom instance
40
+
41
+ The package also exposes the named export `createSupportColor` function that takes an arbitrary write stream (for example, `process.stdout`) and an optional options object to (re-)evaluate color support for an arbitrary stream.
42
+
43
+ ```js
44
+ const { createSupportsColor } = require("color-capable")
45
+
46
+ const stdoutSupportsColor = createSupportsColor(process.stdout)
47
+
48
+ if (stdoutSupportsColor) {
49
+ console.log("Terminal stdout supports color")
50
+ }
51
+
52
+ // `stdoutSupportsColor` is the same as `supportsColor.stdout`
53
+ ```
54
+
55
+ The options object supports a single boolean property `sniffFlags`. By default it is `true`, which instructs the detection to sniff `process.argv` for the multitude of `--color` flags (see _Info_ below). If `false`, then `process.argv` is not considered when determining color support.
56
+
57
+ ## Info
58
+
59
+ It obeys the `--color` and `--no-color` CLI flags.
60
+
61
+ For situations where using `--color` is not possible, use the environment variable `FORCE_COLOR=1` (level 1), `FORCE_COLOR=2` (level 2), or `FORCE_COLOR=3` (level 3) to forcefully enable color, or `FORCE_COLOR=0` to forcefully disable. The use of `FORCE_COLOR` overrides all other color support checks.
62
+
63
+ Explicit 256/Truecolor mode can be enabled using the `--color=256` and `--color=16m` flags, respectively.
64
+
65
+ ## Tests
66
+ This has tests. You can do a git clone and run npm test to try them out.
package/index.js ADDED
@@ -0,0 +1,255 @@
1
+ const process = require("process")
2
+ const os = require("os")
3
+ const tty = require("tty")
4
+ const getIntrinsic = require("es-intrinsic-cache")
5
+ const $String = getIntrinsic("String")
6
+ const $Number = getIntrinsic("Number")
7
+ const hasFlag = require("has-argv-flag")
8
+ const forEach = require("for-each")
9
+ const { min, isUndefined, not, and, or, add, multiply } = require("aura3")
10
+ const zero = require("@positive-numbers/zero")
11
+ const one = require("@positive-numbers/one")
12
+ const two = require("@positive-numbers/two")
13
+ const three = require("@positive-numbers/three")
14
+ const six = require("@positive-numbers/six")
15
+ const ten = require("@positive-numbers/ten")
16
+ const thirtyOne = require("@positive-numbers/thirty-one")
17
+ const seventy = require("@positive-numbers/seventy")
18
+ const eightySix = require("@positive-numbers/eighty-six")
19
+ const ninetyNine = require("@positive-numbers/ninety-nine")
20
+ const oneHundred = require("@positive-numbers/one-hundred")
21
+ const fourThousandNineHundred = multiply(seventy, seventy)
22
+ const tenThousand = multiply(oneHundred, oneHundred)
23
+ const tenThousandFiveHundredEightySix = add(multiply(oneHundred, add(ninetyNine, six)), eightySix)
24
+ const fourteenThousandNineHundredThirtyOne = add(tenThousand, add(fourThousandNineHundred, thirtyOne))
25
+ const True = require("true-value")
26
+ const False = require("false-value")
27
+ const stringifiedTrue = $String(True())
28
+ const stringifiedFalse = $String(False())
29
+ const isEqual = require("@10xly/strict-equals")
30
+ const parseInt = require("number.parseint")
31
+ const includes = require("array-includes")
32
+ const isZero = require("is-eq-zero")
33
+ const construct = require("construct-new")
34
+ const { TernaryCompare } = require("important-extremely-useful-classes")
35
+ const emptyString = require("empty-string")
36
+
37
+ const env = process.env
38
+
39
+ const listOfFlagsThatDenyColor = [
40
+ "no-color",
41
+ "no-colors",
42
+ "color=false",
43
+ "color=never"
44
+ ]
45
+ const listOfFlagsThatHappilyAllowColor = [
46
+ "color",
47
+ "colors",
48
+ "color=true",
49
+ "color=always"
50
+ ]
51
+
52
+ let flagForceColor
53
+
54
+ forEach(listOfFlagsThatDenyColor, function (flagThatDeniesColor) {
55
+ if (hasFlag(flagThatDeniesColor)) {
56
+ flagForceColor = zero
57
+ }
58
+ })
59
+ forEach(listOfFlagsThatHappilyAllowColor, function (flagThatHappilyAllowsColor) {
60
+ if (hasFlag(flagThatHappilyAllowsColor)) {
61
+ flagForceColor = one
62
+ }
63
+ })
64
+
65
+ const NAME_OF_THE_ENV_FORCE_COLOR_FLAG = "FORCE_COLOR"
66
+
67
+ function envForceColor() {
68
+ if (not(NAME_OF_THE_ENV_FORCE_COLOR_FLAG in env)) {
69
+ return
70
+ }
71
+
72
+ if (isEqual(env[NAME_OF_THE_ENV_FORCE_COLOR_FLAG], stringifiedTrue)) {
73
+ return one
74
+ }
75
+
76
+ if (isEqual(env[NAME_OF_THE_ENV_FORCE_COLOR_FLAG], stringifiedFalse)) {
77
+ return zero
78
+ }
79
+
80
+ if (isEqual(env[NAME_OF_THE_ENV_FORCE_COLOR_FLAG].length, zero)) {
81
+ return zero
82
+ }
83
+
84
+ const level = min(parseInt(env.FORCE_COLOR, ten), three)
85
+
86
+ if (not(includes([zero, one, two, three], level))) {
87
+ return
88
+ }
89
+
90
+ return level
91
+ }
92
+
93
+ function translateLevel(level) {
94
+ if (isZero(level)) {
95
+ return False()
96
+ }
97
+
98
+ return {
99
+ level,
100
+ hasBasic: True(),
101
+ has256: level >= two,
102
+ has16m: level >= three,
103
+ }
104
+ }
105
+
106
+ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = True() } = {}) {
107
+ const noFlagForceColor = envForceColor()
108
+ if (not(isUndefined(noFlagForceColor))) {
109
+ flagForceColor = noFlagForceColor
110
+ }
111
+
112
+ let forceColor = construct({
113
+ target: TernaryCompare,
114
+ args: [sniffFlags, () => flagForceColor, () => noFlagForceColor]
115
+ })
116
+ forceColor = forceColor.compare()
117
+ forceColor = forceColor()
118
+
119
+ if (isZero(forceColor)) {
120
+ return forceColor
121
+ }
122
+
123
+ if (sniffFlags) {
124
+ if (or(
125
+ hasFlag("color=16m"),
126
+ or(
127
+ hasFlag("color=full"),
128
+ hasFlag("color=truecolor")
129
+ )
130
+ )) {
131
+ return three
132
+ }
133
+
134
+ if (hasFlag("color=256")) {
135
+ return two
136
+ }
137
+ }
138
+
139
+ // Check for Azure DevOps pipelines.
140
+ // Has to be above the `!streamIsTTY` check.
141
+ if (and("TF_BUILD" in env, "AGENT_NAME" in env)) {
142
+ return one
143
+ }
144
+
145
+ if (and(haveStream, and(not(streamIsTTY), isUndefined(forceColor)))) {
146
+ return zero
147
+ }
148
+
149
+ const min = or(forceColor, zero)
150
+
151
+ if (isEqual(env.TERM, "dumb")) {
152
+ return min
153
+ }
154
+
155
+ if (isEqual(process.platform, "win32")) {
156
+ // Windows 10 build 10586 is the first Windows release that supports 256 colors.
157
+ // Windows 10 build 14931 is the first release that supports 16m/TrueColor.
158
+ const osRelease = os.release().split(".")
159
+ if (
160
+ and(
161
+ $Number(osRelease[zero]) >= ten
162
+ , $Number(osRelease[two]) >= tenThousandFiveHundredEightySix)
163
+ ) {
164
+ if ($Number(osRelease[two]) >= fourteenThousandNineHundredThirtyOne) {
165
+ return three
166
+ } else {
167
+ return two
168
+ }
169
+ }
170
+
171
+ return one
172
+ }
173
+
174
+ if ("CI" in env) {
175
+ if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some(key => key in env)) {
176
+ return three
177
+ }
178
+
179
+ if (["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some(sign => sign in env) || env.CI_NAME === "codeship") {
180
+ return one
181
+ }
182
+
183
+ return min
184
+ }
185
+
186
+ if ("TEAMCITY_VERSION" in env) {
187
+ if (/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION)) {
188
+ return one
189
+ } else {
190
+ return zero
191
+ }
192
+ }
193
+
194
+ if (isEqual(env.COLORTERM, "truecolor")) {
195
+ return three
196
+ }
197
+
198
+ if (isEqual(env.TERM, "xterm-kitty")) {
199
+ return two
200
+ }
201
+
202
+ if (isEqual(env.TERM, "xterm-ghostty")) {
203
+ return three
204
+ }
205
+
206
+ if (isEqual(env.TERM, "wezterm")) {
207
+ return three
208
+ }
209
+
210
+ if ("TERM_PROGRAM" in env) {
211
+ const version = parseInt((or(env.TERM_PROGRAM_VERSION, emptyString)).split(".")[zero], ten)
212
+
213
+ if (isEqual(env.TERM_PROGRAM, "iTerm.app")) {
214
+ if (version >= three) {
215
+ return three
216
+ } else {
217
+ return two
218
+ }
219
+ }
220
+ if (isEqual(env.TERM_PROGRAM, "Apple_Terminal")) {
221
+ return two
222
+ }
223
+ }
224
+
225
+ if (/-256(color)?$/i.test(env.TERM)) {
226
+ return two
227
+ }
228
+
229
+ if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
230
+ return one
231
+ }
232
+
233
+ if ("COLORTERM" in env) {
234
+ return one
235
+ }
236
+
237
+ return min
238
+ }
239
+
240
+ function createSupportsColor(stream, options = {}) {
241
+ const level = _supportsColor(stream, {
242
+ streamIsTTY: and(stream, stream.isTTY),
243
+ ...options,
244
+ })
245
+
246
+ return translateLevel(level)
247
+ }
248
+
249
+ const supportsColor = {
250
+ stdout: createSupportsColor({ isTTY: tty.isatty(one) }),
251
+ stderr: createSupportsColor({ isTTY: tty.isatty(two) }),
252
+ createSupportsColor: createSupportsColor
253
+ }
254
+
255
+ module.exports = supportsColor
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "color-capable",
3
+ "version": "1.0.0",
4
+ "description": "Check if the terminal has support for color.",
5
+ "keywords": [
6
+ "supports",
7
+ "color",
8
+ "terminal",
9
+ "colors",
10
+ "support",
11
+ "has",
12
+ "has-color",
13
+ "supports-color",
14
+ "chalk4096",
15
+ "chalk"
16
+ ],
17
+ "homepage": "https://github.com/Chalk4096/color-capable#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/Chalk4096/color-capable/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/Chalk4096/color-capable.git"
24
+ },
25
+ "license": "MIT",
26
+ "author": "10x'ly Made",
27
+ "type": "commonjs",
28
+ "main": "index.js",
29
+ "scripts": {
30
+ "test": "mocha test.js --timeout 10000000"
31
+ },
32
+ "dependencies": {
33
+ "@10xly/strict-equals": "^1.0.0",
34
+ "@positive-numbers/eighty-six": "^3.0.0",
35
+ "@positive-numbers/ninety-nine": "^3.0.0",
36
+ "@positive-numbers/one": "^3.0.0",
37
+ "@positive-numbers/one-hundred": "^3.0.0",
38
+ "@positive-numbers/seventy": "^3.0.0",
39
+ "@positive-numbers/six": "^3.0.0",
40
+ "@positive-numbers/ten": "^3.0.0",
41
+ "@positive-numbers/thirty-one": "^3.0.0",
42
+ "@positive-numbers/three": "^3.0.0",
43
+ "@positive-numbers/two": "^3.0.0",
44
+ "@positive-numbers/zero": "^3.0.0",
45
+ "array-includes": "^3.1.9",
46
+ "aura3": "^1.0.3-enterprise.stable",
47
+ "construct-new": "^2.0.4",
48
+ "empty-string": "^1.1.1",
49
+ "es-intrinsic-cache": "^1.0.1",
50
+ "false-value": "^2.0.6",
51
+ "for-each": "^0.3.5",
52
+ "has-argv-flag": "^1.1.2",
53
+ "important-extremely-useful-classes": "^3.1.0",
54
+ "is-eq-zero": "^1.1.0",
55
+ "number.parseint": "^1.1.0",
56
+ "true-value": "^2.0.5"
57
+ },
58
+ "devDependencies": {
59
+ "chai": "^6.2.2",
60
+ "mocha": "^11.7.5",
61
+ "proxyquire": "^2.1.3"
62
+ }
63
+ }
package/test.js ADDED
@@ -0,0 +1,429 @@
1
+ /* eslint-env mocha */
2
+ "use strict";
3
+
4
+ const { expect } = require("chai");
5
+ const proxyquire = require("proxyquire").noCallThru();
6
+
7
+ function loadSupportsColor({
8
+ env = {},
9
+ platform = "linux",
10
+ osRelease = "5.0.0",
11
+ stdoutIsTTY = true,
12
+ stderrIsTTY = true,
13
+ argvFlags = [],
14
+ } = {}) {
15
+ const processStub = {
16
+ env: { ...env },
17
+ platform,
18
+ };
19
+
20
+ const osStub = {
21
+ release: () => osRelease,
22
+ };
23
+
24
+ const ttyStub = {
25
+ isatty: (fd) => {
26
+ if (fd === 1) return stdoutIsTTY;
27
+ if (fd === 2) return stderrIsTTY;
28
+ return false;
29
+ },
30
+ };
31
+
32
+ const hasArgvFlagStub = (flag) => argvFlags.includes(flag);
33
+
34
+ const supportsColor = proxyquire("./index", {
35
+ process: processStub,
36
+ os: osStub,
37
+ tty: ttyStub,
38
+ "has-argv-flag": hasArgvFlagStub,
39
+ });
40
+
41
+ return { supportsColor, processStub, osStub, ttyStub };
42
+ }
43
+
44
+ describe("supportsColor", function () {
45
+ describe("createSupportsColor basic behavior", function () {
46
+ it("returns false when level is 0 (e.g. FORCE_COLOR=false)", function () {
47
+ const { supportsColor } = loadSupportsColor({
48
+ env: { FORCE_COLOR: "false" },
49
+ stdoutIsTTY: true,
50
+ });
51
+
52
+ const result = supportsColor.createSupportsColor({ isTTY: true });
53
+ expect(result).to.equal(false);
54
+ });
55
+
56
+ it("returns level 1 when FORCE_COLOR=true", function () {
57
+ const { supportsColor } = loadSupportsColor({
58
+ env: { FORCE_COLOR: "true" },
59
+ stdoutIsTTY: true,
60
+ });
61
+
62
+ const result = supportsColor.createSupportsColor({ isTTY: true });
63
+ expect(result).to.deep.include({
64
+ level: 1,
65
+ hasBasic: true,
66
+ has256: false,
67
+ has16m: false,
68
+ });
69
+ });
70
+
71
+ it("returns level 2 when FORCE_COLOR=2", function () {
72
+ const { supportsColor } = loadSupportsColor({
73
+ env: { FORCE_COLOR: "2" },
74
+ stdoutIsTTY: true,
75
+ });
76
+
77
+ const result = supportsColor.createSupportsColor({ isTTY: true });
78
+ expect(result).to.deep.include({
79
+ level: 2,
80
+ hasBasic: true,
81
+ has256: true,
82
+ has16m: false,
83
+ });
84
+ });
85
+
86
+ it("ignores invalid numeric FORCE_COLOR and falls back to TERM heuristics", function () {
87
+ const { supportsColor } = loadSupportsColor({
88
+ env: { FORCE_COLOR: "10", TERM: "xterm-256color" },
89
+ stdoutIsTTY: true,
90
+ });
91
+
92
+ const result = supportsColor.createSupportsColor({ isTTY: true });
93
+ // no valid FORCE_COLOR => TERM-256color gives level 2
94
+ expect(result.level).to.equal(2);
95
+ });
96
+ });
97
+
98
+ describe("CLI flags (has-argv-flag)", function () {
99
+ it("forces flagForceColor = 0 with no-color", function () {
100
+ const { supportsColor } = loadSupportsColor({
101
+ argvFlags: ["no-color"],
102
+ env: { TERM: "xterm-256color" },
103
+ stdoutIsTTY: true,
104
+ });
105
+
106
+ const result = supportsColor.createSupportsColor({ isTTY: true });
107
+ // translateLevel(0) => false
108
+ expect(result).to.equal(false);
109
+ });
110
+
111
+ it("forces flagForceColor = 1 with color", function () {
112
+ const { supportsColor } = loadSupportsColor({
113
+ argvFlags: ["color"],
114
+ env: {},
115
+ stdoutIsTTY: true,
116
+ });
117
+
118
+ const result = supportsColor.createSupportsColor({ isTTY: true });
119
+ expect(result.level).to.equal(1);
120
+ expect(result.hasBasic).to.equal(true);
121
+ });
122
+
123
+ it("returns level 3 with color=16m flag", function () {
124
+ const { supportsColor } = loadSupportsColor({
125
+ argvFlags: ["color=16m"],
126
+ env: {},
127
+ stdoutIsTTY: true,
128
+ });
129
+
130
+ const result = supportsColor.createSupportsColor({ isTTY: true });
131
+ expect(result.level).to.equal(3);
132
+ expect(result.has16m).to.equal(true);
133
+ });
134
+
135
+ it("returns level 2 with color=256 flag", function () {
136
+ const { supportsColor } = loadSupportsColor({
137
+ argvFlags: ["color=256"],
138
+ env: {},
139
+ stdoutIsTTY: true,
140
+ });
141
+
142
+ const result = supportsColor.createSupportsColor({ isTTY: true });
143
+ expect(result.level).to.equal(2);
144
+ expect(result.has256).to.equal(true);
145
+ });
146
+
147
+ it("does not look at flags when sniffFlags = false", function () {
148
+ const { supportsColor } = loadSupportsColor({
149
+ argvFlags: ["color=16m"],
150
+ env: { TERM: "xterm-256color" },
151
+ stdoutIsTTY: true,
152
+ });
153
+
154
+ const result = supportsColor.createSupportsColor(
155
+ { isTTY: true },
156
+ { sniffFlags: false }
157
+ );
158
+
159
+ // TERM=*-256color gives 2; color=16m is ignored
160
+ expect(result.level).to.equal(2);
161
+ });
162
+ });
163
+
164
+ describe("TTY behavior", function () {
165
+ it("returns false when stream is not TTY and no forceColor", function () {
166
+ const { supportsColor } = loadSupportsColor({
167
+ env: {},
168
+ stdoutIsTTY: false,
169
+ });
170
+
171
+ const result = supportsColor.createSupportsColor({ isTTY: false });
172
+ expect(result).to.equal(false);
173
+ });
174
+
175
+ it("allows color when non-TTY but FORCE_COLOR=true", function () {
176
+ const { supportsColor } = loadSupportsColor({
177
+ env: { FORCE_COLOR: "true" },
178
+ stdoutIsTTY: false,
179
+ });
180
+
181
+ const result = supportsColor.createSupportsColor({ isTTY: false });
182
+ expect(result.level).to.equal(1);
183
+ expect(result.hasBasic).to.equal(true);
184
+ });
185
+ });
186
+
187
+ describe("Azure DevOps behavior", function () {
188
+ it("returns level 1 when TF_BUILD and AGENT_NAME exist", function () {
189
+ const { supportsColor } = loadSupportsColor({
190
+ env: { TF_BUILD: "1", AGENT_NAME: "agent" },
191
+ stdoutIsTTY: true,
192
+ });
193
+
194
+ const result = supportsColor.createSupportsColor({ isTTY: true });
195
+ expect(result.level).to.equal(1);
196
+ });
197
+ });
198
+
199
+ describe("Windows behavior", function () {
200
+ it("returns level 1 for non-Windows 10 releases", function () {
201
+ const { supportsColor } = loadSupportsColor({
202
+ platform: "win32",
203
+ osRelease: "6.3.9600",
204
+ stdoutIsTTY: true,
205
+ });
206
+
207
+ const result = supportsColor.createSupportsColor({ isTTY: true });
208
+ expect(result.level).to.equal(1);
209
+ });
210
+
211
+ it("returns level 2 for Windows 10 build 10586+", function () {
212
+ const { supportsColor } = loadSupportsColor({
213
+ platform: "win32",
214
+ osRelease: "10.0.10586",
215
+ stdoutIsTTY: true,
216
+ });
217
+
218
+ const result = supportsColor.createSupportsColor({ isTTY: true });
219
+ expect(result.level).to.equal(2);
220
+ expect(result.has256).to.equal(true);
221
+ });
222
+
223
+ it("returns level 3 for Windows 10 build 14931+", function () {
224
+ const { supportsColor } = loadSupportsColor({
225
+ platform: "win32",
226
+ osRelease: "10.0.14931",
227
+ stdoutIsTTY: true,
228
+ });
229
+
230
+ const result = supportsColor.createSupportsColor({ isTTY: true });
231
+ expect(result.level).to.equal(3);
232
+ expect(result.has16m).to.equal(true);
233
+ });
234
+ });
235
+
236
+ describe("CI behavior", function () {
237
+ it("returns level 3 on GitHub Actions", function () {
238
+ const { supportsColor } = loadSupportsColor({
239
+ env: { CI: "1", GITHUB_ACTIONS: "true" },
240
+ stdoutIsTTY: true,
241
+ });
242
+
243
+ const result = supportsColor.createSupportsColor({ isTTY: true });
244
+ expect(result.level).to.equal(3);
245
+ });
246
+
247
+ it("returns level 1 on Travis", function () {
248
+ const { supportsColor } = loadSupportsColor({
249
+ env: { CI: "1", TRAVIS: "true" },
250
+ stdoutIsTTY: true,
251
+ });
252
+
253
+ const result = supportsColor.createSupportsColor({ isTTY: true });
254
+ expect(result.level).to.equal(1);
255
+ });
256
+
257
+ it("returns min level when CI vendor is unknown", function () {
258
+ const { supportsColor } = loadSupportsColor({
259
+ env: { CI: "1" },
260
+ stdoutIsTTY: true,
261
+ });
262
+
263
+ const result = supportsColor.createSupportsColor({ isTTY: true });
264
+ // min level when nothing else helps is 0 => false
265
+ expect(result === false || result.level === 0).to.equal(true);
266
+ });
267
+ });
268
+
269
+ describe("TEAMCITY behavior", function () {
270
+ it("returns level 1 for TEAMCITY_VERSION >= 9.1", function () {
271
+ const { supportsColor } = loadSupportsColor({
272
+ env: { TEAMCITY_VERSION: "9.1.0" },
273
+ stdoutIsTTY: true,
274
+ });
275
+
276
+ const result = supportsColor.createSupportsColor({ isTTY: true });
277
+ expect(result.level).to.equal(1);
278
+ });
279
+
280
+ it("returns false for TEAMCITY_VERSION < 9.1", function () {
281
+ const { supportsColor } = loadSupportsColor({
282
+ env: { TEAMCITY_VERSION: "8.1.0" },
283
+ stdoutIsTTY: true,
284
+ });
285
+
286
+ const result = supportsColor.createSupportsColor({ isTTY: true });
287
+ expect(result).to.equal(false);
288
+ });
289
+ });
290
+
291
+ describe("TERM / COLORTERM / TERM_PROGRAM heuristics", function () {
292
+ it("returns level 3 when COLORTERM=truecolor", function () {
293
+ const { supportsColor } = loadSupportsColor({
294
+ env: { COLORTERM: "truecolor" },
295
+ stdoutIsTTY: true,
296
+ });
297
+
298
+ const result = supportsColor.createSupportsColor({ isTTY: true });
299
+ expect(result.level).to.equal(3);
300
+ });
301
+
302
+ it("returns level 2 when TERM=xterm-kitty", function () {
303
+ const { supportsColor } = loadSupportsColor({
304
+ env: { TERM: "xterm-kitty" },
305
+ stdoutIsTTY: true,
306
+ });
307
+
308
+ const result = supportsColor.createSupportsColor({ isTTY: true });
309
+ expect(result.level).to.equal(2);
310
+ });
311
+
312
+ it("returns level 3 when TERM=xterm-ghostty", function () {
313
+ const { supportsColor } = loadSupportsColor({
314
+ env: { TERM: "xterm-ghostty" },
315
+ stdoutIsTTY: true,
316
+ });
317
+
318
+ const result = supportsColor.createSupportsColor({ isTTY: true });
319
+ expect(result.level).to.equal(3);
320
+ });
321
+
322
+ it("returns level 3 when TERM=wezterm", function () {
323
+ const { supportsColor } = loadSupportsColor({
324
+ env: { TERM: "wezterm" },
325
+ stdoutIsTTY: true,
326
+ });
327
+
328
+ const result = supportsColor.createSupportsColor({ isTTY: true });
329
+ expect(result.level).to.equal(3);
330
+ });
331
+
332
+ it("returns level 2 when TERM ends with -256color", function () {
333
+ const { supportsColor } = loadSupportsColor({
334
+ env: { TERM: "xterm-256color" },
335
+ stdoutIsTTY: true,
336
+ });
337
+
338
+ const result = supportsColor.createSupportsColor({ isTTY: true });
339
+ expect(result.level).to.equal(2);
340
+ });
341
+
342
+ it("returns level 1 for generic color-capable TERM", function () {
343
+ const { supportsColor } = loadSupportsColor({
344
+ env: { TERM: "xterm" },
345
+ stdoutIsTTY: true,
346
+ });
347
+
348
+ const result = supportsColor.createSupportsColor({ isTTY: true });
349
+ expect(result.level).to.equal(1);
350
+ });
351
+
352
+ it("returns level 1 if COLORTERM is set even if TERM is weird", function () {
353
+ const { supportsColor } = loadSupportsColor({
354
+ env: { COLORTERM: "yes", TERM: "weirdterm" },
355
+ stdoutIsTTY: true,
356
+ });
357
+
358
+ const result = supportsColor.createSupportsColor({ isTTY: true });
359
+ expect(result.level).to.equal(1);
360
+ });
361
+
362
+ it("returns min (0) for TERM=dumb", function () {
363
+ const { supportsColor } = loadSupportsColor({
364
+ env: { TERM: "dumb" },
365
+ stdoutIsTTY: true,
366
+ });
367
+
368
+ const result = supportsColor.createSupportsColor({ isTTY: true });
369
+ expect(result === false || result.level === 0).to.equal(true);
370
+ });
371
+
372
+ it("returns level 3 for iTerm.app >= 3", function () {
373
+ const { supportsColor } = loadSupportsColor({
374
+ env: {
375
+ TERM_PROGRAM: "iTerm.app",
376
+ TERM_PROGRAM_VERSION: "3.1.0",
377
+ },
378
+ stdoutIsTTY: true,
379
+ });
380
+
381
+ const result = supportsColor.createSupportsColor({ isTTY: true });
382
+ expect(result.level).to.equal(3);
383
+ });
384
+
385
+ it("returns level 2 for iTerm.app < 3", function () {
386
+ const { supportsColor } = loadSupportsColor({
387
+ env: {
388
+ TERM_PROGRAM: "iTerm.app",
389
+ TERM_PROGRAM_VERSION: "2.9.0",
390
+ },
391
+ stdoutIsTTY: true,
392
+ });
393
+
394
+ const result = supportsColor.createSupportsColor({ isTTY: true });
395
+ expect(result.level).to.equal(2);
396
+ });
397
+
398
+ it("returns level 2 for Apple_Terminal", function () {
399
+ const { supportsColor } = loadSupportsColor({
400
+ env: {
401
+ TERM_PROGRAM: "Apple_Terminal",
402
+ TERM_PROGRAM_VERSION: "999.0",
403
+ },
404
+ stdoutIsTTY: true,
405
+ });
406
+
407
+ const result = supportsColor.createSupportsColor({ isTTY: true });
408
+ expect(result.level).to.equal(2);
409
+ });
410
+ });
411
+
412
+ describe("top-level stdout and stderr", function () {
413
+ it("uses tty.isatty(1/2) for default stdout/stderr", function () {
414
+ const { supportsColor } = loadSupportsColor({
415
+ env: { TERM: "xterm-256color" },
416
+ stdoutIsTTY: true,
417
+ stderrIsTTY: false,
418
+ });
419
+
420
+ const { stdout, stderr } = supportsColor;
421
+
422
+ // stdout is TTY and TERM-256color => level >= 2
423
+ expect(stdout === false || stdout.level >= 2).to.equal(true);
424
+
425
+ // stderr is not TTY => likely false or 0
426
+ expect(stderr === false || stderr.level === 0).to.equal(true);
427
+ });
428
+ });
429
+ });