286 lines
9.7 KiB
JavaScript
Executable File
286 lines
9.7 KiB
JavaScript
Executable File
#!/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); }
|