diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..34aeb31 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +temporal: temporal server start-dev diff --git a/README.md b/README.md index 097990c..5db6090 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,25 @@ An oh-my-zsh plugin for switching Temporal contexts (`~/.temporal/config.yml`) s - `temporalctx -`: switch to previous context - `temporalctx -c`: print current context - `temporalctx edit` (`-e`/`--edit`): open config in `$VISUAL`, then `$EDITOR`, else `vi` +- `temporalctx start`: start local Temporal dev server +- `temporalctx stop`: stop local Temporal dev server +- `tctx`: alias for `temporalctx` - `temporal ...`: wrapped by plugin to automatically include current context flags +## Local Dev Server + +`temporalctx start` and `temporalctx stop` support two modes: + +- Overmind mode (recommended, optional): if `overmind` is installed, plugin uses it with a plugin `Procfile` and socket at `~/.temporal/overmind.sock`. +- PID mode (fallback): if `overmind` is not installed, plugin starts/stops `temporal server start-dev` directly and tracks `~/.temporal/temporal-dev-server.pid`. + +Force PID mode even when Overmind is installed: + +```bash +temporalctx start --no-overmind +temporalctx stop --no-overmind +``` + ## Helper function `_temporal_flags` reads the active context and prints CLI flags: @@ -29,10 +46,15 @@ To bypass wrapping for a command/session, set `TEMPORALCTX_DISABLE_WRAP=1`. ./install.sh # or ./install.sh ~/my-custom-zsh/plugins +# or include opinionated tq/td/tl helpers in $ZSHC/temporal.zsh +./install.sh --full ``` Then add `temporalctx` to your `plugins=(...)` in `.zshrc`. +`--full` is optional and installs plugin-owned helper functions (`tq`, `td`, `tl`) by symlinking: +- `${ZSHC:-~/.config/zsh}/temporal.zsh` -> `/temporalctx.full.zsh` + ## Config format ```yaml diff --git a/install.sh b/install.sh index 56973b2..71031d9 100755 --- a/install.sh +++ b/install.sh @@ -4,10 +4,37 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$SCRIPT_DIR" -if [[ $# -gt 1 ]]; then - echo "Usage: ./install.sh [target-directory]" >&2 - exit 1 -fi +usage() { + echo "Usage: ./install.sh [--full] [target-directory]" >&2 +} + +full_mode=0 +user_target="" +while [[ $# -gt 0 ]]; do + case "$1" in + --full) + full_mode=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + --*) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + *) + if [[ -n "$user_target" ]]; then + usage + exit 1 + fi + user_target="$1" + shift + ;; + esac +done if ! command -v temporal >/dev/null 2>&1; then echo "temporal CLI is required but was not found in PATH." >&2 @@ -16,8 +43,7 @@ if ! command -v temporal >/dev/null 2>&1; then fi default_target_base="${ZSH_CUSTOM:-${ZSH:-$HOME/.oh-my-zsh}/custom}/plugins" -if [[ $# -eq 1 ]]; then - user_target="$1" +if [[ -n "$user_target" ]]; then if [[ "$(basename "$user_target")" == "temporalctx" ]]; then target_dir="$user_target" else @@ -73,6 +99,19 @@ else echo "Config already exists at $config_file" fi +if [[ "$full_mode" == "1" ]]; then + zshc_dir="${ZSHC:-$HOME/.config/zsh}" + helper_source="$target_dir/temporalctx.full.zsh" + helper_target="$zshc_dir/temporal.zsh" + mkdir -p "$zshc_dir" + if [[ ! -f "$helper_source" ]]; then + echo "Could not find opinionated helpers at $helper_source" >&2 + exit 1 + fi + ln -sfn "$helper_source" "$helper_target" + echo "Installed opinionated helpers at $helper_target" +fi + echo echo "Installation complete." echo "Add temporalctx to your Oh My Zsh plugins list, for example:" diff --git a/temporalctx.full.zsh b/temporalctx.full.zsh new file mode 100644 index 0000000..3404b70 --- /dev/null +++ b/temporalctx.full.zsh @@ -0,0 +1,27 @@ +# Opinionated Temporal helper aliases/functions. +# Installed by `./install.sh --full`. + +# Query a workflow's state. +# Usage: tq +tq() { + local wf_id="${1:?Usage: tq }" + temporal workflow query \ + --workflow-id "$wf_id" \ + --type getState +} + +# Describe a workflow. +# Usage: td +td() { + local wf_id="${1:?Usage: td }" + temporal workflow describe \ + --workflow-id "$wf_id" +} + +# List recent workflows. +# Usage: tl [limit] +tl() { + local limit="${1:-10}" + temporal workflow list \ + --limit "$limit" +} diff --git a/temporalctx.plugin.zsh b/temporalctx.plugin.zsh index 6f1c192..9618c99 100644 --- a/temporalctx.plugin.zsh +++ b/temporalctx.plugin.zsh @@ -1,6 +1,8 @@ # temporalctx oh-my-zsh plugin # Switch between Temporal contexts defined in ~/.temporal/config.yml. +typeset -g _TEMPORALCTX_PLUGIN_DIR="${${(%):-%x}:A:h}" + _temporalctx_config_file() { if [[ -n "$TEMPORAL_CONFIG" ]]; then print -r -- "$TEMPORAL_CONFIG" @@ -15,6 +17,22 @@ _temporalctx_previous_file() { print -r -- "${config_file:h}/.temporalctx-previous" } +_temporalctx_pid_file() { + local config_file + config_file="$(_temporalctx_config_file)" + print -r -- "${config_file:h}/temporal-dev-server.pid" +} + +_temporalctx_overmind_socket() { + local config_file + config_file="$(_temporalctx_config_file)" + print -r -- "${config_file:h}/overmind.sock" +} + +_temporalctx_procfile() { + print -r -- "${_TEMPORALCTX_PLUGIN_DIR}/Procfile" +} + _temporalctx_default_config() { cat <<'YAML' current-context: local @@ -213,6 +231,85 @@ _temporalctx_edit_config() { ${=editor_cmd} "$config_file" } +_temporalctx_start_local_server() { + local no_overmind pid_file pid socket procfile + no_overmind="${1:-0}" + + if [[ "$no_overmind" != "1" ]] && command -v overmind >/dev/null 2>&1; then + socket="$(_temporalctx_overmind_socket)" + procfile="$(_temporalctx_procfile)" + if [[ ! -f "$procfile" ]]; then + print -u2 -- "temporalctx: Procfile not found at $procfile" + return 1 + fi + OVERMIND_SOCKET="$socket" overmind start -f "$procfile" -D >/dev/null 2>&1 || return 1 + print -r -- "started local dev server via overmind" + return 0 + fi + + pid_file="$(_temporalctx_pid_file)" + + if [[ -f "$pid_file" ]]; then + pid="$(<"$pid_file")" + if [[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1; then + print -u2 -- "temporalctx: local dev server already running (pid $pid)" + return 1 + fi + rm -f -- "$pid_file" + fi + + if [[ -z "$(whence -p temporal)" ]]; then + print -u2 -- "temporalctx: temporal CLI not found in PATH" + return 127 + fi + + command temporal server start-dev >/dev/null 2>&1 & + pid="$!" + print -r -- "$pid" > "$pid_file" + print -r -- "started local dev server (pid $pid)" +} + +_temporalctx_stop_local_server() { + local no_overmind pid_file pid i socket + no_overmind="${1:-0}" + + if [[ "$no_overmind" != "1" ]] && command -v overmind >/dev/null 2>&1; then + socket="$(_temporalctx_overmind_socket)" + if [[ ! -S "$socket" && ! -e "$socket" ]]; then + print -u2 -- "temporalctx: local dev server not running" + return 1 + fi + OVERMIND_SOCKET="$socket" overmind quit >/dev/null 2>&1 || return 1 + print -r -- "stopped local dev server via overmind" + return 0 + fi + + pid_file="$(_temporalctx_pid_file)" + + if [[ ! -f "$pid_file" ]]; then + print -u2 -- "temporalctx: local dev server not running" + return 1 + fi + + pid="$(<"$pid_file")" + if [[ -z "$pid" ]] || ! kill -0 "$pid" >/dev/null 2>&1; then + rm -f -- "$pid_file" + print -u2 -- "temporalctx: local dev server not running" + return 1 + fi + + kill "$pid" >/dev/null 2>&1 || true + for i in {1..20}; do + if ! kill -0 "$pid" >/dev/null 2>&1; then + break + fi + sleep 0.1 + done + + rm -f -- "$pid_file" + print -r -- "stopped local dev server (pid $pid)" +} + typeset -ga _temporalctx_flags _temporalctx_build_flags() { @@ -271,12 +368,31 @@ temporal() { } temporalctx() { + local no_overmind=0 _temporalctx_ensure_config case "$1" in "") _temporalctx_pick_context ;; + start) + if [[ "$2" == "--no-overmind" ]]; then + no_overmind=1 + elif [[ -n "$2" ]]; then + print -u2 -- "temporalctx: unknown option for start: $2" + return 1 + fi + _temporalctx_start_local_server "$no_overmind" + ;; + stop) + if [[ "$2" == "--no-overmind" ]]; then + no_overmind=1 + elif [[ -n "$2" ]]; then + print -u2 -- "temporalctx: unknown option for stop: $2" + return 1 + fi + _temporalctx_stop_local_server "$no_overmind" + ;; edit|-e|--edit) _temporalctx_edit_config ;; @@ -311,3 +427,5 @@ temporalctx() { ;; esac } + +alias tctx='temporalctx' diff --git a/tests/install_test.sh b/tests/install_test.sh index 28e0c50..ef81b33 100755 --- a/tests/install_test.sh +++ b/tests/install_test.sh @@ -44,4 +44,16 @@ PATH="$tmp_root/bin:$PATH" HOME="$home2" "$REPO_ROOT/install.sh" "$custom_target [[ -L "$custom_target/temporalctx" || -d "$custom_target/temporalctx/.git" ]] || fail "custom target should install to /temporalctx" log "case passed: custom target" +# Test: --full installs opinionated helpers into ZSHC/temporal.zsh. +log "case: --full installs opinionated helpers in ZSHC" +home3="$tmp_root/home3" +zshc3="$tmp_root/zshc3" +plugins3="$tmp_root/plugins3" +mkdir -p "$home3" "$zshc3" "$plugins3" +PATH="$tmp_root/bin:$PATH" HOME="$home3" ZSHC="$zshc3" "$REPO_ROOT/install.sh" --full "$plugins3" >"$tmp_root/install3.out" 2>"$tmp_root/install3.err" +[[ -L "$zshc3/temporal.zsh" ]] || fail "--full should install temporal.zsh as a symlink in ZSHC" +link_target="$(readlink "$zshc3/temporal.zsh")" +assert_contains "$link_target" "temporalctx.full.zsh" "--full helper symlink should point to plugin helper file" +log "case passed: --full helpers" + echo "PASS: install.sh" diff --git a/tests/temporalctx_plugin_test.sh b/tests/temporalctx_plugin_test.sh index 95cad43..2660bf6 100755 --- a/tests/temporalctx_plugin_test.sh +++ b/tests/temporalctx_plugin_test.sh @@ -60,6 +60,12 @@ printf 'cloud-prod\n' SH cat > "$tmp_root/bin/temporal" <<'SH' #!/usr/bin/env bash +if [[ "${1:-}" == "server" && "${2:-}" == "start-dev" ]]; then + trap 'exit 0' TERM INT + while true; do + sleep 1 + done +fi printf 'ARGS:' printf ' %s' "$@" printf '\n' @@ -100,6 +106,72 @@ assert_contains "$visual_args" "$cfg" "editor should receive config file path" [[ ! -f "$fallback_log" ]] || fail "EDITOR fallback should not run when VISUAL is set" log "case passed: edit subcommand" +# Test: alias + local dev server start/stop via PID file. +log "case: tctx alias and local dev server lifecycle" +out="$(PATH="$tmp_root/bin:$PATH" TEMPORAL_CONFIG="$cfg" zsh -c ' + source "'$REPO_ROOT'/temporalctx.plugin.zsh" + echo "alias_current=$(tctx -c)" + start_out="$(temporalctx start --no-overmind)" + pid_file="${TEMPORAL_CONFIG:h}/temporal-dev-server.pid" + [[ -f "$pid_file" ]] || exit 22 + pid="$(<"$pid_file")" + kill -0 "$pid" + stop_out="$(temporalctx stop --no-overmind)" + echo "start=${start_out}" + echo "stop=${stop_out}" + echo "pid_file_exists=$([[ -f "$pid_file" ]] && echo yes || echo no)" +')" +assert_contains "$out" "alias_current=cloud-prod" "tctx alias should call temporalctx" +assert_contains "$out" "start=started local dev server (pid " "start should report pid" +assert_contains "$out" "stop=stopped local dev server (pid " "stop should report pid" +assert_contains "$out" "pid_file_exists=no" "stop should remove pid file" +log "case passed: alias and local dev server lifecycle" + +# Test: overmind path when available, and --no-overmind fallback. +log "case: overmind start/stop and --no-overmind fallback" +cat > "$tmp_root/bin/overmind" <<'SH' +#!/usr/bin/env bash +set -euo pipefail +cmd="${1:-}" +shift || true +case "$cmd" in + start) + : "${OVERMIND_SOCKET:?OVERMIND_SOCKET required}" + : > "$OVERMIND_SOCKET" + exit 0 + ;; + quit) + : "${OVERMIND_SOCKET:?OVERMIND_SOCKET required}" + rm -f -- "$OVERMIND_SOCKET" + exit 0 + ;; + *) + echo "unexpected overmind command: $cmd" >&2 + exit 1 + ;; +esac +SH +chmod +x "$tmp_root/bin/overmind" + +out="$(PATH="$tmp_root/bin:$PATH" TEMPORAL_CONFIG="$cfg" zsh -c ' + source "'$REPO_ROOT'/temporalctx.plugin.zsh" + sock="${TEMPORAL_CONFIG:h}/overmind.sock" + echo "start_om=$(temporalctx start)" + [[ -e "$sock" ]] || exit 31 + echo "stop_om=$(temporalctx stop)" + echo "sock_exists=$([[ -e "$sock" ]] && echo yes || echo no)" + echo "start_no_om=$(temporalctx start --no-overmind)" + pid_file="${TEMPORAL_CONFIG:h}/temporal-dev-server.pid" + [[ -f "$pid_file" ]] || exit 32 + echo "stop_no_om=$(temporalctx stop --no-overmind)" +')" +assert_contains "$out" "start_om=started local dev server via overmind" "start should use overmind when available" +assert_contains "$out" "stop_om=stopped local dev server via overmind" "stop should use overmind when available" +assert_contains "$out" "sock_exists=no" "overmind stop should clear socket" +assert_contains "$out" "start_no_om=started local dev server (pid " "--no-overmind should force pid mode start" +assert_contains "$out" "stop_no_om=stopped local dev server (pid " "--no-overmind should force pid mode stop" +log "case passed: overmind and --no-overmind" + # Test: temporal command wrapping + opt-out. log "case: temporal wrapper injects flags and supports opt-out" out="$(PATH="$tmp_root/bin:$PATH" TEMPORAL_CONFIG="$cfg" TEMPORAL_CLOUD_PROD_API_KEY=sekret zsh -c '