=begin Package Manager ** THIS SCRIPT REQUIRES RUBY 2.3+ ** https://gswiki.play.net/Lich_(software)/Installation A modern, federated package manager for Lich. This script provides similiar functionality to the ;repository, but aims to make it much easier to share updates to scripts and avoid relying anyone as a single point-of-failure. Instead of a single server, jinx supports multiple repositories to be more resilient against infrastructure outages or players leaving the community. author: elanthia-online tags: utility, util, repository, repo, update, upgrade, meta, ruby, development version: 0.6.3 Changelog: 0.6.3 (2023-05-17) Add ability to refresh spell data with new effect-list.xml rubocop cleanup 0.6.2 (2023-01-23) Update for Ruby v3 compatibility 0.6.1 (2022-04-07) Fix to prevent downloading data files as script and script files as data. 0.6.0 (2021-12-12) Added pruning for deprecated repos =end require 'fileutils' require 'ostruct' require 'yaml' require 'json' require 'net/https' require 'net/http' require 'open-uri' require 'cgi' module Jinx # our app specific error that we can catch for Jinx errors class Error < StandardError; end def self.urnon?() defined?(Urnon) end def self.mode() urnon? ? :urnon : :lich end end module Jinx module Log def self.out(msg, label: :debug) return _write _view(msg, label) unless msg.is_a?(StandardError) ## pretty-print exception _write _view(msg.message, label) msg.backtrace.to_a.slice(0..5).each do |frame| _write _view(frame, label) end end def self._write(line) # maybe headless env if not defined?(:_respond) $stdout.write(line + "\n") elsif line.include?("<") and line.include?(">") respond(line) else _respond Preset.as(:debug, line) end end def self._env() return "jinx" unless defined?(Script) return Script.current.name end def self._view(msg, label) label = [_env, label].flatten.compact.join(".") safe = msg.inspect # safe = safe.gsub("<", "<").gsub(">", ">") if safe.include?("<") and safe.include?(">") "[#{label}] #{safe}" end def self.pp(msg, label = :debug) respond _view(msg, label) end def self.dump(*args) pp(*args) end def self.mono(text) _respond %[] _respond text _respond %[] end module Preset def self.as(kind, body) %[#{body}] end end end end module Jinx module Opts FLAG_PREFIX = "--" def self.parse_command(h, c) h[c.to_sym] = true end def self.parse_flag(h, f) (name, val) = f[2..-1].split("=") if val.nil? h[name.to_sym] = true else val = val.split(",") h[name.to_sym] = val.size == 1 ? val.first : val end end def self.parse(args = Script.current.vars[1..-1]) OpenStruct.new(**args.to_a.reduce(Hash.new) do |opts, v| if v.start_with?(FLAG_PREFIX) Opts.parse_flag(opts, v) else Opts.parse_command(opts, v) end opts end) end def self.method_missing(method, *args) parse.send(method, *args) end end end module Jinx module Util def self.parse_json(str) begin [:ok, JSON.parse(str, symbolize_names: true)] rescue => exception return [:err, exception] end end def self.fmt_time(seconds) days = (seconds / 86_400).floor seconds = seconds - (days * 86_400) hours = (seconds / 3_600).floor seconds = seconds - (hours * 3_600) minutes = (seconds / 60).floor seconds = (seconds - (minutes * 60)).floor [days, hours, minutes, seconds] .zip(%w(d h m s)) .select { |f| f.first > 0 } .map { |f| f.first.to_s.rjust(2, "0") + f.last } .reduce("") { |acc, col| acc + " " + col } .strip end def self.ago(epoc_time) fmt_time(Time.now.to_i - epoc_time) end def self.unwrap() begin yield rescue Jinx::Error => exception Log.mono <<~ERROR Jinx::Error: #{exception.message} ERROR end end end end module Jinx module Seed ElanthiaOnlineCore = { name: "core", url: %[https://repo.elanthia.online] } ElanthiaOnlineExtras = { name: "elanthia-online", url: %[https://extras.repo.elanthia.online] } FFNGLichRepoArchiveMirror = { name: "mirror", url: %[https://ffnglichrepoarchive.netlify.app] } # deprecated Gtk3ScriptUpdates = { name: "gtk3", url: %[https://gtk3.elanthia.online] } end end module Jinx module Folder LOCK = Mutex.new # dynamic reference to the $data_dir/_jinx folder # this will be useful for testing def self.jinx_data_dir() File.join(data_dir, "_jinx") end def self.data_dir() $data_dir.is_a?(String) or fail Jinx::Error, "$data_dir is not String" $data_dir end def self.script_dir() $script_dir.is_a?(String) or fail Jinx::Error, "$script_dir is not String" $script_dir end def self.engine_dir() $lich_dir.is_a?(String) or fail Jinx::Error, "$lich_dir is not String" $lich_dir end def self.path(*args) File.join(jinx_data_dir, *args.map(&:to_s)) end def self.read(*args) File.read path(*args) end def self.write(file, data) File.write( path(file), data ) end def self.read_yaml(*args) YAML.load(read(*args)) || {} end def self.write_yaml(file, data) Folder.write(file, data.to_yaml) end def self.glob(*args) Dir.glob path(*args) end def self.mkdir(*args) folder = Pathname.new(*args) folder.mkpath end def self.atomic(path) LOCK.synchronize { current_state = Folder.read_yaml(path) return current_state unless block_given? updated_state = yield(current_state) Folder.write_yaml(path, updated_state.is_a?(Hash) ? updated_state : current_state) } end end end module Jinx module Repo class << self include Enumerable FILE = "repos.yaml" def atomic() Folder.atomic(FILE) { |repos| yield(repos) } end def header(repo, script) uri = URI(repo[:url] + script[:header]) begin Net::HTTP.get(uri) rescue => err return err.message end end def manifest(repo) uri = URI(repo[:url] + "/manifest.json") begin fetched = Net::HTTP.get(uri) rescue => err return repo.merge({ err: err.message, available: [] }) end res, decoded = Util.parse_json fetched return repo.merge!(decoded) if res.eql?(:ok) && decoded[:available].is_a?(Array) return Log.out(decoded[:error] || "repo is misconfigured", label: %i(repo error)) if decoded.is_a?(Hash) Log.out(decoded.message, label: %i(repo error)) Log.out(decoded.backtrace, label: %i(repo error)) end def each() Repo.atomic { |repos| repos.each { |repo| (name, info) = repo yield({ name: name }.merge(info)) } } end def lookup(repo_name) Repo.find { |repo| repo[:name].eql?(repo_name.to_sym) } or fail Jinx::Error, "repo(%s) is not known" % repo_name end def exists?(repo_name) any? { |repo| repo[:name].eql?(repo_name.to_sym) } end def create(**argv) name = argv.fetch(:name).to_sym fail "repo(%s) requires a url" % name unless argv[:url].is_a?(String) Repo.atomic { |repos| fail "repo(%s) already exists" % name if repos[name] argv.delete(:name) Log.out("registering repo(%s) at %s" % [name, argv[:url]], label: %i(repo create)) Folder.mkdir(name.to_s) repos[name] = argv repos } end def remove(**argv) repo_name = argv.fetch(:name).to_sym repo = Repo.lookup(repo_name) Repo.atomic { |repos| repos.delete(repo[:name]) Log.mono("repo(%s) has been removed" % repo_name) repos } end def assets_in(repo) repo.fetch(:available, []) end def scripts(repo) assets_in(repo).select { |f| f[:type].nil? || f[:type] == "script" } end def data(repo) assets_in(repo).select { |f| f[:type] == "data" } end def engines(repo) assets_in(repo).select { |f| f[:type] == "engine" } end def print_info(script, i) Log.mono("%s> %s %s" % [ (i + 1).to_s.rjust(4), File.basename(script[:file]).ljust(20), "(modified: %s ago)" % Util.ago(script[:last_commit]) ]) end def print_numbered_list(entries) entries .sort { |a, b| b[:last_commit] - a[:last_commit] } .each_with_index(&method(:print_info)) end def dump(repo, scripts: false, data: false, engines: false) Log.mono "%s:" % repo[:name] Log.mono "url=".rjust(10) + repo[:url] Log.mono "error=".rjust(10) + repo[:err] if repo[:err] Log.mono "updated=".rjust(10) + "%s ago" % Util.ago(repo[:last_updated]) if repo[:last_updated] if engines && self.engines(repo).any? Log.mono "engines:".rjust(10) print_numbered_list engines(repo) end if scripts && self.scripts(repo).any? Log.mono "scripts:".rjust(10) print_numbered_list scripts(repo) end if data && self.data(repo).any? Log.mono "data:".rjust(7) print_numbered_list data(repo) end end end end end module Jinx module Lookup class << self def find_asset_in(repo, name) repo[:available].find do |asset| CGI.unescape(File.basename(asset[:file])).eql?(name) end end end end end module Jinx module Metadata module Base include Enumerable def atomic() Folder.atomic(@file) { |entries| yield(entries) } end def update(repo, asset) name = File.basename(asset.fetch(:file)) repo_name = repo.fetch(:name) Log.out("updating %s file metadata for %s from %s" % [asset[:type], name, repo_name], label: %i(metadata update)) create(**{ name: name, repo: repo_name, digest: asset.fetch(:md5) }) end def each self.atomic { |entries| entries.each { |entry| yield(entry) } } end def find_for_asset(asset) name = File.basename(asset.fetch(:file)).to_s self.atomic do |entries| return entries[name] end end def create(**argv) name = argv.fetch(:name).to_s repo = argv.fetch(:repo).to_s fail "metadata record(%s) requires a name" % name unless argv[:name].is_a?(String) fail "metadata record(%s) requires a repo" % repo unless argv[:repo].is_a?(Symbol) self.atomic { |entries| argv.delete(:name) entries[name] = argv entries } end end class << self def class_for(asset) case asset[:type] when "script", :script, nil ScriptMetadata when "data", :data DatafileMetadata when "engine", :engine EngineMetadata else fail Jinx::Error, "don't recognize #{asset[:type]} type for #{asset[:file]}" end end def update(repo, asset) class_for(asset).update(repo, asset) end def for(asset) class_for(asset).find_for_asset(asset) end end end end module Jinx module DatafileMetadata extend Metadata::Base @file = "data.yaml" end module ScriptMetadata extend Metadata::Base @file = "scripts.yaml" end module EngineMetadata extend Metadata::Base @file = "engine.yaml" end end module Jinx module Installer # ensure an installer command is specific enough that # there is exactly 1 script that matches def self.ensure_specific(file_name, sources) file_name = normalize_filename(file_name) advertised = sources.select { |repo| Repo.manifest(repo) unless repo[:available].is_a?(Array) Log.out("could not load available assets from repo(%s)" % repo[:name], label: %i(warn)) unless repo[:available].is_a?(Array) repo[:available].is_a?(Array) && Jinx::Lookup.find_asset_in(repo, file_name) } if sources.size.eql?(1) && advertised.size.eql?(0) fail Jinx::Error, <<~ERROR repo(#{sources.first[:name]}) does not advertise asset(#{file_name}) ERROR end if advertised.size > 1 fail Jinx::Error, <<~ERROR more than one repo has asset(#{file_name}) please be more specific by adding --repo={name} available from:\n#{advertised.map { |repo| "- %s".rjust(10) % repo[:name] }.join("\n")} ERROR end if advertised.size.eql?(0) fail Jinx::Error, <<~ERROR no known repos have asset(#{file_name}) available ERROR end return advertised.first end def self.normalize_filename(script) return script if script.include?(".") return "%s.lic" % script end def self.open(resource, &block) # modern ruby return URI.parse(resource).open(&block) if defined?(URI) && URI.respond_to?(:open) && URI.respond_to?(:parse) # older rubies where URI.open/2 doesn't exist return Kernel::open(resource, &block) end def self.download(repo, asset, local_location) remote_location = repo[:url] + asset[:file] self.open(remote_location) { |remote| File.open(local_location, "wb") { |local| local.write(remote.read) } } Log.out("file downloaded to %s" % local_location, label: %i(install download)) end end end module Jinx module LichInstaller def self.install(remote_name, sources, local_asset_directory, local_asset_name = nil, overwrite: false, force: false, asset_type: nil) remote_name = Installer.normalize_filename(remote_name) sources = sources.map { |source| Repo.manifest(source) } repo = Installer.ensure_specific(remote_name, sources) asset = Jinx::Lookup.find_asset_in(repo, remote_name) if !asset_type.nil? && asset[:type] != asset_type fail Jinx::Error, <<~ERROR Attempted to download #{remote_name} as type: #{asset_type.upcase} instead of #{asset[:type].upcase}. Please use proper syntax for downloading scripts or data. ;jinx script install {script:name} attempt to install a script ;jinx script update {script:name} attempts to update an installed script ;jinx data install {datafile:name} attempt to install a data file ;jinx data update {datafile:name} attempts to update an installed data file ERROR end local_asset_name = CGI.unescape(File.basename(local_asset_name || asset[:file])) if local_asset_name.eql?("mapdb.json") local_asset_name = Pathname.new(XMLData.game).join( "map-#{asset[:last_commit]}.json" ) end local_asset = File.join(local_asset_directory, local_asset_name) if !force && File.exist?(local_asset) source = File.read(local_asset) digest = Digest::SHA1.new digest.update(source) if digest.base64digest == asset[:md5] Log.out("%s from repo:%s already installed with md5(%s) at %s" % [remote_name, repo[:name], asset[:md5], local_asset], label: %i(install env lich)) return elsif !overwrite fail Jinx::Error, <<~ALREADY_EXISTS #{local_asset} already exists if the overwrite is intentional rerun as: ;jinx #{Jinx::Service.vars[0].gsub(" install ", " update ")} ALREADY_EXISTS elsif (metadata = Metadata.for(asset)) if digest.base64digest != metadata[:digest] fail Jinx::Error, <<~LOCALLY_MODIFIED #{local_asset} has been modified since last download. if the overwrite is intentional rerun as: ;jinx #{Jinx::Service.vars[0]} --force LOCALLY_MODIFIED end end end Log.out("installing %s from repo:%s with md5(%s)" % [remote_name, repo[:name], asset[:md5]], label: %i(install env lich)) Installer.download(repo, asset, local_asset) PostInstall.run(repo, asset, local_asset) end end end module Jinx module PostInstall module MapImages def self.download_missing(repo) ensure_map_directory_exists return if missing_images.none? Log.out("downloading missing map images", label: %i(install env lich map image)) missing_images.each do |image| if (asset = Jinx::Lookup.find_asset_in(repo, image)) Jinx::Installer.download(repo, asset, map_dir.join(image)) else Log.out("no asset found for #{image}", label: %i(install env lich map image)) end end end def self.images_in_mapdb Map.list.map(&:image).compact.sort.uniq end def self.missing_images images_in_mapdb.reject do |image| map_dir.join(image).exist? end end def self.ensure_map_directory_exists unless map_dir.exist? Log.out("creating map directory at %s" % map_dir, label: %i(install env lich map directory)) map_dir.mkdir end end def self.map_dir Pathname.new($lich_dir).join("maps") end end HOOKS = { lich: { "gameobj-data.xml" => proc { GameObj.load_data }, "spell-list.xml" => proc { Spell.load }, "effect-list.xml" => proc { Spell.load }, "mapdb.json" => proc do |repo| Map.reload MapImages.download_missing(repo) end }.freeze, urnon: {}.freeze, }.freeze def self.run(repo, asset, local_asset) if (h = hook_for(asset)) Log.out("running #{Jinx.mode} post-install hooks for %s" % [File.basename(asset[:file])], label: %i(postinstall hook)) h.call(repo, asset, local_asset) end Metadata.update(repo, asset) end def self.hook_for(asset) f = asset[:file] hooks = HOOKS.fetch(Jinx.mode, {}) hooks[f] || hooks[File.basename(f)] end end end module Jinx module LichScript def self.install(script, sources, overwrite: false, force: false) LichInstaller.install(script, sources, Folder.script_dir, overwrite: overwrite, force: force, asset_type: "script") end end end module Jinx module LichData def self.install(data_file, sources, overwrite: false, force: false) LichInstaller.install(data_file, sources, Folder.data_dir, overwrite: overwrite, force: force, asset_type: "data") end end end module Jinx module LichEngine DEFAULT_ENGINE = "lich.rb" def self.install(engine, sources, overwrite: false, force: false) LichInstaller.install( normalize_name(engine), sources, Folder.engine_dir, current_engine, overwrite: overwrite, force: force, ) end def self.normalize_name(raw_engine_name) raw_engine_name ||= DEFAULT_ENGINE case raw_engine_name.chomp.downcase when /^$/, /^lich[.]rbw?$/, /^lich$/ DEFAULT_ENGINE else raw_engine_name end end def self.current_engine File.basename($PROGRAM_NAME) end end end module Jinx module UrnonInstaller ## # Urnon allows for nested script dirs, this ensures local scripts are namespace by repo # ex: $script_dir/core/do_the_thing -> ;core/do_the_thing # def self.ensure_local_repo_dir(script, sources) script = Installer.normalize_filename(script) sources = sources.map { |source| Repo.manifest(source) } repo = Installer.ensure_specific(script, sources) local_repo_dir = File.join(Folder.script_dir, repo[:name].to_s) unless Dir.exist?(local_repo_dir) Log.out("creating local repo directory at %s" % local_repo_dir, label: %i(repo setup)) Dir.mkdir(local_repo_dir) end return local_repo_dir end end end module Jinx module UrnonScript def self.install(script, sources, overwrite: false, force: true) local_repo_dir = UrnonInstaller.ensure_local_repo_dir(script, sources) # we normalized it for the LichInstaller module now LichInstaller.install(script, sources, local_repo_dir, overwrite: overwrite, force: force, asset_type: "script") end end end module Jinx module UrnonData ## # Urnon uses the same api for data as lich currently # def self.install(script, sources, overwrite: false, force: false) LichInstaller.install(script, sources, Folder.data_dir, overwrite: overwrite, force: force, asset_type: "data") end end end module Jinx module Setup def self.apply() # setup jinx folder unless Dir.exist?(Folder.jinx_data_dir) Log.out("creating %s" % Folder.jinx_data_dir, label: %i(setup)) Dir.mkdir(Folder.jinx_data_dir) end # setup repo source list unless File.exist? Folder.path("repos.yaml") Log.out("creating %s" % Folder.path("repos.yaml"), label: %i(setup)) FileUtils.touch Folder.path("repos.yaml") end # seed core repos repos_to_seed = [Seed::ElanthiaOnlineCore, Seed::ElanthiaOnlineExtras, Seed::FFNGLichRepoArchiveMirror] repos_to_seed.each { |repo| Repo.create(**repo) unless Repo.exists?(repo[:name]) } # prune deprecated repos repos_to_prune = [Seed::Gtk3ScriptUpdates] repos_to_prune.each { |repo| Repo.remove(repo) if Repo.exists?(repo[:name]) } # setup script metadata unless File.exist? Folder.path("scripts.yaml") Log.out("creating %s" % Folder.path("scripts.yaml"), label: %i(setup scripts)) FileUtils.touch Folder.path("scripts.yaml") end # setup data file metadata unless File.exist? Folder.path("data.yaml") Log.out("creating %s" % Folder.path("data.yaml"), label: %i(setup data)) FileUtils.touch Folder.path("data.yaml") end # setup engine metadata unless File.exist? Folder.path("engine.yaml") Log.out("creating %s" % Folder.path("engine.yaml"), label: %i(setup engine)) FileUtils.touch Folder.path("engine.yaml") end end end end module Jinx module Service # for runnign in non-lich envs easier def self.vars() if defined?(Script) Script.current.vars else vars = (@vars || []) [vars.join(" "), *vars] end end # for running string commands def self.run(cmd) main cmd.split(" ") end # main cli entry-point def self.main(args) @vars = args argv = Opts.parse(args) # prune flags args = args.reject { |arg| arg.start_with?("--") } # help if argv.help.eql?(true) && args.length.eql?(1) return CLI.help() end # repo list if argv.repo.eql?(true) && argv.list.eql?(true) return CLI.repo_list() end # repo info if argv.repo.eql?(true) && argv.info.eql?(true) return CLI.repo_info(args.last) end # repo add if argv.repo.eql?(true) && argv.add.eql?(true) url = args.at(-1) repo_name = args.at(-2) unless url.start_with?("http://") or url.start_with?("https://") fail Jinx::Error, "invalid scheme only http/https supported\nurl=%s" % url end return CLI.repo_add(repo_name, url) end if argv.repo.eql?(true) && argv.change.eql?(true) url = args.at(-1) repo_name = args.at(-2) (url.start_with?("http://") or url.start_with?("https://") or fail Jinx::Error, "invalid scheme only http/https supported\nurl=%s" % url) return CLI.repo_change(repo_name, url) end # repo rm if argv.repo.eql?(true) && argv.rm.eql?(true) repo_name = args.at(-1) return CLI.repo_rm(repo_name) end # script list if argv.script.eql?(true) && argv.list.eql?(true) return CLI.script_list(argv.repo) end # script info if argv.script.eql?(true) && argv.info.eql?(true) return CLI.script_info(args.last, argv.repo) end # script install if argv.script.eql?(true) && argv.install.eql?(true) return CLI.script_install(args.last, argv.repo, force: argv.force) end # script update if argv.script.eql?(true) && argv.update.eql?(true) return CLI.script_update(args.last, argv.repo, force: argv.force) end # script search if argv.script.eql?(true) && argv.search.eql?(true) return CLI.script_search(args.last, argv.repo) end # data list if argv.data.eql?(true) && argv.list.eql?(true) return CLI.data_list(argv.repo) end # data info if argv.data.eql?(true) && argv.info.eql?(true) return CLI.data_info(args.last, argv.repo) end # data install if argv.data.eql?(true) && argv.install.eql?(true) return CLI.data_install(args.last, argv.repo, force: argv.force) end # data update if argv.data.eql?(true) && argv.update.eql?(true) return CLI.data_update(args.last, argv.repo, force: argv.force) end # engine list if argv.engine.eql?(true) && argv.list.eql?(true) return CLI.engine_list(argv.repo) end # engine update if argv.engine.eql?(true) && argv.update.eql?(true) return CLI.engine_update(args[2], argv.repo, force: argv.force) end Log.out("unknown command", label: %i(cli)) CLI.help() end end end module Jinx module CLI def self.help() Log.mono <<~HELP jinx(mode=#{Jinx.mode}) A federated script repository manager Usage: ;jinx help or ;jinx <subcommand> [<arg1>, ...] Repo commands: repo list list all currently known repositories repo add {repo:name} {repo:url} add a repository from an http url repo info {repo:name} show detailed info about a specific repo repo rm {repo:name} remove a repository by name repo change {repo:name} {repo:url} changes the url for a given repo Script commands: script list list all currently known scripts script info {script:name} shows known info about a remote script script install {script:name} attempt to install a script script update {script:name} attempts to update an installed script script search {pattern} searches for any script matching the pattern Data commands: data list list all currently known data files data info {datafile:name} shows known info about a remote data file data install {datafile:name} attempt to install a data file data update {datafile:name} attempts to update an installed data file data search {pattern} searches for data files matching the pattern Engine commands: engine list list all currently known scripting engines engine update {engine:name} attempts to update your scripting engine Script, data, and engine commands all take an optional --repo={repo:name} argument to perform the action on a specified repo. Without the argument, the action will be attempted on all repos that have been added. HELP end def self.repo_list() Repo.each { |repo| Repo.dump(repo) } end def self.repo_add(repo_name, repo_url) Repo.create(name: repo_name, url: repo_url) end def self.repo_info(repo_name) repo = Repo.lookup(repo_name) Repo.dump( Repo.manifest(repo), scripts: true, data: true, engines: true ) end def self.repo_rm(repo_name) Repo.remove(name: repo_name) end def self.repo_change(repo_name, repo_url) repo = Repo.lookup(repo_name) Repo.atomic { |repos| repos[repo[:name]] = repo.merge({ url: repo_url }) Log.mono("repo(%s) has been changed from\nold=%s\nnew=%s" % [ repo_name, repo[:url], repo_url ]) repos } end def self.script_list(repo_name = nil) # Log.out(repo_name, label: %i(script_list)) Repo .select { |repo| repo_name.nil? or repo[:name].eql?(repo_name.to_sym) } .map { |repo| repo.merge(Repo.manifest(repo)) } .each { |repo| Repo.dump(repo, scripts: true) } end def self.script_info(script_name, repo_name = nil) repos = repo_name.nil? ? Repo.to_a : [Repo.lookup(repo_name)] repo = Installer.ensure_specific(script_name, repos) script_metadata = Repo.scripts(repo).find { |asset| File.basename(asset[:file]) .eql?(Installer.normalize_filename(script_name)) } Log.mono("%s (repo: %s, modified: %s)" % [script_name, repo[:name], Util.ago(script_metadata[:last_commit]) + " ago"]) if script_metadata[:header] Log.mono Repo.header(repo, script_metadata) else Log.mono "no documentation" end end def self.script_install(script, repo_name = nil, force: false) sources = repo_name.nil? ? Repo.to_a : [Repo.lookup(repo_name)] env = defined?(Urnon) ? Jinx::UrnonScript : Jinx::LichScript env.install(script, sources, overwrite: force, force: force) end def self.script_update(script, repo_name = nil, force: false) sources = repo_name.nil? ? Repo.to_a : [Repo.lookup(repo_name)] env = defined?(Urnon) ? Jinx::UrnonScript : Jinx::LichScript env.install(script, sources, overwrite: true, force: force) end def self.script_search(pattern, repo_name) pattern = Regexp.compile(pattern) candidate_repos = repo_name.nil? ? Repo.to_a : [Repo.lookup(repo_name)] matches = candidate_repos .each { |repo| Repo.manifest(repo) } .select { |repo| repo[:available].is_a?(Array) } .map { |repo| Repo.scripts(repo).map { |script| script.merge({ repo: repo[:name] }) } } .flatten(1) .select { |script| File.basename(script[:file]) =~ pattern } Log.mono "found %s #{matches.size == 1 ? "match" : "matches"}" % matches.size matches.each { |script| Log.mono "%s> %s (last updated: %s ago)" % [ script[:repo].to_s.rjust(15), File.basename(script[:file]).rjust(20), Util.ago(script[:last_commit]) ] } end def self.data_list(repo_name = nil) Log.out(repo_name, label: %i(data_list)) Repo .select { |repo| repo_name.nil? or repo[:name].eql?(repo_name.to_sym) } .map { |repo| repo.merge(Repo.manifest(repo)) } .each { |repo| Repo.dump(repo, data: true) } end def self.data_info(datafile_name, repo_name = nil) repos = repo_name.nil? ? Repo.to_a : [Repo.lookup(repo_name)] repo = Installer.ensure_specific(datafile_name, repos) datafile_metadata = Repo.data(repo).find { |asset| File.basename(asset[:file]) .eql?(Installer.normalize_filename(datafile_name)) } Log.mono("%s (repo: %s, modified: %s)" % [datafile_name, repo[:name], Util.ago(datafile_metadata[:last_commit]) + " ago"]) if datafile_metadata[:header] Log.mono Repo.header(repo, datafile_metadata) else Log.mono "no documentation" end end def self.data_install(datafile, repo_name = nil, force: false) sources = repo_name.nil? ? Repo.to_a : [Repo.lookup(repo_name)] env = defined?(Urnon) ? Jinx::UrnonData : Jinx::LichData env.install(datafile, sources, overwrite: force, force: force) end def self.data_update(datafile, repo_name = nil, force: false) sources = repo_name.nil? ? Repo.to_a : [Repo.lookup(repo_name)] env = defined?(Urnon) ? Jinx::UrnonData : Jinx::LichData env.install(datafile, sources, overwrite: true, force: force) end def self.engine_list(repo_name = nil) Log.out(repo_name, label: %i(engine_list)) if repo_name Repo .select { |repo| repo_name.nil? or repo[:name].eql?(repo_name.to_sym) } .map { |repo| repo.merge(Repo.manifest(repo)) } .each { |repo| Repo.dump(repo, engines: true) } end def self.engine_update(engine, repo_name = nil, force: false) sources = repo_name.nil? ? Repo.to_a : [Repo.lookup(repo_name)] env = defined?(Urnon) ? Jinx::UrnonEngine : Jinx::LichEngine env.install(engine, sources, overwrite: true, force: force) end end end if defined?(Lich) or defined?(Urnon) Jinx::Setup.apply() Jinx::Util.unwrap { Jinx::Service.main(Script.current.vars.slice(1..-1)) } end