=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