package OLLM::LuaManifest; use v5.30; use strict; use warnings; use Cwd qw(abs_path); use File::Basename qw(dirname); use File::Spec; # Bridge to the shared Lua module (osglecture-manifest.lua / # osglecture-series-index.lua, see osglecture/ARCHITECTURE.md section 12): # reads project content and does directory discovery through the same # implementation osglecture itself uses at compile time, instead of a # second, independently maintained Perl parser. Every public function here # shells out to texlua; none of them re-implement TOML or the series-unit # directory grammar. my $CLI_SCRIPT_NAME = 'osglecture-manifest-cli.lua'; my $TEXLUA_PATH; my $CLI_SCRIPT_PATH; my $LOOKUP_ERROR; sub _find_texlua { return $TEXLUA_PATH if defined $TEXLUA_PATH; require OLLM::CLI; $TEXLUA_PATH = OLLM::CLI::_find_program('texlua') // ''; return $TEXLUA_PATH; } sub _find_cli_script { return $CLI_SCRIPT_PATH if defined $CLI_SCRIPT_PATH; require OLLM::CLI; my $kpsewhich = OLLM::CLI::_find_program('kpsewhich'); if ($kpsewhich) { my $path = OLLM::CLI::_capture_first_line($kpsewhich, $CLI_SCRIPT_NAME); if (defined($path) && length($path)) { $CLI_SCRIPT_PATH = $path; return $CLI_SCRIPT_PATH; } } # Development fallback: this module's own bundle (ollm/) ships osglecture # as a sibling directory, so the script is reachable without an installed # TDS tree. Resolved relative to this module's own file location, not the # caller's current working directory: a caller running "ollm" from an # arbitrary project directory (not from inside ollm/) must still find it. # A real installation is expected to resolve via kpsewhich above instead. my $module_dir = dirname(abs_path(__FILE__)); my $sibling = File::Spec->catfile( $module_dir, (File::Spec->updir) x 4, 'osglecture', $CLI_SCRIPT_NAME, ); $CLI_SCRIPT_PATH = -f $sibling ? File::Spec->rel2abs($sibling) : ''; return $CLI_SCRIPT_PATH; } sub available { $LOOKUP_ERROR = undef; my $texlua = _find_texlua(); if (!$texlua) { $LOOKUP_ERROR = 'texlua was not found on PATH'; return 0; } my $script = _find_cli_script(); if (!$script) { $LOOKUP_ERROR = "$CLI_SCRIPT_NAME was not found " . '(neither via kpsewhich nor as a sibling of the OLLM installation)'; return 0; } return 1; } sub error { return $LOOKUP_ERROR; } # Runs "texlua " and returns its stdout # split into lines, each further split on tab (the CLI script's TSV # convention). Dies with the subprocess's stderr on a non-zero exit. # # Uses the list form of open (like OLLM::CLI::_capture_doctor_command) # rather than a shell-interpolated qx{}: quoting a command line for a # shell is platform-specific (POSIX quoting rules do not apply to # cmd.exe), so escaping arguments ourselves and handing the result to a # shell breaks on Windows in particular for paths containing backslashes. # The list form bypasses the shell entirely on every platform. sub _run { my (@args) = @_; die "$LOOKUP_ERROR\n" if !available(); my @command = (_find_texlua(), _find_cli_script(), @args); open my $handle, '-|', @command or die "cannot run osglecture-manifest-cli.lua: $!\n"; my $output = do { local $/; <$handle> // '' }; close $handle; my $status = $? >> 8; if ($status != 0) { chomp $output; die "osglecture-manifest-cli.lua failed: $output\n"; } my @lines = split /\n/, $output; return [ map { [ split /\t/, $_ ] } @lines ]; } sub toml_info { my $rows = eval { _run('toml-info') }; if (!$rows) { my $error = $@ || 'unknown error'; chomp $error; return { available => 0, error => $error, name => 'osglecture-toml', version => undef, source => undef, standard => 'TOML 1.0' }; } my ($name, $version, $source) = @{ $rows->[0] }; return { available => 1, error => undef, name => $name, version => $version, source => $source, standard => 'TOML 1.0' }; } # Returns a list of unit hashes shaped like # OLLM::Config::structure_snapshot's own "units" entries, so callers can # compare the two directly. Not cached: structure_snapshot's own signature # is used for build-cache invalidation and must reflect the directory # structure at call time even if it changed since an earlier call in the # same process (renaming or reordering a unit mid-run is a real scenario, # not just a test artifact). sub discover_units { my ($project_root) = @_; my $rows = _run('discover-units', $project_root); return [ map { my ($physical_unit, $physical_number, $unit_scope, $unit_role, $slug) = @$_; { physical_unit => $physical_unit, physical_number => $physical_number, unit_scope => $unit_scope, unit_role => $unit_role, slug => $slug, } } @$rows ]; } 1;