functype-os 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.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # functype-os
2
+
3
+ Functional OS utilities for Node.js using [functype](https://github.com/jordanburke/functype) data structures. Wraps environment variables, path expansion, filesystem operations, platform detection, and config file resolution with `Option`, `Either`, `Task`, and `List`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add functype-os functype
9
+ ```
10
+
11
+ ## Modules
12
+
13
+ ### Env
14
+
15
+ Wraps `process.env` with `Option` and `Either`.
16
+
17
+ ```typescript
18
+ import { Env } from "functype-os"
19
+
20
+ Env("HOME") // Option<string>
21
+ Env.get("HOME") // Option<string>
22
+ Env.getRequired("DATABASE_URL") // Either<EnvError, string>
23
+ Env.getOrDefault("PORT", "3000") // string
24
+ Env.has("CI") // boolean
25
+ Env.entries() // List<readonly [string, string]>
26
+ ```
27
+
28
+ ### Path
29
+
30
+ Pure path expansion — tilde, environment variables, and resolution.
31
+
32
+ ```typescript
33
+ import { expandTilde, expandVars, expandPath, Path } from "functype-os"
34
+
35
+ expandTilde("~/Documents") // "/home/user/Documents"
36
+ expandVars("$HOME/.config") // Either<PathError, string>
37
+ expandPath("~/$APP_DIR/config.toml") // Either<PathError, string> (absolute)
38
+
39
+ Path.join("a", "b") // "a/b"
40
+ Path.resolve("relative") // "/cwd/relative"
41
+ Path.dirname("/a/b/c.txt") // "/a/b"
42
+ Path.basename("/a/b/c.txt") // "c.txt"
43
+ Path.extname("file.ts") // ".ts"
44
+ Path.isAbsolute("/abs") // true
45
+ ```
46
+
47
+ ### Fs
48
+
49
+ Async filesystem operations returning `TaskResult`.
50
+
51
+ ```typescript
52
+ import { Fs } from "functype-os"
53
+
54
+ await Fs.exists("/path/to/file") // TaskResult<boolean>
55
+ await Fs.readFile("/path/to/file") // TaskResult<string>
56
+ await Fs.readFileOpt("/path/to/file") // TaskResult<Option<string>> (None on ENOENT)
57
+ await Fs.readdir("/path/to/dir") // TaskResult<List<string>>
58
+ ```
59
+
60
+ ### Platform
61
+
62
+ OS and container/runtime detection with lazy-cached sync checks.
63
+
64
+ ```typescript
65
+ import { Platform } from "functype-os"
66
+
67
+ Platform.os() // "darwin" | "linux" | "win32" | string
68
+ Platform.arch() // "arm64" | "x64" | ...
69
+ Platform.homeDir() // "/home/user"
70
+ Platform.isWindows() // boolean
71
+ Platform.isMac() // boolean
72
+ Platform.isLinux() // boolean
73
+ Platform.userInfo() // Option<UserInfo>
74
+
75
+ // Container/runtime detection (lazy-cached)
76
+ Platform.isDocker() // boolean
77
+ Platform.isKubernetes() // boolean
78
+ Platform.isWSL() // boolean
79
+ Platform.isCI() // boolean
80
+ Platform.isContainer() // isDocker() || isKubernetes()
81
+ ```
82
+
83
+ ### ConfigResolver
84
+
85
+ Find the first existing config file from a list of candidates with path expansion.
86
+
87
+ ```typescript
88
+ import { ConfigResolver } from "functype-os"
89
+
90
+ const config = await ConfigResolver.resolve({
91
+ candidates: ["./app.toml", "~/.config/app/config.toml", "$APPDATA/app/config.toml"],
92
+ })
93
+ // TaskResult<Option<string>> — Ok(Some("/home/user/.config/app/config.toml"))
94
+
95
+ const required = await ConfigResolver.resolveRequired({
96
+ candidates: ["./app.toml", "~/.config/app/config.toml"],
97
+ })
98
+ // TaskResult<string> — Err(ConfigError) if none found
99
+
100
+ const all = await ConfigResolver.resolveAll({
101
+ candidates: ["./a.toml", "./b.toml", "./c.toml"],
102
+ })
103
+ // TaskResult<List<string>> — all existing paths
104
+ ```
105
+
106
+ ### Errors
107
+
108
+ Discriminated union error types for exhaustive matching.
109
+
110
+ ```typescript
111
+ import type { OsError } from "functype-os"
112
+ import { EnvError, PathError, FsError, ConfigError } from "functype-os"
113
+
114
+ const handleError = (error: OsError) => {
115
+ switch (error._tag) {
116
+ case "EnvError":
117
+ return `Missing env var: ${error.variable}`
118
+ case "PathError":
119
+ return `Path issue: ${error.path} (${error.reason})`
120
+ case "FsError":
121
+ return `FS error: ${error.operation} on ${error.path}`
122
+ case "ConfigError":
123
+ return `No config found in: ${error.candidates.join(", ")}`
124
+ }
125
+ }
126
+ ```
127
+
128
+ ## Requirements
129
+
130
+ - Node.js >= 18
131
+ - `functype` >= 0.49.0 (peer dependency)
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ pnpm install
137
+ pnpm dev # build with watch
138
+ pnpm test # run tests
139
+ pnpm validate # format + lint + typecheck + test + build
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,17 @@
1
+ import { List, Option, TaskResult } from "functype";
2
+
3
+ //#region src/config/ConfigResolver.d.ts
4
+ declare const ConfigResolver: {
5
+ resolve: (options: {
6
+ candidates: readonly string[];
7
+ }) => TaskResult<Option<string>>;
8
+ resolveRequired: (options: {
9
+ candidates: readonly string[];
10
+ }) => TaskResult<string>;
11
+ resolveAll: (options: {
12
+ candidates: readonly string[];
13
+ }) => TaskResult<List<string>>;
14
+ };
15
+ //#endregion
16
+ export { ConfigResolver };
17
+ //# sourceMappingURL=ConfigResolver.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{ConfigError as e}from"../errors/errors.js";import{Fs as t}from"../fs/Fs.js";import{expandPath as n}from"../path/PathExpander.js";import{Err as r,List as i,Ok as a,Option as o}from"functype";const s=e=>{let t=n(e);if(!t.isLeft())return t.orThrow()},c={resolve:async e=>{for(let n of e.candidates){let e=s(n);if(e===void 0)continue;let r=await t.exists(e);if(!r.isErr()&&r.value)return a(o(e))}return a(o(void 0))},resolveRequired:async t=>{let n=await c.resolve(t);if(n.isErr())return r(n.error);let i=n.orThrow();return i.isSome()?a(i.orThrow()):r(e(t.candidates))},resolveAll:async e=>{let n=[];for(let r of e.candidates){let e=s(r);if(e===void 0)continue;let i=await t.exists(e);i.isErr()||i.value&&n.push(e)}return a(i(n))}};export{c as ConfigResolver};
2
+ //# sourceMappingURL=ConfigResolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfigResolver.js","names":[],"sources":["../../src/config/ConfigResolver.ts"],"sourcesContent":["import type { TaskResult } from \"functype\"\nimport { Err, List, Ok, Option } from \"functype\"\n\nimport { ConfigError } from \"../errors/errors\"\nimport { Fs } from \"../fs/Fs\"\nimport { expandPath } from \"../path/PathExpander\"\n\nconst tryExpandPath = (candidate: string): string | undefined => {\n const result = expandPath(candidate)\n if (result.isLeft()) return undefined\n return result.orThrow()\n}\n\nexport const ConfigResolver = {\n resolve: async (options: { candidates: readonly string[] }): TaskResult<Option<string>> => {\n for (const candidate of options.candidates) {\n const expanded = tryExpandPath(candidate)\n if (expanded === undefined) continue\n\n const existsResult = await Fs.exists(expanded)\n if (existsResult.isErr()) continue\n if (existsResult.value) {\n return Ok(Option(expanded))\n }\n }\n return Ok(Option<string>(undefined))\n },\n\n resolveRequired: async (options: { candidates: readonly string[] }): TaskResult<string> => {\n const result = await ConfigResolver.resolve(options)\n if (result.isErr()) return Err(result.error)\n\n const optValue = result.orThrow()\n if (optValue.isSome()) {\n return Ok(optValue.orThrow())\n }\n\n return Err(ConfigError(options.candidates))\n },\n\n resolveAll: async (options: { candidates: readonly string[] }): TaskResult<List<string>> => {\n const found: string[] = []\n\n for (const candidate of options.candidates) {\n const expanded = tryExpandPath(candidate)\n if (expanded === undefined) continue\n\n const existsResult = await Fs.exists(expanded)\n if (existsResult.isErr()) continue\n if (existsResult.value) {\n found.push(expanded)\n }\n }\n\n return Ok(List(found))\n },\n}\n"],"mappings":"qMAOA,MAAM,EAAiB,GAA0C,CAC/D,IAAM,EAAS,EAAW,EAAU,CAChC,MAAO,QAAQ,CACnB,OAAO,EAAO,SAAS,EAGZ,EAAiB,CAC5B,QAAS,KAAO,IAA2E,CACzF,IAAK,IAAM,KAAa,EAAQ,WAAY,CAC1C,IAAM,EAAW,EAAc,EAAU,CACzC,GAAI,IAAa,IAAA,GAAW,SAE5B,IAAM,EAAe,MAAM,EAAG,OAAO,EAAS,CAC1C,MAAa,OAAO,EACpB,EAAa,MACf,OAAO,EAAG,EAAO,EAAS,CAAC,CAG/B,OAAO,EAAG,EAAe,IAAA,GAAU,CAAC,EAGtC,gBAAiB,KAAO,IAAmE,CACzF,IAAM,EAAS,MAAM,EAAe,QAAQ,EAAQ,CACpD,GAAI,EAAO,OAAO,CAAE,OAAO,EAAI,EAAO,MAAM,CAE5C,IAAM,EAAW,EAAO,SAAS,CAKjC,OAJI,EAAS,QAAQ,CACZ,EAAG,EAAS,SAAS,CAAC,CAGxB,EAAI,EAAY,EAAQ,WAAW,CAAC,EAG7C,WAAY,KAAO,IAAyE,CAC1F,IAAM,EAAkB,EAAE,CAE1B,IAAK,IAAM,KAAa,EAAQ,WAAY,CAC1C,IAAM,EAAW,EAAc,EAAU,CACzC,GAAI,IAAa,IAAA,GAAW,SAE5B,IAAM,EAAe,MAAM,EAAG,OAAO,EAAS,CAC1C,EAAa,OAAO,EACpB,EAAa,OACf,EAAM,KAAK,EAAS,CAIxB,OAAO,EAAG,EAAK,EAAM,CAAC,EAEzB"}
@@ -0,0 +1,2 @@
1
+ import { ConfigResolver } from "./ConfigResolver.js";
2
+ export { ConfigResolver };
@@ -0,0 +1 @@
1
+ import{ConfigResolver as e}from"./ConfigResolver.js";export{e as ConfigResolver};
@@ -0,0 +1,16 @@
1
+ import { EnvError } from "../errors/errors.js";
2
+ import { Either, List, Option } from "functype";
3
+
4
+ //#region src/env/Env.d.ts
5
+ declare const EnvConstructor: (name: string) => Option<string>;
6
+ declare const EnvCompanion: {
7
+ get: (name: string) => Option<string>;
8
+ getRequired: (name: string) => Either<EnvError, string>;
9
+ getOrDefault: (name: string, defaultValue: string) => string;
10
+ has: (name: string) => boolean;
11
+ entries: () => List<readonly [string, string]>;
12
+ };
13
+ declare const Env: typeof EnvConstructor & typeof EnvCompanion;
14
+ //#endregion
15
+ export { Env };
16
+ //# sourceMappingURL=Env.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{EnvError as e}from"../errors/errors.js";import{Left as t,List as n,Option as r,Right as i}from"functype";const a=Object.assign(e=>r(process.env[e]),{get:e=>r(process.env[e]),getRequired:n=>{let r=process.env[n];return r===void 0?t(e(n)):i(r)},getOrDefault:(e,t)=>process.env[e]??t,has:e=>process.env[e]!==void 0,entries:()=>n(Object.entries(process.env).filter(e=>e[1]!==void 0).map(([e,t])=>[e,t]))});export{a as Env};
2
+ //# sourceMappingURL=Env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Env.js","names":[],"sources":["../../src/env/Env.ts"],"sourcesContent":["import type { Either } from \"functype\"\nimport { Left, List, Option, Right } from \"functype\"\n\nimport { EnvError } from \"../errors/errors\"\n\nconst EnvConstructor = (name: string): Option<string> => Option(process.env[name])\n\nconst EnvCompanion = {\n get: (name: string): Option<string> => Option(process.env[name]),\n\n getRequired: (name: string): Either<EnvError, string> => {\n const value = process.env[name]\n return value !== undefined ? Right(value) : Left(EnvError(name))\n },\n\n getOrDefault: (name: string, defaultValue: string): string => process.env[name] ?? defaultValue,\n\n has: (name: string): boolean => process.env[name] !== undefined,\n\n entries: (): List<readonly [string, string]> => {\n const pairs: Array<readonly [string, string]> = Object.entries(process.env)\n .filter((entry): entry is [string, string] => entry[1] !== undefined)\n .map(([k, v]) => [k, v] as const)\n return List(pairs)\n },\n}\n\nexport const Env: typeof EnvConstructor & typeof EnvCompanion = Object.assign(EnvConstructor, EnvCompanion)\n"],"mappings":"gHA2BA,MAAa,EAAmD,OAAO,OAtB/C,GAAiC,EAAO,QAAQ,IAAI,GAAM,CAE7D,CACnB,IAAM,GAAiC,EAAO,QAAQ,IAAI,GAAM,CAEhE,YAAc,GAA2C,CACvD,IAAM,EAAQ,QAAQ,IAAI,GAC1B,OAAO,IAAU,IAAA,GAA2B,EAAK,EAAS,EAAK,CAAC,CAAnC,EAAM,EAAM,EAG3C,cAAe,EAAc,IAAiC,QAAQ,IAAI,IAAS,EAEnF,IAAM,GAA0B,QAAQ,IAAI,KAAU,IAAA,GAEtD,YAIS,EAHyC,OAAO,QAAQ,QAAQ,IAAI,CACxE,OAAQ,GAAqC,EAAM,KAAO,IAAA,GAAU,CACpE,KAAK,CAAC,EAAG,KAAO,CAAC,EAAG,EAAE,CAAU,CACjB,CAErB,CAE0G"}
@@ -0,0 +1,2 @@
1
+ import { Env } from "./Env.js";
2
+ export { Env };
@@ -0,0 +1 @@
1
+ import{Env as e}from"./Env.js";export{e as Env};
@@ -0,0 +1,32 @@
1
+ //#region src/errors/errors.d.ts
2
+ type EnvError = {
3
+ readonly _tag: "EnvError";
4
+ readonly variable: string;
5
+ readonly message: string;
6
+ };
7
+ type PathError = {
8
+ readonly _tag: "PathError";
9
+ readonly path: string;
10
+ readonly reason: "unresolved_variable" | "invalid_path";
11
+ readonly message: string;
12
+ };
13
+ type FsError = {
14
+ readonly _tag: "FsError";
15
+ readonly path: string;
16
+ readonly operation: string;
17
+ readonly cause: Error;
18
+ readonly message: string;
19
+ };
20
+ type ConfigError = {
21
+ readonly _tag: "ConfigError";
22
+ readonly candidates: readonly string[];
23
+ readonly message: string;
24
+ };
25
+ type OsError = EnvError | PathError | FsError | ConfigError;
26
+ declare const EnvError: (variable: string, message?: string) => EnvError;
27
+ declare const PathError: (path: string, reason: PathError["reason"], message?: string) => PathError;
28
+ declare const FsError: (path: string, operation: string, cause: Error) => FsError;
29
+ declare const ConfigError: (candidates: readonly string[], message?: string) => ConfigError;
30
+ //#endregion
31
+ export { ConfigError, EnvError, FsError, OsError, PathError };
32
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1,2 @@
1
+ const e=(e,t)=>({_tag:`EnvError`,variable:e,message:t??`Environment variable '${e}' is not set`}),t=(e,t,n)=>({_tag:`PathError`,path:e,reason:t,message:n??(t===`unresolved_variable`?`Unresolved variable in path: ${e}`:`Invalid path: ${e}`)}),n=(e,t,n)=>({_tag:`FsError`,path:e,operation:t,cause:n,message:`${t} failed for '${e}': ${n.message}`}),r=(e,t)=>({_tag:`ConfigError`,candidates:e,message:t??`No config file found among candidates: ${e.join(`, `)}`});export{r as ConfigError,e as EnvError,n as FsError,t as PathError};
2
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/errors/errors.ts"],"sourcesContent":["export type EnvError = {\n readonly _tag: \"EnvError\"\n readonly variable: string\n readonly message: string\n}\n\nexport type PathError = {\n readonly _tag: \"PathError\"\n readonly path: string\n readonly reason: \"unresolved_variable\" | \"invalid_path\"\n readonly message: string\n}\n\nexport type FsError = {\n readonly _tag: \"FsError\"\n readonly path: string\n readonly operation: string\n readonly cause: Error\n readonly message: string\n}\n\nexport type ConfigError = {\n readonly _tag: \"ConfigError\"\n readonly candidates: readonly string[]\n readonly message: string\n}\n\nexport type OsError = EnvError | PathError | FsError | ConfigError\n\nexport const EnvError = (variable: string, message?: string): EnvError => ({\n _tag: \"EnvError\",\n variable,\n message: message ?? `Environment variable '${variable}' is not set`,\n})\n\nexport const PathError = (path: string, reason: PathError[\"reason\"], message?: string): PathError => ({\n _tag: \"PathError\",\n path,\n reason,\n message:\n message ?? (reason === \"unresolved_variable\" ? `Unresolved variable in path: ${path}` : `Invalid path: ${path}`),\n})\n\nexport const FsError = (path: string, operation: string, cause: Error): FsError => ({\n _tag: \"FsError\",\n path,\n operation,\n cause,\n message: `${operation} failed for '${path}': ${cause.message}`,\n})\n\nexport const ConfigError = (candidates: readonly string[], message?: string): ConfigError => ({\n _tag: \"ConfigError\",\n candidates,\n message: message ?? `No config file found among candidates: ${candidates.join(\", \")}`,\n})\n"],"mappings":"AA6BA,MAAa,GAAY,EAAkB,KAAgC,CACzE,KAAM,WACN,WACA,QAAS,GAAW,yBAAyB,EAAS,cACvD,EAEY,GAAa,EAAc,EAA6B,KAAiC,CACpG,KAAM,YACN,OACA,SACA,QACE,IAAY,IAAW,sBAAwB,gCAAgC,IAAS,iBAAiB,KAC5G,EAEY,GAAW,EAAc,EAAmB,KAA2B,CAClF,KAAM,UACN,OACA,YACA,QACA,QAAS,GAAG,EAAU,eAAe,EAAK,KAAK,EAAM,UACtD,EAEY,GAAe,EAA+B,KAAmC,CAC5F,KAAM,cACN,aACA,QAAS,GAAW,0CAA0C,EAAW,KAAK,KAAK,GACpF"}
@@ -0,0 +1,2 @@
1
+ import { ConfigError, EnvError, FsError, OsError, PathError } from "./errors.js";
2
+ export { ConfigError, EnvError, FsError, type OsError, PathError };
@@ -0,0 +1 @@
1
+ import{ConfigError as e,EnvError as t,FsError as n,PathError as r}from"./errors.js";export{e as ConfigError,t as EnvError,n as FsError,r as PathError};
@@ -0,0 +1,12 @@
1
+ import { List, Option, TaskResult } from "functype";
2
+
3
+ //#region src/fs/Fs.d.ts
4
+ declare const Fs: {
5
+ exists: (path: string) => TaskResult<boolean>;
6
+ readFile: (path: string, encoding?: BufferEncoding) => TaskResult<string>;
7
+ readFileOpt: (path: string, encoding?: BufferEncoding) => TaskResult<Option<string>>;
8
+ readdir: (path: string) => TaskResult<List<string>>;
9
+ };
10
+ //#endregion
11
+ export { Fs };
12
+ //# sourceMappingURL=Fs.d.ts.map
package/dist/fs/Fs.js ADDED
@@ -0,0 +1,2 @@
1
+ import{FsError as e}from"../errors/errors.js";import{Err as t,List as n,Ok as r,Option as i}from"functype";import*as a from"node:fs/promises";const o={exists:async e=>{try{return await a.access(e),r(!0)}catch{return r(!1)}},readFile:async(n,i=`utf8`)=>{try{return r(await a.readFile(n,{encoding:i}))}catch(r){return t(e(n,`readFile`,r instanceof Error?r:Error(String(r))))}},readFileOpt:async(n,o=`utf8`)=>{try{return r(i(await a.readFile(n,{encoding:o})))}catch(a){return a instanceof Error&&`code`in a&&a.code===`ENOENT`?r(i(void 0)):t(e(n,`readFile`,a instanceof Error?a:Error(String(a))))}},readdir:async i=>{try{return r(n(await a.readdir(i)))}catch(n){return t(e(i,`readdir`,n instanceof Error?n:Error(String(n))))}}};export{o as Fs};
2
+ //# sourceMappingURL=Fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Fs.js","names":[],"sources":["../../src/fs/Fs.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\"\n\nimport type { TaskResult } from \"functype\"\nimport { Err, List, Ok, Option } from \"functype\"\n\nimport { FsError } from \"../errors/errors\"\n\nexport const Fs = {\n exists: async (path: string): TaskResult<boolean> => {\n try {\n await fs.access(path)\n return Ok(true)\n } catch {\n return Ok(false)\n }\n },\n\n readFile: async (path: string, encoding: BufferEncoding = \"utf8\"): TaskResult<string> => {\n try {\n const content = await fs.readFile(path, { encoding })\n return Ok(content)\n } catch (error) {\n return Err(FsError(path, \"readFile\", error instanceof Error ? error : new Error(String(error))))\n }\n },\n\n readFileOpt: async (path: string, encoding: BufferEncoding = \"utf8\"): TaskResult<Option<string>> => {\n try {\n const content = await fs.readFile(path, { encoding })\n return Ok(Option(content))\n } catch (error) {\n if (error instanceof Error && \"code\" in error && error.code === \"ENOENT\") {\n return Ok(Option<string>(undefined))\n }\n return Err(FsError(path, \"readFile\", error instanceof Error ? error : new Error(String(error))))\n }\n },\n\n readdir: async (path: string): TaskResult<List<string>> => {\n try {\n const entries = await fs.readdir(path)\n return Ok(List(entries))\n } catch (error) {\n return Err(FsError(path, \"readdir\", error instanceof Error ? error : new Error(String(error))))\n }\n },\n}\n"],"mappings":"8IAOA,MAAa,EAAK,CAChB,OAAQ,KAAO,IAAsC,CACnD,GAAI,CAEF,OADA,MAAM,EAAG,OAAO,EAAK,CACd,EAAG,GAAK,MACT,CACN,OAAO,EAAG,GAAM,GAIpB,SAAU,MAAO,EAAc,EAA2B,SAA+B,CACvF,GAAI,CAEF,OAAO,EADS,MAAM,EAAG,SAAS,EAAM,CAAE,WAAU,CAAC,CACnC,OACX,EAAO,CACd,OAAO,EAAI,EAAQ,EAAM,WAAY,aAAiB,MAAQ,EAAY,MAAM,OAAO,EAAM,CAAC,CAAC,CAAC,GAIpG,YAAa,MAAO,EAAc,EAA2B,SAAuC,CAClG,GAAI,CAEF,OAAO,EAAG,EADM,MAAM,EAAG,SAAS,EAAM,CAAE,WAAU,CAAC,CAC5B,CAAC,OACnB,EAAO,CAId,OAHI,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,SACvD,EAAG,EAAe,IAAA,GAAU,CAAC,CAE/B,EAAI,EAAQ,EAAM,WAAY,aAAiB,MAAQ,EAAY,MAAM,OAAO,EAAM,CAAC,CAAC,CAAC,GAIpG,QAAS,KAAO,IAA2C,CACzD,GAAI,CAEF,OAAO,EAAG,EADM,MAAM,EAAG,QAAQ,EAAK,CACf,CAAC,OACjB,EAAO,CACd,OAAO,EAAI,EAAQ,EAAM,UAAW,aAAiB,MAAQ,EAAY,MAAM,OAAO,EAAM,CAAC,CAAC,CAAC,GAGpG"}
@@ -0,0 +1,2 @@
1
+ import { Fs } from "./Fs.js";
2
+ export { Fs };
@@ -0,0 +1 @@
1
+ import{Fs as e}from"./Fs.js";export{e as Fs};
@@ -0,0 +1,7 @@
1
+ import { ConfigResolver } from "./config/ConfigResolver.js";
2
+ import { ConfigError, EnvError, FsError, OsError, PathError } from "./errors/errors.js";
3
+ import { Env } from "./env/Env.js";
4
+ import { Fs } from "./fs/Fs.js";
5
+ import { Path, expandPath, expandTilde, expandVars } from "./path/PathExpander.js";
6
+ import { Platform, UserInfo } from "./platform/Platform.js";
7
+ export { ConfigError, ConfigResolver, Env, EnvError, Fs, FsError, type OsError, Path, PathError, Platform, type UserInfo, expandPath, expandTilde, expandVars };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{ConfigError as e,EnvError as t,FsError as n,PathError as r}from"./errors/errors.js";import{Fs as i}from"./fs/Fs.js";import{Path as a,expandPath as o,expandTilde as s,expandVars as c}from"./path/PathExpander.js";import{ConfigResolver as l}from"./config/ConfigResolver.js";import"./config/index.js";import{Env as u}from"./env/Env.js";import"./env/index.js";import"./fs/index.js";import"./path/index.js";import{Platform as d}from"./platform/Platform.js";import"./platform/index.js";export{e as ConfigError,l as ConfigResolver,u as Env,t as EnvError,i as Fs,n as FsError,a as Path,r as PathError,d as Platform,o as expandPath,s as expandTilde,c as expandVars};
@@ -0,0 +1,21 @@
1
+ import { PathError } from "../errors/errors.js";
2
+ import { Either } from "functype";
3
+
4
+ //#region src/path/PathExpander.d.ts
5
+ declare const expandTilde: (p: string) => string;
6
+ declare const expandVars: (p: string) => Either<PathError, string>;
7
+ declare const expandPath: (p: string) => Either<PathError, string>;
8
+ declare const Path: {
9
+ expand: (p: string) => Either<PathError, string>;
10
+ expandTilde: (p: string) => string;
11
+ expandVars: (p: string) => Either<PathError, string>;
12
+ join: (...segments: string[]) => string;
13
+ resolve: (...segments: string[]) => string;
14
+ dirname: (p: string) => string;
15
+ basename: (p: string, suffix?: string) => string;
16
+ extname: (p: string) => string;
17
+ isAbsolute: (p: string) => boolean;
18
+ };
19
+ //#endregion
20
+ export { Path, expandPath, expandTilde, expandVars };
21
+ //# sourceMappingURL=PathExpander.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{PathError as e}from"../errors/errors.js";import{Left as t,Right as n}from"functype";import*as r from"node:os";import*as i from"node:path";const a=e=>e===`~`?r.homedir():e.startsWith(`~/`)||e.startsWith(`~\\`)?i.join(r.homedir(),e.slice(2)):e,o=r=>{let i=[],a=r.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g,(e,t,n)=>{let r=t??n??``,a=process.env[r];return a===void 0?(i.push(r),e):a});return i.length>0?t(e(r,`unresolved_variable`,`Unresolved variables: ${i.join(`, `)}`)):n(a)},s=e=>{let t=o(a(e));return t.isLeft()?t:n(i.resolve(t.orThrow()))},c={expand:s,expandTilde:a,expandVars:o,join:(...e)=>i.join(...e),resolve:(...e)=>i.resolve(...e),dirname:e=>i.dirname(e),basename:(e,t)=>i.basename(e,t),extname:e=>i.extname(e),isAbsolute:e=>i.isAbsolute(e)};export{c as Path,s as expandPath,a as expandTilde,o as expandVars};
2
+ //# sourceMappingURL=PathExpander.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PathExpander.js","names":[],"sources":["../../src/path/PathExpander.ts"],"sourcesContent":["import * as os from \"node:os\"\nimport * as nodePath from \"node:path\"\n\nimport type { Either } from \"functype\"\nimport { Left, Right } from \"functype\"\n\nimport { PathError } from \"../errors/errors\"\n\nexport const expandTilde = (p: string): string => {\n if (p === \"~\") return os.homedir()\n if (p.startsWith(\"~/\") || p.startsWith(\"~\\\\\")) return nodePath.join(os.homedir(), p.slice(2))\n return p\n}\n\nexport const expandVars = (p: string): Either<PathError, string> => {\n const unresolvedVars: string[] = []\n const result = p.replace(\n /\\$\\{([^}]+)\\}|\\$([A-Za-z_][A-Za-z0-9_]*)/g,\n (_match, braced: string | undefined, bare: string | undefined) => {\n const varName = braced ?? bare ?? \"\"\n const value = process.env[varName]\n if (value === undefined) {\n unresolvedVars.push(varName)\n return _match\n }\n return value\n },\n )\n\n if (unresolvedVars.length > 0) {\n return Left(PathError(p, \"unresolved_variable\", `Unresolved variables: ${unresolvedVars.join(\", \")}`))\n }\n\n return Right(result)\n}\n\nexport const expandPath = (p: string): Either<PathError, string> => {\n const tildeExpanded = expandTilde(p)\n const varsResult = expandVars(tildeExpanded)\n if (varsResult.isLeft()) return varsResult\n return Right(nodePath.resolve(varsResult.orThrow()))\n}\n\nexport const Path = {\n expand: expandPath,\n expandTilde,\n expandVars,\n join: (...segments: string[]): string => nodePath.join(...segments),\n resolve: (...segments: string[]): string => nodePath.resolve(...segments),\n dirname: (p: string): string => nodePath.dirname(p),\n basename: (p: string, suffix?: string): string => nodePath.basename(p, suffix),\n extname: (p: string): string => nodePath.extname(p),\n isAbsolute: (p: string): boolean => nodePath.isAbsolute(p),\n}\n"],"mappings":"iJAQA,MAAa,EAAe,GACtB,IAAM,IAAY,EAAG,SAAS,CAC9B,EAAE,WAAW,KAAK,EAAI,EAAE,WAAW,MAAM,CAAS,EAAS,KAAK,EAAG,SAAS,CAAE,EAAE,MAAM,EAAE,CAAC,CACtF,EAGI,EAAc,GAAyC,CAClE,IAAM,EAA2B,EAAE,CAC7B,EAAS,EAAE,QACf,6CACC,EAAQ,EAA4B,IAA6B,CAChE,IAAM,EAAU,GAAU,GAAQ,GAC5B,EAAQ,QAAQ,IAAI,GAK1B,OAJI,IAAU,IAAA,IACZ,EAAe,KAAK,EAAQ,CACrB,GAEF,GAEV,CAMD,OAJI,EAAe,OAAS,EACnB,EAAK,EAAU,EAAG,sBAAuB,yBAAyB,EAAe,KAAK,KAAK,GAAG,CAAC,CAGjG,EAAM,EAAO,EAGT,EAAc,GAAyC,CAElE,IAAM,EAAa,EADG,EAAY,EAAE,CACQ,CAE5C,OADI,EAAW,QAAQ,CAAS,EACzB,EAAM,EAAS,QAAQ,EAAW,SAAS,CAAC,CAAC,EAGzC,EAAO,CAClB,OAAQ,EACR,cACA,aACA,MAAO,GAAG,IAA+B,EAAS,KAAK,GAAG,EAAS,CACnE,SAAU,GAAG,IAA+B,EAAS,QAAQ,GAAG,EAAS,CACzE,QAAU,GAAsB,EAAS,QAAQ,EAAE,CACnD,UAAW,EAAW,IAA4B,EAAS,SAAS,EAAG,EAAO,CAC9E,QAAU,GAAsB,EAAS,QAAQ,EAAE,CACnD,WAAa,GAAuB,EAAS,WAAW,EAAE,CAC3D"}
@@ -0,0 +1,2 @@
1
+ import { Path, expandPath, expandTilde, expandVars } from "./PathExpander.js";
2
+ export { Path, expandPath, expandTilde, expandVars };
@@ -0,0 +1 @@
1
+ import{Path as e,expandPath as t,expandTilde as n,expandVars as r}from"./PathExpander.js";export{e as Path,t as expandPath,n as expandTilde,r as expandVars};
@@ -0,0 +1,31 @@
1
+ import { Option } from "functype";
2
+
3
+ //#region src/platform/Platform.d.ts
4
+ type UserInfo = {
5
+ readonly username: string;
6
+ readonly uid: number;
7
+ readonly gid: number;
8
+ readonly shell: string | null;
9
+ readonly homedir: string;
10
+ };
11
+ declare const Platform: {
12
+ os: () => "darwin" | "linux" | "win32" | string;
13
+ arch: () => string;
14
+ homeDir: () => string;
15
+ tmpDir: () => string;
16
+ hostname: () => string;
17
+ eol: () => string;
18
+ pathSep: () => string;
19
+ isWindows: () => boolean;
20
+ isMac: () => boolean;
21
+ isLinux: () => boolean;
22
+ userInfo: () => Option<UserInfo>;
23
+ isDocker: () => boolean;
24
+ isKubernetes: () => boolean;
25
+ isWSL: () => boolean;
26
+ isCI: () => boolean;
27
+ isContainer: () => boolean;
28
+ };
29
+ //#endregion
30
+ export { Platform, UserInfo };
31
+ //# sourceMappingURL=Platform.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{Option as e}from"functype";import*as t from"node:os";import*as n from"node:path";import*as r from"node:fs";const i=()=>{try{return r.statSync(`/.dockerenv`),!0}catch{return!1}},a=()=>{try{return r.readFileSync(`/proc/self/cgroup`,`utf8`).includes(`docker`)}catch{return!1}},o=e=>{let t={};return()=>(`value`in t||(t.value=e()),t.value)},s=o(()=>i()||a()),c=o(()=>{try{return r.readFileSync(`/proc/self/cgroup`,`utf8`).includes(`kube`)}catch{return!1}}),l=o(()=>{try{let e=r.readFileSync(`/proc/version`,`utf8`);return e.includes(`Microsoft`)||e.includes(`WSL`)}catch{return!1}}),u=o(()=>process.env.CI!==void 0||process.env.GITHUB_ACTIONS!==void 0||process.env.GITLAB_CI!==void 0||process.env.CIRCLECI!==void 0||process.env.JENKINS_URL!==void 0||process.env.TRAVIS!==void 0||process.env.BUILDKITE!==void 0),d={os:()=>process.platform,arch:()=>process.arch,homeDir:()=>t.homedir(),tmpDir:()=>t.tmpdir(),hostname:()=>t.hostname(),eol:()=>t.EOL,pathSep:()=>n.sep,isWindows:()=>process.platform===`win32`,isMac:()=>process.platform===`darwin`,isLinux:()=>process.platform===`linux`,userInfo:()=>{try{let n=t.userInfo();return e({username:n.username,uid:n.uid,gid:n.gid,shell:n.shell,homedir:n.homedir})}catch{return e(void 0)}},isDocker:()=>s(),isKubernetes:()=>c(),isWSL:()=>l(),isCI:()=>u(),isContainer:()=>d.isDocker()||d.isKubernetes()};export{d as Platform};
2
+ //# sourceMappingURL=Platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Platform.js","names":[],"sources":["../../src/platform/Platform.ts"],"sourcesContent":["import * as fs from \"node:fs\"\nimport * as os from \"node:os\"\nimport * as path from \"node:path\"\n\nimport { Option } from \"functype\"\n\nexport type UserInfo = {\n readonly username: string\n readonly uid: number\n readonly gid: number\n readonly shell: string | null\n readonly homedir: string\n}\n\nconst hasDockerEnv = (): boolean => {\n try {\n fs.statSync(\"/.dockerenv\")\n return true\n } catch {\n return false\n }\n}\nconst hasDockerCGroup = (): boolean => {\n try {\n return fs.readFileSync(\"/proc/self/cgroup\", \"utf8\").includes(\"docker\")\n } catch {\n return false\n }\n}\n\nconst memo = <T>(fn: () => T): (() => T) => {\n const cache: { value?: T } = {}\n return () => {\n if (!(\"value\" in cache)) {\n cache.value = fn()\n }\n return cache.value as T\n }\n}\n\nconst cachedIsDocker = memo(() => hasDockerEnv() || hasDockerCGroup())\nconst cachedIsKube = memo(() => {\n try {\n return fs.readFileSync(\"/proc/self/cgroup\", \"utf8\").includes(\"kube\")\n } catch {\n return false\n }\n})\nconst cachedIsWSL = memo(() => {\n try {\n const version = fs.readFileSync(\"/proc/version\", \"utf8\")\n return version.includes(\"Microsoft\") || version.includes(\"WSL\")\n } catch {\n return false\n }\n})\nconst cachedIsCI = memo(\n () =>\n process.env[\"CI\"] !== undefined ||\n process.env[\"GITHUB_ACTIONS\"] !== undefined ||\n process.env[\"GITLAB_CI\"] !== undefined ||\n process.env[\"CIRCLECI\"] !== undefined ||\n process.env[\"JENKINS_URL\"] !== undefined ||\n process.env[\"TRAVIS\"] !== undefined ||\n process.env[\"BUILDKITE\"] !== undefined,\n)\n\nexport const Platform = {\n os: (): \"darwin\" | \"linux\" | \"win32\" | string => process.platform,\n arch: (): string => process.arch,\n homeDir: (): string => os.homedir(),\n tmpDir: (): string => os.tmpdir(),\n hostname: (): string => os.hostname(),\n eol: (): string => os.EOL,\n pathSep: (): string => path.sep,\n\n isWindows: (): boolean => process.platform === \"win32\",\n isMac: (): boolean => process.platform === \"darwin\",\n isLinux: (): boolean => process.platform === \"linux\",\n\n userInfo: (): Option<UserInfo> => {\n try {\n const info = os.userInfo()\n return Option({\n username: info.username,\n uid: info.uid,\n gid: info.gid,\n shell: info.shell,\n homedir: info.homedir,\n })\n } catch {\n return Option<UserInfo>(undefined)\n }\n },\n\n isDocker: (): boolean => cachedIsDocker(),\n isKubernetes: (): boolean => cachedIsKube(),\n isWSL: (): boolean => cachedIsWSL(),\n isCI: (): boolean => cachedIsCI(),\n\n isContainer: (): boolean => Platform.isDocker() || Platform.isKubernetes(),\n}\n"],"mappings":"kHAcA,MAAM,MAA8B,CAClC,GAAI,CAEF,OADA,EAAG,SAAS,cAAc,CACnB,QACD,CACN,MAAO,KAGL,MAAiC,CACrC,GAAI,CACF,OAAO,EAAG,aAAa,oBAAqB,OAAO,CAAC,SAAS,SAAS,MAChE,CACN,MAAO,KAIL,EAAW,GAA2B,CAC1C,IAAM,EAAuB,EAAE,CAC/B,WACQ,UAAW,IACf,EAAM,MAAQ,GAAI,EAEb,EAAM,QAIX,EAAiB,MAAW,GAAc,EAAI,GAAiB,CAAC,CAChE,EAAe,MAAW,CAC9B,GAAI,CACF,OAAO,EAAG,aAAa,oBAAqB,OAAO,CAAC,SAAS,OAAO,MAC9D,CACN,MAAO,KAET,CACI,EAAc,MAAW,CAC7B,GAAI,CACF,IAAM,EAAU,EAAG,aAAa,gBAAiB,OAAO,CACxD,OAAO,EAAQ,SAAS,YAAY,EAAI,EAAQ,SAAS,MAAM,MACzD,CACN,MAAO,KAET,CACI,EAAa,MAEf,QAAQ,IAAI,KAAU,IAAA,IACtB,QAAQ,IAAI,iBAAsB,IAAA,IAClC,QAAQ,IAAI,YAAiB,IAAA,IAC7B,QAAQ,IAAI,WAAgB,IAAA,IAC5B,QAAQ,IAAI,cAAmB,IAAA,IAC/B,QAAQ,IAAI,SAAc,IAAA,IAC1B,QAAQ,IAAI,YAAiB,IAAA,GAChC,CAEY,EAAW,CACtB,OAAiD,QAAQ,SACzD,SAAoB,QAAQ,KAC5B,YAAuB,EAAG,SAAS,CACnC,WAAsB,EAAG,QAAQ,CACjC,aAAwB,EAAG,UAAU,CACrC,QAAmB,EAAG,IACtB,YAAuB,EAAK,IAE5B,cAA0B,QAAQ,WAAa,QAC/C,UAAsB,QAAQ,WAAa,SAC3C,YAAwB,QAAQ,WAAa,QAE7C,aAAkC,CAChC,GAAI,CACF,IAAM,EAAO,EAAG,UAAU,CAC1B,OAAO,EAAO,CACZ,SAAU,EAAK,SACf,IAAK,EAAK,IACV,IAAK,EAAK,IACV,MAAO,EAAK,MACZ,QAAS,EAAK,QACf,CAAC,MACI,CACN,OAAO,EAAiB,IAAA,GAAU,GAItC,aAAyB,GAAgB,CACzC,iBAA6B,GAAc,CAC3C,UAAsB,GAAa,CACnC,SAAqB,GAAY,CAEjC,gBAA4B,EAAS,UAAU,EAAI,EAAS,cAAc,CAC3E"}
@@ -0,0 +1,2 @@
1
+ import { Platform, UserInfo } from "./Platform.js";
2
+ export { Platform, type UserInfo };
@@ -0,0 +1 @@
1
+ import{Platform as e}from"./Platform.js";export{e as Platform};
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "functype-os",
3
+ "version": "0.1.0",
4
+ "description": "Functional OS utilities using functype data structures — env vars, path expansion, file ops, platform detection",
5
+ "keywords": [
6
+ "functype",
7
+ "functional",
8
+ "os",
9
+ "env",
10
+ "path",
11
+ "platform",
12
+ "typescript"
13
+ ],
14
+ "author": "jordan.burke@gmail.com",
15
+ "license": "MIT",
16
+ "homepage": "https://github.com/jordanburke/functype-os",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/jordanburke/functype-os"
20
+ },
21
+ "peerDependencies": {
22
+ "functype": ">=0.49.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^24.11.2",
26
+ "eslint-config-prettier": "^10.1.8",
27
+ "functype": "^0.49.0",
28
+ "ts-builds": "^2.5.0",
29
+ "tsdown": "^0.20.3",
30
+ "vitest": "^4.0.18"
31
+ },
32
+ "type": "module",
33
+ "main": "./dist/index.js",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js",
40
+ "default": "./dist/index.js"
41
+ },
42
+ "./env": {
43
+ "types": "./dist/env/index.d.ts",
44
+ "import": "./dist/env/index.js",
45
+ "default": "./dist/env/index.js"
46
+ },
47
+ "./path": {
48
+ "types": "./dist/path/index.d.ts",
49
+ "import": "./dist/path/index.js",
50
+ "default": "./dist/path/index.js"
51
+ },
52
+ "./fs": {
53
+ "types": "./dist/fs/index.d.ts",
54
+ "import": "./dist/fs/index.js",
55
+ "default": "./dist/fs/index.js"
56
+ },
57
+ "./platform": {
58
+ "types": "./dist/platform/index.d.ts",
59
+ "import": "./dist/platform/index.js",
60
+ "default": "./dist/platform/index.js"
61
+ },
62
+ "./config": {
63
+ "types": "./dist/config/index.d.ts",
64
+ "import": "./dist/config/index.js",
65
+ "default": "./dist/config/index.js"
66
+ },
67
+ "./errors": {
68
+ "types": "./dist/errors/index.d.ts",
69
+ "import": "./dist/errors/index.js",
70
+ "default": "./dist/errors/index.js"
71
+ }
72
+ },
73
+ "files": [
74
+ "lib",
75
+ "dist"
76
+ ],
77
+ "prettier": "ts-builds/prettier",
78
+ "engines": {
79
+ "node": ">=18.0.0"
80
+ },
81
+ "scripts": {
82
+ "validate": "ts-builds validate",
83
+ "format": "ts-builds format",
84
+ "format:check": "ts-builds format:check",
85
+ "lint": "ts-builds lint",
86
+ "lint:check": "ts-builds lint:check",
87
+ "typecheck": "ts-builds typecheck",
88
+ "test": "ts-builds test",
89
+ "test:watch": "ts-builds test:watch",
90
+ "test:coverage": "ts-builds test:coverage",
91
+ "build": "ts-builds build",
92
+ "dev": "ts-builds dev"
93
+ }
94
+ }