Mainly because os (re)instalation TUXEDO OS 3

This commit is contained in:
2024-10-21 16:06:54 +02:00
parent ded3151a67
commit f3bf9e0612
30 changed files with 133 additions and 3421 deletions

View File

@ -1,418 +0,0 @@
#!/usr/bin/env node
/* jshint esversion: 8,-W097, -W040, node: true, expr: true, undef: true */
const /* dependencies */
[ fs, readline, https, { spawn } ]= [ "fs", "readline", "https", "child_process" ].map(p=> require(p));
const /* helper for coloring console | main program params */
colors= { e: "\x1b[38;2;252;76;76m", s: "\x1b[38;2;76;252;125m", w: "\x1b[33m", R: "\x1b[0m", y: "\x1b[38;2;200;190;90m", g: "\x1b[38;2;150;150;150m" },
info= {
name: __filename.slice(__filename.lastIndexOf("/")+1, __filename.lastIndexOf(".")),
version: "1.2.1",
description: "Helper for working with “packages” stored in GitHub releases.",
config: `${__filename.slice(0, __filename.lastIndexOf("."))}.json`,
folder: __filename.slice(0, __filename.lastIndexOf("/")+1),
commands: [
{
cmd: "help", args: [ "--help", "-h" ],
desc: "Shows this text"
},
{
cmd: "config", args: [ "--config" ],
desc: "Opens config file in terminal editor (defaults to vim)"
},
{
cmd: "check", args: [ "--check", "-c" ],
desc: "Shows/checks updates for registered packages"
},
{
cmd: "update", args: [ "--update", "-u" ], param: "group",
desc: "Installs lates versions of registered packages"
},
{
cmd: "uninstall", args: [ "--uninstall", "-u" ], param: "package",
desc: "Deletes downloaded file and moves package to the 'skip' group"
},
{
cmd: "register", args: [ "--register", "--change" ], param: "package",
desc: "Add package infos to internal list to be able installing/updating"
},
{
cmd: "remove", args: [ "--remove" ], param: "package",
desc: ([
"Uninstall package if needed (see `-u`)",
"And remove it from internal list (see `--config`)"
]).join(". ")
}
],
params: {
group: ([
"You can label each package to update only choosen one",
"There are sereved options:",
" - '' (empty): these packages are includes in all groups",
" - 'all': in case of `--update` process all packages (except skipped)",
" - 'skip': these packages are “uninstalled”",
" No updates will be downloaded",
"Group can be setted via '--register'"
]).join(". "),
package: ([
"Represents package identificator, it is in fact GitHub repository path",
"So, it schould be in the form `username/repository`"
]).join(". ")
}
};
printMain();
const current= getCurrent(process.argv.slice(2));
(function main_(){
const { cmd }= current.command;
if(!cmd) return Promise.resolve("No arguments (use `--help` for showing all oprions).");
switch(cmd){
case "help": return Promise.resolve(printHelp());
case "config": return vim_(info.config);
}
const config= getConfig();
switch(cmd){
case "register": return register_(config);
}
if(!config.packages) return Promise.resolve("No packages yet!");
switch(cmd){
case "check": return check_(config);
case "update": return update_(config);
case "uninstall":
case "remove":
return uninstall_(cmd, config);
}
})()
.then(function(message){
if(message)
log(1, `Operation '${current.command.cmd}' successfull: @s_${message}`);
process.exit();
})
.catch(error);
async function uninstall_(cmd, config){
const progress= [
[ "Deleting file", "not needed" ],
[ "Check out from updates", "yes" ],
[ "Remove from packages list", "no" ]
];
const pkg_name= current.param;
const pkg_index= config.packages.findIndex(({ repository })=> repository===pkg_name);
if(pkg_index===-1) return "nothing to do (maybe typo)";
const pkg= config.packages[pkg_index];
const { downloads }= pkg;
if(downloads&&fs.existsSync(downloads)){
try{ fs.unlinkSync(downloads); progress[0][1]= "done"; }
catch (_){ progress[0][1]= colors.e+"error, try manually "+downloads; }
}
Reflect.deleteProperty(pkg, "last_update");
Reflect.set(pkg, "group", "skip");
progress[1][1]= "done";
if(cmd!=="remove") return gotoEnd();
const y= await promt_(`Are you realy want to remove package ${pkg.repository} (yes/no)`, "no");
if(y!=="yes") return gotoEnd();
config.packages.splice(pkg_index, 1);
progress[2][1]= "done";
return gotoEnd();
function gotoEnd(){
const o= progress.reduce((o, [ k, v ])=> Reflect.set(o, k, v)&&o, {});
logSection(" ", pkg_name, o);
save(config);
}
}
function vim_(file){ return new Promise(function(resolve, reject){
const cmd= spawn((process.env.EDITOR||"vim")+(process.platform==="win32"?".bat":""), [ file ], { stdio: 'inherit' });
cmd.on('exit', e=> e ? reject("Editor error, try manually: "+file) : resolve("OK"));
});}
async function update_(config){
const filter= current.param;
const is_all= filter==="all";
let updates= [];
log(1, "Collecting packages to download:");
for(const [
i, { repository, last_update, group, file_name, exec, downloaded, tag_name_regex }
] of Object.entries(config.packages)){
if(group==="skip") continue;
if(!is_all&&group&&filter!==group) continue;
const { tag_name, published_at, html_url, assets_url }= await githubRelease_(repository, tag_name_regex);
const status= packageStatus(last_update, published_at);
if(status!==3) continue;
const assets= await downloadJSON_(repository, assets_url);
if(!assets.length){
console.log(" Nothing to download: Visit "+html_url);
continue;
}
const options= assets.map(({ name, download_count, size })=>
`${name} | size: ${Math.round(size/1048576)}MB | downloads: ${download_count}`);
logSection(" ", " "+repository, {
"Version": tag_name,
"Url": html_url
});
logSection(" ", " Available assets:", options);
const choose= await promt_(" Choose (empty for skip)", "");
if(choose==="") continue;
const { browser_download_url: url, name: remote_name, size }= assets[choose];
updates.push({
index: i,
file_name, exec, downloaded,
repository, version: tag_name, last_update: published_at,
url, remote_name, size
});
}
if(!updates.length){
log(2, "No packages in "+`group ${filter} needs updates.`);
return Promise.resolve("nothing to update");
}
log(1, "Downloading:");
return applySequentially_(updates, async function(todo){
const to= todo.file_name ? info.folder+todo.file_name : (
todo.downloaded ? todo.downloaded : info.folder+todo.remote_name);
const d= await downloadFile_(to, todo);
return Object.assign(todo, d);
})
.then(function(dones){
log(1, "Finalizing:");
let e= 0;
for(const nth of dones){
if(!nth.success){
e+= 1;
log(2, `${nth.repository}: @e_${nth.message}`);
continue;
}
Object.assign(config.packages[nth.index], registerDownloads(nth));
}
save(config);
const { length }= dones;
const msg= `updated ${length-e} of ${length} packages.`;
return e ? Promise.reject(msg) : Promise.resolve(msg);
});
}
function registerDownloads({ repository, last_update, message: downloads, exec, version }){
let msg= colors.s+"OK";
if(exec==="yes"){
try{ fs.chmodSync(downloads, 0o755); }
catch(e){ msg= colors.e+"try manual `chmod+x` for '"+downloads+"'"; }
}
log(2, `${repository}: ${msg}`);
return { last_update, downloads, version };
}
async function check_({ packages }){
let updates= 0, skipped= 0;
for(const { repository, name, version, last_update, group, tag_name_regex } of packages){
const { tag_name, published_at }= await githubRelease_(repository, tag_name_regex);
const status= packageStatus(last_update, published_at);
updates+= status===3;
const skip= group==="skip";
skipped+= skip;
log(2, `@g_${repository} [${group}]: `+( !version ? "not installed" : packageStatusText(status, skip) ));
}
const u= updates-skipped;
const s= skipped ? ` (inc. skipped: ${updates})` : "";
return (!u ? "" : colors.w)+u+" update(s) available"+s;
}
async function register_(config){
const { param: repository }= current;
if(!Reflect.has(config, "packages")) Reflect.set(config, "packages", []);
const packages= Reflect.get(config, "packages");
let local_id= packages.findIndex(p=> p.repository===repository);
if(local_id===-1)
local_id= packages.push({ repository })-1;
const local= config.packages[local_id];
const remote= await githubRepo_(repository) || {};
log(1, "Registering: "+repository);
const spaces= " ";
local.name= await promt_(spaces+"Name", local.name || remote.name || "");
if(!local.description) local.description= remote.description;
logLines(2, [
"@g_Group info:",
"- you can update specific packages by using their group name",
"- There some reserved options:",
" - '' (empty): will be included in all groups",
" - 'skip': will be always skipped"
]);
local.group= await promt_(spaces+"Group", local.group || "");
local.file_name= await promt_(spaces+"File Name", local.file_name || local.name.toLowerCase().replace(/\s/g, "-") || "");
local.exec= await promt_(spaces+"Make executable (yes/no)", local.exec || "no");
save(config);
return `${repository}: saved`;
}
function packageStatusText(status, skip){
const s= skip ? colors.R+"skipped "+colors.g : "";
switch(status){
case 0: return s+"nothing to compare";
case 1: return s+"@s_up-to-date";
case 2: return s+"newer";
case 3: return s+"@e_outdated/not instaled";
}
}
function packageStatus(local, remote){
if(!remote) return 0;
if(!local) return 3;
if(remote===local) return 1;
return 2+(local<remote);
}
function logSection(spaces, name, data){
console.log(spaces+name);
for(const [ key, value ] of Object.entries(data))
console.log(spaces.repeat(2)+colors.g+key+": "+value.replace(/@(\w)_/g, (_, m)=> colors[m])+colors.R);
}
function githubRelease_(repository, tag_name_regex= ""){
return downloadJSON_(repository, "https://api.github.com/repos/"+repository+"/releases")
.then(data=> data.find(function find({ draft, published_at, tag_name }){
if(draft||!published_at) return false;
if(!tag_name_regex) return true;
return (new RegExp(tag_name_regex, 'g')).test(tag_name);
})||{});
}
function githubRepo_(repository){ return downloadJSON_(repository, "https://api.github.com/repos/"+repository); }
function promt_(q, def){
const rl= readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise(function(resolve){
rl.question(q+": ", a=> { rl.close(); resolve(a); });
rl.write(def);
});
}
function getConfig(){
let config;
try{ config= JSON.parse(fs.readFileSync(info.config)); }
catch(e){ config= {}; log(1, "@w_Missing or corrupted config file. Creates empty one."); }
return config;
}
function save(config){
return fs.writeFileSync(info.config, JSON.stringify(config, null, " "));
}
function getCurrent(args){
let command, command_arg, param;
const hasArg= arg=> ({ args })=> args.includes(arg);
for(let i=0, { length }= args, arg; i<length; i++){
arg= args[i];
if(!command){
command= info.commands.find(hasArg(arg));
command_arg= arg;
continue;
}
if(!command.param||typeof param!=="undefined")
break;
param= arg;
}
if(!command)
command= { cmd: "" };
if(command.param&&typeof param==="undefined")
return error(`Missign arguments for '${command_arg}'.`);
return { command, param };
}
function downloadJSON_(repository, url){
return downloadText_(url)
.then(function(data){
try{
const response= JSON.parse(data);
if(Reflect.has(response, "message")) throw new Error(response.message);
return Promise.resolve(JSON.parse(data));
} catch(e){
log(1, "Received data: "+data);
log(1, "@e_"+e);
return Promise.reject(`JSON from '${repository}' failed.`);
}
});
}
function downloadText_(url){
return get_(url)
.then(function(response){ return new Promise(function(resolve){
let data= "";
response.on("data", chunk=> data+= chunk);
response.on("end", ()=> resolve(data));
}); });
}
function downloadFile_(to, { url, repository, size }){
const file= fs.createWriteStream(to);
return get_(url)
.then(r=> get_(r.headers.location))
.then(function(response){ return new Promise(function(resolve){
let progress= 0, pc_prev= 0, avg= 0;
const start= new Date();
const i= setInterval(function(){
readline.clearLine(process.stdout);
const pc= (100*progress/size).toFixed(2);
if(!pc_prev) pc_prev= pc;
else {
avg= ((100-pc)/(60*(pc-pc_prev))).toFixed(2);
pc_prev= 0;
}
const running= ((new Date()-start)/60000).toFixed(2);
log(2, repository+": "+pc+"%"+` (end in ~${avg} mins, running ${running} mins)`);
readline.moveCursor(process.stdout, 0, -1);
}, 500);
response.on('data', function(chunk){
file.write(chunk);
progress+= chunk.length;
});
response.on('end', function(){
clearInterval(i);
readline.clearLine(process.stdout);
log(2, repository+": @s_OK");
file.close(()=> resolve({ success: 1, message: to })); /* close() is async, call cb after close completes. */
});
}); })
.catch(({ message })=> {
fs.unlink(to); // Delete the file async. (But we don't check the result)
return { success: 0, message };
});
}
function get_(url){ return new Promise(function(resolve, reject){
https.get(
url,
{ headers: { 'Cache-Control': 'no-cache', 'User-Agent': 'node' } },
resolve
).on("error", reject);
});}
function applySequentially_(input, pF){
const data= [];
let p= pF(input[0]);
const tie= nth=> result_mth=> ( data.push(result_mth), pF(input[nth]) );
for(let i= 1, { length }= input; i<length; i++)
p= p.then(tie(i));
return p.then(o=> (data.push(o), data));
}
function error(message){
const help_text= `@w_See help using '${info.commands[0].args[0]}'.`;
log(1, `@e_Error: ${message} ${help_text}`);
return process.exit(1);
}
function printMain(){
const { name, version, description }= info;
log(1, `@w_${name}@${version}`);
log(1, description);
const cmds= info.commands.map(({args})=> args[0].replace("--", "")).join(", ");
log(1, `@w_Usage: ${name} --[cmd] [param]`);
log(2, `…cmd: ${cmds}`);
log(2, "…param: Based on cmd\n");
}
function printHelp(){
log(1, "@s_Help:");
log(2, "Commands:");
info.commands.forEach(({ args, param, desc })=> {
const args_text= args.join("|");
param= param ? " "+param : "";
log(3, `@g_${args_text}@R_${param}`);
logLines(4, desc);
});
log(2, "Params:");
for(const [ param, desc ] of Object.entries(info.params)){
log(3, `@g_${param}`);
logLines(4, desc);
}
}
function log(tab, text){
return console.log(" ".repeat(tab)+text.replace(/@(\w)_/g, (_, m)=> colors[m])+colors.R);
}
function logLines(tab, multiline_text){
if(!Array.isArray(multiline_text)) multiline_text= multiline_text.split(/(?<=\.) /g);
return log(tab, multiline_text.join("\n"+" ".repeat(tab)));
}

View File

@ -1,238 +0,0 @@
{
"packages": [
{
"repository": "shiftkey/desktop",
"name": "GitHub Desktop",
"group": "dev",
"file_name": "github-desktop",
"exec": "yes",
"description": "Fork of GitHub Desktop to support various Linux distributions",
"last_update": "2024-03-31T17:49:36Z",
"downloads": "/home/jaandrle/bin/github-desktop",
"version": "release-3.3.12-linux2"
},
{
"repository": "jaandrle/jaaCSS-cli",
"name": "jaaCSS",
"description": "EXPERIMENT Helper for managing functional CSS classes",
"group": "dev",
"file_name": "jaaCSS.js",
"exec": "yes",
"downloads": "/home/jaandrle/bin/jaaCSS.js",
"version": "v1.3.2",
"last_update": "2022-09-02T13:33:16Z"
},
{
"repository": "th-ch/youtube-music",
"name": "youtube-music",
"description": "YouTube Music Desktop App bundled with custom plugins (and built-in ad blocker / downloader)",
"group": "nondev",
"file_name": "youtube-music",
"exec": "yes",
"last_update": "2024-03-26T10:58:44Z",
"downloads": "/home/jaandrle/bin/youtube-music",
"version": "v3.3.5"
},
{
"repository": "ArchGPT/insomnium",
"name": "insomnium",
"description": "Insomnium is a fast local API testing tool that is privacy-focused and 100% local. For testing GraphQL, REST, WebSockets and gRPC. This is a fork of Kong/insomnia",
"group": "dev",
"file_name": "insomnium",
"exec": "yes",
"last_update": "2023-11-13T10:03:28Z",
"downloads": "/home/jaandrle/bin/insomnium",
"tag_name_regex": "core@.*",
"version": "core@0.2.3-a"
},
{
"repository": "Kong/insomnia",
"name": "insomnia",
"description": "The open-source, cross-platform API client for GraphQL, REST, and gRPC.",
"group": "skip",
"file_name": "insomnia",
"exec": "yes",
"last_update": "2023-10-16T10:03:28Z",
"downloads": "/home/jaandrle/bin/insomnia",
"tag_name_regex": "core@.*",
"version": "core@8.3.0"
},
{
"repository": "rvpanoz/luna",
"name": "luna",
"description": "Manage npm dependencies through a modern UI.",
"group": "skip",
"file_name": "luna",
"exec": "yes"
},
{
"repository": "angela-d/wifi-channel-watcher",
"name": "wifi-channel-watcher",
"group": "skip",
"file_name": "wifi-channel-watcher",
"exec": "no",
"description": "Monitor channel usage of neighboring routers & get an alert if your active channel is not optimal.\tTroubleshoot wifi without lifting a finger!"
},
{
"repository": "vinceliuice/Tela-circle-icon-theme",
"name": "Tela-circle-icon-theme",
"description": "Tela-circle-icon-theme",
"group": "themes",
"file_name": "tela-circle-icon-theme.zip",
"last_update": "2021-07-19T14:12:05Z",
"exec": "no"
},
{
"repository": "AppImage/AppImageKit",
"name": "AppImageKit",
"group": "skip",
"file_name": "appimagekit",
"exec": "yes",
"description": "Package desktop applications as AppImages that run on common Linux-based operating systems, such as RHEL, CentOS, openSUSE, SLED, Ubuntu, Fedora, debian and derivatives. Join #AppImage on irc.freenode.net"
},
{
"repository": "dynobo/normcap",
"name": "NormCap",
"description": "Switched to flatpak version | OCR powered screen-capture tool to capture information instead of images",
"group": "skip",
"file_name": "normcap",
"exec": "yes",
"last_update": "2023-12-12T22:23:37Z",
"downloads": "/home/jaandrle/bin/normcap",
"version": "v0.5.2"
},
{
"repository": "upscayl/upscayl",
"name": "upscayl",
"description": "🆙 Upscayl - Free and Open Source AI Image Upscaler for Linux, MacOS and Windows built with Linux-First philosophy.",
"group": "nondev",
"file_name": "upscayl",
"exec": "yes",
"last_update": "2024-02-29T16:31:09Z",
"downloads": "/home/jaandrle/bin/upscayl",
"version": "v2.10.0"
},
{
"repository": "RasmusLindroth/tut",
"name": "tut",
"description": "TUI for Mastodon with vim inspired keys",
"group": "nondev",
"file_name": "tut",
"exec": "yes",
"last_update": "2023-01-26T17:48:00Z",
"downloads": "/home/jaandrle/bin/tut",
"version": "2.0.1"
},
{
"repository": "sunner/ChatALL",
"name": "ChatALL",
"description": " Concurrently chat with ChatGPT, Bing Chat, bard, Alpaca, Vincuna, Claude, ChatGLM, MOSS, iFlytek Spark, ERNIE and more, discover the best answers",
"group": "skip",
"file_name": "chatall",
"exec": "yes",
"last_update": "2023-09-30T14:08:00Z",
"downloads": "/home/jaandrle/bin/chatall",
"version": "v1.50.73"
},
{
"repository": "jaandrle/bs",
"name": "bs",
"description": "The simplest possible build system using executables",
"group": "dev",
"file_name": "bs",
"exec": "yes",
"last_update": "2024-03-28T13:16:41Z",
"downloads": "/home/jaandrle/bin/bs",
"version": "v0.7.4"
},
{
"repository": "h3poteto/fedistar",
"name": "Fedistar",
"description": "Multi-column Mastodon, Pleroma, and Friendica client for desktop",
"group": "nondev",
"file_name": "fedistar",
"exec": "yes",
"last_update": "2024-03-29T15:39:36Z",
"downloads": "/home/jaandrle/bin/fedistar",
"version": "v1.9.2"
},
{
"repository": "ollama/ollama",
"name": "ollama",
"description": "Get up and running with Llama 2 and other large language models locally",
"group": "ai",
"file_name": "ollama",
"exec": "yes",
"last_update": "2024-03-10T02:24:04Z",
"downloads": "/home/jaandrle/bin/ollama",
"version": "v0.1.29"
},
{
"repository": "neovim/neovim",
"name": "neovim",
"tag_name_regex": "v.*",
"description": "Vim-fork focused on extensibility and usability",
"group": "dev-test",
"file_name": "nvim",
"exec": "yes",
"downloads": "/home/jaandrle/bin/nvim",
"version": "v0.9.5",
"last_update": "2023-12-30T13:31:47Z"
},
{
"repository": "viarotel-org/escrcpy",
"name": "Escrcpy",
"description": "📱 Graphical Scrcpy to display and control Android, devices powered by Electron. | 使用图形化的 Scrcpy 显示和控制您的 Android 设备,由 Electron 驱动。",
"group": "dev",
"file_name": "escrcpy",
"exec": "yes",
"last_update": "2024-03-29T03:30:14Z",
"downloads": "/home/jaandrle/bin/escrcpy",
"version": "v1.17.8"
},
{
"repository": "drovp/drovp",
"name": "drovp",
"description": "Desktop app for encoding, converting, upscaling, and much more.",
"group": "dev-test",
"file_name": "drovp",
"exec": "yes",
"last_update": "2023-12-06T11:30:02Z",
"downloads": "/home/jaandrle/bin/drovp",
"version": "0.8.0"
},
{
"repository": "janhq/jan",
"name": "Jan",
"description": "Jan is an open source alternative to ChatGPT that runs 100% offline on your computer",
"group": "ai",
"file_name": "jan",
"exec": "yes",
"last_update": "2024-03-11T06:34:40Z",
"downloads": "/home/jaandrle/bin/jan",
"version": "v0.4.8"
},
{
"repository": "Bin-Huang/chatbox",
"name": "Chatbox",
"description": "Chatbox is a desktop client for ChatGPT, Claude and other LLMs, available on Windows, Mac, Linux",
"group": "ai",
"file_name": "Chatbox",
"exec": "yes",
"last_update": "2024-03-15T15:58:59Z",
"downloads": "/home/jaandrle/bin/Chatbox",
"version": "v1.3.1"
},
{
"repository": "Helium314/HeliBoard",
"name": "HeliBoard",
"description": "Customizable and privacy-conscious open-source keyboard",
"group": "android",
"file_name": "heliboard.apk",
"exec": "no",
"last_update": "2024-03-31T20:11:03Z",
"downloads": "/home/jaandrle/bin/heliboard.apk",
"version": "v1.0"
}
]
}

View File

@ -34,8 +34,10 @@
*/
import { join } from "node:path";
const path_config= $.xdg.config`github-releases`;
const path_config_json= join(path_config, "config.json");
const path_config_lock= join(path_config, "lock");
const paths= {
/** config file path — JSON stringify of {@link Config} */ config: join(path_config, "config.json"),
/** path to lock file to prevent multiple instances */ lock: join(path_config, "lock"),
};
const path_temp= $.xdg.temp`github-releases.json`;
let url_api= "github";
const urls_api= {
@ -52,7 +54,7 @@ const css= echo.css`
`;
$.api()
.version("2.1.0")
.version("2.2.0")
.describe("Helper for working with “packages” stored in GitHub releases.")
.option("--verbose", "Verbose output (WIP)")
.option("--group, -G", "Filter by group (not awaiable for noGRA)")
@ -61,25 +63,71 @@ $.api()
"- GitHub (default): https://api.github.com/repos/",
"- Ungh: https://ungh.cc/repos/", "(not awaiable for noGRA)" ], "github")
.command("unlock", "[noGRA] DANGER: Removes lock file. Use only if you know what you are doing!")
.action(function(){
s.rm(path_config_lock);
})
.command("config [mode]", [ "[noGR] Config (file), use `mode` with these options:",
.action(function(){
s.rm(paths.lock);
})
.command("config [mode]", [ "[noGRA] Config (file), use `mode` with these options:",
"- `edit`: opens config file in terminal editor using `$EDITOR` (defaults to vim)",
"- `path`: prints path to config file"
])
.action(async function(mode= "path"){
switch(mode){
case "path": echo(path_config_json); break;
case "path": echo(paths.config); break;
case "edit":
const editor= $.env.EDITOR || "vim";
await s.runA`${editor} ${path_config_json}`.pipe(process.stdout);
await s.runA`${editor} ${paths.config}`.pipe(process.stdout);
break;
default:
echo(`Unknown mode: '${mode}'. See '--help' for details.`);
}
$.exit(0);
})
.command("edit <repository>", "Edit “package” information")
.alias("add")
.action(async function(repository){
if(!repository || !repository.includes("/"))
$.error(`Invalid repository: '${repository}'. Repository must be in the form '<owner>/<repo>'.`);
const config= /** @type {Config} */ ( readConfig() );
const i= config.packages.findIndex(r=> r.repository===repository);
echo(repository + `${i==-1 ? "New" : "Edit"} package:`);
echo(`Use <tab> to autocomplete${i===-1 ? "" : " and empty to keep current value"}.`);
echo("");
const pkg= config.packages[i] || { repository, group: "" };
const groups= [ ...new Set(config.packages.map(r=> r.group)) ];
const q= (question, initial, ...c)=> {
const completions= [ ...new Set([initial, ...c.flat()]) ].filter(Boolean);
if(initial) question+= ` (current \`${initial}\`)`;
question= echo.format("%c"+question, css.pkg);
return s.read({ "-p": question+": ", completions }).then(pipe(
value=> value || initial,
value=> value ? value : $.error(`Missing '${question}'.`)
));
};
try{
const name= await q("Name", pkg.name);
echo("(i) use `skip` as part of the group to skip it during checking/updating (“just register package”).");
const group= await q("Group", pkg.group, groups);
const { description: description_remote }= await fetch(urls_api[url_api]+repository).then(r=> r.json()).catch(_=> ({}));
const description= await q("Description", pkg.description, description_remote);
const file_name= await q("File name", pkg.file_name, repository.split("/"));
const downloads= config.target+file_name;
const exec= await q("Is executable", pkg.exec, [ "yes", "no" ]);
echo("(i) The glare is used to determine the right file to download. It is regular expression.");
const glare= await q("Glare", pkg.glare);
const pkg_edit= Object.assign({}, pkg,
{ repository, name, description, group, file_name, exec, downloads, glare });
config.packages[i===-1 ? config.packages.length : i]= pkg_edit;
s.echo(JSON.stringify(config, null, "\t")).to(paths.config);
echo(`%cSaved into config file '${paths.config}'.`, css.ok);
$.exit(0);
} catch(e){
if(e instanceof $.Error) echo("%c"+e, css.err);
else echo();
$.exit(1);
}
})
.command("ls", [ "Lists registered packages",
"Repositories marked with `-` signifies that the package is in the 'skip' group.",
"These are registered by this script but not managed by it (updates, etc).",
@ -88,7 +136,7 @@ $.api()
.action(function(filter){
const config = readConfig();
for(const { repository, version, description, group } of grepPackages(config, filter))
if(group!=="skip")
if(group && !group.includes("skip"))
echo(`+ %c${repository}%c@${version ? version : "—"}: %c${description}`, css.pkg, css.unset, css.skip);
else
echo(`- %c${repository}: ${description}`, css.skip);
@ -106,17 +154,16 @@ $.api()
})
.command("update", "Updates registered packages")
.action(async function(filter){
if(s.test("-f", path_config_lock))
return $.error(`The lock file '${path_config_lock}' already exists! Check if some other instance is running.`);
s.touch(path_config_lock);
if(s.test("-f", paths.lock))
return $.error(`The lock file '${paths.lock}' already exists! Check if some other instance is running.`);
s.touch(paths.lock);
const config = readConfig();
const results= await check(grepPackages(config, filter));
const start= Date.now();
let done= 0;
let todo= [];
echo("Collecting packages to update…");
for(const { status, value } of results){
if(status!==3 || value.local.group==="skip") continue;
if(status!==3 || (value.local.group || "skip").includes("skip")) continue;
echo("%c"+value.local.repository, css.pkg);
todo.push(download(
value,
@ -141,9 +188,9 @@ $.api()
const { local, remote }= value;
echo("%c✓ "+local.repository+"%c@"+remote.tag_name, css.ok, css.skip);
}
s.echo(JSON.stringify(config, null, "\t")).to(path_config_json);
s.echo(JSON.stringify(config, null, "\t")).to(paths.config);
}
s.rm(path_config_lock);
s.rm(paths.lock);
$.exit(0);
})
.parse();
@ -186,7 +233,7 @@ function grepPackages({ packages }, { group, repository, api, verbose }){
}
function echoPkgStatus(status, { local, remote }){
let status_css, status_text;
if(local.group==="skip"){
if(local.group && local.group.includes("skip")){
status_text= "skipped";
status_css= "skip";
} else {
@ -198,7 +245,7 @@ function echoPkgStatus(status, { local, remote }){
}
/**
* @param {Config.packages} packages
* @return {{ status: 0|1|2|3, value: { remote: GitHubRelease, local: ConfigPackage } }}
* @return {Promise<{ status: 0|1|2|3, value: { remote: GitHubRelease, local: ConfigPackage } }>}
* */
async function check(packages, cache){
return (await pipe(
@ -245,9 +292,9 @@ async function fetchRelease({ repository, tag_name_regex }, cache){
}
function readConfig(){
if(!s.test("-f", path_config_json)) return { packages: [] };
if(!s.test("-f", paths.config)) return { packages: [] };
const out= Object.assign({ target: "~/bin/" },
s.cat(path_config_json).xargs(JSON.parse));
s.cat(paths.config).xargs(JSON.parse));
if(out.target.startsWith("~/")) out.target= $.xdg.home(out.target.slice(2));
return out;
}