fullnative 0.2.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/MIGRATION.md +161 -0
  3. package/README.md +128 -34
  4. package/dist/core/env/env.d.ts +37 -5
  5. package/dist/core/env/env.d.ts.map +1 -1
  6. package/dist/core/env/env.js +27 -6
  7. package/dist/core/env/env.js.map +1 -1
  8. package/dist/core/env/index.d.ts +1 -1
  9. package/dist/core/env/index.d.ts.map +1 -1
  10. package/dist/core/env/index.js.map +1 -1
  11. package/dist/core/file/File.d.ts +13 -11
  12. package/dist/core/file/File.d.ts.map +1 -1
  13. package/dist/core/file/File.js +15 -14
  14. package/dist/core/file/File.js.map +1 -1
  15. package/dist/core/file/Folder.d.ts +11 -11
  16. package/dist/core/file/Folder.d.ts.map +1 -1
  17. package/dist/core/file/Folder.js +44 -27
  18. package/dist/core/file/Folder.js.map +1 -1
  19. package/dist/core/index.d.ts +2 -1
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/core/index.js +1 -0
  22. package/dist/core/index.js.map +1 -1
  23. package/dist/core/process/Command.d.ts +19 -0
  24. package/dist/core/process/Command.d.ts.map +1 -1
  25. package/dist/core/process/Command.js +36 -1
  26. package/dist/core/process/Command.js.map +1 -1
  27. package/dist/core/process/Result.d.ts +10 -2
  28. package/dist/core/process/Result.d.ts.map +1 -1
  29. package/dist/core/process/Result.js +12 -2
  30. package/dist/core/process/Result.js.map +1 -1
  31. package/dist/core/process/types.d.ts +4 -0
  32. package/dist/core/process/types.d.ts.map +1 -1
  33. package/dist/core/shell/Shell.d.ts +33 -5
  34. package/dist/core/shell/Shell.d.ts.map +1 -1
  35. package/dist/core/shell/Shell.js +49 -7
  36. package/dist/core/shell/Shell.js.map +1 -1
  37. package/dist/core/shell/types.d.ts +7 -3
  38. package/dist/core/shell/types.d.ts.map +1 -1
  39. package/dist/core/utils/index.d.ts +4 -0
  40. package/dist/core/utils/index.d.ts.map +1 -0
  41. package/dist/core/utils/index.js +4 -0
  42. package/dist/core/utils/index.js.map +1 -0
  43. package/dist/core/utils/sleep.d.ts +14 -0
  44. package/dist/core/utils/sleep.d.ts.map +1 -0
  45. package/dist/core/utils/sleep.js +16 -0
  46. package/dist/core/utils/sleep.js.map +1 -0
  47. package/dist/core/utils/tempDir.d.ts +59 -0
  48. package/dist/core/utils/tempDir.d.ts.map +1 -0
  49. package/dist/core/utils/tempDir.js +92 -0
  50. package/dist/core/utils/tempDir.js.map +1 -0
  51. package/dist/core/utils/timeout.d.ts +40 -0
  52. package/dist/core/utils/timeout.d.ts.map +1 -0
  53. package/dist/core/utils/timeout.js +54 -0
  54. package/dist/core/utils/timeout.js.map +1 -0
  55. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.0] - 2026-10-05
9
+
10
+ First stable release. The public API is now frozen: from here on, breaking changes require a major version.
11
+
12
+ ### Breaking changes
13
+
14
+ - **`File.moveTo`, `File.moveInto`, `File.rename`, `Folder.moveTo`, `Folder.moveInto`, `Folder.rename` no longer mutate the instance.** They move/rename the file or folder on disk and return a **new** instance (`Promise<File>` / `Promise<Folder>`) pointing at the new path; the original instance keeps its old path. This unifies their semantics with `copyTo`/`copyInto`, which already returned new instances, and removes the aliasing bugs caused by silent `this.path` mutation. (`File.moveTo`/`rename` returned `this`; `Folder.moveTo`/`rename` returned `this`.)
15
+ - **`Result.lines` now returns all lines** of stdout as `stdout.split(/\r?\n/)`, **including empty lines** — an output ending with a newline produces a final empty line. The previous behavior (empty lines filtered out) is still available in the new `Result.nonEmptyLines` getter.
16
+ - **`ShellConfig.shell` was removed.** It was dead configuration: the actual shell is chosen internally by the library (`/bin/sh` on Unix, `cmd.exe` on Windows). Remove the field from your `Shell` options — there is no replacement.
17
+
18
+ ### Added
19
+
20
+ - `File.absolute` getter: absolute path resolved against `process.cwd()` (parity with `Folder.absolute`).
21
+ - `Result.nonEmptyLines` getter: stdout split into lines with empty lines filtered out (the pre-1.0 `lines` behavior).
22
+ - `Command.withAbort(signal)`: immutable builder method that associates an `AbortSignal` with the command. On abort the process is killed with `SIGTERM`, `LiveProcess` ends with `stopped === true`, and `wait()` resolves with a normal `Result` (never rejects with `AbortError`). The signal is deliberately not passed to the native `spawn()`, which would emit an `error` event with `AbortError`.
23
+ - `env.load()` options: accepts `string | { path?: string; override?: boolean }` (`LoadOptions`, default `".env"`). With `override: true` the file's variables overwrite existing `process.env` entries; by default the real environment always wins (unchanged semantics). The string signature `load("./file.env")` keeps working.
24
+ - `env.load()` now returns `Promise<Record<string, string>>` with the variables actually applied by the call: in normal mode only the keys that weren't already in `process.env`, with `override: true` every resolved variable.
25
+ - New `utils` module: `sleep(ms)` (promise-based pause), `timeout(promise, ms, message?)` (deadline for any promise, rejects with the new `TimeoutError` class — `name === "TimeoutError"`, default message includes the ms), and `tempDir({ prefix?, keep? })` returning a `TempDir` instance (`extends Folder`, with `dispose()`); created with `fs.mkdtemp` under `os.tmpdir()` (default prefix `"fullnative-"`), auto-removed at process exit via a single global `exit` hook unless `keep: true`. Types `LoadOptions` and `TempDirOptions` are exported.
26
+
27
+ ### Fixed
28
+
29
+ - `Folder.delete(recursive)` uses `fs.rm(path, { recursive, force: true })` instead of the deprecated `fs.rmdir` (`DEP0147`, deprecated since Node 22 and slated for removal). Return semantics unchanged: `true` if it existed and was deleted.
30
+ - `Folder.tree()` rendering: subdirectories were painted twice, indentation was duplicated after each level, and non-last-child directories were forced to the `└──` connector. Now every entry is rendered exactly once, with correct `├──`/`└──` connectors and child prefixes `│ ` (not-last) / ` ` (last).
31
+ - `Shell.cd(target)` resolves relative paths against the session's current cwd (`path.resolve`) and adopts absolute paths as-is. Before, the literal string was stored, producing a silently invalid `cwd` that exploded later on the first command.
32
+ - `Shell.pipe()` hardened: validates that `stdout`/`stdin` exist at every hop (clear `TypeError` when missing) and attaches safe error listeners to piped streams so stream errors (`EPIPE`, e.g. when an intermediate command dies) resolve as a failed `Result` instead of crashing the host process with unhandled errors.
33
+
34
+ ### Documented
35
+
36
+ - The `$` tagged template generates POSIX-style quoting (`sh`/`bash`), which is **not** safe for `cmd.exe` on Windows. Windows-safe quoting is a future feature.
37
+
8
38
  ## [0.1.0] - 2026-08-14
9
39
 
10
40
  ### Added
package/MIGRATION.md ADDED
@@ -0,0 +1,161 @@
1
+ # Migration guide: 0.x → 1.0
2
+
3
+ `fullnative` 1.0 is the first stable release; the public API is frozen from here on. This guide covers the three changes that can actually break your code when upgrading from 0.x, plus the additions that come along for free.
4
+
5
+ ```bash
6
+ npm install fullnative@1.0.0
7
+ # or
8
+ pnpm add fullnative@1.0.0
9
+ ```
10
+
11
+ ---
12
+
13
+ ## 1. `moveTo()` / `moveInto()` / `rename()` no longer mutate — they return a new instance
14
+
15
+ **What changed:** `File.moveTo`, `File.moveInto`, `File.rename`, `Folder.moveTo`, `Folder.moveInto` and `Folder.rename` physically move/rename the file or folder on disk (same as before), but they no longer mutate the instance or return `this`. They return a **new** instance (`Promise<File>` / `Promise<Folder>`) pointing at the new path, while the original instance keeps its old path.
16
+
17
+ This matches `copyTo()` / `copyInto()`, which already returned a new instance.
18
+
19
+ **Why:** silently mutating `this.path` made any alias of the same object desynchronize — a `File` passed to a helper could change its path without the caller knowing. Returning a new instance removes that entire class of bugs.
20
+
21
+ ### Before (0.x)
22
+
23
+ ```ts
24
+ const f = new File("./data.txt");
25
+
26
+ // Code that relied on mutation:
27
+ await f.moveTo("./archive/data.txt");
28
+ console.log(f.path); // "./archive/data.txt" — mutated!
29
+ await f.rename("data.old"); // also mutated f
30
+ // `f` was the single handle to the moved file
31
+ ```
32
+
33
+ ### After (1.0)
34
+
35
+ ```ts
36
+ const f = new File("./data.txt");
37
+
38
+ const moved = await f.moveTo("./archive/data.txt");
39
+ console.log(f.path); // "./data.txt" — unchanged
40
+ console.log(moved.path); // "./archive/data.txt"
41
+
42
+ const renamed = await moved.rename("data.old");
43
+ // renamed points at "./archive/data.old"; `moved` still points at "./archive/data.txt"
44
+
45
+ // Use the returned instance for everything that follows:
46
+ const contents = await moved.read();
47
+ ```
48
+
49
+ ### How to migrate
50
+
51
+ - **If you used the return value as `this` for chaining** (`await f.moveTo(x).rename(y)` — no; chained awaits):
52
+
53
+ ```ts
54
+ // 0.x
55
+ await file.moveTo("./a");
56
+ await file.rename("b"); // renamed because `file` had mutated
57
+
58
+ // 1.0 — carry the returned instance forward
59
+ const moved = await file.moveTo("./a");
60
+ const renamed = await moved.rename("b");
61
+ ```
62
+
63
+ - **If you relied on the original instance changing:** replace your old variable with the returned one, or reassign it explicitly:
64
+
65
+ ```ts
66
+ // Keep the same variable name if you want old code to keep working:
67
+ file = await file.moveTo("./archive/data.txt");
68
+ ```
69
+
70
+ The same applies to `Folder`:
71
+
72
+ ```ts
73
+ const project = new Folder("./my-app");
74
+ const moved = await project.moveTo("./archive/my-app");
75
+ await moved.createFile("marker.txt", "ok"); // writes into the moved dir
76
+ ```
77
+
78
+ ---
79
+
80
+ ## 2. `Result.lines` now includes empty lines — use `nonEmptyLines` for the old behavior
81
+
82
+ **What changed:** `Result.lines` is now the plain `String.split(/\r?\n/)` of stdout, **including empty lines**. An output that ends with `\n` — which is the common case — now produces a final empty line in the array.
83
+
84
+ The old behavior (empty lines filtered out) is still there: use the new **`Result.nonEmptyLines`** getter.
85
+
86
+ **Why:** a method named `lines` that behaves like `split` should behave like `split`. Silent filtering corrupted cases where empty lines are data (CSVs, logs, blocks separated by blank lines).
87
+
88
+ ### Before (0.x)
89
+
90
+ ```ts
91
+ const result = await proc.run("printf", "a\n\nb\n");
92
+ result.lines; // ["a", "b"] — empty lines silently filtered
93
+ ```
94
+
95
+ ### After (1.0)
96
+
97
+ ```ts
98
+ const result = await proc.run("printf", "a\n\nb\n");
99
+ result.lines; // ["a", "", "b", ""] — plain split, including the trailing empty line
100
+ result.nonEmptyLines; // ["a", "b"] — the old filtered behavior
101
+ ```
102
+
103
+ ### How to migrate
104
+
105
+ Search your code for `\.lines` on command results and decide, case by case:
106
+
107
+ - You were counting on filtered empty lines → switch to `result.nonEmptyLines` (drop-in, same array shape).
108
+ - You want raw lines (e.g. to preserve blank separators or reconstruct the exact output) → keep `result.lines`.
109
+
110
+ If you never filtered them yourself and just consumed the array, `nonEmptyLines` is almost always what you were implicitly assuming.
111
+
112
+ ---
113
+
114
+ ## 3. Remove `ShellConfig.shell` — it was never used
115
+
116
+ **What changed:** the `shell` field was removed from the `Shell` constructor options. It was dead configuration: `fullnative` chooses the actual shell internally (`/bin/sh` on Unix, `cmd.exe` on Windows), and no option can change that.
117
+
118
+ ### Before (0.x)
119
+
120
+ ```ts
121
+ // Compiled with a warning at best — the field was never respected
122
+ const sh = new Shell({ cwd: "/my/project", shell: "/bin/bash" });
123
+ ```
124
+
125
+ ### After (1.0)
126
+
127
+ ```ts
128
+ // Just remove the field
129
+ const sh = new Shell({ cwd: "/my/project" });
130
+ ```
131
+
132
+ ### How to migrate
133
+
134
+ Delete the property. There is no replacement: if TypeScript was enforcing it, the build itself will point you to every occurrence. Type-checking will catch this one mechanically — `cwd` and `env` remain valid options.
135
+
136
+ ---
137
+
138
+ ## Non-breaking additions in the same release
139
+
140
+ Everything below is additive — no action needed:
141
+
142
+ - **`File.absolute`** — absolute path resolved against `process.cwd()` (parity with `Folder.absolute`).
143
+ - **`Result.nonEmptyLines`** — covered above; new getter, no conflicts.
144
+ - **`Command.withAbort(signal)`** — associate an `AbortSignal` with a command; on abort the process is killed with `SIGTERM`, the `LiveProcess` ends with `stopped === true`, and `wait()` resolves with a normal `Result`. The signal is deliberately not passed to the native `spawn()` to avoid a native `AbortError` rejection.
145
+ - **`load()` options and return value** — accepts `string | { path?: string; override?: boolean }` and now returns a `Promise<Record<string, string>>` with the variables actually applied. The string signature `load("./file.env")` and the default semantics (the real environment always wins) are unchanged, so existing calls keep working. Use `override: true` when you want the file to overwrite the environment.
146
+ - **New `utils` module** — `sleep(ms)`, `timeout(promise, ms, message?)` with `TimeoutError`, and `tempDir({ prefix?, keep? })` returning a `TempDir` (extends `Folder`, with `dispose()` and auto-cleanup at process exit unless `keep: true`).
147
+
148
+ ### Behavioral fixes you may notice (no migration needed)
149
+
150
+ - `Folder.delete(recursive)` now uses `fs.rm` under the hood (the deprecated `fs.rmdir` is gone) — return semantics unchanged.
151
+ - `Folder.tree()` output changed: correct `├──`/`└──` connectors, no duplicated entries or indentation. If you parse tree output, re-verify your parsing.
152
+ - `Shell.cd()` resolves relative targets against the session cwd instead of storing the literal string.
153
+ - `Shell.pipe()` no longer crashes the host process on stream errors (`EPIPE`); a broken pipeline resolves as a failed `Result`.
154
+
155
+ ---
156
+
157
+ ## Questions
158
+
159
+ - **Do I have to change anything for `load()`?** No — the string signature still works and the default merge semantics did not change. The return value is new information you can start using (or ignore).
160
+ - **Can I keep mutating-style code by reassigning?** Yes: `file = await file.moveTo(dest)` restores the old feel at the cost of one extra assignment. We recommend holding both instances when you need the history.
161
+ - **Is `$` safe on Windows?** The quoting is POSIX-style, which is not safe for `cmd.exe`. Windows-safe quoting is on the roadmap; until then, avoid `$` in Windows scripts.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
- # fullnative
1
+ ![logo](/logo.png)
2
2
 
3
- TypeScript toolchain for Node.js — files, folders, processes, shell sessions, and environment variables with a clean, object-oriented API for stateful resources and functional utilities for the rest.
3
+ # Full Native
4
+
5
+ TypeScript toolchain for Node.js — files, folders, processes, shell sessions, environment variables, and promise utilities with a clean, object-oriented API for stateful resources and functional utilities for the rest.
4
6
 
5
7
  [![npm](https://img.shields.io/npm/v/fullnative.svg)](https://www.npmjs.com/package/fullnative)
6
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
@@ -11,7 +13,8 @@ Node's built-in modules are powerful but verbose. `fullnative` wraps them in sim
11
13
 
12
14
  - **Zero dependencies** — uses only Node.js built-ins (`fs`, `child_process`, `crypto`, `util.parseEnv`)
13
15
  - **Fully typed** — ships with `.d.ts` declarations for every module
14
- - **Object-oriented for stateful resources** (`File`, `Folder`, `Process`, `Shell` are classes you instantiate and chain), **functional utilities for the rest** (`env` module is plain functions)
16
+ - **Object-oriented for stateful resources** (`File`, `Folder`, `Process`, `Shell` are classes you instantiate and chain), **functional utilities for the rest** (`env` and `utils` modules are plain functions)
17
+ - **Immutable movement semantics** — `moveTo()`, `moveInto()` and `rename()` return a **new** instance pointing at the new path, like `copyTo()`; the original instance never mutates (`File`, `Folder`)
15
18
  - **Node.js >= 20.12** — takes advantage of native `util.parseEnv` for `.env` parsing
16
19
  - **ESM only** — ships as `"type": "module"` with `import`/`export`. No CJS support.
17
20
 
@@ -25,13 +28,15 @@ pnpm add fullnative
25
28
  yarn add fullnative
26
29
  ```
27
30
 
31
+ > **Upgrading from 0.x?** 1.0 is the first stable release and ships intentional breaking changes (immutable `moveTo()`/`rename()`, `Result.lines` split semantics, removed `ShellConfig.shell`). See [MIGRATION.md](./MIGRATION.md) for a step-by-step guide.
32
+
28
33
  ## Quick start
29
34
 
30
35
  ```ts
31
- import { File, Folder, Process, Shell, load } from "fullnative";
36
+ import { File, Folder, Process, Shell, load, sleep, tempDir } from "fullnative";
32
37
 
33
38
  // Load .env variables with ${VAR} interpolation
34
- await load();
39
+ const applied = await load(); // record of the variables actually applied
35
40
 
36
41
  // Work with files
37
42
  const config = new File("./config.json");
@@ -41,6 +46,11 @@ await config.writeJson({ port: 3000 });
41
46
  const proc = new Process();
42
47
  const result = await proc.run("git", "log", "--oneline");
43
48
  console.log(result.stdout);
49
+
50
+ // Promise utilities: auto-cleaned temp dir + readable pause
51
+ const tmp = await tempDir();
52
+ await tmp.file("scratch.txt").write("working...");
53
+ await sleep(500);
44
54
  ```
45
55
 
46
56
  ---
@@ -57,7 +67,9 @@ console.log(result.stdout);
57
67
  - [Shell](#shell)
58
68
  - [Job](#job)
59
69
  - [env](#env)
70
+ - [utils](#utils)
60
71
  - [API Reference](#api-reference)
72
+ - [Migration guide](./MIGRATION.md)
61
73
  - [Requirements](#requirements)
62
74
  - [Roadmap](#roadmap)
63
75
 
@@ -97,10 +109,13 @@ const hash = await file.hash("sha256");
97
109
  const same = await file.equals(new File("./other.txt"));
98
110
  const matches = await file.contentEquals("exact content");
99
111
 
100
- // Copy returns a NEW File; move/rename mutate this.path and return this
101
- const backup = await file.copyTo("./backup/data.txt"); // new File
102
- await file.moveTo("./archive/data.txt"); // same instance, path updated
103
- await file.rename("renamed.txt"); // same instance, path updated
112
+ // Copy & move return a NEW instance; the original never mutates
113
+ const copy = await file.copyTo("./backup/data.txt"); // new File at "./backup/data.txt"
114
+ const moved = await file.moveTo("./archive/data.txt"); // new File at "./archive/data.txt"
115
+ const renamed = await file.rename("renamed.txt"); // new File at "./archive/renamed.txt"
116
+ const tucked = await file.moveInto("./archive"); // new File at "./archive/data.txt" (keeps name)
117
+
118
+ // file.path is still "./data.txt" — only the returned instances point elsewhere
104
119
 
105
120
  // Streams
106
121
  const readable = file.readStream(); // Readable stream
@@ -111,6 +126,7 @@ const size = await file.size(); // bytes
111
126
  const empty = await file.isEmpty(); // true if 0 bytes
112
127
  const exists = await file.exists();
113
128
  const stat = await file.stat(); // fs.Stats
129
+ const abs = file.absolute; // absolute path resolved against process.cwd()
114
130
 
115
131
  // Delete
116
132
  await file.delete(); // returns true if existed
@@ -118,8 +134,8 @@ await file.delete(); // returns true if existed
118
134
 
119
135
  **Key behaviors:**
120
136
  - `write()`, `append()`, `prepend()` create parent directories automatically.
121
- - `copyTo()` / `copyInto()` return a **new** `File` instance.
122
- - `moveTo()` / `moveInto()` / `rename()` mutate `this.path` and return `this` for chaining.
137
+ - `copyTo()`, `copyInto()`, `moveTo()`, `moveInto()` and `rename()` return a **new** `File` instance; the original instance keeps its old path.
138
+ - `absolute` resolves the instance path against `process.cwd()`.
123
139
 
124
140
  ---
125
141
 
@@ -136,7 +152,7 @@ const project = new Folder("./my-app");
136
152
  await project.ensure(); // create if missing
137
153
  await project.create(); // mkdir -p
138
154
  await project.clear(); // empty contents
139
- await project.delete(true); // recursive delete
155
+ await project.delete(true); // recursive delete (fs.rm with force)
140
156
 
141
157
  // Navigation (returns references, doesn't check existence)
142
158
  const entry: File = project.file("index.ts");
@@ -172,19 +188,22 @@ const matched = await project.matchFiles(/\.test\.ts$/); // File[]
172
188
  const newFile = await project.createFile("note.txt", "hello");
173
189
  const newDir = await project.createDir("utils");
174
190
 
175
- // Copy & move (same semantics as File)
176
- const copy = await project.copyTo("./backup/my-app"); // new Folder
177
- await project.moveTo("./archive/my-app"); // same instance
178
- await project.rename("renamed-app"); // same instance
191
+ // Copy & move return a NEW instance; the original never mutates
192
+ const copy = await project.copyTo("./backup/my-app"); // new Folder at "./backup/my-app"
193
+ const moved = await project.moveTo("./archive/my-app"); // new Folder at "./archive/my-app"
194
+ const renamed = await project.rename("renamed-app"); // new Folder at "./renamed-app"
195
+ const tucked = await project.moveInto("./archive"); // new Folder at "./archive/my-app" (keeps name)
196
+
197
+ // project.path is still "./my-app" — only the returned instances point elsewhere
179
198
 
180
199
  // Tree visualization
181
200
  const tree = await project.tree();
182
201
  console.log(tree);
183
- // └── my-app/
184
- // ├── index.ts
185
- // ├── src/
186
- // │ └── utils.ts
187
- // └── package.json
202
+ // my-app/
203
+ // ├── index.ts
204
+ // ├── src/
205
+ // │ └── utils.ts
206
+ // └── package.json
188
207
  ```
189
208
 
190
209
  ---
@@ -203,7 +222,8 @@ const result = await proc.run("git", "log", "--oneline");
203
222
  console.log(result.stdout); // captured output
204
223
  console.log(result.ok); // true if exitCode === 0
205
224
  console.log(result.exitCode); // 0
206
- console.log(result.lines); // stdout split into lines (empty lines filtered)
225
+ console.log(result.lines); // stdout split into lines (includes empty lines)
226
+ console.log(result.nonEmptyLines); // stdout split into lines (empty lines filtered)
207
227
  console.log(result.durationMs); // execution time in ms
208
228
 
209
229
  // Get stdout only (trimmed)
@@ -232,6 +252,7 @@ const cmd = proc
232
252
  .in("/my/project") // set cwd
233
253
  .withEnv({ CI: "true" }) // merge env vars
234
254
  .withTimeout(30000) // kill after 30s
255
+ .withAbort(controller.signal) // kill with SIGTERM when the signal aborts
235
256
  .withInput("stdin data\n") // pipe to stdin
236
257
  .throwOnError(); // reject on non-zero exit
237
258
 
@@ -240,6 +261,11 @@ const out = await cmd.output(); // stdout.trim()
240
261
  const handle = cmd.spawn(); // returns LiveProcess
241
262
  ```
242
263
 
264
+ `withAbort(signal)` associates an `AbortSignal` with the command — the same primitive used by `fetch`, `fs` and timers, so it integrates with any framework that already uses `AbortController`. Notes:
265
+
266
+ - The signal is **not** passed to the native `spawn()`. On abort, the process is killed with `SIGTERM`, the `LiveProcess` ends with `stopped === true`, and `wait()` resolves with a normal `Result` — it never rejects with an `AbortError`.
267
+ - `withTimeout(ms)` kills the process with `SIGTERM` after the deadline, exactly like before.
268
+
243
269
  ### LiveProcess (interactive handle)
244
270
 
245
271
  A running process you can interact with in real time.
@@ -285,7 +311,8 @@ result.exitCode; // 0
285
311
  result.ok; // true
286
312
  result.failed; // false
287
313
  result.output; // stdout + stderr, trimmed
288
- result.lines; // ["{\"ok\":true}"]
314
+ result.lines; // ['{"ok":true}', ''] — plain split, includes the empty trailing line
315
+ result.nonEmptyLines; // ['{"ok":true}'] — empty lines filtered out
289
316
  result.durationMs; // 12
290
317
  result.json(); // { ok: true } — parses stdout as JSON
291
318
  result.json<{ ok: boolean }>(); // typed
@@ -294,6 +321,8 @@ result.json<{ ok: boolean }>(); // typed
294
321
  result.throwIfFailed(); // throws ProcessError if exitCode !== 0, returns this if ok
295
322
  ```
296
323
 
324
+ `lines` is a plain `String.split(/\r?\n/)` of stdout, so it includes empty lines — an output that ends with a newline produces a final empty line. For the old filtered behavior, use `nonEmptyLines`.
325
+
297
326
  ---
298
327
 
299
328
  ## ProcessError
@@ -331,6 +360,8 @@ The `kind` field distinguishes between:
331
360
 
332
361
  A shell session with state (cwd, env, aliases, history) that delegates to `Process` internally.
333
362
 
363
+ The config object accepts only `cwd` and `env` — the shell binary itself is chosen internally (`/bin/sh` on Unix, `cmd.exe` on Windows), so there is nothing to configure.
364
+
334
365
  ```ts
335
366
  import { Shell } from "fullnative";
336
367
 
@@ -341,7 +372,8 @@ const result = await sh.run("npm install && npm run build");
341
372
  console.log(result.ok);
342
373
 
343
374
  // Change directory
344
- sh.cd("./src");
375
+ sh.cd("./src"); // relative paths resolve against the session cwd
376
+ sh.cd("/other/project"); // absolute paths are adopted as-is
345
377
  console.log(sh.cwd); // "/my/project/src"
346
378
 
347
379
  // Environment variables
@@ -363,6 +395,8 @@ sh.clearHistory();
363
395
 
364
396
  Interpolates values with automatic shell quoting. Strings with special characters are escaped; arrays expand to separate arguments.
365
397
 
398
+ > **Windows limitation:** the quoting style is POSIX (`sh`/`bash`), so the generated scripts are **not** safe for `cmd.exe` on Windows, where escaping rules differ.
399
+
366
400
  ```ts
367
401
  const branch = "main";
368
402
  const files = ["a.ts", "b.ts"];
@@ -382,6 +416,8 @@ const result = await sh.pipe("cat log.txt", "grep ERROR", "wc -l");
382
416
  console.log(result.stdout.trim()); // error count
383
417
  ```
384
418
 
419
+ Fails visibly, never crashes the host: every hop validates that the needed `stdout`/`stdin` streams exist (clear `TypeError` if missing), and stream errors like `EPIPE` are absorbed by safe listeners — a broken pipeline resolves as a failed `Result` instead of crashing the process.
420
+
385
421
  #### `chain()` — Sequential execution (stop on error)
386
422
 
387
423
  ```ts
@@ -476,9 +512,15 @@ Load `.env` files with `${VAR}` interpolation using Node's native `util.parseEnv
476
512
  import { load, get, requireEnv } from "fullnative";
477
513
 
478
514
  // Load .env (default path: ".env")
479
- await load();
515
+ const applied = await load();
516
+ // applied: Record<string, string> — variables actually applied by this call
517
+
518
+ // Load a specific file
480
519
  await load("./.env.production");
481
520
 
521
+ // Options object: path + override
522
+ await load({ path: "./.env.production", override: true });
523
+
482
524
  // Get a variable
483
525
  const port = get("PORT"); // string | undefined
484
526
  const host = get("HOST", "localhost"); // string (fallback)
@@ -486,31 +528,85 @@ const key = requireEnv("API_KEY"); // string — throws if missing
486
528
  ```
487
529
 
488
530
  **Behavior:**
531
+ - Accepts `string | { path?: string; override?: boolean }` (`LoadOptions`). With no argument, loads `".env"`.
532
+ - Returns `Promise<Record<string, string>>` with the variables **actually applied**: in normal mode only the keys that weren't already in `process.env`; with `override: true`, every resolved variable.
489
533
  - Parses with `util.parseEnv` (handles quotes, comments, multiline values).
490
534
  - Interpolates `${VAR}` references across multiple passes (resolves chains like `A=${B}`, `B=${C}`, `C=value`).
491
535
  - Circular references (`A=${B}`, `B=${A}`) are cut off after 5 passes — no infinite loops.
492
- - Variables already set in `process.env` are **never overwritten** — the real environment always wins.
536
+ - Default merge semantics unchanged: variables already set in `process.env` are **never overwritten** — the real environment always wins. Use `override: true` to let the file win.
493
537
  - `requireEnv("KEY")` throws `Error` with the key name if the variable is missing.
494
538
  - **`load()` rejects if the file doesn't exist** — wrap in try/catch if you want optional loading. There is no silent mode.
495
539
 
496
540
  ---
497
541
 
542
+ ## utils
543
+
544
+ Functional utilities for the patterns that show up in every serious automation: pausing, deadlines, and disposable temp directories.
545
+
546
+ ```ts
547
+ import { sleep, timeout, TimeoutError, tempDir, TempDir } from "fullnative";
548
+
549
+ // sleep(ms) — readable pause, always resolves (even with ms <= 0)
550
+ await sleep(200); // wait 200ms before continuing
551
+
552
+ // timeout(promise, ms, message?) — deadline for ANY promise
553
+ const data = await timeout(fetchJSON("/api/data"), 5000);
554
+ // rejects with TimeoutError if fetchJSON takes longer than 5s
555
+
556
+ try {
557
+ await timeout(slowTask(), 1_000);
558
+ } catch (err) {
559
+ if (err instanceof TimeoutError) {
560
+ // distinguish deadlines from real failures — decide: retry or abort
561
+ }
562
+ }
563
+ // Default message: "Operation timed out after {ms}ms"; custom message supported
564
+
565
+ // tempDir() — disposable temp directory (fs.mkdtemp under os.tmpdir())
566
+ const tmp = await tempDir(); // TempDir (extends Folder), prefix "fullnative-"
567
+ const mine = await tempDir({ prefix: "myapp-" });
568
+
569
+ await tmp.file("scratch.txt").write("hello"); // full Folder API inherited
570
+ console.log(tmp.path); // unique absolute path
571
+
572
+ await tmp.dispose(); // removes it now (fs.rm recursive + force), idempotent
573
+ // Without dispose(): if keep !== true, the directory is auto-removed
574
+ // via a single process "exit" hook — one listener total for all temp dirs
575
+
576
+ // keep: true — no auto-cleanup; removal is your responsibility
577
+ const persistent = await tempDir({ keep: true });
578
+ ```
579
+
580
+ **Behavior:**
581
+ - `sleep(ms)` resolves to `void`; it never rejects.
582
+ - `timeout()` cleans up its timer (`clearTimeout` via `finally`) when the underlying promise wins the race, and never produces unhandled rejections from late rejections of the wrapped promise.
583
+ - `TempDir` extends `Folder`, so it inherits the entire directory API (`list`, `createFile`, `walk`, ...).
584
+ - `dispose()` is idempotent: it removes the directory and takes the path out of the auto-cleanup registry, so there is no double delete attempt at exit.
585
+ - Auto-cleanup uses **one** global `process.once("exit")` hook registered once per process — never one listener per temp directory.
586
+
587
+ ---
588
+
498
589
  ## API Reference
499
590
 
500
591
  | Class / Function | Description |
501
592
  |---|---|
502
- | `File` | File operations: read, write, JSON, hash, streams, copy, move, rename, replace, permissions, truncate, touch |
593
+ | `File` | File operations: read, write, JSON, hash, streams, copy, move, rename, replace, permissions, truncate, touch, absolute |
503
594
  | `Folder` | Directory operations: list, walk, walkIter, walkFilesIter, tree, find, matchFiles, watch, copy, move, rename |
504
595
  | `Process` | Execute native commands: run, output, shell, spawn, spawnScript, exists, which |
505
- | `Command` | Immutable builder: withArgs, in, withEnv, withTimeout, withInput, throwOnError |
596
+ | `Command` | Immutable builder: withArgs, in, withEnv, withTimeout, withAbort, withInput, throwOnError |
506
597
  | `LiveProcess` | Running process: stdin/stdout/stderr, kill, forceKill, wait, onOutput, elapsed, stopped |
507
- | `Result` | Finished command: stdout, stderr, output, lines, json, ok, failed, throwIfFailed |
598
+ | `Result` | Finished command: stdout, stderr, output, lines, nonEmptyLines, json, ok, failed, throwIfFailed |
508
599
  | `ProcessError` | Structured error: command, args, kind, exitCode, signal, stderr, cause |
509
600
  | `Shell` | Shell session: run, $, pipe, chain, ifOk, ifFail, bg, cd, set/unset, alias, history, killAll |
510
601
  | `Job` | Background process: name, autoRestart, restartCount, onRestart, kill, wait, result |
511
- | `load` | Load `.env` file with `${VAR}` interpolation — rejects if file missing |
602
+ | `load` | Load `.env` file with `${VAR}` interpolation — accepts `string` or `LoadOptions`, returns the variables applied, rejects if file missing |
512
603
  | `get` | Get env var with optional fallback |
513
604
  | `requireEnv` | Get env var or throw if missing |
605
+ | `sleep` | Promise-based pause for `ms` milliseconds |
606
+ | `timeout` | Race any promise against a deadline — rejects with `TimeoutError` |
607
+ | `TimeoutError` | Error thrown when a `timeout()` deadline expires |
608
+ | `tempDir` | Create a unique temp directory (`TempDir`) with optional auto-cleanup at process exit |
609
+ | `TempDir` | Temp directory class extending `Folder`, with `dispose()` |
514
610
 
515
611
  ## Requirements
516
612
 
@@ -522,7 +618,7 @@ const key = requireEnv("API_KEY"); // string — throws if missing
522
618
 
523
619
  ```bash
524
620
  pnpm install
525
- pnpm test # run 150 tests
621
+ pnpm test # run 172 tests
526
622
  pnpm run typecheck # type check
527
623
  pnpm run build # compile to dist/
528
624
  ```
@@ -531,12 +627,10 @@ pnpm run build # compile to dist/
531
627
 
532
628
  Planned for future releases:
533
629
 
534
- - **`sleep(ms)`** — promise-based delay
535
630
  - **`waitFor(fn, opts)`** — poll until a condition is met
536
631
  - **`retry(fn, opts)`** — retry with backoff strategies
537
- - **`timeout(promise, ms)`** — race a promise against a timer
538
632
  - **`onShutdown(fn)`** — register graceful shutdown handlers (SIGINT/SIGTERM)
539
- - **`tempDir()`** — create and auto-cleanup a temporary directory
633
+ - **Windows-safe quoting for `$`** — cmd.exe-specific escaping (POSIX quoting documented above)
540
634
  - **Dual CJS/ESM support** — if there's demand from legacy projects
541
635
 
542
636
  ## License
@@ -1,16 +1,48 @@
1
+ /**
2
+ * Opciones de `load`.
3
+ *
4
+ * Se acepta directamente un `string` (equivalente a `{ path }`) para
5
+ * mantener la firma previa. Sin argumento, se usa la ruta default.
6
+ */
7
+ export interface LoadOptions {
8
+ /**
9
+ * Ruta al archivo `.env` a cargar (default `".env"`, relativa al cwd del
10
+ * proceso).
11
+ */
12
+ path?: string;
13
+ /**
14
+ * Si es `true`, las variables del archivo **sobrescriben** las que ya
15
+ * existan en `process.env`. Default `false`: el entorno real siempre gana
16
+ * y las variables preexistentes no se tocan (merge con `??=`).
17
+ */
18
+ override?: boolean;
19
+ }
1
20
  /**
2
21
  * Carga un archivo `.env`, interpola referencias `${VAR}` y las mergea a
3
- * `process.env` **sin pisar** variables que ya estén seteadas en el entorno
4
- * real. El entorno real siempre tiene prioridad sobre el archivo.
22
+ * `process.env`.
23
+ *
24
+ * Semántica de merge (breaking en 1.0):
25
+ * - **Default**: las variables ya seteadas en el entorno real **no se
26
+ * tocan** (merge con `??=`); el entorno real siempre tiene prioridad sobre
27
+ * el archivo.
28
+ * - **`override: true`**: las variables del archivo **sobrescriben** las que
29
+ * ya existan en `process.env`.
30
+ *
31
+ * Acepta la firma string (`load("./ruta.env")`, equivalente a
32
+ * `{ path: "./ruta.env" }`) o un objeto `LoadOptions`. Sin argumento, usa la
33
+ * ruta default `".env"`.
5
34
  *
6
35
  * El parseo se realiza con `util.parseEnv` de Node.js. Los valores `undefined`
7
36
  * resultantes del parseo se descartan antes de interpolar.
8
37
  *
9
- * @param path Ruta al archivo `.env` (default `".env"`)
10
- * @returns Nada; muta `process.env` como efecto secundario
38
+ * @param options Ruta al archivo `.env` (default `".env"`) u objeto de
39
+ * opciones `LoadOptions`
40
+ * @returns Un registro con las variables **efectivamente aplicadas** a
41
+ * `process.env` por esta llamada: en modo normal, solo las que no existían
42
+ * previamente; en modo `override: true`, todas las resueltas
11
43
  * @throws {Error} Si el archivo no existe (ENOENT) u otro error de lectura
12
44
  */
13
- export declare function load(envPath?: string): Promise<void>;
45
+ export declare function load(options?: string | LoadOptions): Promise<Record<string, string>>;
14
46
  /**
15
47
  * Devuelve el valor de una env var, o `undefined` si no existe.
16
48
  * @param key Nombre de la variable
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../../src/core/env/env.ts"],"names":[],"mappings":"AA4FA;;;;;;;;;;;GAWG;AACH,wBAAsB,IAAI,CAAC,OAAO,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAY1D;AAED;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAErD;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;AAgB3D;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9C"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../../src/core/env/env.ts"],"names":[],"mappings":"AA4FA;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,IAAI,CACxB,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAuBjC;AAED;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAErD;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;AAgB3D;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9C"}