@effected/xdg 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/AppDirs.js ADDED
@@ -0,0 +1,208 @@
1
+ import { NativeDirs } from "./NativeDirs.js";
2
+ import { CurrentPlatform, Xdg } from "./Xdg.js";
3
+ import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
4
+
5
+ //#region src/AppDirs.ts
6
+ /**
7
+ * The four directory kinds XDG separates, plus the runtime directory.
8
+ *
9
+ * @public
10
+ */
11
+ const AppDirKind = Schema.Literals([
12
+ "config",
13
+ "data",
14
+ "cache",
15
+ "state",
16
+ "runtime"
17
+ ]);
18
+ /**
19
+ * Indicates that an application directory could not be created.
20
+ *
21
+ * @remarks
22
+ * The only way `AppDirs` fails. Resolution cannot fail — it happens once, at
23
+ * layer construction, from an environment that is already resolved — so this
24
+ * error means exactly one thing: the `mkdir -p` did not work. `directory` says
25
+ * which kind, `path` says where, and `cause` carries the underlying
26
+ * `PlatformError` structurally. v3 carried `reason: String(e)` and a `directory`
27
+ * that could also be the string `"all"`.
28
+ *
29
+ * @public
30
+ */
31
+ var AppDirsError = class extends Schema.TaggedErrorClass()("AppDirsError", {
32
+ /** Which directory kind failed. */
33
+ directory: AppDirKind,
34
+ /** The path that could not be created. */
35
+ path: Schema.String,
36
+ /** The underlying failure, preserved structurally. */
37
+ cause: Schema.Defect()
38
+ }) {
39
+ get message() {
40
+ return `Failed to create the ${this.directory} directory at "${this.path}"`;
41
+ }
42
+ };
43
+ /**
44
+ * The fully resolved, app-namespaced directories.
45
+ *
46
+ * @public
47
+ */
48
+ var ResolvedAppDirs = class extends Schema.Class("ResolvedAppDirs")({
49
+ /** The app's configuration directory. */
50
+ config: Schema.String,
51
+ /** The app's data directory. */
52
+ data: Schema.String,
53
+ /** The app's cache directory. */
54
+ cache: Schema.String,
55
+ /** The app's state directory. */
56
+ state: Schema.String,
57
+ /**
58
+ * The app's runtime directory.
59
+ *
60
+ * @remarks
61
+ * Absent unless `$XDG_RUNTIME_DIR` is set or `dirs.runtime` overrides it.
62
+ * There is no defensible fallback for a runtime directory — it must be
63
+ * user-owned, mode 0700 and cleaned on logout — so inventing one would be a
64
+ * lie, and the key is simply absent.
65
+ */
66
+ runtime: Schema.optionalKey(Schema.String),
67
+ /**
68
+ * Where to **look** for configuration, in priority order.
69
+ *
70
+ * @remarks
71
+ * The app's own config directory, then each `$XDG_CONFIG_DIRS` entry
72
+ * namespaced. This is the half of the XDG spec v3 ignored entirely, and it is
73
+ * what makes {@link XdgConfig.resolver} a real search rather than a single stat.
74
+ */
75
+ configSearchPath: Schema.Array(Schema.String),
76
+ /** Where to look for data files, in priority order. */
77
+ dataSearchPath: Schema.Array(Schema.String)
78
+ }) {};
79
+ /**
80
+ * Resolve one directory kind through the five-level precedence.
81
+ *
82
+ * 1. an explicit `dirs.<kind>` override;
83
+ * 2. the XDG environment variable, namespaced (`$XDG_CONFIG_HOME/<ns>`);
84
+ * 3. the native directory, when `native: true` and the platform has one;
85
+ * 4. `$HOME/<fallbackDir>` — all four kinds collapse to the one directory;
86
+ * 5. `$HOME/.<namespace>`.
87
+ *
88
+ * Rungs 4 and 5 are deliberately **not** the XDG spec's per-kind defaults
89
+ * (`~/.config`, `~/.local/share`, …). This is inherited v3 behaviour and is kept:
90
+ * a CLI wanting spec defaults passes them as `dirs` overrides. The deviation is
91
+ * documented rather than left for a reader to discover.
92
+ */
93
+ const resolveDir = (input) => {
94
+ const { override, xdgHome, native, options, home, path } = input;
95
+ if (override !== void 0) return override;
96
+ if (xdgHome !== void 0) return path.join(xdgHome, options.namespace);
97
+ if (Option.isSome(native)) return native.value;
98
+ if (options.fallbackDir !== void 0) return path.join(home, options.fallbackDir);
99
+ return path.join(home, `.${options.namespace}`);
100
+ };
101
+ /** The app's own directory, then each system directory, namespaced. Nearest first. */
102
+ const searchPath = (own, systemDirs, namespace, path) => [own, ...systemDirs.map((dir) => path.join(dir, namespace))];
103
+ const resolveAll = (options, paths, platform, path) => {
104
+ const native = options.native ? NativeDirs.resolve({
105
+ platform,
106
+ namespace: options.namespace,
107
+ paths,
108
+ path
109
+ }) : Option.none();
110
+ const forKind = (xdgHome, nativeDir, override) => resolveDir({
111
+ override,
112
+ xdgHome,
113
+ native: Option.map(native, nativeDir),
114
+ options,
115
+ home: paths.home,
116
+ path
117
+ });
118
+ const config = forKind(paths.configHome, (n) => n.config, options.dirs?.config);
119
+ const data = forKind(paths.dataHome, (n) => n.data, options.dirs?.data);
120
+ const cache = forKind(paths.cacheHome, (n) => n.cache, options.dirs?.cache);
121
+ const state = forKind(paths.stateHome, (n) => n.state, options.dirs?.state);
122
+ const runtime = options.dirs?.runtime ?? (paths.runtimeDir !== void 0 ? path.join(paths.runtimeDir, options.namespace) : void 0);
123
+ return ResolvedAppDirs.make({
124
+ config,
125
+ data,
126
+ cache,
127
+ state,
128
+ ...runtime !== void 0 && { runtime },
129
+ configSearchPath: searchPath(config, paths.configDirs, options.namespace, path),
130
+ dataSearchPath: searchPath(data, paths.dataDirs, options.namespace, path)
131
+ });
132
+ };
133
+ /**
134
+ * A namespace is a single path component. Anything else escapes the app's own
135
+ * directories, so it **dies** rather than resolving somewhere surprising. This
136
+ * is wiring, not input: only code supplies a namespace, so a bad one is a
137
+ * programmer error and belongs on the defect channel, not in `E`.
138
+ */
139
+ const badNamespace = (namespace) => {
140
+ if (namespace.length === 0) return /* @__PURE__ */ new Error("AppDirs.layer: `namespace` must not be empty");
141
+ if (/[/\\]/.test(namespace) || namespace === "." || namespace === "..") return /* @__PURE__ */ new Error(`AppDirs.layer: \`namespace\` must be a single path component, received ${JSON.stringify(namespace)}`);
142
+ };
143
+ /**
144
+ * App-namespaced XDG directories, with on-demand creation.
145
+ *
146
+ * @remarks
147
+ * `AppDirs.layer` is a layer-**returning function**: calling it twice builds two
148
+ * independent services. Bind its result to a const and provide that const, per
149
+ * the layer memoization discipline.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * const AppDirsLayer = AppDirs.layer({ namespace: "myapp", native: true });
154
+ * const XdgLayer = Layer.mergeAll(Xdg.layer, AppDirsLayer.pipe(Layer.provide(Xdg.layer)));
155
+ * ```
156
+ *
157
+ * @public
158
+ */
159
+ var AppDirs = class AppDirs extends Context.Service()("@effected/xdg/AppDirs") {
160
+ /**
161
+ * Resolve the namespace's directories against the ambient {@link Xdg}
162
+ * environment and platform.
163
+ *
164
+ * @remarks
165
+ * The error channel is `never`. The one failure that could happen during
166
+ * resolution — an unset `HOME` — surfaces on {@link Xdg.layer} as an
167
+ * `XdgEnvError`, before an `AppDirs` exists at all. v3 laundered it into an
168
+ * `AppDirsError({ directory: "all" })`.
169
+ */
170
+ static layer(options) {
171
+ return Layer.effect(AppDirs, Effect.gen(function* () {
172
+ const invalid = badNamespace(options.namespace);
173
+ if (invalid !== void 0) return yield* Effect.die(invalid);
174
+ const paths = yield* Xdg;
175
+ const fs = yield* FileSystem.FileSystem;
176
+ const path = yield* Path.Path;
177
+ const platform = yield* CurrentPlatform;
178
+ const dirs = resolveAll(options, paths, platform, path);
179
+ const makeDir = (kind, target) => fs.makeDirectory(target, { recursive: true }).pipe(Effect.mapError((cause) => new AppDirsError({
180
+ directory: kind,
181
+ path: target,
182
+ cause
183
+ })), Effect.as(target));
184
+ const runtimeDir = dirs.runtime;
185
+ const ensureRuntime = (runtimeDir === void 0 ? Effect.succeed(Option.none()) : Effect.map(makeDir("runtime", runtimeDir), Option.some)).pipe(Effect.withSpan("AppDirs.ensureRuntime"));
186
+ return {
187
+ namespace: options.namespace,
188
+ dirs,
189
+ ensureConfig: makeDir("config", dirs.config).pipe(Effect.withSpan("AppDirs.ensureConfig")),
190
+ ensureData: makeDir("data", dirs.data).pipe(Effect.withSpan("AppDirs.ensureData")),
191
+ ensureCache: makeDir("cache", dirs.cache).pipe(Effect.withSpan("AppDirs.ensureCache")),
192
+ ensureState: makeDir("state", dirs.state).pipe(Effect.withSpan("AppDirs.ensureState")),
193
+ ensureRuntime,
194
+ ensure: Effect.gen(function* () {
195
+ yield* makeDir("config", dirs.config);
196
+ yield* makeDir("data", dirs.data);
197
+ yield* makeDir("cache", dirs.cache);
198
+ yield* makeDir("state", dirs.state);
199
+ if (dirs.runtime !== void 0) yield* makeDir("runtime", dirs.runtime);
200
+ return dirs;
201
+ }).pipe(Effect.withSpan("AppDirs.ensure"))
202
+ };
203
+ }));
204
+ }
205
+ };
206
+
207
+ //#endregion
208
+ export { AppDirKind, AppDirs, AppDirsError, ResolvedAppDirs };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Spencer Beggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NativeDirs.js ADDED
@@ -0,0 +1,71 @@
1
+ import { Option, Schema } from "effect";
2
+
3
+ //#region src/NativeDirs.ts
4
+ /**
5
+ * The OS-native application directories for one namespace.
6
+ *
7
+ * @remarks
8
+ * On platforms without OS-level separation of these concerns, `config`, `data`
9
+ * and `state` collapse to the same directory — `~/Library/Application Support/<ns>`
10
+ * on macOS — while `cache` stays distinct.
11
+ *
12
+ * @public
13
+ */
14
+ var NativeDirs = class NativeDirs extends Schema.Class("NativeDirs")({
15
+ /** Where configuration lives. */
16
+ config: Schema.String,
17
+ /** Where application data lives. */
18
+ data: Schema.String,
19
+ /** Where discardable cached data lives. */
20
+ cache: Schema.String,
21
+ /** Where persistent-but-regenerable state lives. */
22
+ state: Schema.String
23
+ }) {
24
+ /**
25
+ * Map a platform and an environment onto the native directories for a namespace.
26
+ *
27
+ * @remarks
28
+ * **Pure**: no filesystem, no environment, no `process.platform`. Every input
29
+ * is a parameter, which is what makes the whole platform matrix testable
30
+ * without any platform IO. Paths are joined through the supplied `Path`, so a
31
+ * win32 `Path` layer yields win32 separators — v3 interpolated `/` on every
32
+ * platform.
33
+ *
34
+ * - **darwin** — `config`/`data`/`state` under `~/Library/Application Support/<ns>`;
35
+ * `cache` under `~/Library/Caches/<ns>`.
36
+ * - **win32** — `config`/`data` under `%APPDATA%/<ns>`; `cache` under
37
+ * `%LOCALAPPDATA%/<ns>/Cache`; `state` under `%LOCALAPPDATA%/<ns>`. When the
38
+ * variables are unset, `%APPDATA%` falls back to `<home>/AppData/Roaming` and
39
+ * `%LOCALAPPDATA%` to `<home>/AppData/Local`.
40
+ * - **everything else** — `Option.none()`. On Linux, XDG *is* the native
41
+ * convention, so there is no override to apply; returning `none` rather than
42
+ * a duplicate of the XDG answer is what lets a precedence ladder skip the
43
+ * rung cleanly instead of shadowing the rung below it.
44
+ */
45
+ static resolve(input) {
46
+ const { platform, namespace, paths, path } = input;
47
+ if (platform === "darwin") {
48
+ const appSupport = path.join(paths.home, "Library", "Application Support", namespace);
49
+ return Option.some(NativeDirs.make({
50
+ config: appSupport,
51
+ data: appSupport,
52
+ cache: path.join(paths.home, "Library", "Caches", namespace),
53
+ state: appSupport
54
+ }));
55
+ }
56
+ if (platform === "win32") {
57
+ const roaming = paths.appData ?? path.join(paths.home, "AppData", "Roaming");
58
+ const local = paths.localAppData ?? path.join(paths.home, "AppData", "Local");
59
+ return Option.some(NativeDirs.make({
60
+ config: path.join(roaming, namespace),
61
+ data: path.join(roaming, namespace),
62
+ cache: path.join(local, namespace, "Cache"),
63
+ state: path.join(local, namespace)
64
+ }));
65
+ }
66
+ return Option.none();
67
+ }
68
+ };
69
+
70
+ //#endregion
71
+ export { NativeDirs };
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @effected/xdg
2
+
3
+ [![npm](https://img.shields.io/npm/v/@effected%2Fxdg?label=npm&color=cb3837)](https://www.npmjs.com/package/@effected/xdg)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
6
+ [![TypeScript 6.0](https://img.shields.io/badge/TypeScript-6.0-3178c6.svg)](https://www.typescriptlang.org/)
7
+
8
+ XDG Base Directory resolution for Effect. `Xdg` reads the environment — `$HOME`, the four `*_HOME` variables, `$XDG_RUNTIME_DIR`, and the `$XDG_CONFIG_DIRS` / `$XDG_DATA_DIRS` search paths — once, at layer construction. `AppDirs` turns that into the config, data, cache, state and runtime directories for one application namespace, with on-demand creation. `NativeDirs` supplies the macOS and Windows conventions for applications that want them, and `XdgConfig` plugs the whole thing into [`@effected/config-file`](../config-file) as a resolver chain and a save target.
9
+
10
+ > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
+ > development against a single pinned Effect v4 beta. Packages graduate to
12
+ > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
+ > exactly the ones the kit is built and tested against, install
14
+ > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
15
+ >
16
+ > **Stability: unstable.** This package's API surface is not yet considered
17
+ > complete and may change across `0.x` releases. Pin an exact version — even a
18
+ > package marked *stable* before `1.0.0` can introduce a breaking change by
19
+ > accident, and an exact pin turns that into a type-check error rather than a
20
+ > runtime surprise. Full policy: [release strategy](https://github.com/spencerbeggs/effected#release-strategy).
21
+
22
+ ## Why @effected/xdg
23
+
24
+ Path resolution is not IO, and modeling it as IO poisons everything downstream. The environment is fixed for the life of a process, so this package reads it exactly once — when the layer is built — and the service's shape *is* the resolved value. `appDirs.dirs.config` is a `string`, not an `Effect<string, XdgEnvError>`. Reading a path cannot fail, cannot be observed to do IO and drops straight into config-file's `defaultPath` slot, which is typed `Effect<string, never, R>` and would otherwise need an `orDie` to satisfy.
25
+
26
+ Two more things follow from taking the spec seriously. The system search paths are half of XDG and are usually skipped: a config lookup here probes the app's own config directory — whichever rung of the precedence below resolved it — *and then* each `$XDG_CONFIG_DIRS` entry, namespaced, and it absorbs failure per candidate — an unreadable `/etc/xdg` means "this candidate did not match", never "abort the search and hide the perfectly readable file below it". And the runtime directory has no fallback ladder, because there is no defensible one: it must be user-owned and mode 0700, so when `$XDG_RUNTIME_DIR` is unset the key is simply absent rather than pointing somewhere invented.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install @effected/xdg effect @effect/platform-node
32
+ ```
33
+
34
+ ```bash
35
+ pnpm add @effected/xdg effect @effect/platform-node
36
+ ```
37
+
38
+ Requires Node.js >=24.11.0.
39
+
40
+ `effect` v4 is a peer dependency, and so are `@effected/walker` (the upward-traversal and search primitives the resolvers are built on) and `@effected/config-file` (whose `ConfigResolver` seam `XdgConfig` implements). Package managers that install peers automatically will pull them in; add them to your manifest explicitly if yours does not. There are no runtime dependencies.
41
+
42
+ Creating directories needs a `FileSystem` and a `Path` implementation, provided once at the edge — from `@effect/platform-node` on Node. Resolution itself needs neither.
43
+
44
+ ## Quick start
45
+
46
+ Build `AppDirs` for your namespace, provide it the `Xdg` environment and the platform layers, and read the paths:
47
+
48
+ ```ts
49
+ import { AppDirs, Xdg } from "@effected/xdg";
50
+ import { NodeFileSystem, NodePath } from "@effect/platform-node";
51
+ import { Effect, Layer } from "effect";
52
+
53
+ const PlatformLive = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer);
54
+
55
+ const AppDirsLive = AppDirs.layer({ namespace: "myapp", native: true }).pipe(
56
+ Layer.provide(Layer.mergeAll(Xdg.layer, PlatformLive)),
57
+ );
58
+
59
+ const program = Effect.gen(function* () {
60
+ const appDirs = yield* AppDirs;
61
+ console.log(appDirs.dirs.config);
62
+ // Linux, XDG_CONFIG_HOME set: $XDG_CONFIG_HOME/myapp
63
+ // Linux, XDG_CONFIG_HOME unset: $HOME/.myapp
64
+ // macOS with `native: true`: $HOME/Library/Application Support/myapp
65
+ return yield* appDirs.ensureConfig;
66
+ // The same path, now created on disk.
67
+ });
68
+
69
+ Effect.runPromise(program.pipe(Effect.provide(AppDirsLive)));
70
+ ```
71
+
72
+ `AppDirs.layer` is a layer-returning *function*, not a layer: calling it twice builds two independent services. Bind its result to a const, as above, and provide that const.
73
+
74
+ ## Precedence
75
+
76
+ Each of `config`, `data`, `cache` and `state` resolves through five rungs, first match wins:
77
+
78
+ 1. an explicit `dirs.<kind>` override — an absolute path, and it wins outright;
79
+ 2. the XDG variable, namespaced: `$XDG_CONFIG_HOME/<namespace>`;
80
+ 3. the OS-native directory, when `native: true` and the platform has one;
81
+ 4. `$HOME/<fallbackDir>` — all four kinds collapse into that one directory;
82
+ 5. `$HOME/.<namespace>`.
83
+
84
+ Rungs 4 and 5 are deliberately not the spec's per-kind defaults (`~/.config`, `~/.local/share`): an application that wants those passes them as `dirs` overrides. The runtime directory skips the ladder entirely — an override, or `$XDG_RUNTIME_DIR/<namespace>`, or nothing.
85
+
86
+ `NativeDirs.resolve` is pure — platform, namespace, environment and `Path` all arrive as parameters — so the whole matrix is testable with no filesystem and no `process.platform` read. On macOS `config`, `data` and `state` collapse to `~/Library/Application Support/<ns>` while `cache` stays under `~/Library/Caches/<ns>`; on Windows they split across `%APPDATA%` and `%LOCALAPPDATA%`. Everywhere else it returns `Option.none()`, because on Linux XDG *is* the native convention and there is nothing to override.
87
+
88
+ The platform is a `Context.Reference`, never a global read. Pin it in a test with `Layer.succeed(CurrentPlatform, "win32")` and exercise the Windows paths on a Mac.
89
+
90
+ ## Config files
91
+
92
+ `XdgConfig` is the bridge into `@effected/config-file`. `resolver` searches the app's whole config search path, `nativeResolver` probes the OS-native directory, and `savePath` names the default write target:
93
+
94
+ ```ts
95
+ import { ConfigFile, JsonCodec, MergeStrategy } from "@effected/config-file";
96
+ import { XdgConfig } from "@effected/xdg";
97
+ import { Schema } from "effect";
98
+
99
+ class AppShape extends Schema.Class<AppShape>("AppShape")({
100
+ port: Schema.Number,
101
+ host: Schema.String,
102
+ }) {}
103
+
104
+ class AppConfig extends ConfigFile.Service<AppConfig, AppShape>()("myapp/Config") {}
105
+
106
+ export const AppConfigLive = ConfigFile.layer(AppConfig, {
107
+ schema: AppShape,
108
+ codec: JsonCodec,
109
+ resolvers: [
110
+ XdgConfig.resolver({ filename: "config.json" }),
111
+ XdgConfig.nativeResolver({ namespace: "myapp", filename: "config.json" }),
112
+ ],
113
+ strategy: MergeStrategy.firstMatch<AppShape>(),
114
+ defaultPath: XdgConfig.savePath("config.json"),
115
+ });
116
+ ```
117
+
118
+ Order matters: put `resolver` before `nativeResolver` so an existing `~/.config/<app>/config.json` still wins over the OS-native directory. `savePath` does not create the directory — `ConfigFile.save` already creates the parent of whatever path it is handed.
119
+
120
+ ## Errors
121
+
122
+ | Tag | Means | Recovery |
123
+ | --- | --- | --- |
124
+ | `XdgEnvError` | `$HOME` is not set. Carries `variable` and the structural `cause` — the underlying `ConfigError`. | The one environment failure there is. Every other XDG variable is optional by construction, and its absence is a resolved default rather than an error. |
125
+ | `AppDirsError` | A directory could not be created. Carries `directory` (which kind), `path` and the structural `cause`. | The only way `AppDirs` fails, because resolution already happened. Check permissions. |
126
+
127
+ A namespace that is empty, or contains a path separator, or is exactly `.` or `..`, is a **defect** at layer construction rather than a typed error. It can only come from code, and `namespace: "../.."` would resolve the application's directories outside `$HOME` entirely.
128
+
129
+ ## Features
130
+
131
+ - `Xdg` / `XdgPaths` — the resolved XDG environment as a value, including the `$XDG_CONFIG_DIRS` and `$XDG_DATA_DIRS` search paths, split and defaulted per the spec. `Xdg.layerFrom` serves fixed paths for tests.
132
+ - `AppDirs` / `ResolvedAppDirs` — the five app-namespaced directories plus the config and data search paths, with `ensureConfig`, `ensureData`, `ensureCache`, `ensureState`, `ensureRuntime` and `ensure` for on-demand creation.
133
+ - `NativeDirs` — the macOS and Windows conventions, resolved purely from a platform, a namespace and an environment.
134
+ - `CurrentPlatform` — the platform as a `Context.Reference`, defaulting to `process.platform` and overridable in a test.
135
+ - `XdgConfig` — `resolver`, `nativeResolver` and `savePath`, dropping straight into `@effected/config-file`'s `ConfigResolver` and `defaultPath` slots.
136
+ - `XdgEnvError` / `AppDirsError` — tagged errors carrying their cause structurally.
137
+
138
+ ## License
139
+
140
+ [MIT](LICENSE)
package/Xdg.js ADDED
@@ -0,0 +1,189 @@
1
+ import { Config, Context, Effect, Layer, Option, Schema } from "effect";
2
+
3
+ //#region src/Xdg.ts
4
+ /**
5
+ * The operating system the path decisions are taken against.
6
+ *
7
+ * @remarks
8
+ * The members are Node's `process.platform` values, modeled as a schema rather
9
+ * than borrowed from the ambient `NodeJS.Platform` type so the package's public
10
+ * surface names nothing it does not own.
11
+ *
12
+ * @public
13
+ */
14
+ const XdgPlatform = Schema.Literals([
15
+ "aix",
16
+ "android",
17
+ "darwin",
18
+ "freebsd",
19
+ "haiku",
20
+ "linux",
21
+ "openbsd",
22
+ "sunos",
23
+ "win32",
24
+ "cygwin",
25
+ "netbsd"
26
+ ]);
27
+ const detectPlatform = () => {
28
+ const platform = globalThis.process?.platform;
29
+ return Schema.is(XdgPlatform)(platform) ? platform : "linux";
30
+ };
31
+ /**
32
+ * The platform every native-directory decision is taken against.
33
+ *
34
+ * @remarks
35
+ * A {@link https://effect.website | Context.Reference}, not a global read. It
36
+ * defaults to `process.platform`, so production behaviour is what you expect;
37
+ * a test pins macOS or Windows semantics with
38
+ * `Layer.succeed(CurrentPlatform, "win32")` and exercises the whole native-path
39
+ * matrix without touching a real filesystem or the real platform.
40
+ *
41
+ * @public
42
+ */
43
+ const CurrentPlatform = Context.Reference("@effected/xdg/CurrentPlatform", { defaultValue: detectPlatform });
44
+ /**
45
+ * Indicates that the environment cannot satisfy XDG directory resolution.
46
+ *
47
+ * @remarks
48
+ * Raised only for `HOME`: every other XDG variable is optional by construction,
49
+ * and its absence is a resolved default rather than a failure. `cause` carries
50
+ * the underlying `ConfigError` structurally — v3 flattened it to a string.
51
+ *
52
+ * @public
53
+ */
54
+ var XdgEnvError = class extends Schema.TaggedErrorClass()("XdgEnvError", {
55
+ /** The environment variable that was required and not found. */
56
+ variable: Schema.String,
57
+ /** The underlying failure, preserved structurally. */
58
+ cause: Schema.Defect()
59
+ }) {
60
+ get message() {
61
+ return `The ${this.variable} environment variable is not set`;
62
+ }
63
+ };
64
+ /**
65
+ * The XDG Base Directory environment, resolved.
66
+ *
67
+ * @remarks
68
+ * Every field but `home` is optional because the corresponding variable is:
69
+ * `Schema.optionalKey`, so an unset variable is an **absent key** and the read
70
+ * is `paths.configHome ?? fallback`. v3 modeled these as `Option`, which forced
71
+ * `Option.some(...)` into the construction API of everything downstream.
72
+ *
73
+ * `configDirs` and `dataDirs` are the colon-separated system search paths, split
74
+ * and defaulted per the spec (`/etc/xdg` and `/usr/local/share:/usr/share`).
75
+ * They are what make a config lookup a genuine ordered search rather than a
76
+ * single stat.
77
+ *
78
+ * @public
79
+ */
80
+ var XdgPaths = class extends Schema.Class("XdgPaths")({
81
+ /** `$HOME`. The one variable that must be set. */
82
+ home: Schema.String,
83
+ /** `$XDG_CONFIG_HOME`. */
84
+ configHome: Schema.optionalKey(Schema.String),
85
+ /** `$XDG_DATA_HOME`. */
86
+ dataHome: Schema.optionalKey(Schema.String),
87
+ /** `$XDG_CACHE_HOME`. */
88
+ cacheHome: Schema.optionalKey(Schema.String),
89
+ /** `$XDG_STATE_HOME`. */
90
+ stateHome: Schema.optionalKey(Schema.String),
91
+ /** `$XDG_RUNTIME_DIR`. Absent on most non-Linux systems. */
92
+ runtimeDir: Schema.optionalKey(Schema.String),
93
+ /** `%APPDATA%`, on Windows. */
94
+ appData: Schema.optionalKey(Schema.String),
95
+ /** `%LOCALAPPDATA%`, on Windows. */
96
+ localAppData: Schema.optionalKey(Schema.String),
97
+ /** `$XDG_CONFIG_DIRS`, split on `:`. Defaults to `["/etc/xdg"]`. */
98
+ configDirs: Schema.Array(Schema.String),
99
+ /** `$XDG_DATA_DIRS`, split on `:`. Defaults to `["/usr/local/share", "/usr/share"]`. */
100
+ dataDirs: Schema.Array(Schema.String)
101
+ }) {};
102
+ /**
103
+ * Split a `PATH`-style variable, dropping empty entries.
104
+ *
105
+ * @remarks
106
+ * Per the XDG spec, an unset **or empty** variable takes the default — so
107
+ * `XDG_CONFIG_DIRS=""` is not "no system directories", it is `/etc/xdg`.
108
+ */
109
+ const splitDirs = (raw, fallback) => {
110
+ if (raw === void 0) return fallback;
111
+ const parts = raw.split(":").filter((entry) => entry.length > 0);
112
+ return parts.length === 0 ? fallback : parts;
113
+ };
114
+ /**
115
+ * XDG Base Directory environment resolution.
116
+ *
117
+ * @remarks
118
+ * The service's shape **is** {@link XdgPaths}: the environment is read once, at
119
+ * layer construction, and the service is the resolved value. v3 exposed nine
120
+ * `Effect`s that each re-read the environment on every access, which is why
121
+ * every path downstream of it was fallible. Here `yield* Xdg` gives you a record
122
+ * of strings, and the only failure — an unset `HOME` — happens once, where the
123
+ * layer is built.
124
+ *
125
+ * @public
126
+ */
127
+ var Xdg = class Xdg extends Context.Service()("@effected/xdg/Xdg") {
128
+ /**
129
+ * Read the XDG environment through Effect's `Config`.
130
+ *
131
+ * @remarks
132
+ * Reads from the ambient `ConfigProvider`, which defaults to `process.env`.
133
+ * A test drives it with `ConfigProvider.layer(ConfigProvider.fromUnknown({…}))`
134
+ * and never mutates the real environment.
135
+ */
136
+ static layer = Layer.effect(Xdg, Effect.gen(function* () {
137
+ /**
138
+ * `Config<T>` IS an `Effect<T, ConfigError>` in v4, so it pipes directly.
139
+ * Every `ConfigError` becomes an `XdgEnvError` naming the variable it came from.
140
+ */
141
+ const asEnvError = (name, config) => Effect.catchTag(config, "ConfigError", (cause) => Effect.fail(new XdgEnvError({
142
+ variable: name,
143
+ cause
144
+ })));
145
+ const home = yield* asEnvError("HOME", Config.string("HOME"));
146
+ /**
147
+ * An unset variable is `Option.none()`, not a failure. The residual
148
+ * `ConfigError` a `Config.option` can still raise is a *provider* failure
149
+ * (a `ConfigProvider` backed by a directory or a `.env` file that cannot be
150
+ * read), not a missing key — so it is mapped rather than swallowed.
151
+ */
152
+ const read = (name) => Effect.map(asEnvError(name, Config.option(Config.string(name))), Option.getOrUndefined);
153
+ const configHome = yield* read("XDG_CONFIG_HOME");
154
+ const dataHome = yield* read("XDG_DATA_HOME");
155
+ const cacheHome = yield* read("XDG_CACHE_HOME");
156
+ const stateHome = yield* read("XDG_STATE_HOME");
157
+ const runtimeDir = yield* read("XDG_RUNTIME_DIR");
158
+ const appData = yield* read("APPDATA");
159
+ const localAppData = yield* read("LOCALAPPDATA");
160
+ const configDirs = yield* read("XDG_CONFIG_DIRS");
161
+ const dataDirs = yield* read("XDG_DATA_DIRS");
162
+ return XdgPaths.make({
163
+ home,
164
+ ...configHome !== void 0 && { configHome },
165
+ ...dataHome !== void 0 && { dataHome },
166
+ ...cacheHome !== void 0 && { cacheHome },
167
+ ...stateHome !== void 0 && { stateHome },
168
+ ...runtimeDir !== void 0 && { runtimeDir },
169
+ ...appData !== void 0 && { appData },
170
+ ...localAppData !== void 0 && { localAppData },
171
+ configDirs: splitDirs(configDirs, ["/etc/xdg"]),
172
+ dataDirs: splitDirs(dataDirs, ["/usr/local/share", "/usr/share"])
173
+ });
174
+ }));
175
+ /**
176
+ * Serve fixed paths instead of reading the environment.
177
+ *
178
+ * @remarks
179
+ * The test layer, and the escape hatch for an application that resolves its
180
+ * environment some other way. It needs no filesystem — v3's test layer reached
181
+ * past the platform abstraction for a `node:fs` temp directory.
182
+ */
183
+ static layerFrom(paths) {
184
+ return Layer.succeed(Xdg, paths);
185
+ }
186
+ };
187
+
188
+ //#endregion
189
+ export { CurrentPlatform, Xdg, XdgEnvError, XdgPaths, XdgPlatform };
package/XdgConfig.js ADDED
@@ -0,0 +1,115 @@
1
+ import { NativeDirs } from "./NativeDirs.js";
2
+ import { CurrentPlatform, Xdg } from "./Xdg.js";
3
+ import { AppDirs } from "./AppDirs.js";
4
+ import { Effect, FileSystem, Option, Path } from "effect";
5
+ import { Walker } from "@effected/walker";
6
+
7
+ //#region src/XdgConfig.ts
8
+ /**
9
+ * Search the app's XDG config search path for `filename`.
10
+ *
11
+ * @remarks
12
+ * Probes the app's own config directory first, then each `$XDG_CONFIG_DIRS`
13
+ * entry namespaced — `~/.config/myapp/rc`, then `/etc/xdg/myapp/rc`. v3 probed
14
+ * only the first of those; the system search path is half the XDG spec and it
15
+ * was missing.
16
+ *
17
+ * The scan runs through `Walker.firstMatch`, so a failure on one candidate means
18
+ * "this candidate did not match" and the search continues to the next. That is a
19
+ * bug fixed, not a refactor: v3 wrapped the whole resolver in a single
20
+ * `catchAll`, so an unreadable `/etc/xdg` aborted the probe and hid a perfectly
21
+ * readable `~/.config`. Not-found and cannot-look stay indistinguishable to the
22
+ * caller, which is the resolver contract — `resolve`'s error channel is `never`.
23
+ *
24
+ * Place it **before** `nativeResolver` in a chain, so an existing
25
+ * `~/.config/<app>` still wins over the OS-native directory.
26
+ *
27
+ * @public
28
+ */
29
+ const resolver = (options) => ({
30
+ name: "xdg",
31
+ resolve: Effect.gen(function* () {
32
+ const appDirs = yield* AppDirs;
33
+ const path = yield* Path.Path;
34
+ const fs = yield* FileSystem.FileSystem;
35
+ const candidates = appDirs.dirs.configSearchPath.map((dir) => path.join(dir, options.filename));
36
+ return yield* Walker.firstMatch(candidates, (candidate) => fs.exists(candidate));
37
+ })
38
+ });
39
+ /**
40
+ * Probe the OS-native config directory for `filename`.
41
+ *
42
+ * @remarks
43
+ * Resolves the native config directory for `namespace`
44
+ * (`~/Library/Application Support/<ns>` on macOS, `%APPDATA%\<ns>` on Windows)
45
+ * and checks whether `filename` is there. On Linux and everywhere else
46
+ * {@link NativeDirs.resolve} yields `Option.none()`, so this resolver returns
47
+ * `Option.none()` without probing at all — the XDG resolver already owns
48
+ * `~/.config` there.
49
+ *
50
+ * Takes `namespace` rather than reading it off {@link AppDirs}: the native
51
+ * directory is a property of the OS convention, not of however the app happened
52
+ * to configure its XDG directories, and a caller may well probe a *different*
53
+ * namespace than the one their `AppDirs` was built for.
54
+ *
55
+ * @public
56
+ */
57
+ const nativeResolver = (options) => ({
58
+ name: "native",
59
+ resolve: Effect.gen(function* () {
60
+ const paths = yield* Xdg;
61
+ const platform = yield* CurrentPlatform;
62
+ const path = yield* Path.Path;
63
+ const fs = yield* FileSystem.FileSystem;
64
+ const native = NativeDirs.resolve({
65
+ platform,
66
+ namespace: options.namespace,
67
+ paths,
68
+ path
69
+ });
70
+ if (Option.isNone(native)) return Option.none();
71
+ return yield* Walker.firstMatch([path.join(native.value.config, options.filename)], (candidate) => fs.exists(candidate));
72
+ })
73
+ });
74
+ /**
75
+ * The default save target for a config file: `<app config dir>/<filename>`.
76
+ *
77
+ * @remarks
78
+ * Drops straight into `ConfigFileOptions.defaultPath`, whose slot is typed
79
+ * `Effect<string, never, RR>`. That infallible channel is the whole reason
80
+ * {@link AppDirs} resolves at layer-construction time: with v3's per-access
81
+ * resolution this could fail, and a consumer had to `orDie` it into the slot.
82
+ *
83
+ * It does **not** create the directory — `ConfigFile.save` already `mkdir -p`s
84
+ * the parent of whatever path it is given.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * const layer = ConfigFile.layer(AppConfig, {
89
+ * schema: AppShape,
90
+ * codec: JsonCodec,
91
+ * strategy: MergeStrategy.firstMatch<AppShape>(),
92
+ * resolvers: [XdgConfig.resolver({ filename: "config.json" })],
93
+ * defaultPath: XdgConfig.savePath("config.json"),
94
+ * });
95
+ * ```
96
+ *
97
+ * @public
98
+ */
99
+ const savePath = (filename) => Effect.gen(function* () {
100
+ const appDirs = yield* AppDirs;
101
+ return (yield* Path.Path).join(appDirs.dirs.config, filename);
102
+ });
103
+ /**
104
+ * The bridge from XDG directories into `@effected/config-file`.
105
+ *
106
+ * @public
107
+ */
108
+ const XdgConfig = {
109
+ resolver,
110
+ nativeResolver,
111
+ savePath
112
+ };
113
+
114
+ //#endregion
115
+ export { XdgConfig };
package/index.d.ts ADDED
@@ -0,0 +1,392 @@
1
+ import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
2
+ import { ConfigResolver } from "@effected/config-file";
3
+ //#region src/Xdg.d.ts
4
+ /**
5
+ * The operating system the path decisions are taken against.
6
+ *
7
+ * @remarks
8
+ * The members are Node's `process.platform` values, modeled as a schema rather
9
+ * than borrowed from the ambient `NodeJS.Platform` type so the package's public
10
+ * surface names nothing it does not own.
11
+ *
12
+ * @public
13
+ */
14
+ declare const XdgPlatform: Schema.Literals<readonly ["aix", "android", "darwin", "freebsd", "haiku", "linux", "openbsd", "sunos", "win32", "cygwin", "netbsd"]>;
15
+ /**
16
+ * The decoded form of {@link (XdgPlatform:variable)}.
17
+ *
18
+ * @public
19
+ */
20
+ type XdgPlatform = typeof XdgPlatform.Type;
21
+ /**
22
+ * The platform every native-directory decision is taken against.
23
+ *
24
+ * @remarks
25
+ * A {@link https://effect.website | Context.Reference}, not a global read. It
26
+ * defaults to `process.platform`, so production behaviour is what you expect;
27
+ * a test pins macOS or Windows semantics with
28
+ * `Layer.succeed(CurrentPlatform, "win32")` and exercises the whole native-path
29
+ * matrix without touching a real filesystem or the real platform.
30
+ *
31
+ * @public
32
+ */
33
+ declare const CurrentPlatform: Context.Reference<XdgPlatform>;
34
+ declare const XdgEnvError_base: Schema.Class<XdgEnvError, Schema.TaggedStruct<"XdgEnvError", {
35
+ /** The environment variable that was required and not found. */
36
+ readonly variable: Schema.String;
37
+ /** The underlying failure, preserved structurally. */
38
+ readonly cause: Schema.Defect;
39
+ }>, import("effect/Cause").YieldableError>;
40
+ /**
41
+ * Indicates that the environment cannot satisfy XDG directory resolution.
42
+ *
43
+ * @remarks
44
+ * Raised only for `HOME`: every other XDG variable is optional by construction,
45
+ * and its absence is a resolved default rather than a failure. `cause` carries
46
+ * the underlying `ConfigError` structurally — v3 flattened it to a string.
47
+ *
48
+ * @public
49
+ */
50
+ declare class XdgEnvError extends XdgEnvError_base {
51
+ get message(): string;
52
+ }
53
+ declare const XdgPaths_base: Schema.Class<XdgPaths, Schema.Struct<{
54
+ /** `$HOME`. The one variable that must be set. */
55
+ readonly home: Schema.String;
56
+ /** `$XDG_CONFIG_HOME`. */
57
+ readonly configHome: Schema.optionalKey<Schema.String>;
58
+ /** `$XDG_DATA_HOME`. */
59
+ readonly dataHome: Schema.optionalKey<Schema.String>;
60
+ /** `$XDG_CACHE_HOME`. */
61
+ readonly cacheHome: Schema.optionalKey<Schema.String>;
62
+ /** `$XDG_STATE_HOME`. */
63
+ readonly stateHome: Schema.optionalKey<Schema.String>;
64
+ /** `$XDG_RUNTIME_DIR`. Absent on most non-Linux systems. */
65
+ readonly runtimeDir: Schema.optionalKey<Schema.String>;
66
+ /** `%APPDATA%`, on Windows. */
67
+ readonly appData: Schema.optionalKey<Schema.String>;
68
+ /** `%LOCALAPPDATA%`, on Windows. */
69
+ readonly localAppData: Schema.optionalKey<Schema.String>;
70
+ /** `$XDG_CONFIG_DIRS`, split on `:`. Defaults to `["/etc/xdg"]`. */
71
+ readonly configDirs: Schema.$Array<Schema.String>;
72
+ /** `$XDG_DATA_DIRS`, split on `:`. Defaults to `["/usr/local/share", "/usr/share"]`. */
73
+ readonly dataDirs: Schema.$Array<Schema.String>;
74
+ }>, {}>;
75
+ /**
76
+ * The XDG Base Directory environment, resolved.
77
+ *
78
+ * @remarks
79
+ * Every field but `home` is optional because the corresponding variable is:
80
+ * `Schema.optionalKey`, so an unset variable is an **absent key** and the read
81
+ * is `paths.configHome ?? fallback`. v3 modeled these as `Option`, which forced
82
+ * `Option.some(...)` into the construction API of everything downstream.
83
+ *
84
+ * `configDirs` and `dataDirs` are the colon-separated system search paths, split
85
+ * and defaulted per the spec (`/etc/xdg` and `/usr/local/share:/usr/share`).
86
+ * They are what make a config lookup a genuine ordered search rather than a
87
+ * single stat.
88
+ *
89
+ * @public
90
+ */
91
+ declare class XdgPaths extends XdgPaths_base {}
92
+ declare const Xdg_base: Context.ServiceClass<Xdg, "@effected/xdg/Xdg", XdgPaths>;
93
+ /**
94
+ * XDG Base Directory environment resolution.
95
+ *
96
+ * @remarks
97
+ * The service's shape **is** {@link XdgPaths}: the environment is read once, at
98
+ * layer construction, and the service is the resolved value. v3 exposed nine
99
+ * `Effect`s that each re-read the environment on every access, which is why
100
+ * every path downstream of it was fallible. Here `yield* Xdg` gives you a record
101
+ * of strings, and the only failure — an unset `HOME` — happens once, where the
102
+ * layer is built.
103
+ *
104
+ * @public
105
+ */
106
+ declare class Xdg extends Xdg_base {
107
+ /**
108
+ * Read the XDG environment through Effect's `Config`.
109
+ *
110
+ * @remarks
111
+ * Reads from the ambient `ConfigProvider`, which defaults to `process.env`.
112
+ * A test drives it with `ConfigProvider.layer(ConfigProvider.fromUnknown({…}))`
113
+ * and never mutates the real environment.
114
+ */
115
+ static readonly layer: Layer.Layer<Xdg, XdgEnvError>;
116
+ /**
117
+ * Serve fixed paths instead of reading the environment.
118
+ *
119
+ * @remarks
120
+ * The test layer, and the escape hatch for an application that resolves its
121
+ * environment some other way. It needs no filesystem — v3's test layer reached
122
+ * past the platform abstraction for a `node:fs` temp directory.
123
+ */
124
+ static layerFrom(paths: XdgPaths): Layer.Layer<Xdg>;
125
+ }
126
+ //#endregion
127
+ //#region src/AppDirs.d.ts
128
+ /**
129
+ * The four directory kinds XDG separates, plus the runtime directory.
130
+ *
131
+ * @public
132
+ */
133
+ declare const AppDirKind: Schema.Literals<readonly ["config", "data", "cache", "state", "runtime"]>;
134
+ /**
135
+ * The decoded form of {@link (AppDirKind:variable)}.
136
+ *
137
+ * @public
138
+ */
139
+ type AppDirKind = typeof AppDirKind.Type;
140
+ declare const AppDirsError_base: Schema.Class<AppDirsError, Schema.TaggedStruct<"AppDirsError", {
141
+ /** Which directory kind failed. */
142
+ readonly directory: Schema.Literals<readonly ["config", "data", "cache", "state", "runtime"]>;
143
+ /** The path that could not be created. */
144
+ readonly path: Schema.String;
145
+ /** The underlying failure, preserved structurally. */
146
+ readonly cause: Schema.Defect;
147
+ }>, import("effect/Cause").YieldableError>;
148
+ /**
149
+ * Indicates that an application directory could not be created.
150
+ *
151
+ * @remarks
152
+ * The only way `AppDirs` fails. Resolution cannot fail — it happens once, at
153
+ * layer construction, from an environment that is already resolved — so this
154
+ * error means exactly one thing: the `mkdir -p` did not work. `directory` says
155
+ * which kind, `path` says where, and `cause` carries the underlying
156
+ * `PlatformError` structurally. v3 carried `reason: String(e)` and a `directory`
157
+ * that could also be the string `"all"`.
158
+ *
159
+ * @public
160
+ */
161
+ declare class AppDirsError extends AppDirsError_base {
162
+ get message(): string;
163
+ }
164
+ declare const ResolvedAppDirs_base: Schema.Class<ResolvedAppDirs, Schema.Struct<{
165
+ /** The app's configuration directory. */
166
+ readonly config: Schema.String;
167
+ /** The app's data directory. */
168
+ readonly data: Schema.String;
169
+ /** The app's cache directory. */
170
+ readonly cache: Schema.String;
171
+ /** The app's state directory. */
172
+ readonly state: Schema.String;
173
+ /**
174
+ * The app's runtime directory.
175
+ *
176
+ * @remarks
177
+ * Absent unless `$XDG_RUNTIME_DIR` is set or `dirs.runtime` overrides it.
178
+ * There is no defensible fallback for a runtime directory — it must be
179
+ * user-owned, mode 0700 and cleaned on logout — so inventing one would be a
180
+ * lie, and the key is simply absent.
181
+ */
182
+ readonly runtime: Schema.optionalKey<Schema.String>;
183
+ /**
184
+ * Where to **look** for configuration, in priority order.
185
+ *
186
+ * @remarks
187
+ * The app's own config directory, then each `$XDG_CONFIG_DIRS` entry
188
+ * namespaced. This is the half of the XDG spec v3 ignored entirely, and it is
189
+ * what makes {@link XdgConfig.resolver} a real search rather than a single stat.
190
+ */
191
+ readonly configSearchPath: Schema.$Array<Schema.String>;
192
+ /** Where to look for data files, in priority order. */
193
+ readonly dataSearchPath: Schema.$Array<Schema.String>;
194
+ }>, {}>;
195
+ /**
196
+ * The fully resolved, app-namespaced directories.
197
+ *
198
+ * @public
199
+ */
200
+ declare class ResolvedAppDirs extends ResolvedAppDirs_base {}
201
+ /**
202
+ * Per-kind absolute directory overrides. Each wins outright over every other rung.
203
+ *
204
+ * @public
205
+ */
206
+ interface AppDirOverrides {
207
+ /** An absolute path for the config directory. */
208
+ readonly config?: string;
209
+ /** An absolute path for the data directory. */
210
+ readonly data?: string;
211
+ /** An absolute path for the cache directory. */
212
+ readonly cache?: string;
213
+ /** An absolute path for the state directory. */
214
+ readonly state?: string;
215
+ /** An absolute path for the runtime directory. */
216
+ readonly runtime?: string;
217
+ }
218
+ /**
219
+ * Options for {@link AppDirs.layer}.
220
+ *
221
+ * @remarks
222
+ * Plain optional fields, not `Option`s: v3 made callers write
223
+ * `fallbackDir: Option.some(".myapp"), dirs: Option.none()`. `Option` is an
224
+ * internal representation, not an input format.
225
+ *
226
+ * @public
227
+ */
228
+ interface AppDirsOptions {
229
+ /**
230
+ * The application namespace — one path component.
231
+ *
232
+ * @remarks
233
+ * Must be non-empty and free of path separators. A namespace containing `..`
234
+ * or `/` would resolve the app's directories outside `$HOME` entirely, so it
235
+ * is rejected as a **defect** at layer construction: it can only come from
236
+ * code, never from user input.
237
+ */
238
+ readonly namespace: string;
239
+ /**
240
+ * Use the OS-native directories where the platform has them. Defaults to `false`.
241
+ *
242
+ * @remarks
243
+ * Only consulted when no XDG variable and no explicit override applies, and
244
+ * only on darwin and win32 — on Linux there is nothing to override.
245
+ */
246
+ readonly native?: boolean;
247
+ /**
248
+ * A single dot-directory under `$HOME` that all four kinds collapse to.
249
+ *
250
+ * @remarks
251
+ * Relative to `$HOME`: `fallbackDir: ".myapp"` gives `$HOME/.myapp`.
252
+ */
253
+ readonly fallbackDir?: string;
254
+ /** Absolute per-kind overrides. */
255
+ readonly dirs?: AppDirOverrides;
256
+ }
257
+ /**
258
+ * App-namespaced directory resolution and on-demand creation.
259
+ *
260
+ * @remarks
261
+ * `dirs` is a **value**, not an `Effect`: the environment is fixed when the
262
+ * layer is built, so resolution happens there, exactly once. Reading a path
263
+ * cannot fail and cannot be observed to do IO. Only the `ensure*` operations
264
+ * touch the filesystem, and they are the only fallible members.
265
+ *
266
+ * @public
267
+ */
268
+ interface AppDirsShape {
269
+ /** The namespace these directories were resolved for. */
270
+ readonly namespace: string;
271
+ /** The resolved directories. Reading them cannot fail. */
272
+ readonly dirs: ResolvedAppDirs;
273
+ /** Create the config directory if it does not exist, and return it. */
274
+ readonly ensureConfig: Effect.Effect<string, AppDirsError>;
275
+ /** Create the data directory if it does not exist, and return it. */
276
+ readonly ensureData: Effect.Effect<string, AppDirsError>;
277
+ /** Create the cache directory if it does not exist, and return it. */
278
+ readonly ensureCache: Effect.Effect<string, AppDirsError>;
279
+ /** Create the state directory if it does not exist, and return it. */
280
+ readonly ensureState: Effect.Effect<string, AppDirsError>;
281
+ /**
282
+ * Create the runtime directory if there is one, and return it.
283
+ *
284
+ * @remarks
285
+ * `Option.none()` when no runtime directory is configured — nothing is created
286
+ * and nothing fails.
287
+ */
288
+ readonly ensureRuntime: Effect.Effect<Option.Option<string>, AppDirsError>;
289
+ /** Create every directory that exists in the resolution, and return them all. */
290
+ readonly ensure: Effect.Effect<ResolvedAppDirs, AppDirsError>;
291
+ }
292
+ declare const AppDirs_base: Context.ServiceClass<AppDirs, "@effected/xdg/AppDirs", AppDirsShape>;
293
+ /**
294
+ * App-namespaced XDG directories, with on-demand creation.
295
+ *
296
+ * @remarks
297
+ * `AppDirs.layer` is a layer-**returning function**: calling it twice builds two
298
+ * independent services. Bind its result to a const and provide that const, per
299
+ * the layer memoization discipline.
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * const AppDirsLayer = AppDirs.layer({ namespace: "myapp", native: true });
304
+ * const XdgLayer = Layer.mergeAll(Xdg.layer, AppDirsLayer.pipe(Layer.provide(Xdg.layer)));
305
+ * ```
306
+ *
307
+ * @public
308
+ */
309
+ declare class AppDirs extends AppDirs_base {
310
+ /**
311
+ * Resolve the namespace's directories against the ambient {@link Xdg}
312
+ * environment and platform.
313
+ *
314
+ * @remarks
315
+ * The error channel is `never`. The one failure that could happen during
316
+ * resolution — an unset `HOME` — surfaces on {@link Xdg.layer} as an
317
+ * `XdgEnvError`, before an `AppDirs` exists at all. v3 laundered it into an
318
+ * `AppDirsError({ directory: "all" })`.
319
+ */
320
+ static layer(options: AppDirsOptions): Layer.Layer<AppDirs, never, Xdg | FileSystem.FileSystem | Path.Path>;
321
+ }
322
+ //#endregion
323
+ //#region src/NativeDirs.d.ts
324
+ declare const NativeDirs_base: Schema.Class<NativeDirs, Schema.Struct<{
325
+ /** Where configuration lives. */
326
+ readonly config: Schema.String;
327
+ /** Where application data lives. */
328
+ readonly data: Schema.String;
329
+ /** Where discardable cached data lives. */
330
+ readonly cache: Schema.String;
331
+ /** Where persistent-but-regenerable state lives. */
332
+ readonly state: Schema.String;
333
+ }>, {}>;
334
+ /**
335
+ * The OS-native application directories for one namespace.
336
+ *
337
+ * @remarks
338
+ * On platforms without OS-level separation of these concerns, `config`, `data`
339
+ * and `state` collapse to the same directory — `~/Library/Application Support/<ns>`
340
+ * on macOS — while `cache` stays distinct.
341
+ *
342
+ * @public
343
+ */
344
+ declare class NativeDirs extends NativeDirs_base {
345
+ /**
346
+ * Map a platform and an environment onto the native directories for a namespace.
347
+ *
348
+ * @remarks
349
+ * **Pure**: no filesystem, no environment, no `process.platform`. Every input
350
+ * is a parameter, which is what makes the whole platform matrix testable
351
+ * without any platform IO. Paths are joined through the supplied `Path`, so a
352
+ * win32 `Path` layer yields win32 separators — v3 interpolated `/` on every
353
+ * platform.
354
+ *
355
+ * - **darwin** — `config`/`data`/`state` under `~/Library/Application Support/<ns>`;
356
+ * `cache` under `~/Library/Caches/<ns>`.
357
+ * - **win32** — `config`/`data` under `%APPDATA%/<ns>`; `cache` under
358
+ * `%LOCALAPPDATA%/<ns>/Cache`; `state` under `%LOCALAPPDATA%/<ns>`. When the
359
+ * variables are unset, `%APPDATA%` falls back to `<home>/AppData/Roaming` and
360
+ * `%LOCALAPPDATA%` to `<home>/AppData/Local`.
361
+ * - **everything else** — `Option.none()`. On Linux, XDG *is* the native
362
+ * convention, so there is no override to apply; returning `none` rather than
363
+ * a duplicate of the XDG answer is what lets a precedence ladder skip the
364
+ * rung cleanly instead of shadowing the rung below it.
365
+ */
366
+ static resolve(input: {
367
+ readonly platform: XdgPlatform;
368
+ readonly namespace: string;
369
+ readonly paths: XdgPaths;
370
+ readonly path: Path.Path;
371
+ }): Option.Option<NativeDirs>;
372
+ }
373
+ //#endregion
374
+ //#region src/XdgConfig.d.ts
375
+ /**
376
+ * The bridge from XDG directories into `@effected/config-file`.
377
+ *
378
+ * @public
379
+ */
380
+ declare const XdgConfig: {
381
+ readonly resolver: (options: {
382
+ readonly filename: string;
383
+ }) => ConfigResolver<AppDirs | FileSystem.FileSystem | Path.Path>;
384
+ readonly nativeResolver: (options: {
385
+ readonly namespace: string;
386
+ readonly filename: string;
387
+ }) => ConfigResolver<Xdg | FileSystem.FileSystem | Path.Path>;
388
+ readonly savePath: (filename: string) => Effect.Effect<string, never, AppDirs | Path.Path>;
389
+ };
390
+ //#endregion
391
+ export { AppDirKind, type AppDirOverrides, AppDirs, AppDirsError, type AppDirsOptions, type AppDirsShape, CurrentPlatform, NativeDirs, ResolvedAppDirs, Xdg, XdgConfig, XdgEnvError, XdgPaths, XdgPlatform };
392
+ //# sourceMappingURL=index.d.ts.map
package/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { NativeDirs } from "./NativeDirs.js";
2
+ import { CurrentPlatform, Xdg, XdgEnvError, XdgPaths, XdgPlatform } from "./Xdg.js";
3
+ import { AppDirKind, AppDirs, AppDirsError, ResolvedAppDirs } from "./AppDirs.js";
4
+ import { XdgConfig } from "./XdgConfig.js";
5
+
6
+ export { AppDirKind, AppDirs, AppDirsError, CurrentPlatform, NativeDirs, ResolvedAppDirs, Xdg, XdgConfig, XdgEnvError, XdgPaths, XdgPlatform };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@effected/xdg",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "XDG Base Directory resolution for Effect: environment paths, app-namespaced directories, native OS conventions and config-file resolvers.",
6
+ "keywords": [
7
+ "xdg",
8
+ "basedir",
9
+ "appdirs",
10
+ "config",
11
+ "paths",
12
+ "effect",
13
+ "effected"
14
+ ],
15
+ "homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/xdg#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/spencerbeggs/effected/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/spencerbeggs/effected.git",
22
+ "directory": "packages/xdg"
23
+ },
24
+ "license": "MIT",
25
+ "author": {
26
+ "name": "C. Spencer Beggs",
27
+ "email": "spencer@beggs.codes",
28
+ "url": "https://spencerbeg.gs"
29
+ },
30
+ "sideEffects": false,
31
+ "type": "module",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./index.d.ts",
35
+ "import": "./index.js"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "peerDependencies": {
40
+ "@effected/config-file": "0.1.0",
41
+ "@effected/walker": "0.1.0",
42
+ "effect": "4.0.0-beta.98"
43
+ },
44
+ "engines": {
45
+ "node": ">=24.11.0"
46
+ }
47
+ }
@@ -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.58.9"
9
+ }
10
+ ]
11
+ }