NAME
Skilletor::Apply - Apply a build plan to disk – diff, atomic writes, conflicts, removal (spec §6.1–6.3)
VERSION
version 0.001
SYNOPSIS
use Skilletor::Apply qw( apply );
my $res = apply(
[ { key => 'skills/foo', type => 'skill', name => 'foo', source => 'shared', version => 'git:ab12cd3',
output => [ [ 'skills/foo/SKILL.md', $bytes ] ], executable => { 'skills/foo/run.sh' => 1 } } ],
target_dir => "$home/.claude",
force => 0,
keep => [ 'skills/offline-item' ],
);
# { added, updated, removed, unchanged, skipped, conflicts, overwritten, written, leftInPlace }
DESCRIPTION
The port of the reference's apply.ts (spec §6.1–6.3). It compares each item's built output – bytes and the owner's executable bit – with the disk and the lock, writes only the differences (atomically: a temp file beside the target, then a rename), removes the files an item no longer contains and deletes the items no longer declared. The lock is the ownership boundary: only paths it holds are ever overwritten or deleted.
Every path of an item is checked before its first write. A path on disk that the lock does not hold, or a symbolic link at or below the item's own path, is a conflict and blocks the whole item – nothing of it is written or removed – unless
forceadopts the file, replaces the link or deletes the file at a claimed path (an agent's plain name beside its.local.file). A directory where a file goes blocks it even withforce.A managed file that diverged from its lock hash is overwritten and named in
overwritten; a file whose bit alone is wrong is rewritten silently.Nothing is written or deleted through a link at or below an item's path; a removal stopped by one is named in
leftInPlace.An error after the first write leaves a lock that owns what landed, those entries marked
partial(k71), so a retry resumes.Paths equal ignoring case that are one file on disk (a case-insensitive file system, k136) are one path.
The lock is rewritten only when its bytes change, and deleted when it is left without entries.
apply
apply( \@plan, target_dir => $dir, root_of => sub { $root_for_key }, force => 0, keep => \@keys );
A plan item: { key, type, name, source, version, output => [ [ rel, bytes ] ], executable => { rel => 1 }?, skipped?, inBlock?, via => [...]?, claims => [...]? }. root_of answers the root a lock key's files live under (default target_dir, which also holds the lock). keep names lock keys preserved untouched although not in the plan.
The result holds lock keys in added, updated, removed, unchanged, skipped; { key, path, replace?, notFile? } in conflicts; { key, path } in overwritten, written (written or adopted this run) and leftInPlace. Throws Skilletor::Error::Apply for an invalid item name.
is_valid_item_name
True for a name the apply accepts: ASCII letters, digits, ., _, -.
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.