@stonyx/utils 0.2.3-alpha.23 → 0.2.3-alpha.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/file.d.ts +24 -0
- package/dist/file.js +57 -6
- package/dist/prompt.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,10 +75,12 @@ Creates a file at the given path.
|
|
|
75
75
|
|
|
76
76
|
#### `updateFile(filePath, data, options={})`
|
|
77
77
|
|
|
78
|
-
Updates a file atomically by writing to a
|
|
78
|
+
Updates a file atomically by writing to a sibling swap file first, then renaming it over the target. Throws if the file does not exist.
|
|
79
79
|
|
|
80
80
|
* `options.json` — boolean, serialize as JSON.
|
|
81
81
|
|
|
82
|
+
**Concurrency: last writer wins.** Concurrent `updateFile` calls on the same path are safe — each uses a swap file unique to its process and call, so neither caller can throw `ENOENT` or persist the other's bytes — but they are not serialized. The surviving value is whichever caller renames last.
|
|
83
|
+
|
|
82
84
|
#### `copyFile(sourcePath, targetPath, options={})`
|
|
83
85
|
|
|
84
86
|
Copies a file from source to target.
|
|
@@ -245,6 +247,8 @@ Prompts the user with `(y/N)` and resolves to `true` only if the answer is `"y"`
|
|
|
245
247
|
* `options.input` — Readable stream (default: `process.stdin`).
|
|
246
248
|
* `options.output` — Writable stream (default: `process.stdout`).
|
|
247
249
|
|
|
250
|
+
**TTY required:** Rejects with an error if `process.stdin` is not a TTY and no custom `input` stream is provided. For headless/container deployments, use `autoMigrate` config or provide a custom input stream.
|
|
251
|
+
|
|
248
252
|
#### `prompt(question, options={})`
|
|
249
253
|
|
|
250
254
|
Prompts the user with a question and resolves to the trimmed input string.
|
|
@@ -252,6 +256,8 @@ Prompts the user with a question and resolves to the trimmed input string.
|
|
|
252
256
|
* `options.input` — Readable stream (default: `process.stdin`).
|
|
253
257
|
* `options.output` — Writable stream (default: `process.stdout`).
|
|
254
258
|
|
|
259
|
+
**TTY required:** Rejects with an error if `process.stdin` is not a TTY and no custom `input` stream is provided.
|
|
260
|
+
|
|
255
261
|
```js
|
|
256
262
|
import { confirm, prompt } from '@stonyx/utils/prompt';
|
|
257
263
|
|
package/dist/file.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
1
2
|
interface FileOptions {
|
|
2
3
|
json?: boolean;
|
|
3
4
|
}
|
|
4
5
|
interface ReadFileOptions extends FileOptions {
|
|
5
6
|
missingFileCallback?: (filePath: string) => string | Record<string, unknown>;
|
|
7
|
+
encoding?: BufferEncoding | null;
|
|
6
8
|
}
|
|
7
9
|
interface DeleteFileOptions {
|
|
8
10
|
ignoreAccessFailure?: boolean;
|
|
@@ -21,18 +23,40 @@ interface FileImportMeta {
|
|
|
21
23
|
path: string;
|
|
22
24
|
}
|
|
23
25
|
export declare function createFile(filePath: string, data: string | Record<string, unknown>, options?: FileOptions): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Atomically replace the contents of an existing file.
|
|
28
|
+
*
|
|
29
|
+
* Writes to a swap file that is a **sibling** of the target — so `rename` stays
|
|
30
|
+
* on one filesystem and therefore stays atomic — then renames it over the
|
|
31
|
+
* target. The swap name carries `process.pid` and a UUID rather than a
|
|
32
|
+
* timestamp: a whole-second token collided between concurrent callers, which
|
|
33
|
+
* made one caller throw `ENOENT` and the other silently persist bytes it had
|
|
34
|
+
* not written (abofs/stonyx-utils#44).
|
|
35
|
+
*
|
|
36
|
+
* Concurrency contract: **last writer wins**. Unique swap names remove the
|
|
37
|
+
* `ENOENT` and the byte-level clobber, but overlapping calls on one path still
|
|
38
|
+
* race on which value lands last. `updateFile` does not serialise, and callers
|
|
39
|
+
* needing a serialisation guarantee must supply their own — an in-process queue
|
|
40
|
+
* would not provide one across processes anyway.
|
|
41
|
+
*/
|
|
24
42
|
export declare function updateFile(filePath: string, data: string | Record<string, unknown>, options?: FileOptions): Promise<void>;
|
|
25
43
|
interface CopyFileOptions {
|
|
26
44
|
overwrite?: boolean;
|
|
27
45
|
}
|
|
28
46
|
export declare function copyFile(sourcePath: string, targetPath: string, options?: CopyFileOptions): Promise<boolean>;
|
|
47
|
+
export declare function readFile(filePath: string, options: ReadFileOptions & {
|
|
48
|
+
encoding: null;
|
|
49
|
+
}): Promise<Buffer>;
|
|
29
50
|
export declare function readFile(filePath: string, options: ReadFileOptions & {
|
|
30
51
|
json: true;
|
|
31
52
|
}): Promise<Record<string, unknown>>;
|
|
32
53
|
export declare function readFile(filePath: string, options?: ReadFileOptions): Promise<string>;
|
|
33
54
|
export declare function deleteFile(filePath: string, options?: DeleteFileOptions): Promise<void>;
|
|
55
|
+
export declare function deleteFileSync(filePath: string, options?: DeleteFileOptions): void;
|
|
34
56
|
export declare function deleteDirectory(dir: string): Promise<void>;
|
|
35
57
|
export declare function createDirectory(dir: string): Promise<void>;
|
|
58
|
+
export declare function createReadStream(filePath: string, options?: Parameters<typeof fs.createReadStream>[1]): fs.ReadStream;
|
|
59
|
+
export declare function createWriteStream(filePath: string, options?: Parameters<typeof fs.createWriteStream>[1]): fs.WriteStream;
|
|
36
60
|
export declare function forEachFileImport(dir: string, callback: (output: unknown, meta: FileImportMeta) => void | Promise<void>, options?: ForEachFileImportOptions): Promise<void>;
|
|
37
61
|
export declare function fileExists(filePath: string): Promise<boolean>;
|
|
38
62
|
export {};
|
package/dist/file.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { getTimestamp } from './date.js';
|
|
2
1
|
import { kebabCaseToCamelCase } from './string.js';
|
|
3
2
|
import { objToJson } from './object.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
4
|
import { promises as fsp } from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
6
7
|
function isNodeError(error) {
|
|
7
8
|
return error instanceof Error && 'code' in error;
|
|
8
9
|
}
|
|
@@ -16,12 +17,39 @@ export async function createFile(filePath, data, options = {}) {
|
|
|
16
17
|
throw error instanceof Error ? error : new Error(String(error));
|
|
17
18
|
}
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Atomically replace the contents of an existing file.
|
|
22
|
+
*
|
|
23
|
+
* Writes to a swap file that is a **sibling** of the target — so `rename` stays
|
|
24
|
+
* on one filesystem and therefore stays atomic — then renames it over the
|
|
25
|
+
* target. The swap name carries `process.pid` and a UUID rather than a
|
|
26
|
+
* timestamp: a whole-second token collided between concurrent callers, which
|
|
27
|
+
* made one caller throw `ENOENT` and the other silently persist bytes it had
|
|
28
|
+
* not written (abofs/stonyx-utils#44).
|
|
29
|
+
*
|
|
30
|
+
* Concurrency contract: **last writer wins**. Unique swap names remove the
|
|
31
|
+
* `ENOENT` and the byte-level clobber, but overlapping calls on one path still
|
|
32
|
+
* race on which value lands last. `updateFile` does not serialise, and callers
|
|
33
|
+
* needing a serialisation guarantee must supply their own — an in-process queue
|
|
34
|
+
* would not provide one across processes anyway.
|
|
35
|
+
*/
|
|
19
36
|
export async function updateFile(filePath, data, options = {}) {
|
|
20
37
|
try {
|
|
21
38
|
await fsp.access(filePath);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
// pid + UUID, never a timestamp: the swap name is a uniqueness token, and
|
|
40
|
+
// whole seconds cannot discriminate between concurrent callers.
|
|
41
|
+
const swapFile = `${filePath}.temp-${process.pid}-${randomUUID()}`;
|
|
42
|
+
// `wx` turns any residual name collision into a loud EEXIST rather than a
|
|
43
|
+
// silent overwrite of another caller's swap bytes.
|
|
44
|
+
await fsp.writeFile(swapFile, options.json ? objToJson(data) : String(data), { encoding: 'utf8', flag: 'wx' });
|
|
45
|
+
try {
|
|
46
|
+
await fsp.rename(swapFile, filePath);
|
|
47
|
+
}
|
|
48
|
+
catch (renameError) {
|
|
49
|
+
// Leave no orphan behind, and never let the cleanup mask the real error.
|
|
50
|
+
await fsp.unlink(swapFile).catch(() => { });
|
|
51
|
+
throw renameError;
|
|
52
|
+
}
|
|
25
53
|
}
|
|
26
54
|
catch (error) {
|
|
27
55
|
throw error instanceof Error ? error : new Error(String(error));
|
|
@@ -58,8 +86,13 @@ export async function readFile(filePath, options = {}) {
|
|
|
58
86
|
try {
|
|
59
87
|
filePath = path.resolve(filePath);
|
|
60
88
|
await fsp.access(filePath);
|
|
61
|
-
const
|
|
62
|
-
|
|
89
|
+
const encoding = options.encoding === undefined ? 'utf8' : options.encoding;
|
|
90
|
+
const fileData = await fsp.readFile(filePath, { encoding: encoding });
|
|
91
|
+
if (encoding === null)
|
|
92
|
+
return fileData;
|
|
93
|
+
if (options.json)
|
|
94
|
+
return JSON.parse(fileData);
|
|
95
|
+
return fileData;
|
|
63
96
|
}
|
|
64
97
|
catch (error) {
|
|
65
98
|
const { missingFileCallback } = options;
|
|
@@ -81,12 +114,30 @@ export async function deleteFile(filePath, options) {
|
|
|
81
114
|
}
|
|
82
115
|
await fsp.unlink(filePath);
|
|
83
116
|
}
|
|
117
|
+
export function deleteFileSync(filePath, options) {
|
|
118
|
+
try {
|
|
119
|
+
filePath = path.resolve(filePath);
|
|
120
|
+
fs.accessSync(filePath);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (options?.ignoreAccessFailure)
|
|
124
|
+
return;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
fs.unlinkSync(filePath);
|
|
128
|
+
}
|
|
84
129
|
export async function deleteDirectory(dir) {
|
|
85
130
|
await fsp.rm(dir, { recursive: true, force: true });
|
|
86
131
|
}
|
|
87
132
|
export async function createDirectory(dir) {
|
|
88
133
|
await fsp.mkdir(dir, { recursive: true });
|
|
89
134
|
}
|
|
135
|
+
export function createReadStream(filePath, options) {
|
|
136
|
+
return fs.createReadStream(path.resolve(filePath), options);
|
|
137
|
+
}
|
|
138
|
+
export function createWriteStream(filePath, options) {
|
|
139
|
+
return fs.createWriteStream(path.resolve(filePath), options);
|
|
140
|
+
}
|
|
90
141
|
export async function forEachFileImport(dir, callback, options = {}) {
|
|
91
142
|
if (typeof callback !== 'function')
|
|
92
143
|
throw new Error('Callback must be valid function');
|
package/dist/prompt.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createInterface } from 'readline';
|
|
2
2
|
export function confirm(question, { input, output } = {}) {
|
|
3
|
+
if (!input && !process.stdin.isTTY) {
|
|
4
|
+
return Promise.reject(new Error('Interactive confirm() requires a TTY on stdin. ' +
|
|
5
|
+
'For headless/container deployments, use the autoMigrate config option instead.'));
|
|
6
|
+
}
|
|
3
7
|
const rl = createInterface({
|
|
4
8
|
input: input ?? process.stdin,
|
|
5
9
|
output: output ?? process.stdout,
|
|
@@ -12,6 +16,10 @@ export function confirm(question, { input, output } = {}) {
|
|
|
12
16
|
});
|
|
13
17
|
}
|
|
14
18
|
export function prompt(question, { input, output } = {}) {
|
|
19
|
+
if (!input && !process.stdin.isTTY) {
|
|
20
|
+
return Promise.reject(new Error('Interactive prompt() requires a TTY on stdin. ' +
|
|
21
|
+
'For headless/container deployments, configure non-interactive alternatives instead.'));
|
|
22
|
+
}
|
|
15
23
|
const rl = createInterface({
|
|
16
24
|
input: input ?? process.stdin,
|
|
17
25
|
output: output ?? process.stdout,
|