1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as NodeDns from "node:dns";
4import * as NodeOS from "node:os";
5import * as NodeSea from "node:sea";
6
7export const Platform = Context.Reference<NodeJS.Platform>("@t3tools/shared/HostProcess/Platform", {
8 defaultValue: () => process.platform,
9});
10
11export const Architecture = Context.Reference<NodeJS.Architecture>(
12 "@t3tools/shared/HostProcess/Architecture",
13 {
14 defaultValue: () => process.arch,
15 },
16);
17
18export const Hostname = Context.Reference<string>("@t3tools/shared/HostProcess/Hostname", {
19 defaultValue: () => NodeOS.hostname(),
20});
21
22export const HomeDirectory = Context.Reference<string>(
23 "@t3tools/shared/HostProcess/HomeDirectory",
24 {
25 defaultValue: () => NodeOS.homedir(),
26 },
27);
28
29export const Environment = Context.Reference<NodeJS.ProcessEnv>(
30 "@t3tools/shared/HostProcess/Environment",
31 {
32 defaultValue: () => process.env,
33 },
34);
35
36export const WorkingDirectory = Context.Reference<string>(
37 "@t3tools/shared/HostProcess/WorkingDirectory",
38 {
39 defaultValue: () => process.cwd(),
40 },
41);
42
43export const ExecutablePath = Context.Reference<string>(
44 "@t3tools/shared/HostProcess/ExecutablePath",
45 {
46 defaultValue: () => process.execPath,
47 },
48);
49
50export const Arguments = Context.Reference<ReadonlyArray<string>>(
51 "@t3tools/shared/HostProcess/Arguments",
52 {
53 defaultValue: () => process.argv,
54 },
55);
56
57/**
58 * The command the shell was given, before Node resolved it to the binary:
59 * `t3` for a PATH lookup, `./t3` or the launcher symlink for an explicit
60 * path. `process.argv[0]` and `execPath` are always the resolved binary.
61 */
62export const InvokedAs = Context.Reference<string>("@t3tools/shared/HostProcess/InvokedAs", {
63 defaultValue: () => process.argv0,
64});
65
66/**
67 * Whether this process is a Node single-executable rather than a script run
68 * by a Node on the machine. Code that needs a sibling file or a Node to run
69 * one branches on this: an executable hosts such things as hidden
70 * subcommands of itself.
71 */
72export const IsExecutable = Context.Reference<boolean>("@t3tools/shared/HostProcess/IsExecutable", {
73 defaultValue: () => NodeSea.isSea(),
74});
75
76/**
77 * Every IP address this machine answers to: the interface addresses, plus
78 * whatever the resolver returns for the machine's own hostname. The latter
79 * matters because a hostname can map to an address no interface carries —
80 * Debian-style hosts put `127.0.1.1` in `/etc/hosts` — and a program that
81 * records "its" address by resolving its hostname (Firefox's profile lock
82 * does) will write that one. "Is this address ours" has to accept both.
83 *
84 * Best effort: a failed lookup just leaves the interface set.
85 */
86export const Addresses = Context.Reference<Effect.Effect<ReadonlySet<string>>>(
87 "@t3tools/shared/HostProcess/Addresses",
88 {
89 defaultValue: () =>
90 Effect.gen(function* () {
91 const interfaces = Object.values(NodeOS.networkInterfaces())
92 .flat()
93 .flatMap((entry) => (entry ? [entry.address] : []));
94 const resolved = yield* Effect.tryPromise(() =>
95 NodeDns.promises.lookup(NodeOS.hostname(), { all: true }),
96 ).pipe(
97 Effect.map((entries) => entries.map((entry) => entry.address)),
98 Effect.orElseSucceed(() => [] as ReadonlyArray<string>),
99 );
100 return new Set([...interfaces, ...resolved]);
101 }),
102 },
103);
104
105/** Undefined on platforms without POSIX uids (Windows). */
106export const UserId = Context.Reference<number | undefined>("@t3tools/shared/HostProcess/UserId", {
107 defaultValue: () => process.getuid?.(),
108});
109
110export const isWindows = Effect.map(Platform, (platform) => platform === "win32");