@effected/env 0.1.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.
@@ -0,0 +1,19 @@
1
+ import { agentCiKeys } from "./agentCi.js";
2
+ import { colorKeys } from "./colorDepth.js";
3
+ import { terminalKeys } from "./osc8/detect.js";
4
+
5
+ //#region src/internal/keys.ts
6
+ /**
7
+ * Every environment variable any detector reads: the union of the agent/CI, colour and terminal key lists, without
8
+ * duplicates.
9
+ *
10
+ * @internal
11
+ */
12
+ const allKeys = [.../* @__PURE__ */ new Set([
13
+ ...agentCiKeys,
14
+ ...colorKeys,
15
+ ...terminalKeys
16
+ ])];
17
+
18
+ //#endregion
19
+ export { allKeys };
@@ -0,0 +1,148 @@
1
+ import { envIsTruthy } from "./env.js";
2
+ import { compareSemver } from "./semver.js";
3
+ import { NO_CAPS, lookupTerminal } from "./terminals.js";
4
+ import { detectWrapper } from "./wrappers.js";
5
+ import { Option } from "effect";
6
+
7
+ //#region src/internal/osc8/detect.ts
8
+ const explanationFor = (reason, terminal, terminalVersion, wrapper) => {
9
+ switch (reason) {
10
+ case "force-env": return "FORCE_HYPERLINK env var is set";
11
+ case "no-hyperlink-env": return "NO_HYPERLINK env var is set";
12
+ case "no-color-env": return "NO_COLOR env var is set";
13
+ case "not-a-tty": return "stdout is not a TTY";
14
+ case "wrapper-strips": return `inside ${wrapper?.name ?? "wrapper"}; passthrough not verifiable without subprocess`;
15
+ case "terminal-known-supported": return `detected ${terminal}${terminalVersion ? ` ${terminalVersion}` : ""}`;
16
+ case "terminal-known-unsupported": return `detected ${terminal}; terminal does not support OSC8`;
17
+ case "terminal-known-too-old": return `detected ${terminal} ${terminalVersion ?? ""}; below minimum version`;
18
+ case "terminal-unknown": return "no identifying signal matched";
19
+ }
20
+ };
21
+ /**
22
+ * Run the 7-rule precedence ladder for a single stream's TTY state.
23
+ * Shared by stdout and stderr so the two paths cannot drift.
24
+ */
25
+ const evaluateGate = (isTTY, force, noHyperlink, noColor, wrapper, match) => {
26
+ if (force) return {
27
+ supported: true,
28
+ reason: "force-env",
29
+ override: "force-hyperlink"
30
+ };
31
+ if (noHyperlink) return {
32
+ supported: false,
33
+ reason: "no-hyperlink-env",
34
+ override: "no-hyperlink"
35
+ };
36
+ if (noColor) return {
37
+ supported: false,
38
+ reason: "no-color-env",
39
+ override: "no-color"
40
+ };
41
+ if (!isTTY) return {
42
+ supported: false,
43
+ reason: "not-a-tty",
44
+ override: null
45
+ };
46
+ if (wrapper) return {
47
+ supported: false,
48
+ reason: "wrapper-strips",
49
+ override: null
50
+ };
51
+ if (!match) return {
52
+ supported: false,
53
+ reason: "terminal-unknown",
54
+ override: null
55
+ };
56
+ if (!match.entry.supported) return {
57
+ supported: false,
58
+ reason: "terminal-known-unsupported",
59
+ override: null
60
+ };
61
+ if (match.entry.minVersion && (match.identify.version === null || compareSemver(match.identify.version, match.entry.minVersion) < 0)) return {
62
+ supported: false,
63
+ reason: "terminal-known-too-old",
64
+ override: null
65
+ };
66
+ return {
67
+ supported: true,
68
+ reason: "terminal-known-supported",
69
+ override: null
70
+ };
71
+ };
72
+ /**
73
+ * Determine OSC8 support from a process snapshot. Pure: same input ⇒ same
74
+ * output.
75
+ */
76
+ const detect = (snap) => {
77
+ const { env, isStdoutTTY, isStderrTTY } = snap;
78
+ const wrapper = detectWrapper(env);
79
+ const force = envIsTruthy(env.FORCE_HYPERLINK);
80
+ const noHyperlink = envIsTruthy(env.NO_HYPERLINK);
81
+ const noColor = envIsTruthy(env.NO_COLOR, "no-color");
82
+ const match = lookupTerminal(env);
83
+ const terminal = match?.entry.name ?? null;
84
+ const terminalRaw = match?.identify.rawIdentifier ?? null;
85
+ const terminalVersion = match?.identify.version ?? null;
86
+ const stdout = evaluateGate(isStdoutTTY, force, noHyperlink, noColor, wrapper, match);
87
+ const stderr = evaluateGate(isStderrTTY, force, noHyperlink, noColor, wrapper, match);
88
+ const capabilities = match?.entry.capabilities ?? NO_CAPS;
89
+ return {
90
+ supported: stdout.supported,
91
+ supportedForStderr: stderr.supported,
92
+ reason: stdout.reason,
93
+ explanation: explanationFor(stdout.reason, terminal, terminalVersion, wrapper),
94
+ terminal,
95
+ terminalRaw,
96
+ terminalVersion,
97
+ wrapper,
98
+ isStdoutTTY,
99
+ isStderrTTY,
100
+ override: stdout.override,
101
+ capabilities
102
+ };
103
+ };
104
+ /**
105
+ * Project {@link detect} onto {@link Osc8Detection}: `supported` becomes
106
+ * `stdout`, `supportedForStderr` becomes `stderr`, and the terminal name and
107
+ * version become an `Option`.
108
+ */
109
+ const detectOsc8 = (env, isStdoutTTY, isStderrTTY) => {
110
+ const info = detect({
111
+ env,
112
+ isStdoutTTY,
113
+ isStderrTTY
114
+ });
115
+ return {
116
+ stdout: info.supported,
117
+ stderr: info.supportedForStderr,
118
+ terminal: Option.map(Option.fromNullishOr(info.terminal), (name) => ({
119
+ name,
120
+ version: Option.fromNullishOr(info.terminalVersion)
121
+ }))
122
+ };
123
+ };
124
+ /**
125
+ * Every environment variable name the ported detectors read. A caller that
126
+ * builds an {@link Env} from `Config` reads exactly these keys.
127
+ */
128
+ const terminalKeys = [
129
+ "FORCE_HYPERLINK",
130
+ "NO_HYPERLINK",
131
+ "NO_COLOR",
132
+ "TMUX",
133
+ "STY",
134
+ "TERM",
135
+ "TERM_PROGRAM",
136
+ "TERM_PROGRAM_VERSION",
137
+ "VTE_VERSION",
138
+ "KONSOLE_VERSION",
139
+ "WT_SESSION",
140
+ "KITTY_WINDOW_ID",
141
+ "TERMINAL_EMULATOR",
142
+ "TERMINAL_NAME",
143
+ "ConEmuPID",
144
+ "TERMINOLOGY"
145
+ ];
146
+
147
+ //#endregion
148
+ export { detect, detectOsc8, terminalKeys };
@@ -0,0 +1,21 @@
1
+ //#region src/internal/osc8/env.ts
2
+ const DEFAULT_FALSY = /* @__PURE__ */ new Set([
3
+ "0",
4
+ "false",
5
+ "off",
6
+ "no"
7
+ ]);
8
+ /**
9
+ * Evaluate whether an env-var value should be considered truthy.
10
+ *
11
+ * @param value - The env-var value (commonly `process.env.SOMETHING`).
12
+ * @param spec - Which semantics to apply. Default: "default".
13
+ */
14
+ const envIsTruthy = (value, spec = "default") => {
15
+ if (value === void 0 || value === "") return false;
16
+ if (spec === "no-color") return true;
17
+ return !DEFAULT_FALSY.has(value.toLowerCase());
18
+ };
19
+
20
+ //#endregion
21
+ export { envIsTruthy };
@@ -0,0 +1,46 @@
1
+ //#region src/internal/osc8/semver.ts
2
+ const SEMVER_RE = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/;
3
+ /**
4
+ * Compare two semver-like strings (`MAJOR[.MINOR[.PATCH]]`). Prerelease tags
5
+ * (e.g. `-beta`, `-rc.1`) are stripped and ignored. Malformed inputs are
6
+ * treated as equal — this is a permissive helper, not a strict parser.
7
+ *
8
+ * @returns negative if `a` is less than `b`, positive if `a` is greater than `b`, 0 if equal or malformed.
9
+ */
10
+ const compareSemver = (a, b) => {
11
+ const parsed = (s) => {
12
+ const m = SEMVER_RE.exec(s);
13
+ if (!m) return null;
14
+ return [
15
+ Number(m[1] ?? 0),
16
+ Number(m[2] ?? 0),
17
+ Number(m[3] ?? 0)
18
+ ];
19
+ };
20
+ const aa = parsed(a);
21
+ const bb = parsed(b);
22
+ if (!aa || !bb) return 0;
23
+ for (let i = 0; i < 3; i++) {
24
+ const diff = (aa[i] ?? 0) - (bb[i] ?? 0);
25
+ if (diff !== 0) return diff;
26
+ }
27
+ return 0;
28
+ };
29
+ /**
30
+ * Parse a VTE_VERSION env value. VTE encodes versions as
31
+ * `MAJOR * 10000 + MINOR * 100 + PATCH`. So `5202` means `0.52.2`.
32
+ */
33
+ const parseVteVersion = (raw) => {
34
+ if (!raw) return null;
35
+ const n = Number.parseInt(raw, 10);
36
+ if (!Number.isFinite(n) || n < 0) return null;
37
+ return `${Math.floor(n / 1e4)}.${Math.floor(n % 1e4 / 100)}.${n % 100}`;
38
+ };
39
+ /**
40
+ * Parse a KONSOLE_VERSION env value. Konsole uses calendar versioning
41
+ * packed identically to VTE: `YY * 10000 + MM * 100 + PATCH`.
42
+ */
43
+ const parseKonsoleVersion = parseVteVersion;
44
+
45
+ //#endregion
46
+ export { compareSemver, parseKonsoleVersion, parseVteVersion };
@@ -0,0 +1,319 @@
1
+ import { parseKonsoleVersion, parseVteVersion } from "./semver.js";
2
+
3
+ //#region src/internal/osc8/terminals.ts
4
+ const NO_CAPS = {
5
+ params: false,
6
+ fileUrls: false,
7
+ fileUrlsRemoteUnsafe: false
8
+ };
9
+ const TERMINALS = [
10
+ {
11
+ name: "iTerm.app",
12
+ identify: (env) => env.TERM_PROGRAM === "iTerm.app" ? {
13
+ version: env.TERM_PROGRAM_VERSION ?? null,
14
+ rawIdentifier: env.TERM_PROGRAM
15
+ } : null,
16
+ supported: true,
17
+ minVersion: "3.1.0",
18
+ capabilities: {
19
+ params: true,
20
+ fileUrls: true,
21
+ fileUrlsRemoteUnsafe: false
22
+ }
23
+ },
24
+ {
25
+ name: "Apple_Terminal",
26
+ identify: (env) => env.TERM_PROGRAM === "Apple_Terminal" ? {
27
+ version: env.TERM_PROGRAM_VERSION ?? null,
28
+ rawIdentifier: env.TERM_PROGRAM
29
+ } : null,
30
+ supported: false,
31
+ minVersion: null,
32
+ capabilities: NO_CAPS
33
+ },
34
+ {
35
+ name: "VTE",
36
+ identify: (env) => env.VTE_VERSION ? {
37
+ version: parseVteVersion(env.VTE_VERSION),
38
+ rawIdentifier: env.VTE_VERSION
39
+ } : null,
40
+ supported: true,
41
+ minVersion: "0.50.0",
42
+ capabilities: {
43
+ params: false,
44
+ fileUrls: true,
45
+ fileUrlsRemoteUnsafe: false
46
+ }
47
+ },
48
+ {
49
+ name: "Konsole",
50
+ identify: (env) => env.KONSOLE_VERSION ? {
51
+ version: parseKonsoleVersion(env.KONSOLE_VERSION),
52
+ rawIdentifier: env.KONSOLE_VERSION
53
+ } : null,
54
+ supported: true,
55
+ minVersion: "22.4.0",
56
+ capabilities: {
57
+ params: true,
58
+ fileUrls: true,
59
+ fileUrlsRemoteUnsafe: false
60
+ }
61
+ },
62
+ {
63
+ name: "WezTerm",
64
+ identify: (env) => env.TERM_PROGRAM === "WezTerm" ? {
65
+ version: env.TERM_PROGRAM_VERSION ?? null,
66
+ rawIdentifier: env.TERM_PROGRAM
67
+ } : null,
68
+ supported: true,
69
+ minVersion: null,
70
+ capabilities: {
71
+ params: true,
72
+ fileUrls: true,
73
+ fileUrlsRemoteUnsafe: false
74
+ }
75
+ },
76
+ {
77
+ name: "kitty",
78
+ identify: (env) => {
79
+ if (env.TERM === "xterm-kitty") return {
80
+ version: null,
81
+ rawIdentifier: env.TERM
82
+ };
83
+ if (env.KITTY_WINDOW_ID) return {
84
+ version: null,
85
+ rawIdentifier: env.KITTY_WINDOW_ID
86
+ };
87
+ return null;
88
+ },
89
+ supported: true,
90
+ minVersion: null,
91
+ capabilities: {
92
+ params: true,
93
+ fileUrls: true,
94
+ fileUrlsRemoteUnsafe: false
95
+ }
96
+ },
97
+ {
98
+ name: "vscode",
99
+ identify: (env) => env.TERM_PROGRAM === "vscode" ? {
100
+ version: env.TERM_PROGRAM_VERSION ?? null,
101
+ rawIdentifier: env.TERM_PROGRAM
102
+ } : null,
103
+ supported: true,
104
+ minVersion: "1.71.0",
105
+ capabilities: {
106
+ params: false,
107
+ fileUrls: true,
108
+ fileUrlsRemoteUnsafe: true
109
+ }
110
+ },
111
+ {
112
+ name: "Hyper",
113
+ identify: (env) => env.TERM_PROGRAM === "Hyper" ? {
114
+ version: env.TERM_PROGRAM_VERSION ?? null,
115
+ rawIdentifier: env.TERM_PROGRAM
116
+ } : null,
117
+ supported: true,
118
+ minVersion: "3.0.0",
119
+ capabilities: {
120
+ params: false,
121
+ fileUrls: true,
122
+ fileUrlsRemoteUnsafe: false
123
+ }
124
+ },
125
+ {
126
+ name: "mintty",
127
+ identify: (env) => env.TERM_PROGRAM === "mintty" ? {
128
+ version: env.TERM_PROGRAM_VERSION ?? null,
129
+ rawIdentifier: env.TERM_PROGRAM
130
+ } : null,
131
+ supported: true,
132
+ minVersion: "3.6.0",
133
+ capabilities: {
134
+ params: true,
135
+ fileUrls: true,
136
+ fileUrlsRemoteUnsafe: false
137
+ }
138
+ },
139
+ {
140
+ name: "WindowsTerminal",
141
+ identify: (env) => env.WT_SESSION ? {
142
+ version: null,
143
+ rawIdentifier: env.WT_SESSION
144
+ } : null,
145
+ supported: true,
146
+ minVersion: null,
147
+ capabilities: {
148
+ params: false,
149
+ fileUrls: true,
150
+ fileUrlsRemoteUnsafe: false
151
+ }
152
+ },
153
+ {
154
+ name: "Alacritty",
155
+ identify: (env) => env.TERM === "alacritty" ? {
156
+ version: null,
157
+ rawIdentifier: env.TERM
158
+ } : null,
159
+ supported: true,
160
+ minVersion: null,
161
+ capabilities: {
162
+ params: false,
163
+ fileUrls: true,
164
+ fileUrlsRemoteUnsafe: false
165
+ }
166
+ },
167
+ {
168
+ name: "Ghostty",
169
+ identify: (env) => env.TERM_PROGRAM === "ghostty" ? {
170
+ version: env.TERM_PROGRAM_VERSION ?? null,
171
+ rawIdentifier: env.TERM_PROGRAM
172
+ } : null,
173
+ supported: true,
174
+ minVersion: null,
175
+ capabilities: {
176
+ params: true,
177
+ fileUrls: true,
178
+ fileUrlsRemoteUnsafe: false
179
+ }
180
+ },
181
+ {
182
+ name: "JediTerm",
183
+ identify: (env) => env.TERMINAL_EMULATOR === "JetBrains-JediTerm" ? {
184
+ version: null,
185
+ rawIdentifier: env.TERMINAL_EMULATOR
186
+ } : null,
187
+ supported: true,
188
+ minVersion: null,
189
+ capabilities: {
190
+ params: false,
191
+ fileUrls: true,
192
+ fileUrlsRemoteUnsafe: false
193
+ }
194
+ },
195
+ {
196
+ name: "Tabby",
197
+ identify: (env) => env.TERM_PROGRAM === "Tabby" ? {
198
+ version: env.TERM_PROGRAM_VERSION ?? null,
199
+ rawIdentifier: env.TERM_PROGRAM
200
+ } : null,
201
+ supported: true,
202
+ minVersion: null,
203
+ capabilities: {
204
+ params: true,
205
+ fileUrls: true,
206
+ fileUrlsRemoteUnsafe: false
207
+ }
208
+ },
209
+ {
210
+ name: "Foot",
211
+ identify: (env) => env.TERM === "foot" || env.TERM === "foot-extra" ? {
212
+ version: null,
213
+ rawIdentifier: env.TERM
214
+ } : null,
215
+ supported: true,
216
+ minVersion: null,
217
+ capabilities: {
218
+ params: true,
219
+ fileUrls: true,
220
+ fileUrlsRemoteUnsafe: false
221
+ }
222
+ },
223
+ {
224
+ name: "Rio",
225
+ identify: (env) => env.TERM_PROGRAM === "rio" ? {
226
+ version: env.TERM_PROGRAM_VERSION ?? null,
227
+ rawIdentifier: env.TERM_PROGRAM
228
+ } : null,
229
+ supported: true,
230
+ minVersion: null,
231
+ capabilities: {
232
+ params: false,
233
+ fileUrls: true,
234
+ fileUrlsRemoteUnsafe: false
235
+ }
236
+ },
237
+ {
238
+ name: "Contour",
239
+ identify: (env) => env.TERMINAL_NAME === "contour" ? {
240
+ version: null,
241
+ rawIdentifier: env.TERMINAL_NAME
242
+ } : null,
243
+ supported: true,
244
+ minVersion: null,
245
+ capabilities: {
246
+ params: true,
247
+ fileUrls: true,
248
+ fileUrlsRemoteUnsafe: false
249
+ }
250
+ },
251
+ {
252
+ name: "ConEmu",
253
+ identify: (env) => env.ConEmuPID ? {
254
+ version: null,
255
+ rawIdentifier: env.ConEmuPID
256
+ } : null,
257
+ supported: true,
258
+ minVersion: null,
259
+ capabilities: {
260
+ params: false,
261
+ fileUrls: true,
262
+ fileUrlsRemoteUnsafe: false
263
+ }
264
+ },
265
+ {
266
+ name: "WarpTerminal",
267
+ identify: (env) => env.TERM_PROGRAM === "WarpTerminal" ? {
268
+ version: env.TERM_PROGRAM_VERSION ?? null,
269
+ rawIdentifier: env.TERM_PROGRAM
270
+ } : null,
271
+ supported: true,
272
+ minVersion: null,
273
+ capabilities: {
274
+ params: false,
275
+ fileUrls: true,
276
+ fileUrlsRemoteUnsafe: false
277
+ }
278
+ },
279
+ {
280
+ name: "WaveTerminal",
281
+ identify: (env) => env.TERM_PROGRAM === "WaveTerminal" ? {
282
+ version: env.TERM_PROGRAM_VERSION ?? null,
283
+ rawIdentifier: env.TERM_PROGRAM
284
+ } : null,
285
+ supported: true,
286
+ minVersion: null,
287
+ capabilities: {
288
+ params: false,
289
+ fileUrls: true,
290
+ fileUrlsRemoteUnsafe: false
291
+ }
292
+ },
293
+ {
294
+ name: "Terminology",
295
+ identify: (env) => env.TERMINOLOGY === "1" ? {
296
+ version: null,
297
+ rawIdentifier: env.TERMINOLOGY
298
+ } : null,
299
+ supported: false,
300
+ minVersion: null,
301
+ capabilities: NO_CAPS
302
+ }
303
+ ];
304
+ /**
305
+ * Find the first allowlist entry whose `identify()` returns non-null.
306
+ */
307
+ const lookupTerminal = (env) => {
308
+ for (const entry of TERMINALS) {
309
+ const id = entry.identify(env);
310
+ if (id) return {
311
+ entry,
312
+ identify: id
313
+ };
314
+ }
315
+ return null;
316
+ };
317
+
318
+ //#endregion
319
+ export { NO_CAPS, lookupTerminal };
@@ -0,0 +1,23 @@
1
+ //#region src/internal/osc8/wrappers.ts
2
+ /**
3
+ * Detect a multiplexer wrapper from the env. Conservative — `passesThrough`
4
+ * is always `false` because we cannot verify version or config without
5
+ * spawning a subprocess, which this package never does.
6
+ *
7
+ * Users who know their tmux ≥ 3.4 has `set -g allow-passthrough on` can
8
+ * opt back in via FORCE_HYPERLINK=1.
9
+ */
10
+ const detectWrapper = (env) => {
11
+ if (env.TMUX) return {
12
+ name: "tmux",
13
+ passesThrough: false
14
+ };
15
+ if (env.STY) return {
16
+ name: "screen",
17
+ passesThrough: false
18
+ };
19
+ return null;
20
+ };
21
+
22
+ //#endregion
23
+ export { detectWrapper };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@effected/env",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Effect-native environment detection: agent, CI, terminal colour level, hyperlinks, columns and audience — read once through Config, swapped in tests with layerTest",
6
+ "keywords": [
7
+ "effect",
8
+ "env",
9
+ "ci",
10
+ "agent",
11
+ "color",
12
+ "tty",
13
+ "osc8",
14
+ "hyperlinks",
15
+ "effected"
16
+ ],
17
+ "homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/env#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/spencerbeggs/effected/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/spencerbeggs/effected.git",
24
+ "directory": "packages/env"
25
+ },
26
+ "license": "MIT",
27
+ "author": {
28
+ "name": "C. Spencer Beggs",
29
+ "email": "spencer@beggs.codes",
30
+ "url": "https://spencerbeg.gs"
31
+ },
32
+ "sideEffects": false,
33
+ "type": "module",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./index.d.ts",
37
+ "import": "./index.js",
38
+ "default": "./index.js"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "peerDependencies": {
43
+ "effect": "^4.0.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=24.11.0"
47
+ }
48
+ }
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.59.1"
9
+ }
10
+ ]
11
+ }