commit 1cba10695e5d2d2af7be04bae13e708f63b9dc57 Author: Daniel Flanagan Date: Sat Feb 12 01:47:23 2022 -0600 Basic plugin installation solid diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 0000000..b947704 --- /dev/null +++ b/manifest.yml @@ -0,0 +1,16 @@ +_version: '20220212062110' + +plugins: + echo: + remote: '/home/daniel/code/pluggable-cli/plugins/echo' + run: '{plugin_dir}/echo.sh' + # preInstallCommand: + # installCommand: git clone $remote + # may also want to checkout a provided version tag and copy contents + # and only clone to a "plugin-repos" dir + # might want to omit git history, too? + # postInstallCommand: + + shutup: + remote: '/home/daniel/code/pluggable-cli/plugins/shutup' + run: '{plugin_dir}/shutup.sh' diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..3e5beaf --- /dev/null +++ b/readme.md @@ -0,0 +1,41 @@ +# Pluggable CLI + +- Each subcommand is a plugin? +- Versioning and/or stability will be important? + - Maybe plugins _never_ change and instead you prefix them with a version? + - How can we build stability into the system? +- Some subset or all plugins are already known +- Auto-download plugins when attempting to run a command +- Completions for subcommands + - Are completions provided for commands not-yet-installed? Does attempting to + complete a subcommand's commands install the plugin and process its + completions? +- Will have configuration + +# Core functions + +- What plugins are available to me? + - HTTP GET (and cache?) some known human- and machine-readable manifest + - JSON, YAML, Cue, or Ion? +- Install a plugin + - HTTP GET +- Delete a plugin + - rm -r dir +- Update self + - download new binary and replace self with it +- Update plugin + - replace plugin +- Run a plugin with some given arguments + - call plugin with args + +# Components I See + +- Core + - Knows where to find manifest (may cache locally) + - Installs, updates, deletes plugins using information in manifest + - Can update or uninstall itself + - Can run plugins +- Manifest + - Contains information about where to find plugins and their versions +- Plugins + - Probably dumb scripts that call fancier things diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 0000000..6970097 --- /dev/null +++ b/src/core.ts @@ -0,0 +1,95 @@ +import {yaml, fs, path} from "./deps.ts" + +const MANIFEST_URL = "file:///home/daniel/code/pluggable-cli/manifest.yml" +const PLUGINS_DIR = "/home/daniel/.home/.cache/installed-plugins" + +fs.ensureDir(PLUGINS_DIR) + +const verboseOutput = true + +const responseBody = await (await fetch(MANIFEST_URL)).text() +const manifest: any = yaml.parse(responseBody) + +for await (const dir of Deno.readDir(PLUGINS_DIR)) { + if (dir.isDirectory) { + if (manifest.plugins[dir.name]) { + manifest.plugins[dir.name].installed = true + } + } +} +// console.debug(manifest) + +async function usage() { + console.info("cli-poc: a CLI proof-of-concept") + console.info(" -h, --help: Show usage") + console.info(" install-plugin ") + console.info(" update-plugin ") + console.info(" remove-plugin ") + console.info(" list-plugins") +} + +async function listPlugins() { + for (const pluginName in manifest.plugins) { + let text = pluginName + if (manifest.plugins[pluginName].installed) text += " [installed]" + console.info(text) + } +} + +async function installedPlugins() { + +} + +async function installPlugin(pluginName: string) { + const pluginManifestData = manifest.plugins[pluginName] + if (!pluginManifestData) { + console.error(`plugin ${pluginName} has no entry`) + Deno.exit(1) + } + const cmd = ["git", "clone", pluginManifestData.remote, pluginName] + const installCommand = Deno.run({ + cwd: PLUGINS_DIR, + stdout: "piped", + stderr: "piped", + cmd + }) + const status = await installCommand.status() + if (status.code != 0) { + console.error(`Installing plugin using command ${cmd} failed:\n${await installCommand.output()}\n${await installCommand.stderrOutput()}`) + Deno.exit(status.code) + } + return installCommand +} + +async function deletePlugin(pluginName: string) { + await fs.emptyDir(path.join(PLUGINS_DIR, pluginName)) + await Deno.remove(path.join(PLUGINS_DIR, pluginName)) +} + +async function updatePlugin(pluginName: string) { + await deletePlugin(pluginName) + await installPlugin(pluginName) +} + +const subcommand = Deno.args[0] +if (Deno.args.includes("-h") || subcommand == "help" || subcommand == "" || Deno.args.includes("--help")) { + usage() +} else if (subcommand == "update-plugin") { + await updatePlugin(Deno.args[1]) +} else if (subcommand == "remove-plugin") { + await deletePlugin(Deno.args[1]) +} else if (subcommand == "install-plugin") { + await installPlugin(Deno.args[1]) +} else if (subcommand == "list-plugins") { + listPlugins() +} else { + if (manifest.plugins[subcommand].installed !== true) { + // console.warn(`Installing missing ${subcommand} plugin...`) + await installPlugin(subcommand) + } + const subcommandCommand = Deno.run({ + cmd: [manifest.plugins[subcommand].run.replace("{plugin_dir}", path.join(PLUGINS_DIR, subcommand))].concat(Deno.args.slice(1)) + }) + const status = await subcommandCommand.status() + if (status.code != 0) Deno.exit(status.code) +} diff --git a/src/deps.ts b/src/deps.ts new file mode 100644 index 0000000..b6e0be4 --- /dev/null +++ b/src/deps.ts @@ -0,0 +1,6 @@ +export * as xdg from 'https://deno.land/x/xdg@v9.4.0/src/mod.deno.ts'; +export * as media_types from "https://deno.land/x/media_types@v2.12.1/mod.ts"; +export * as compare_versions from "https://deno.land/x/compare_versions@0.4.0/mod.ts"; +export * as yaml from "https://deno.land/std@0.125.0/encoding/yaml.ts" +export * as fs from "https://deno.land/std@0.125.0/fs/mod.ts"; +export * as path from "https://deno.land/std@0.125.0/path/mod.ts";