diff --git a/AGENTS.md b/AGENTS.md index 8158ce9..569646f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ - Preserve normal CLI behavior expectations: stdin/stdout composition, scriptability, and minimal surprise for shell users. ## Current Repo State -- Current entrypoint and installer are `bin/psh.sh`. +- Current entrypoint is `bin/psh.sh`. CLI install is `install.sh` (XDG only). The Omarchy plugin is installed with `omarchy plugin add` / `omarchy plugin install`. - Tests live in `tests/` and use the npm-installed Bats runner at `node_modules/bats/bin/bats`. - `Makefile` provides `make test` and `make install-smoke` shortcuts. - Do not invent build/lint commands until the relevant executable config exists. @@ -22,7 +22,7 @@ - Bats test dependency check: `test -x node_modules/bats/bin/bats && command -v setsid && command -v script`. - Bats test suite: `node_modules/bats/bin/bats tests`. - Make test shortcut: `make test`. -- Local installer smoke check: `PSH_INSTALL_DIR=$(mktemp -d) sh bin/psh.sh install` installs `psh` into the temp directory. +- Local installer smoke check: `XDG_DATA_HOME=$(mktemp -d) PSH_INSTALL_DIR=$(mktemp -d) bash install.sh` writes the payload under `XDG_DATA_HOME/psh` and the launcher into the temp bin directory. - Dependency check without API key or config: `XDG_CONFIG_HOME=$(mktemp -d) bin/psh.sh run clean up docker` exits 2 with `psh: API key is required; run \`psh setup\` or set provider API key env var`. - Smoke checks with OpenAI access: `OPENAI_API_KEY=... bin/psh.sh run clean up docker`, `OPENAI_API_KEY=... bin/psh.sh clean up docker`, and `printf %s "clean up docker" | OPENAI_API_KEY=... bin/psh.sh run` verify prompt ingestion and command generation. - Smoke check with Fireworks access: `PSH_PROVIDER=fireworks FIREWORKS_API_KEY=... bin/psh.sh run clean up docker` verifies Fireworks command generation. diff --git a/BarWidget.qml b/BarWidget.qml new file mode 100644 index 0000000..c49818c --- /dev/null +++ b/BarWidget.qml @@ -0,0 +1,62 @@ +import QtQuick +import Quickshell +import qs.Ui + +BarWidget { + id: root + moduleName: "com.mdtrr.promptshell" + + readonly property bool opened: panelLoader.item ? panelLoader.item.opened === true : false + readonly property bool popoutSwitchClosing: panelLoader.item ? panelLoader.item.popoutSwitchClosing === true : false + + function open() { + if (panelLoader.item) panelLoader.item.open() + } + + function close() { + if (panelLoader.item) panelLoader.item.close() + } + + function toggle() { + if (panelLoader.item) panelLoader.item.toggle() + } + + function closeForPopoutSwitch() { + if (panelLoader.item) panelLoader.item.closeForPopoutSwitch() + } + + function injectPanel() { + var target = panelLoader.item + if (!target) return + if ("bar" in target) target.bar = root.bar + if ("anchorItem" in target) target.anchorItem = button + if ("hostWidget" in target) target.hostWidget = root + } + + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + onBarChanged: injectPanel() + + Loader { + id: panelLoader + active: true + source: Qt.resolvedUrl("Panel.qml") + visible: false + onLoaded: { + root.injectPanel() + Qt.callLater(root.injectPanel) + } + } + + WidgetButton { + id: button + anchors.fill: parent + bar: root.bar + text: "psh" + tooltipText: "Prompt Shell" + onPressed: function(buttonCode) { + if (buttonCode === Qt.LeftButton) root.toggle() + } + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12a1438..e2348a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ make install-smoke ## Code Style -- POSIX shell for the CLI, including integrated install/uninstall commands. +- POSIX shell for the CLI. CLI install is `install.sh` (bash). Omarchy plugin install is `omarchy plugin add` / `omarchy plugin install`. - Keep changes small and focused. - Preserve stdin/stdout composition and scriptability. - Use `/dev/tty` for interactive-only UI. diff --git a/Makefile b/Makefile index 70392d9..980da84 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ deps: syntax: sh -n bin/psh.sh + bash -n install.sh bash -n tests/helpers/psh.bash test: syntax @@ -16,4 +17,8 @@ test: syntax $(BATS) tests install-smoke: - install_dir=$$(mktemp -d) && PSH_INSTALL_DIR=$$install_dir sh bin/psh.sh install && test -x "$$install_dir/psh" && PSH_INSTALL_DIR=$$install_dir "$$install_dir/psh" uninstall && test ! -e "$$install_dir/psh" + data_home=$$(mktemp -d) && install_dir=$$(mktemp -d) && \ + XDG_DATA_HOME=$$data_home PSH_INSTALL_DIR=$$install_dir bash install.sh && \ + test -x "$$install_dir/psh" && test -x "$$data_home/psh/psh.sh" && \ + XDG_DATA_HOME=$$data_home PSH_INSTALL_DIR=$$install_dir "$$install_dir/psh" uninstall && \ + test ! -e "$$install_dir/psh" && test ! -e "$$data_home/psh/psh.sh" diff --git a/Panel.qml b/Panel.qml new file mode 100644 index 0000000..c4e4ed7 --- /dev/null +++ b/Panel.qml @@ -0,0 +1,82 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Ui + +Panel { + id: root + moduleName: "com.mdtrr.promptshell" + manageIpc: false + + property var anchorItem: null + property var hostWidget: null + + function open() { + root.controller.show() + Qt.callLater(function() { + if (promptInput) promptInput.forceActiveFocus() + }) + } + + function close() { + root.controller.hide() + } + + function switchPanel(direction) { + if (root.bar && typeof root.bar.switchPanelFrom === "function") + return root.bar.switchPanelFrom(root.hostWidget || root, direction) + return false + } + + function runPrompt() { + var prompt = promptInput.text.trim() + if (prompt === "") return + Quickshell.execDetached(["omarchy-launch-tui", "psh", "run", prompt]) + promptInput.text = "" + root.close() + } + + KeyboardPanel { + id: panel + anchorItem: root.anchorItem + owner: root.hostWidget || root + bar: root.bar + open: root.opened + focusTarget: keyCatcher + contentWidth: panel.fittedContentWidth(Style.space(320)) + contentHeight: panel.fittedContentHeight(content.implicitHeight) + + PanelKeyCatcher { + id: keyCatcher + anchors.fill: parent + onCloseRequested: root.close() + onTabRequested: function(direction) { root.switchPanel(direction) } + + Column { + id: content + width: parent.width + spacing: Style.space(8) + + Text { + width: parent.width + text: "Prompt Shell" + color: root.bar ? root.bar.foreground : Color.foreground + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.subtitle + font.bold: true + } + + TextInput { + id: promptInput + width: parent.width + color: root.bar ? root.bar.foreground : Color.foreground + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.body + wrapMode: TextInput.Wrap + Keys.onReturnPressed: root.runPrompt() + Keys.onEnterPressed: root.runPrompt() + } + } + } + } +} diff --git a/README.md b/README.md index b72d425..0288bc2 100644 --- a/README.md +++ b/README.md @@ -10,32 +10,47 @@ printf %s "clean up docker" | psh run ## Install -Install with curl: +Two install targets: + +**CLI** — `install.sh` writes `psh` into XDG directories. It does not install the Omarchy plugin. ```sh -curl -fsSL https://raw.githubusercontent.com/modoterra/promptshell/main/bin/psh.sh | sh -s -- install +curl -fsSL https://raw.githubusercontent.com/modoterra/promptshell/main/install.sh | bash ``` -The `-s` flag tells `sh` to read the downloaded script from stdin and pass `install` to `psh`. +From a checkout: + +```sh +bash install.sh +``` -Or with wget: +Layout: + +- Payload: `${XDG_DATA_HOME:-$HOME/.local/share}/psh/psh.sh` +- Launcher: `${XDG_BIN_HOME:-$HOME/.local/bin}/psh` (override with `PSH_INSTALL_DIR`) +- Bash completion: `${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions/psh` +- Config: `${XDG_CONFIG_HOME:-$HOME/.config}/psh/config.json` ```sh -wget -qO- https://raw.githubusercontent.com/modoterra/promptshell/main/bin/psh.sh | sh -s -- install +curl -fsSL https://raw.githubusercontent.com/modoterra/promptshell/main/install.sh | PSH_INSTALL_DIR=/usr/local/bin bash ``` -By default, the installer writes `psh` to `$HOME/.local/bin`. Override the destination with `PSH_INSTALL_DIR`: +Refresh an existing CLI install: ```sh -curl -fsSL https://raw.githubusercontent.com/modoterra/promptshell/main/bin/psh.sh | PSH_INSTALL_DIR=/usr/local/bin sh -s -- install +psh update ``` -Uninstall the installed `psh` binary: +**Omarchy plugin** — native plugin install. This is the bar widget only; run `install.sh` as well if `psh` is not already on `PATH`. ```sh -psh uninstall +omarchy plugin add https://github.com/modoterra/promptshell.git --enable ``` +`omarchy plugin install` is an alias of `omarchy plugin add`. Enable later with `omarchy plugin enable com.mdtrr.promptshell` if you omit `--enable`. + +Remove the CLI with `psh uninstall` (add `--purge` to drop config). Remove the plugin with `omarchy plugin remove com.mdtrr.promptshell`. + ## Requirements - `jq` is required. diff --git a/bin/psh.sh b/bin/psh.sh index 31ebb06..e52cbb5 100755 --- a/bin/psh.sh +++ b/bin/psh.sh @@ -10,7 +10,8 @@ usage() { printf ' psh [-v|-vv|-vvv] setup\n' printf ' psh [-v|-vv|-vvv] setup model\n' printf ' psh install\n' - printf ' psh uninstall\n' + printf ' psh update\n' + printf ' psh uninstall [--purge]\n' } config_file() { @@ -24,18 +25,72 @@ require_jq() { fi } -resolve_install_dir() { +require_home() { action=$1 - if [ -z "${PSH_INSTALL_DIR:-}" ]; then - if [ -z "${HOME:-}" ]; then - printf 'psh %s: HOME is required unless PSH_INSTALL_DIR is set\n' "$action" >&2 - exit 2 - fi + if [ -z "${HOME:-}" ]; then + printf 'psh %s: HOME is required unless install path env vars are set\n' "$action" >&2 + exit 2 + fi +} - printf '%s/.local/bin\n' "$HOME" - else +xdg_data_home() { + if [ -n "${XDG_DATA_HOME:-}" ]; then + printf '%s\n' "$XDG_DATA_HOME" + return 0 + fi + + require_home "$1" + printf '%s/.local/share\n' "$HOME" +} + +xdg_bin_home() { + if [ -n "${PSH_INSTALL_DIR:-}" ]; then printf '%s\n' "$PSH_INSTALL_DIR" + return 0 + fi + + if [ -n "${XDG_BIN_HOME:-}" ]; then + printf '%s\n' "$XDG_BIN_HOME" + return 0 + fi + + require_home "$1" + printf '%s/.local/bin\n' "$HOME" +} + +payload_dir() { + printf '%s/psh\n' "$(xdg_data_home "$1")" +} + +payload_path() { + printf '%s/psh.sh\n' "$(payload_dir "$1")" +} + +launcher_path() { + printf '%s/%s\n' "$(xdg_bin_home "$1")" "${PSH_INSTALL_NAME:-psh}" +} + +completion_path() { + printf '%s/bash-completion/completions/psh\n' "$(xdg_data_home "$1")" +} + +shell_quote() { + printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" +} + +install_file() { + inst_src=$1 + inst_dst=$2 + inst_mode=${3:-0755} + + mkdir -p "$(dirname "$inst_dst")" + + if command -v install >/dev/null 2>&1; then + install -m "$inst_mode" "$inst_src" "$inst_dst" + else + cp "$inst_src" "$inst_dst" + chmod "$inst_mode" "$inst_dst" fi } @@ -79,15 +134,54 @@ download_install_source() { fi } -install_command() { - if [ "$#" -gt 0 ]; then - usage >&2 - exit 2 +write_launcher() { + launcher_payload=$1 + launcher_dst=$2 + launcher_tmp=$(mktemp) + + printf '#!/bin/sh\nexec %s "$@"\n' "$(shell_quote "$launcher_payload")" >"$launcher_tmp" + install_file "$launcher_tmp" "$launcher_dst" 0755 + rm -f "$launcher_tmp" +} + +write_bash_completion() { + completion_dst=$1 + completion_tmp=$(mktemp) + + cat >"$completion_tmp" <<'EOF' +_psh() { + local cur prev + COMPREPLY=() + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + + case $prev in + setup) + COMPREPLY=($(compgen -W "model" -- "$cur")) + return 0 + ;; + uninstall) + COMPREPLY=($(compgen -W "--purge" -- "$cur")) + return 0 + ;; + esac + + if [ "$COMP_CWORD" -eq 1 ]; then + COMPREPLY=($(compgen -W "-v -vv -vvv run setup install update uninstall help --help" -- "$cur")) fi +} + +complete -F _psh psh +EOF - install_dir=$(resolve_install_dir install) - install_name=${PSH_INSTALL_NAME:-psh} - target=$install_dir/$install_name + install_file "$completion_tmp" "$completion_dst" 0644 + rm -f "$completion_tmp" +} + +install_xdg() { + payload=$(payload_path install) + launcher=$(launcher_path install) + completion=$(completion_path install) install_tmp=$(mktemp) trap 'rm -f "$install_tmp"' EXIT HUP INT TERM @@ -99,50 +193,109 @@ install_command() { download_install_source "$install_tmp" fi - mkdir -p "$install_dir" - - if command -v install >/dev/null 2>&1; then - install -m 0755 "$install_tmp" "$target" - else - cp "$install_tmp" "$target" - chmod 0755 "$target" - fi + install_file "$install_tmp" "$payload" 0755 + write_launcher "$payload" "$launcher" + write_bash_completion "$completion" rm -f "$install_tmp" trap - EXIT HUP INT TERM - printf 'psh install: installed %s\n' "$target" + printf 'psh install: payload %s\n' "$payload" + printf 'psh install: launcher %s\n' "$launcher" + printf 'psh install: config %s\n' "$(config_file)" + printf 'psh install: completion %s\n' "$completion" + printf 'psh install: run `psh setup` before the first hosted-provider request\n' + bin_dir=$(xdg_bin_home install) case :$PATH: in - *:"$install_dir":*) ;; + *:"$bin_dir":*) ;; *) - printf 'psh install: add %s to PATH to run `psh` directly\n' "$install_dir" + printf 'psh install: add %s to PATH to run `psh` directly\n' "$bin_dir" ;; esac } -uninstall_command() { +install_command() { if [ "$#" -gt 0 ]; then usage >&2 exit 2 fi - install_dir=$(resolve_install_dir uninstall) - install_name=${PSH_INSTALL_NAME:-psh} - target=$install_dir/$install_name + install_xdg +} + +update_command() { + if [ "$#" -gt 0 ]; then + usage >&2 + exit 2 + fi + + install_xdg +} + +uninstall_command() { + purge=0 + + while [ "$#" -gt 0 ]; do + case $1 in + --purge) + purge=1 + shift + ;; + *) + usage >&2 + exit 2 + ;; + esac + done + + launcher=$(launcher_path uninstall) + payload=$(payload_path uninstall) + completion=$(completion_path uninstall) + data_dir=$(payload_dir uninstall) + removed=0 - if [ -d "$target" ]; then - printf 'psh uninstall: expected a file at %s\n' "$target" >&2 + if [ -d "$launcher" ]; then + printf 'psh uninstall: expected a file at %s\n' "$launcher" >&2 exit 1 fi - if [ ! -f "$target" ] && [ ! -L "$target" ]; then - printf 'psh uninstall: not installed at %s\n' "$target" - return 0 + if [ -f "$launcher" ] || [ -L "$launcher" ]; then + rm -f "$launcher" + printf 'psh uninstall: removed %s\n' "$launcher" + removed=1 fi - rm -f "$target" - printf 'psh uninstall: removed %s\n' "$target" + if [ -f "$payload" ] || [ -L "$payload" ]; then + rm -f "$payload" + printf 'psh uninstall: removed %s\n' "$payload" + removed=1 + fi + + if [ -f "$completion" ] || [ -L "$completion" ]; then + rm -f "$completion" + printf 'psh uninstall: removed %s\n' "$completion" + removed=1 + fi + + if [ -d "$data_dir" ]; then + rmdir "$data_dir" 2>/dev/null || true + fi + + if [ "$purge" -eq 1 ]; then + config=$(config_file) + config_dir=$(dirname "$config") + if [ -f "$config" ]; then + rm -f "$config" + printf 'psh uninstall: removed %s\n' "$config" + removed=1 + fi + rmdir "$config_dir" 2>/dev/null || true + fi + + if [ "$removed" -eq 0 ]; then + printf 'psh uninstall: not installed at %s\n' "$launcher" + fi } terminal_printf() { @@ -1570,7 +1723,7 @@ dispatch_command() { requested_command=$1 case $requested_command in - run|setup|install|uninstall|help|-h|--help) + run|setup|install|update|uninstall|help|-h|--help) shift ;; *) @@ -1588,6 +1741,9 @@ dispatch_command() { install) install_command "$@" ;; + update) + update_command "$@" + ;; uninstall) uninstall_command "$@" ;; diff --git a/docs/omarchy-plugin.md b/docs/omarchy-plugin.md new file mode 100644 index 0000000..4e243ea --- /dev/null +++ b/docs/omarchy-plugin.md @@ -0,0 +1,40 @@ +# Prompt Shell Omarchy plugin + +Bar widget that opens a prompt panel and runs `psh` in a terminal. + +## Install + +```sh +omarchy plugin add https://github.com/modoterra/promptshell.git --enable +``` + +`omarchy plugin install` is the same command. This clones the repository as a Quickshell plugin. It does not install the `psh` CLI. + +Install the CLI separately: + +```sh +bash install.sh +``` + +Or: + +```sh +curl -fsSL https://raw.githubusercontent.com/modoterra/promptshell/main/install.sh | bash +``` + +If you omit `--enable`: + +```sh +omarchy plugin enable com.mdtrr.promptshell +``` + +## Usage + +Click **psh** on the bar, type a natural-language shell task, and press Enter. The panel launches `omarchy-launch-tui psh run ` so approval still happens in a real terminal. + +## Remove + +```sh +omarchy plugin remove com.mdtrr.promptshell +psh uninstall +``` diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..1218eab --- /dev/null +++ b/install.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Install Prompt Shell (psh) into XDG directories. Does not install the Omarchy plugin. +# Plugin install: omarchy plugin add https://github.com/modoterra/promptshell.git + +set -euo pipefail + +if [[ $# -gt 0 ]]; then + printf 'usage: install.sh\n' >&2 + exit 2 +fi + +script_path=${BASH_SOURCE[0]:-} +repo_psh= + +if [[ -n $script_path && -f $script_path ]]; then + script_dir=$(CDPATH= cd "$(dirname "$script_path")" && pwd) + if [[ -f $script_dir/bin/psh.sh ]]; then + repo_psh=$script_dir/bin/psh.sh + fi +fi + +if [[ -n $repo_psh ]]; then + exec sh "$repo_psh" install +fi + +raw_base=${PSH_RAW_BASE:-https://raw.githubusercontent.com/modoterra/promptshell/main} +source_url=$raw_base/bin/psh.sh +work_dir=$(mktemp -d) +trap 'rm -rf "$work_dir"' EXIT +payload=$work_dir/psh.sh + +if command -v curl >/dev/null 2>&1; then + curl -fsSL "$source_url" -o "$payload" +elif command -v wget >/dev/null 2>&1; then + wget -qO "$payload" "$source_url" +else + printf 'install.sh: curl or wget is required\n' >&2 + exit 2 +fi + +sh "$payload" install diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..86f3a53 --- /dev/null +++ b/manifest.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "com.mdtrr.promptshell", + "name": "Prompt Shell", + "version": "1.0.0", + "author": "Modoterra", + "license": "MIT", + "description": "Type a natural-language shell task and run it with psh in a terminal.", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Prompt Shell", + "description": "Open a prompt panel that launches psh in a terminal", + "category": "Tools", + "allowMultiple": false, + "defaultSection": "right" + } +} diff --git a/tests/helpers/psh.bash b/tests/helpers/psh.bash index c21a617..b7c9cdb 100644 --- a/tests/helpers/psh.bash +++ b/tests/helpers/psh.bash @@ -31,6 +31,8 @@ setup_psh_test() { unset PSH_RAW_BASE unset PSH_EXPECT_INSTALL_SOURCE unset PSH_INSTALL_SOURCE_FILE + unset XDG_DATA_HOME + unset XDG_BIN_HOME } teardown_psh_test() { diff --git a/tests/psh.bats b/tests/psh.bats index 462e5ef..ac02dfe 100644 --- a/tests/psh.bats +++ b/tests/psh.bats @@ -17,31 +17,114 @@ teardown() { [[ "$output" == *"usage: psh"* ]] [[ "$output" == *"psh [-v|-vv|-vvv] run PROMPT"* ]] [[ "$output" == *"psh install"* ]] + [[ "$output" == *"psh update"* ]] [[ "$output" == *"psh uninstall"* ]] + [[ "$output" != *"psh install omarchy"* ]] } -@test "install and uninstall manage psh in the configured directory" { +@test "install writes XDG payload, launcher, and completion" { + local data_home=$PSH_TEST_ROOT/xdg-data + local bin_home=$PSH_TEST_ROOT/xdg-bin + + run env XDG_DATA_HOME="$data_home" XDG_BIN_HOME="$bin_home" "$PSH_REPO_ROOT/bin/psh.sh" install + + assert_status 0 + [[ "$output" == *"payload $data_home/psh/psh.sh"* ]] + [[ "$output" == *"launcher $bin_home/psh"* ]] + [[ "$output" == *"completion $data_home/bash-completion/completions/psh"* ]] + [ -x "$data_home/psh/psh.sh" ] + [ -x "$bin_home/psh" ] + [ -f "$data_home/bash-completion/completions/psh" ] + [ ! -e "$data_home/psh/omarchy" ] + + run "$bin_home/psh" --help + + assert_status 0 + [[ "$output" == *"usage: psh"* ]] +} + +@test "install honors PSH_INSTALL_DIR for the launcher" { local install_dir=$PSH_TEST_ROOT/install-bin + local data_home=$PSH_TEST_ROOT/xdg-data - run env PSH_INSTALL_DIR="$install_dir" "$PSH_REPO_ROOT/bin/psh.sh" install + run env PSH_INSTALL_DIR="$install_dir" XDG_DATA_HOME="$data_home" "$PSH_REPO_ROOT/bin/psh.sh" install assert_status 0 - [[ "$output" == *"installed $install_dir/psh"* ]] + [[ "$output" == *"launcher $install_dir/psh"* ]] [ -x "$install_dir/psh" ] + [ -x "$data_home/psh/psh.sh" ] +} - run "$install_dir/psh" --help +@test "uninstall removes launcher payload and completion and leaves config" { + local data_home=$PSH_TEST_ROOT/xdg-data + local bin_home=$PSH_TEST_ROOT/xdg-bin + local config_dir=$XDG_CONFIG_HOME/psh + + mkdir -p "$config_dir" + printf '%s\n' '{"provider":"openai"}' >"$config_dir/config.json" + + env XDG_DATA_HOME="$data_home" XDG_BIN_HOME="$bin_home" "$PSH_REPO_ROOT/bin/psh.sh" install >/dev/null + + run env XDG_DATA_HOME="$data_home" XDG_BIN_HOME="$bin_home" "$PSH_REPO_ROOT/bin/psh.sh" uninstall assert_status 0 - [[ "$output" == *"usage: psh"* ]] + [[ "$output" == *"removed $bin_home/psh"* ]] + [[ "$output" == *"removed $data_home/psh/psh.sh"* ]] + [ ! -e "$bin_home/psh" ] + [ ! -e "$data_home/psh/psh.sh" ] + [ ! -e "$data_home/bash-completion/completions/psh" ] + [ -f "$config_dir/config.json" ] +} + +@test "uninstall --purge removes config" { + local data_home=$PSH_TEST_ROOT/xdg-data + local bin_home=$PSH_TEST_ROOT/xdg-bin + local config_dir=$XDG_CONFIG_HOME/psh + + mkdir -p "$config_dir" + printf '%s\n' '{"provider":"openai"}' >"$config_dir/config.json" + + env XDG_DATA_HOME="$data_home" XDG_BIN_HOME="$bin_home" "$PSH_REPO_ROOT/bin/psh.sh" install >/dev/null - run env PSH_INSTALL_DIR="$install_dir" "$install_dir/psh" uninstall + run env XDG_DATA_HOME="$data_home" XDG_BIN_HOME="$bin_home" "$PSH_REPO_ROOT/bin/psh.sh" uninstall --purge assert_status 0 - [[ "$output" == *"removed $install_dir/psh"* ]] - [ ! -e "$install_dir/psh" ] + [ ! -e "$config_dir/config.json" ] } @test "piped script can install with sh -s -- install" { + local data_home=$PSH_TEST_ROOT/xdg-data + local install_dir=$PSH_TEST_ROOT/pipe-install-bin + local raw_base=https://example.test/promptshell + + install_mock_raw_curl + export PSH_RAW_BASE=$raw_base + export PSH_EXPECT_INSTALL_SOURCE=$raw_base/bin/psh.sh + export PSH_INSTALL_SOURCE_FILE=$PSH_REPO_ROOT/bin/psh.sh + + run sh -c 'curl -fsSL "$1" | PSH_INSTALL_DIR="$2" XDG_DATA_HOME="$3" PSH_RAW_BASE="$4" sh -s -- install' sh "$PSH_EXPECT_INSTALL_SOURCE" "$install_dir" "$data_home" "$PSH_RAW_BASE" + + assert_status 0 + [[ "$output" == *"launcher $install_dir/psh"* ]] + [ -x "$install_dir/psh" ] + [ -x "$data_home/psh/psh.sh" ] +} + +@test "install.sh installs the CLI from a checkout" { + local data_home=$PSH_TEST_ROOT/xdg-data + local bin_home=$PSH_TEST_ROOT/xdg-bin + + run env XDG_DATA_HOME="$data_home" XDG_BIN_HOME="$bin_home" bash "$PSH_REPO_ROOT/install.sh" + + assert_status 0 + [[ "$output" == *"launcher $bin_home/psh"* ]] + [ -x "$bin_home/psh" ] + [ -x "$data_home/psh/psh.sh" ] + [ ! -e "$XDG_CONFIG_HOME/omarchy/plugins/com.mdtrr.promptshell" ] +} + +@test "piped install.sh downloads psh and installs the CLI" { + local data_home=$PSH_TEST_ROOT/xdg-data local install_dir=$PSH_TEST_ROOT/pipe-install-bin local raw_base=https://example.test/promptshell @@ -50,11 +133,12 @@ teardown() { export PSH_EXPECT_INSTALL_SOURCE=$raw_base/bin/psh.sh export PSH_INSTALL_SOURCE_FILE=$PSH_REPO_ROOT/bin/psh.sh - run sh -c 'curl -fsSL "$1" | PSH_INSTALL_DIR="$2" PSH_RAW_BASE="$3" sh -s -- install' sh "$PSH_EXPECT_INSTALL_SOURCE" "$install_dir" "$PSH_RAW_BASE" + run bash -c 'cat "$1" | PSH_INSTALL_DIR="$2" XDG_DATA_HOME="$3" PSH_RAW_BASE="$4" bash' bash "$PSH_REPO_ROOT/install.sh" "$install_dir" "$data_home" "$PSH_RAW_BASE" assert_status 0 - [[ "$output" == *"installed $install_dir/psh"* ]] + [[ "$output" == *"launcher $install_dir/psh"* ]] [ -x "$install_dir/psh" ] + [ -x "$data_home/psh/psh.sh" ] } @test "missing API key exits 2 before contacting provider" {