NAME

Skilletor::Config - Load, merge and validate the user, project and local skilletor config

VERSION

version 0.001

SYNOPSIS

use Skilletor::Config;

my $config = Skilletor::Config->load(
  home        => $ENV{HOME},
  project_dir => '/path/to/project',     # omit: user scope only
);

for my $item ( @{ $config->project->{install} } ) {
  my $src = $config->source( $item->{source} );
  printf "%s %s from %s\n", $item->{type}, $item->{name}, $src->{git} // $src->{url} // $src->{local};
}

# editing one file, as `skilletor add` / `install` do
Skilletor::Config->add_source( $file, shared => { git => 'https://github.com/Getty/skills', ref => 'main' } );
Skilletor::Config->add_install_entry( $file, skill => 'perl-moo@shared' );

DESCRIPTION

The port of the reference's config.ts (spec §3). Three files, from an injected home and project root:

<home>/.claude/skilletor.json              user
<project>/.claude/skilletor.json           project (committed)
<project>/.claude/skilletor.local.json     local overrides

A missing file reads as an empty config. Loading happens in the constructor; everything that can be wrong throws a Skilletor::Error::Config whose message is the reference's, and the first error reported is the one the reference reports first – the files and keys are checked in its order.

Sources merge per field: the project scope sees project < user < local; each backend field (git, url, local) keeps the origin of its file (user for the user and the local file, project for the committed one); ref and path are the last file's that gives one, and "ref": null drops a lower file's ref. An entry without a backend only changes another file's source; a source no file gives a backend is an error. The user scope sees the user config's sources alone.

Returned values are plain Perl data with the reference's field names, so a dump compares with the TypeScript structures directly:

source:   { name, git?, ref?, url?, local?, path?, origins => { git => 'user', ... } }
item:     { type, name, source, target, raw }
wildcard: { type, source, pattern, raw }
bundle:   { name, source, raw }
scope:    { scope, install, wildcards, bundles, vars, gitignore, claudeSettings, targets? }

gitignore and claudeSettings are JSON::MaybeXS booleans; vars is a hash that keeps the merged key order.

home

Stands in for ~. Required: no default from $ENV{HOME}, so a test or an embedding application never reads the real home by accident.

project_dir

The project root; without it only the user scope is loaded.

unchecked

{ path => $file, name => $source }: that source of that file is declared without being checked and without a backend (what source remove needs to load a config it is about to fix). Never synced.

user_path

<home>/.claude/skilletor.json.

project_path

<project>/.claude/skilletor.json, or undef.

local_path

The three files; the project ones are undef without a project.

load

Skilletor::Config->load(%args);

new, for reading code.

sources

The project scope's merged sources, an array in the reference's Map order.

source

$config->source($name);

One of them by name, or undef.

source_names

The merged sources' names, in Map order.

user_sources

The user scope's sources, an array in Map order.

user_source

$config->user_source($name);

user_source_names

The user scope's sources: the user config's alone.

sources_of

The sources a scope resolves against: user or project.

source_of

$config->sources_of('user');
$config->source_of( project => $name );

The sources a scope resolves against.

check_interval

The in-session check's throttle in seconds (default 1800).

user

The user scope's config.

project

The project scope's config, or undef.

scope

$config->scope('project');

The scope configs; project is undef without a project dir.

has_project

True when a project dir was given.

to_data

The whole result shaped as the reference's LoadedConfig, Maps as arrays of [name, source] pairs in Map order – for comparing with the TypeScript side.

source_fields

Skilletor::Config->source_fields( $def, 'file: sources.name', $needs_backend );

One source definition checked field by field, as the loader and add check it; returns the fields present. $needs_backend defaults to true.

source_entry

Skilletor::Config->source_entry( $file, $name );

add_source

Skilletor::Config->add_source( $file, $name, $def );

A plain $def is written in the reference's field order (git, ref, url, local, path); a "js_ordered" in Skilletor::JSON one keeps its own.

remove_source

Skilletor::Config->remove_source( $file, $name );

Drops the source; an emptied sources goes too.

add_install_entry

Skilletor::Config->add_install_entry( $file, $type, $entry );

find_install_entries

Skilletor::Config->find_install_entries( $file, $name, $source, $type );

Entries matching a name (ignoring case; a pattern as written), optionally of one source and one type: [ { type, entry } ].

remove_install_entries

Removes them; returns how many.

find_wildcard_entries

Skilletor::Config->find_wildcard_entries( $file, $source, $type );

[ { type, entry, pattern } ].

bundle_entries

[ { name, source, entry } ] of install.bundles.

add_bundle_entry

Skilletor::Config->add_bundle_entry( $file, 'perl@shared' );

Appends to install.bundles unless that bundle of that source is listed.

remove_bundle_entries

Skilletor::Config->remove_bundle_entries( $file, $name, $source );

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.