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 force adopts 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 with force.

  • 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.