diff --git a/manifest.yml b/manifest.yml index f90f47f..e2031c0 100644 --- a/manifest.yml +++ b/manifest.yml @@ -6,13 +6,47 @@ plugins: run: '{plugin_dir}/echo.sh' # this is the default installCommand, so we don't really need to specify it installCommand: ['git', 'clone', '{remote}', '{plugin_dir}'] + preInstallCommand: ['bash', '-c', 'echo "is this thing on?"'] + # postInstallCommand: # none of these work, but wouldn't be tough to implement if needed - # preInstallCommand: # 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: 'https://git.lyte.dev/lytedev/shutup-cli-plugin' run: '{plugin_dir}/shutup.sh' + + 'go-hello-world': + preInstallCommand: + # install golang using asdf + # if we wanted, we could have an asdf plugin and install it here like + # poc-cli install-plugin asdf + # and it would make sure the shell and everything is ready to go + - 'bash' + - '-c' + - 'asdf plugin-add golang; asdf install golang latest; asdf global golang latest; curl -o /tmp/hello-world.go https://raw.githubusercontent.com/mmcgrana/gobyexample/master/examples/hello-world/hello-world.go' + installCommand: + - bash + - '-c' + - 'cd {plugin_dir} && go build /tmp/hello-world.go' + run: '{plugin_dir}/hello-world' + + 'elixir-hello-jason': + preInstallCommand: + # obviously, installing elixir and erlang through asdf can be a little + # hairy in the real world, but a contrived example suits this + # proof-of-concept just fine + - 'bash' + - '-c' + - 'asdf plugin-add erlang; asdf plugin-add elixir; asdf install erlang latest; asdf install elixir latest; asdf global erlang latest; asdf global elixir latest' + installCommand: + - bash + - '-c' + # remember, instead of installing :jason, we might install internal libraries! + - 'printf "%s\n%s" "Mix.install([:jason])" "IO.puts(Jason.encode!(%{hello: :world}))" > {plugin_dir}/script.exs' + run: + - elixir + - '{plugin_dir}/script.exs' + + # in the real world, most of these scripts would probably live within the plugin themselves to be easier to write diff --git a/readme.md b/readme.md index 3e5beaf..e18b3a8 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,41 @@ +# CLI Proof-of-Concept + +## Known Issues + +- The plugin directory being present doesn't _really_ mean it's "installed" ;P + +## Compile + +```sh +deno compile -A -o poc-cli src/core.ts +``` + +You can cross-compile, too. See `deno compile --help` for `--target`. + +## Install + +I dunno, but probably something like this: + +```sh +sudo mv poc-cli /usr/bin +sudo chown root:root /usr/bin/poc-cli +sudo chmod 755 /usr/bin/poc-cli +``` + +## Usage + +It's self-documenting! + +``` +poc-cli +``` + +----- + +# Notes Below! + +----- + # Pluggable CLI - Each subcommand is a plugin? diff --git a/src/core.ts b/src/core.ts index fcbeefb..8d40c9d 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,7 +1,8 @@ import {yaml, fs, path, xdg} from "./deps.ts" -const MANIFEST_URL = "https://git.lyte.dev/lytedev/pluggable-cli-deno/raw/branch/master/manifest.yml" -const PLUGINS_DIR = path.join(xdg.cache(), "/cli-poc-installed-plugins") +// const MANIFEST_URL = "https://git.lyte.dev/lytedev/pluggable-cli-deno/raw/branch/master/manifest.yml" +const MANIFEST_URL = "file:///home/daniel/code/pluggable-cli/manifest.yml" +const PLUGINS_DIR = path.join(xdg.cache(), "/poc-cli-installed-plugins") fs.ensureDir(PLUGINS_DIR) @@ -19,14 +20,20 @@ for await (const dir of Deno.readDir(PLUGINS_DIR)) { } // console.debug(manifest) +async function showConfig() { + console.info("MANIFEST_URL:", MANIFEST_URL) + console.info("PLUGINS_DIR:", PLUGINS_DIR) +} + async function usage() { - console.info("cli-poc: a CLI proof-of-concept") + console.info("poc-cli: 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(" ensure-plugin ") console.info(" list-plugins") + console.info(" show-config") } async function listPlugins() { @@ -37,8 +44,23 @@ async function listPlugins() { } } -async function installedPlugins() { - +async function runCommandAndMaybeExit(cmd: string[] | string, additionalOptions?: Partial) { + if (typeof cmd === 'string') { + cmd = [cmd] + } + const opts = Object.assign({ + stdout: "piped", + stderr: "piped", + cmd + }, additionalOptions) + // console.debug(opts) + const runner = Deno.run(opts) + const status = await runner.status() + if (status.code != 0) { + console.error(`Command ${JSON.stringify(cmd)} failed:\n${new TextDecoder().decode(await runner.output())}\n${new TextDecoder().decode(await runner.stderrOutput())}`) + Deno.exit(status.code) + } + return runner } async function installPlugin(pluginName: string) { @@ -47,25 +69,25 @@ async function installPlugin(pluginName: string) { console.error(`plugin ${pluginName} has no entry`) Deno.exit(1) } - const cmd = (pluginManifestData.installCommand || ["git", "clone", pluginManifestData.remote, pluginName]).map((segment: string) => + + if (pluginManifestData.preInstallCommand) { + await runCommandAndMaybeExit(pluginManifestData.preInstallCommand) + } + + const cmd = (pluginManifestData.installCommand || ["git", "clone", "{remote}", "{plugin_dir}"]).map((segment: string) => segment .replace('{remote}', pluginManifestData.remote) .replace('{plugin_dir}', path.join(PLUGINS_DIR, pluginName)) ) - console.debug(cmd) + fs.ensureDir(path.join(PLUGINS_DIR, pluginName)) + const result = await runCommandAndMaybeExit(cmd) - const installCommand = Deno.run({ - stdout: "piped", - stderr: "piped", - cmd - }) - const status = await installCommand.status() - if (status.code != 0) { - console.error(`Installing plugin using command ${cmd} failed:\n${new TextDecoder().decode(await installCommand.output())}\n${new TextDecoder().decode(await installCommand.stderrOutput())}`) - Deno.exit(status.code) + if (pluginManifestData.postInstallCommand) { + await runCommandAndMaybeExit(pluginManifestData.postInstallCommand) } - return installCommand + + return result } async function deletePlugin(pluginName: string) { @@ -79,14 +101,14 @@ async function updatePlugin(pluginName: string) { } async function ensurePlugin(pluginName: string) { - if (manifest.plugins[subcommand].installed !== true) { + if (!manifest.plugins[subcommand].installed) { // console.warn(`Installing missing ${subcommand} plugin...`) await installPlugin(subcommand) } } const subcommand = Deno.args[0] -if (Deno.args.includes("-h") || subcommand == "help" || subcommand == "" || Deno.args.includes("--help")) { +if (Deno.args.includes("-h") || subcommand == "help" || subcommand == "" || Deno.args.includes("--help") || Deno.args.length < 1) { usage() } else if (subcommand == "update-plugin") { await updatePlugin(Deno.args[1]) @@ -98,11 +120,11 @@ if (Deno.args.includes("-h") || subcommand == "help" || subcommand == "" || Deno await installPlugin(Deno.args[1]) } else if (subcommand == "list-plugins") { listPlugins() +} else if (subcommand == "show-config") { + showConfig() } else { await ensurePlugin(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) + const cmd = (typeof manifest.plugins[subcommand].run === 'string' ? + [manifest.plugins[subcommand].run] : manifest.plugins[subcommand].run).map((segment: string) => segment.replace("{plugin_dir}", path.join(PLUGINS_DIR, subcommand))) + await runCommandAndMaybeExit(cmd.concat(Deno.args.slice(1)), {stdout: "inherit", stderr: "inherit"}) }