⚡ Mainly because os (re)instalation TUXEDO OS 3
This commit is contained in:
@ -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)));
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user