Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions mqlaunch/lib/mqobsidian/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,23 @@ doctor_mqobsidian_manifest() {

# Coordinates doctor mqobsidian views behavior.
doctor_mqobsidian_views() {
local root key rel type path status=0
# Both renames are zsh survival, not style: $path is tied to $PATH, and
# $status is read-only. This line used to declare locals for both, so the
# doctor worked from bash command mode and died from the zsh menu.
local root key rel type target rc=0
root="$(resolve_mqobsidian_dir)"
while IFS= read -r key; do
rel="$(resolve_view_relative_path "$key" 2>/dev/null)"
type="$(resolve_view_type "$key" 2>/dev/null)"
path="$root/$rel"
if { [[ "$type" == "folder" && -d "$path" ]] || [[ "$type" == "file" && -f "$path" ]]; }; then
target="$root/$rel"
if { [[ "$type" == "folder" && -d "$target" ]] || [[ "$type" == "file" && -f "$target" ]]; }; then
_doc_ok "view $key -> $rel"
else
_doc_missing "view $key -> $rel"
status=1
rc=1
fi
done < <(list_supported_views)
return $status
return $rc
}

# Coordinates doctor mqobsidian open command behavior.
Expand Down
42 changes: 35 additions & 7 deletions mqlaunch/lib/mqobsidian/manifest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,50 @@
# read-only. The manifest is the single source for supported views. Depends on
# errors.sh.

# Resolved here, at source time, and not inside the function below.
#
# bin/mqlaunch is bash but the interactive launcher is zsh, and zsh has no
# BASH_SOURCE — so reading it per call made command mode work and the menu
# fail. Source time is the only moment either shell can still say where this
# file lives: inside a zsh function $0 holds the function name, not the path.
# Same idiom as ui/terminal-ui/mq-ui.sh; `-` rather than `:-` so it survives a
# caller running under `set -u`.
_mqobs_manifest_self="${BASH_SOURCE[0]-}"
[ -n "$_mqobs_manifest_self" ] || _mqobs_manifest_self="$0"
_MQOBS_MANIFEST_DIR="$(cd "$(dirname "$_mqobs_manifest_self")/../../config/mqobsidian" 2>/dev/null && pwd)"
unset _mqobs_manifest_self

# Gets mqobsidian manifest path.
get_mqobsidian_manifest_path() {
local dir
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../config/mqobsidian" && pwd)"
printf '%s\n' "$dir/views.json"
if [[ -z "${_MQOBS_MANIFEST_DIR:-}" ]]; then
mqobsidian_error "Manifest directory not found: expected mqlaunch/config/mqobsidian next to the consumer lib"
return 1
fi
printf '%s\n' "$_MQOBS_MANIFEST_DIR/views.json"
}

# jq reads the manifest, so a missing jq means no view resolves at all. Without
# this the failure surfaced two steps later as "view key is not defined", which
# sends the operator to inspect views.json instead of their PATH.
_mqobs_require_jq() {
command -v jq >/dev/null 2>&1 && return 0
mqobsidian_error "jq is required to read views.json. Install: brew install jq"
return 1
}

# Coordinates list supported views behavior.
list_supported_views() {
local mf
mf="$(get_mqobsidian_manifest_path)"
_mqobs_require_jq || return 1
mf="$(get_mqobsidian_manifest_path)" || return 1
jq -r '.[].key' "$mf"
}

# Resolves view relative path.
resolve_view_relative_path() {
local key="$1" mf out
mf="$(get_mqobsidian_manifest_path)"
_mqobs_require_jq || return 1
mf="$(get_mqobsidian_manifest_path)" || return 1
out="$(jq -r --arg k "$key" '.[] | select(.key==$k) | .relative_path' "$mf")"
if [[ -z "$out" ]]; then
mqobsidian_error "Requested view key is not defined in views.json: $key"
Expand All @@ -32,7 +58,8 @@ resolve_view_relative_path() {
# Resolves view type.
resolve_view_type() {
local key="$1" mf out
mf="$(get_mqobsidian_manifest_path)"
_mqobs_require_jq || return 1
mf="$(get_mqobsidian_manifest_path)" || return 1
out="$(jq -r --arg k "$key" '.[] | select(.key==$k) | .type' "$mf")"
if [[ -z "$out" ]]; then
mqobsidian_error "Requested view key is not defined in views.json: $key"
Expand All @@ -44,6 +71,7 @@ resolve_view_type() {
# Resolves view label.
resolve_view_label() {
local key="$1" mf
mf="$(get_mqobsidian_manifest_path)"
_mqobs_require_jq || return 1
mf="$(get_mqobsidian_manifest_path)" || return 1
jq -r --arg k "$key" '.[] | select(.key==$k) | .label' "$mf"
}
32 changes: 18 additions & 14 deletions mqlaunch/lib/mqobsidian/open.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,37 @@ build_view_absolute_path() {
printf '%s/%s\n' "$root" "$rel"
}

# Coordinates assert view target exists behavior.
# `target`, never `path`: in zsh $path is a special array tied to $PATH, so a
# `local path` blanks PATH for the whole call tree. That is what made menu
# option 3 report "command not found: jq" on a machine with jq installed —
# resolve_view_relative_path ran inside a function that had shadowed PATH.
# Same family as the read-only $status trap.
assert_view_target_exists() {
local key="$1" path type
path="$(build_view_absolute_path "$key")" || return 1
local key="$1" target type
target="$(build_view_absolute_path "$key")" || return 1
type="$(resolve_view_type "$key")" || return 1
if [[ "$type" == "folder" && ! -d "$path" ]]; then
mqobsidian_error "Target path from manifest does not exist (folder): $path"
if [[ "$type" == "folder" && ! -d "$target" ]]; then
mqobsidian_error "Target path from manifest does not exist (folder): $target"
return 1
fi
if [[ "$type" == "file" && ! -f "$path" ]]; then
mqobsidian_error "Target path from manifest does not exist (file): $path"
if [[ "$type" == "file" && ! -f "$target" ]]; then
mqobsidian_error "Target path from manifest does not exist (file): $target"
return 1
fi
printf '%s\n' "$path"
printf '%s\n' "$target"
}

# The single place that invokes the OS opener. Override MQOBS_OPENER (e.g. to
# `echo`) for tests, or to route to an editor later.
open_mqobsidian_path() {
local path="$1"
"${MQOBS_OPENER:-open}" "$path"
local target="$1"
"${MQOBS_OPENER:-open}" "$target"
}

# Opens mqobsidian target.
open_mqobsidian_target() {
local key="$1" path
path="$(assert_view_target_exists "$key")" || return 1
mqobsidian_info "Opening $key → $path"
open_mqobsidian_path "$path"
local key="$1" target
target="$(assert_view_target_exists "$key")" || return 1
mqobsidian_info "Opening $key → $target"
open_mqobsidian_path "$target"
}
18 changes: 16 additions & 2 deletions mqlaunch/lib/repo-picker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,25 @@ run_github_repo_picker() {

row_bold "GITHUB REPO PICKER"
empty_row
row "Hämtar dina repos från GitHub..."
print_footer

# The gh call is about three quarters of a second of nothing before fzf takes
# over the screen, and the old static "Hämtar..." row could not tell a slow
# network from a hung one. Fetching first, behind ui_spinner, makes the wait
# legible. It has to be a fetch-then-pipe rather than wrapping the whole
# pipeline: fzf owns stdin, and ui_spinner backgrounds what it wraps.
local repos=""
repos="$(ui_spinner "Hämtar dina repos från GitHub" \
"$gh_bin" repo list --limit 1000 --json nameWithOwner --jq '.[].nameWithOwner' 2>/dev/null)" || repos=""

if [[ -z "$repos" ]]; then
ui_err "Kunde inte hämta repos från GitHub. Kontrollera gh auth status."
pause_enter
return 1
fi

selected="$(
"$gh_bin" repo list --limit 1000 --json nameWithOwner --jq '.[].nameWithOwner' 2>/dev/null \
printf '%s\n' "$repos" \
| "$fzf_bin" \
--reverse \
--border \
Expand Down
16 changes: 15 additions & 1 deletion terminal/menus/mq-dev-menu.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
#!/usr/bin/env bash

# Resolved at source time because dev_repo_path's fallback needs it and cannot
# recompute it: BASH_SOURCE is unset under zsh (the interactive launcher), and
# inside a zsh function $0 holds the function name, not the file. `-` rather
# than `:-` so it survives a caller running under `set -u`. Same idiom as
# ui/terminal-ui/mq-ui.sh and mqlaunch/lib/mqobsidian/manifest.sh.
_mq_dev_menu_self="${BASH_SOURCE[0]-}"
[ -n "$_mq_dev_menu_self" ] || _mq_dev_menu_self="$0"
_MQ_DEV_MENU_DIR="$(cd "$(dirname "$_mq_dev_menu_self")" 2>/dev/null && pwd)"
unset _mq_dev_menu_self

# Runs a bundled dev script with a clear missing-file fallback.
run_dev_script() {
local label="$1"
Expand Down Expand Up @@ -32,7 +42,11 @@ dev_repo_path() {
return
fi

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
script_dir="${_MQ_DEV_MENU_DIR:-}"
if [[ -z "$script_dir" ]]; then
printf '%s\n' "$relative_path"
return 1
fi
repo_root="$(cd "$script_dir/../.." && pwd)"
printf '%s/%s\n' "$repo_root" "$relative_path"
}
Expand Down
31 changes: 25 additions & 6 deletions tests/dev-menu-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ MENU="$ROOT/terminal/menus/mq-dev-menu.sh"

echo "SMOKE: dev menu"

echo "[1/6] the menu file exists and parses"
echo "[1/7] the menu file exists and parses"
test -f "$MENU"
bash -n "$MENU"

echo "[2/6] every action reachable before the regrouping still has a route"
echo "[2/7] every action reachable before the regrouping still has a route"
# Listed as the handler or script each choice must reach, not as menu text.
# Anything dropped from the front menu has to reappear in a submenu; this is the
# check that a regrouping did not quietly become a deletion.
Expand Down Expand Up @@ -56,7 +56,7 @@ if [[ -n "$missing" ]]; then
fi
echo " ok: all 17 original actions still reachable"

echo "[3/6] the numbers printed are the numbers answered"
echo "[3/7] the numbers printed are the numbers answered"
# Compares the two lists, so it fails on a gap, a duplicate, or an option with
# no arm.
#
Expand Down Expand Up @@ -91,7 +91,7 @@ if sorted(printed) != answered:
print(f" ok: 1-{len(printed)}, in order, each with an arm")
PY

echo "[4/6] the front menu is within the operator-choice limit"
echo "[4/7] the front menu is within the operator-choice limit"
# Counted from the numbered rows the panel prints, which is what the ROADMAP
# limit is about — what an operator is asked to choose between on one screen.
count="$(python3 - "$MENU" <<'PY'
Expand All @@ -112,7 +112,7 @@ if (( count > 10 )); then
fi
echo " ok: $count numbered choices on the front menu"

echo "[5/6] each submenu answers every row it prints"
echo "[5/7] each submenu answers every row it prints"
python3 - "$MENU" <<'PY'
import re, sys

Expand All @@ -136,7 +136,7 @@ sys.exit(1 if failed else 0)
PY
echo " ok: submenu rows and arms agree"

echo "[6/6] each grouped row actually opens its submenu"
echo "[6/7] each grouped row actually opens its submenu"
# Steps 2-5 read the file. This runs the menu, because a case arm that names a
# function proves nothing about whether the function opens.
#
Expand Down Expand Up @@ -174,4 +174,23 @@ for expected in Prompts Folders Menus; do
done
echo " ok: Prompts, Folders and Menus all open"

echo "[7/7] dev_repo_path resolves without BASE_DIR, under both shells"
# The fallback branch read ${BASH_SOURCE[0]} inside a function, which is unset
# under zsh — the same split that broke the mqobsidian manifest reader from the
# menu while command mode kept working. Only reachable with BASE_DIR unset, so
# assert it directly rather than trusting that the launcher always sets it.
for shell in bash zsh; do
out="$("$shell" -c "
set -u
unset BASE_DIR
source '$MENU'
dev_repo_path tools/scripts/lint.sh
" 2>&1)"
test "$out" = "$ROOT/tools/scripts/lint.sh" || {
echo "FAIL: $shell resolved dev_repo_path to: $out" >&2
exit 1
}
done
echo " ok: bash and zsh agree"

echo "OK: dev menu smoke test passed"
2 changes: 2 additions & 0 deletions tests/manifest.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,5 @@ pulse-cli-color-contract-smoke.sh active -
operator-usage-message-smoke.sh active -
menu-exit-contract-smoke.sh active -
theme-manager-path-smoke.sh active -
ui-spinner-smoke.sh active -
mqobsidian-manifest-shell-parity-smoke.sh active -
90 changes: 90 additions & 0 deletions tests/mqobsidian-manifest-shell-parity-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# The mqobsidian manifest reader has to work under both shells the launcher
# uses, and has to name the real problem when a dependency is missing.
#
# Two bugs shipped together in menu option 3 (open the roadmap doc):
#
# get_mqobsidian_manifest_path:2: BASH_SOURCE[0]: parameter not set
# get_mqobsidian_manifest_path:cd:2: no such file or directory: /../../config/mqobsidian
# resolve_view_relative_path:3: command not found: jq
# [mqobsidian][error] Requested view key is not defined in views.json: roadmap-doc
#
# The first is a shell split: bin/mqlaunch is bash, so command mode
# (`mqlaunch obsidian doctor`) resolved the manifest fine, while the
# interactive menu runs terminal/launchers/mqlaunch.sh, which is zsh — and
# zsh has no BASH_SOURCE. Sibling libs already handle this at source time;
# manifest.sh read it inside a function, where even zsh's $0 is no help
# because there it holds the function name.
#
# The second is the diagnosis: whatever went wrong upstream, the operator was
# told the view key was undefined. It is defined. Sending someone to inspect
# views.json when the actual fault is BASH_SOURCE or a missing jq is worse
# than saying nothing.
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LIB="$ROOT/mqlaunch/lib/mqobsidian"
EXPECTED="$ROOT/mqlaunch/config/mqobsidian/views.json"

echo "SMOKE: mqobsidian manifest reader, bash and zsh parity"

echo "[1/8] libs and manifest exist"
test -f "$LIB/errors.sh"
test -f "$LIB/manifest.sh"
test -f "$EXPECTED"

load="source '$LIB/errors.sh'; source '$LIB/manifest.sh'"

echo "[2/8] bash resolves the manifest path"
got="$(bash -c "set -u; $load; get_mqobsidian_manifest_path")"
test "$got" = "$EXPECTED"

echo "[3/8] zsh resolves the same path, with no unset-parameter error"
# `set -u` is what the launcher runs under (terminal/launchers/mqlaunch.sh:3).
out="$(zsh -c "set -u; $load; get_mqobsidian_manifest_path" 2>&1)"
test "$out" = "$EXPECTED"

echo "[4/8] zsh resolves the view that option 3 opens"
out="$(zsh -c "set -u; $load; resolve_view_relative_path roadmap-doc" 2>&1)"
test "$out" = "docs/roadmap-token-reduction.md"

echo "[5/8] zsh resolves type and label too"
out="$(zsh -c "set -u; $load; resolve_view_type roadmap-doc" 2>&1)"
test "$out" = "file"
out="$(zsh -c "set -u; $load; resolve_view_label roadmap-doc" 2>&1)"
test -n "$out"

echo "[6/8] opening a view under zsh does not blank PATH"
# This is the fault behind "command not found: jq" on a machine that has jq.
# assert_view_target_exists declared `local path`, and in zsh $path is a
# special array tied to $PATH — so PATH was empty for everything it called,
# including the jq that reads the manifest. A fake vault keeps the assertion
# about the shell, not about what happens to be in the real one.
VAULT="$(mktemp -d)"
trap 'rm -rf "$VAULT"' EXIT
# systems/ and memory/ are what the resolver uses to recognise a vault.
mkdir -p "$VAULT/docs" "$VAULT/systems" "$VAULT/memory"
touch "$VAULT/docs/roadmap-token-reduction.md"

open_load="source '$LIB/errors.sh'; source '$LIB/resolve.sh'; source '$LIB/manifest.sh'; source '$LIB/open.sh'"
out="$(zsh -c "set -u; export MQ_OBSIDIAN_DIR='$VAULT'; $open_load; assert_view_target_exists roadmap-doc" 2>&1)"
test "$out" = "$VAULT/docs/roadmap-token-reduction.md"

# And no `local path` / `local status` may come back into the consumer lib.
! grep -qE '^[[:space:]]*local .*\b(path|status)\b' "$LIB"/*.sh

echo "[7/8] the doctor runs under zsh, where \$status is read-only"
out="$(zsh -c "set -u; export MQ_OBSIDIAN_DIR='$VAULT'; $open_load; source '$LIB/doctor.sh'; doctor_mqobsidian_views" 2>&1 || true)"
! grep -qi "read-only variable" <<<"$out"
grep -q "view roadmap-doc" <<<"$out"

echo "[8/8] a missing jq is reported as a missing jq"
# PATH is stripped after sourcing, so the source-time path resolution still
# has dirname; only the jq lookup fails. The old code blamed views.json.
rc=0
out="$(zsh -c "set -u; $load; PATH=/nonexistent; resolve_view_relative_path roadmap-doc" 2>&1)" || rc=$?
test "$rc" -ne 0
grep -q "jq" <<<"$out"
! grep -q "not defined in views.json" <<<"$out"

echo "OK: mqobsidian manifest shell parity passed"
Loading
Loading