dotfiles/bin/§vim_plugins.mjs

286 lines
9.7 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env nodejsscript
/* jshint esversion: 11,-W097, -W040, module: true, node: true, expr: true, undef: true *//* global echo, $, pipe, s, fetch, cyclicLoop */
//TODO: save options!?
$.is_fatal= true;
const dirs= { vim_root: $.xdg.home`.vim` };
Object.assign(dirs, {
pack: dirs.vim_root+"/pack/",
bundle: dirs.vim_root+"/bundle/",
one_files: dirs.vim_root+"/bundle/__one_files/plugin/" });
const file_one_file= dirs.bundle+"one_file.json";
const runResToArr= pipe( s.$().run, ({ stderr, stdout })=> stdout+stderr, o=> o.split("\n"));
const css= echo.css`
.code{ color: yellow; }
.code::before, .code::after{ content: "\`"; }
.url{ color: lightblue; }
.bold{ color: magenta; }
`;
$.api()
.version("2023-04-18")
.describe([
"Utility for managing vim plugins “native” way. It uses two types:",
`- “old” way (${f("bundle", css.code)}): inspiration from ${f("https://shapeshed.com/vim-packages/", css.url)}`,
`- vim8 native package managing (${f("pack", css.code)}): see for example ${f("https://shapeshed.com/vim-packages/", css.url)}`
])
.command("path <type>", [ "Prints paths for given package type",
"Use (S is alias for this script name):",
"- "+f("cd `S path bundle`", css.code),
"- "+f("cd `S path one_files`", css.code),
"- "+f("cd `S path pack`", css.code)
])
.action(function(type){ echo(dirs[type]); $.exit(0); })
.command("clone <type> <url>", [ "Add/install new package.",
`Use ${f("bundle", css.code )}/${f("pack", css.code)} to specify the package ${f("type", css.code)}.`,
`The ${f("url", css.url)} should be a URL to the script itself or url of the git repository or github repository in the form of ${f("username/reponame", css.url)}.`
])
.alias("C")
.option("--target, -t [target]", `In case of ${f("pack", css.code)} type, specify the target sub-directory (typically/defaults ${f("start", css.code)}).`)
.option("--branch, -b [branch]", `In case of ${f("git", css.bold)} repository, specify the branch if it is different from default one.`)
.action(function(type, url, options){
switch(type){
case "bundle": return addBundle(url);
case "pack": return addPack(url, options);
}
echo("Nothing todo, check given arguments (compare to `--help`):", { type, url, options });
$.exit(1);
})
.command("remove <type> <path>", [ "Remove/uninstall package.",
`As ${f("type", css.bold)}/${f("path", css.bold)} use output printed by ${f("list", css.code)} command.`
])
.alias("R").alias("rm")
.action(function(type, path){
switch(type){
case "bundle": return removeBundle(path);
case "pack": return removePack(path);
}
echo("Nothing todo, check given arguments (compare to `--help`):", { type, path });
$.exit(1);
})
.command("list", "List all plugins paths/url/… (based on option).")
.alias("L").alias("ls")
.option("--type, -t [type]", `Defaults to list of paths (${f("paths", css.code)}). Use ${f("repos", css.code)} to show plugins origin.`)
.example("list")
.example("list --type paths")
.example("list --type repos")
.action(actionList)
.command("export", "List all plugins in the form that can be imported by this Utility.")
.action(actionList.bind(null, { type: "json" }))
.command("status", "Loops through all installed plugins and shows overall status.")
.alias("S")
.action(actionStatus)
.command("pull", "Loops through all installed plugins and updates them.")
.alias("P").alias("update")
.action(actionUpdate)
.parse();
function removePack(path){
s.cd(dirs.pack);
s.$("-V").rm("-rf", path);
const root= dirs.pack+path.split("/")[0];
const { code, stdout }= s.$().find(root+"/*/*");
if(!code) echo(stdout);
else if(s.test("-d", root))
s.$("-V").rm("-rf", root);
$.exit(0);
}
function removeBundle(path){
const is_onefile= dirs.one_files.endsWith(path.split("/").slice(0, 2).join("/")+"/");
const name= path.slice(path.lastIndexOf("/")+1);
s.cd(dirs.bundle);
if(is_onefile){
s.rm("-f", path);
pipe( s.cat, JSON.parse, f=> f.filter(u=> !u.endsWith(name)), JSON.stringify, s.echo )
(file_one_file).to(file_one_file);
} else {
s.run`git submodule deinit -f ${path}`;
s.run`git rm ${path}`;
s.rm("-rf", ".git/modules/"+path);
}
const type= is_onefile ? "file" : "repo";
s.run`git commit --all -m "Remove ${type}: ${name}"`;
$.exit(0);
}
async function addBundle(url){
const is_onefile= url.endsWith(".vim");
if(!is_onefile)
url= gitUrl(url);
const name= url.split(/[\/\.]/g).at(is_onefile ? -2 : -1);
s.cd(dirs.bundle);
if(is_onefile){
const file= await fetch(url).then(r=> r.text());
s.echo(file).to(dirs.one_files+name+".vim");
const log= new Set(s.cat(file_one_file).xargs(JSON.parse));
log.add(url);
s.echo(JSON.stringify([ ...log ])).to(file_one_file);
} else {
s.run`git submodule init`;
s.run`git submodule add ${url}`;
}
s.run`git add .`;
const type= is_onefile ? "file" : "repo";
s.run`git commit -m "Added ${type}: ${name}"`;
$.exit(0);
}
/** @param {string} url @param {{ target: "start", branch?: string }} options */
function addPack(url, { target= "start", branch }){
url= gitUrl(url);
const author= url.split(/[:\/]/).at(-2);
const path= dirs.pack+author+"/"+target;
s.mkdir("-p", path);
s.cd(path);
branch= !branch ? "" : `-b ${branch}`;
s.run`git clone ${branch} --single-branch ${url} --depth 1`;
$.exit(0);
}
function gitUrl(url_candidate){
if(url_candidate.endsWith(".git"))
return url_candidate;
return "git@github.com:"+url_candidate;
}
async function actionUpdate(){
const css= echo.css`
.success{ color: lightgreen; }
.success::before{ content: "✓ "; }
`;
updateRepo(dirs.bundle, getBundle());
const todo= getOneFilesUrls();
const progress= echoProgress(todo.length, "Downloaded one-file plugins");
await Promise.all(todo.map(function(url, i){
return fetch(url).then(r=> {
progress.update(i, url);
return r.text();
}).then(f=> s.echo(f).to(dirs.one_files+fileName(url)));
}));
echo("One-file plugin(s) updated.");
try{
s.cd(dirs.bundle).$().run`git commit -m "Update"`;
} catch(e){
echo(e?.message);
}
updateRepo(dirs.pack, getPack());
$.exit(0);
function updateRepo(dir, paths){
echo(dir);
const progress= echoProgress(paths.length, "Pulling");
const todo= paths.map(function(p, i){
progress.update(i, p);
return pull(p);
}).filter(isUpToDate);
if(!todo.length)
return echo("%cAll up-to-date!", css.success);
todo.forEach(([ p, result ])=> echo("%c"+p+"\n", css.success, result.join("\n")));
}
function pull(p){
s.cd(p);
return [ p, runResToArr("git pull") ];
}
function isUpToDate([ _, result ]){
return result[0]===" Already up-to-date.";
}
}
function actionList({ type= "paths" }){
if("paths"===type){
echo("%cbundle", css.bold, dirs.bundle);
getOneFiles().forEach(echoPath);
getBundle().forEach(echoPath);
echo("%cpack", css.bold, dirs.pack);
getPack().forEach(echoPath);
$.exit(0);
}
const progress= echoProgress(3, "Collecting plugins urls");
progress.update(0, dirs.bundle);
const urls_bundle= getBundle().map(getRepo);
progress.update(1, dirs.bundle);
const urls_onefiles= getOneFilesUrls();
progress.update(2, dirs.pack);
const urls_pack= getPack().map(getRepo);
if("repos"===type){
const echoUrl= pipe(
u=> u.replace("git@github.com:", "https://github.com/"),
u=> f(u, css.url),
echo
)
echo("%cbundle", css.bold, dirs.bundle);
urls_bundle.forEach(echoUrl);
echo("%cbundle", css.bold, dirs.one_files);
urls_onefiles.forEach(echoUrl);
echo("%cpack", css.bold, dirs.pack);
urls_pack.forEach(echoUrl);
}
if("json"===type){ //TODO: save options!?
const o= {};
o.bundle= urls_bundle;
o.one_files= urls_onefiles;
o.pack= urls_pack;
echo(JSON.stringify(o));
}
$.exit(0);
function getRepo(p){ s.cd(p); return runResToArr("git remote -v")[0].split(/\s+/g)[1]; }
}
function actionStatus(){
const css= echo.css`
.success { color: lightgreen; }
.success::before { content: "✓ "; }
`;
check(dirs.bundle, getBundle());
echo("Onefiles plugins are not supported yet");
check(dirs.pack, getPack());
$.exit(0);
function check(root, repos){
echo(root);
const progress= echoProgress(repos.length);
const results= repos.flatMap(function(p, i){
progress.update(i, p);
s.cd(p);
const result= runResToArr("git fetch --dry-run --verbose")
.filter(l=> !l ? false : l.startsWith("From") || (!l.startsWith(" = [up-to-date]") && !l.startsWith("POST") ));
if(result.length===1) return [];
return [ [ p, result.join("\n") ] ];
});
if(!results.length)
return echo("%cup-to-date", css.success);
results.forEach(([ p, l ])=> {
echoPath(p);
echo(l);
});
}
}
import { relative } from 'node:path';
function echoPath(path){ return echo(formatPath(path)); }
function formatPath(path){
const type= path.startsWith(dirs.bundle) ? "bundle" : "pack";
return echo.format("%c"+relative(dirs[type], path), "color:lightblue");
}
import { stdout } from 'node:process';
function echoProgress(length, message_start= "Working"){
if(typeof stdout.isTTY === "undefined") return { update(){} };
const css= echo.css`
.progress { color: lightblue; }
`;
echo.use("-R", `${message_start} (%c0/${length}%c)`, css.progress);
return {
update(i, status){
const s= status ? `: ${status}` : "";
return echo.use("-R", `${message_start} (%c${i+1}/${length}%c)${s}`, css.progress);
}
};
}
function getPack(){ return s.ls(dirs.pack).flatMap(f=> s.find(dirs.pack+f+"/start/*/")[0]).filter(Boolean); }
function getBundle(){ return s.cd(dirs.bundle).grep("path", ".gitmodules").split("\n").filter(Boolean).map(l=> dirs.bundle+l.split(" = ")[1]); }
function getOneFiles(){ return s.find(dirs.one_files+"*"); }
function getOneFilesUrls(){ return s.cat(file_one_file).xargs(JSON.parse); }
function fileName(url){ return url.split("/").pop(); }
/** Quick formating of one piece of text. */
function f(text, css){ return echo.format("%c"+text, css); }