NAME

Skilletor::FSUtil - File and path helpers that behave as the reference's Node calls do

VERSION

version 0.001

SYNOPSIS

use Skilletor::FSUtil qw( read_text write_text inner_segments path_join );

my $text = read_text('/home/me/.claude/skilletor.json');    # as readFileSync(…, "utf8")
write_text( $file, $json."\n" );                            # temp file + rename

my $segs = inner_segments('skills/../x');                   # 'dotdot'

DESCRIPTION

The port of the reference's fsutil.ts, plus the pieces of Node's node:path and node:fs whose behaviour reaches a file name, a message or a hash: path.join, path.normalize and path.resolve (POSIX), and readFileSync(…, "utf8")'s lossy decoding. Plain functions, exported on request.

read_text

The file's bytes decoded as UTF-8, each ill-formed sequence replaced by one U+FFFD as the WHATWG decoder does (Node's behaviour); a BOM stays.

decode_utf8_lossy

The decoding "read_text" uses, for bytes already in hand.

encode_text

A character string as UTF-8 bytes, a lone surrogate as U+FFFD – what Node's Buffer.from(string) and createHash().update(string) make of it.

write_text

write_text( $file, $text, $executable );

"atomic_write" of "encode_text".

atomic_write

atomic_write( $file, $bytes, $executable );

Creates the parent directories, writes a .skilletor-tmp-* file beside the target with mode 0666 (0777 when $executable) less the umask, and renames it over the target. Croaks on characters above 0xFF: encode first.

inner_segments

inner_segments('a/./b');    # ['a', 'b']
inner_segments('/a');       # 'absolute'
inner_segments('a/../b');   # 'dotdot'
inner_segments('.');        # 'root'

The segments of a path relative to some root, or why it names nothing beneath it. \ separates like /.

path_join

path.join of Node's POSIX path module.

path_normalize

path.normalize of Node's POSIX path module.

path_resolve

path.join, path.normalize, path.resolve of Node's POSIX path module.

is_absolute

path.isAbsolute; on Windows a drive or backslash path too.

real_or_resolved

The real path of an existing path, else its "path_resolve" form.

same_path

True when two paths name the same location once symlinks are resolved.

same_file

True when writing one path would write the other: the same target once links are followed (a missing file or dangling link by its real parent), or the same inode (a hard link). What tells two spellings of a name apart on a case-insensitive file system. Never throws.

to_posix

The path with / between its parts (a no-op off Windows).

node_fs_error

node_fs_error( 0 + $!, 'scandir', $path );   # "ENOENT: no such file or directory, scandir '/x'"

The message Node gives a failed node:fs call, for an errno.

hash_bytes

sha256: and the hex digest – the hash of a lock entry's file.

is_executable

True when a mode has the owner's executable bit.

SUPPORT

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.