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.
- package/CHANGELOG.md +30 -0
- package/MIGRATION.md +161 -0
- package/README.md +128 -34
- package/dist/core/env/env.d.ts +37 -5
- package/dist/core/env/env.d.ts.map +1 -1
- package/dist/core/env/env.js +27 -6
- package/dist/core/env/env.js.map +1 -1
- package/dist/core/env/index.d.ts +1 -1
- package/dist/core/env/index.d.ts.map +1 -1
- package/dist/core/env/index.js.map +1 -1
- package/dist/core/file/File.d.ts +13 -11
- package/dist/core/file/File.d.ts.map +1 -1
- package/dist/core/file/File.js +15 -14
- package/dist/core/file/File.js.map +1 -1
- package/dist/core/file/Folder.d.ts +11 -11
- package/dist/core/file/Folder.d.ts.map +1 -1
- package/dist/core/file/Folder.js +44 -27
- package/dist/core/file/Folder.js.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/process/Command.d.ts +19 -0
- package/dist/core/process/Command.d.ts.map +1 -1
- package/dist/core/process/Command.js +36 -1
- package/dist/core/process/Command.js.map +1 -1
- package/dist/core/process/Result.d.ts +10 -2
- package/dist/core/process/Result.d.ts.map +1 -1
- package/dist/core/process/Result.js +12 -2
- package/dist/core/process/Result.js.map +1 -1
- package/dist/core/process/types.d.ts +4 -0
- package/dist/core/process/types.d.ts.map +1 -1
- package/dist/core/shell/Shell.d.ts +33 -5
- package/dist/core/shell/Shell.d.ts.map +1 -1
- package/dist/core/shell/Shell.js +49 -7
- package/dist/core/shell/Shell.js.map +1 -1
- package/dist/core/shell/types.d.ts +7 -3
- package/dist/core/shell/types.d.ts.map +1 -1
- package/dist/core/utils/index.d.ts +4 -0
- package/dist/core/utils/index.d.ts.map +1 -0
- package/dist/core/utils/index.js +4 -0
- package/dist/core/utils/index.js.map +1 -0
- package/dist/core/utils/sleep.d.ts +14 -0
- package/dist/core/utils/sleep.d.ts.map +1 -0
- package/dist/core/utils/sleep.js +16 -0
- package/dist/core/utils/sleep.js.map +1 -0
- package/dist/core/utils/tempDir.d.ts +59 -0
- package/dist/core/utils/tempDir.d.ts.map +1 -0
- package/dist/core/utils/tempDir.js +92 -0
- package/dist/core/utils/tempDir.js.map +1 -0
- package/dist/core/utils/timeout.d.ts +40 -0
- package/dist/core/utils/timeout.d.ts.map +1 -0
- package/dist/core/utils/timeout.js +54 -0
- package/dist/core/utils/timeout.js.map +1 -0
- 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
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
|
-
|
|
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
|
[](https://www.npmjs.com/package/fullnative)
|
|
6
8
|
[](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`
|
|
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
|
|
101
|
-
const
|
|
102
|
-
await file.moveTo("./archive/data.txt");
|
|
103
|
-
await file.rename("renamed.txt");
|
|
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()`
|
|
122
|
-
- `
|
|
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
|
|
176
|
-
const copy = await project.copyTo("./backup/my-app");
|
|
177
|
-
await project.moveTo("./archive/my-app");
|
|
178
|
-
await project.rename("renamed-app");
|
|
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
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
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
|
|
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; // [
|
|
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
|
-
-
|
|
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
|
|
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
|
-
-
|
|
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
|
package/dist/core/env/env.d.ts
CHANGED
|
@@ -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
|
|
4
|
-
*
|
|
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
|
|
10
|
-
*
|
|
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(
|
|
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
|
|
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"}
|