v0.6.6: strip CR from jq output + 0600 oauth file + TAB slash completion
Bug 1 (PRIMARY — unblocks OAuth on MobaXterm): jqf now strips \r from every
jq output. Root cause of the multi-day "OAuth token unavailable" cascade was
CRLF-tainted .oauth.json: `fetched_at=$(jqf ... '.fetched_at')` captured
"1716826990\r", and `$((fetched_at + expires_in))` crashed with the cryptic
"invalid arithmetic operator (error token is \"\"") — bash trying to print
the embedded CR. Single-point fix in jqf covers every caller (ensure, refresh,
status, debug, login). Added belt-and-suspenders `printf '%d'` coercion on
every numeric capture so any future non-CR junk falls back to 0 instead of
crashing arithmetic. /oauth-debug now reports CR/LF byte counts so future
CRLF taint is visible at a glance.
Bug 2 (security): .oauth.json was landing at 0644 on Cygwin/MobaXterm even
though both cmd_login and cmd_refresh called `chmod 600`. Introduced
secure_install (install -m 600 → cp+chmod → mv+chmod fallback chain) so the
mode is set atomically at placement. Also added umask 077 to cmd_refresh
(only cmd_login had it) so the .new sidecar is created tight from the start,
plus a pre-mv chmod 600 on the sidecar for fs-where-install-doesn't-stick.
On a fully POSIX FS this is now triple-redundant; on Cygwin NTFS we get as
close to 0600 as the ACL emulation will allow.
Feature 1: TAB completion for slash commands. New _LARRY_SLASH_CMDS canonical
array near read_user_input, __larry_complete_slash uses `bind -x` to read
$READLINE_LINE / $READLINE_POINT and rewrites the buffer in-place. Prefix
matching is primary; subsequence fuzzy is the fallback. Non-slash lines and
mid-arg TABs fall back to literal-tab insertion so muscle memory isn't
broken. Heredoc continuation lines DO NOT get completion (binding only fires
on the first read). /help section documents the behavior with examples.
Smoke-tested on macOS:
- CRLF-tainted .oauth.json: ensure returns access_token cleanly, status &
debug print real numbers + human timestamps (no bash arith crash).
- secure_install: file ends at 0600 even when source was 0644.
- Completion: /h→/help, /ss→lists ssh-*, /ssh-h→/ssh-hosts, /q→/quit,
/oa→/oauth-debug, /sssp→/ssh-setup (fuzzy), /xyz→silent, non-slash and
"/cmd args"→literal tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
dd44d361c3
commit
9f97d15f9a
169
larry.sh
169
larry.sh
@ -36,7 +36,7 @@ set -o pipefail
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Config
|
# Config
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
LARRY_VERSION="0.6.5"
|
LARRY_VERSION="0.6.6"
|
||||||
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
||||||
LARRY_BASE_URL="${LARRY_BASE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main}"
|
LARRY_BASE_URL="${LARRY_BASE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main}"
|
||||||
LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-${LARRY_BASE_URL}/larry.sh}"
|
LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-${LARRY_BASE_URL}/larry.sh}"
|
||||||
@ -1226,6 +1226,16 @@ Slash commands:
|
|||||||
/help this help
|
/help this help
|
||||||
|
|
||||||
Multi-line input: start with '<<' on its own line, end with 'EOF' on its own line.
|
Multi-line input: start with '<<' on its own line, end with 'EOF' on its own line.
|
||||||
|
|
||||||
|
TAB completion (v0.6.6):
|
||||||
|
Type '/' followed by any prefix and press TAB.
|
||||||
|
/h<TAB> → /help
|
||||||
|
/ss<TAB> → lists every /ssh-* command
|
||||||
|
/ssh-h<TAB> → /ssh-hosts
|
||||||
|
/q<TAB> → /quit
|
||||||
|
Subsequence fuzzy is the fallback when no prefix matches (e.g. /sssp finds
|
||||||
|
/ssh-setup). Non-slash input falls back to inserting a literal tab so
|
||||||
|
normal typing isn't disturbed.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1256,6 +1266,157 @@ _run_ssh_helper() {
|
|||||||
"$helper" "$@" || true
|
"$helper" "$@" || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Slash-command TAB completion (v0.6.6)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# _LARRY_SLASH_CMDS — canonical list of slash commands. This is the single
|
||||||
|
# source of truth for the TAB-completion function. The case statement in
|
||||||
|
# main_loop is the dispatcher; this array is what the user sees when they
|
||||||
|
# fuzzy-match. Keep them in sync when adding new commands.
|
||||||
|
#
|
||||||
|
# Excluded on purpose: command aliases (e.g. /exit and /q both map to
|
||||||
|
# /quit) — completing to the canonical form is friendlier than offering
|
||||||
|
# every spelling.
|
||||||
|
_LARRY_SLASH_CMDS=(
|
||||||
|
/help
|
||||||
|
/quit
|
||||||
|
/sys
|
||||||
|
/pwd
|
||||||
|
/env
|
||||||
|
/auth
|
||||||
|
/login
|
||||||
|
/logout
|
||||||
|
/oauth-debug
|
||||||
|
/lesson
|
||||||
|
/lessons
|
||||||
|
/export
|
||||||
|
/phi
|
||||||
|
/unmask
|
||||||
|
/tokens
|
||||||
|
/ssh
|
||||||
|
/ssh-hosts
|
||||||
|
/ssh-add
|
||||||
|
/ssh-remove
|
||||||
|
/ssh-pass
|
||||||
|
/ssh-setup
|
||||||
|
/ssh-close
|
||||||
|
/ssh-status
|
||||||
|
/redetect
|
||||||
|
/sites
|
||||||
|
/site
|
||||||
|
/reset
|
||||||
|
/model
|
||||||
|
/cd
|
||||||
|
/load
|
||||||
|
)
|
||||||
|
|
||||||
|
# __larry_complete_slash — bound to TAB via `bind -x` (see _install_readline_tab).
|
||||||
|
#
|
||||||
|
# Reads READLINE_LINE (the current line buffer) and READLINE_POINT (cursor
|
||||||
|
# position, 0-indexed). If the line starts with "/" and the cursor is on the
|
||||||
|
# first word, we attempt prefix completion against _LARRY_SLASH_CMDS:
|
||||||
|
#
|
||||||
|
# * exactly one match → replace the line with the match (+ a trailing space
|
||||||
|
# for commands that take an arg, e.g. "/site ")
|
||||||
|
# * many matches → print them under the prompt and re-display the line
|
||||||
|
# (readline will redraw automatically when we return)
|
||||||
|
# * zero matches → silent no-op (readline does NOT insert a literal
|
||||||
|
# tab, matching slash-aware completion in modern
|
||||||
|
# shells)
|
||||||
|
#
|
||||||
|
# If the line does NOT start with "/" we insert a literal tab so the user's
|
||||||
|
# muscle memory for whitespace alignment / indented heredocs still works.
|
||||||
|
#
|
||||||
|
# Refs:
|
||||||
|
# bash(1) — READLINE Variables: $READLINE_LINE, $READLINE_POINT.
|
||||||
|
# bash(1) — `bind -x '"\C-x": shell-function'` binds a key to a shell
|
||||||
|
# function that may read/modify $READLINE_LINE in place. Available since
|
||||||
|
# bash 4.0.
|
||||||
|
__larry_complete_slash() {
|
||||||
|
local line="$READLINE_LINE"
|
||||||
|
local point="${READLINE_POINT:-0}"
|
||||||
|
# Only complete when the buffer is a single token starting with '/'.
|
||||||
|
# If there's whitespace before the cursor, we treat it as "user typing
|
||||||
|
# arguments to a command", not "user wants to complete the command name".
|
||||||
|
case "$line" in
|
||||||
|
/*)
|
||||||
|
# Has it already been word-split (a space anywhere in the line)? If yes,
|
||||||
|
# fall through to literal-tab. Completion is for the command name only.
|
||||||
|
case "$line" in
|
||||||
|
*' '*)
|
||||||
|
READLINE_LINE="${line:0:point}"$'\t'"${line:point}"
|
||||||
|
READLINE_POINT=$((point + 1))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Non-slash line — insert a literal tab at the cursor.
|
||||||
|
READLINE_LINE="${line:0:point}"$'\t'"${line:point}"
|
||||||
|
READLINE_POINT=$((point + 1))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Build the match list. Primary: prefix match. If exactly one prefix match,
|
||||||
|
# complete it. If many, print them. If zero prefix matches AND the input is
|
||||||
|
# at least 2 chars, try a subsequence fuzzy match as a polish; if that
|
||||||
|
# yields exactly one, complete to it.
|
||||||
|
local prefix="$line"
|
||||||
|
local matches=() cmd
|
||||||
|
for cmd in "${_LARRY_SLASH_CMDS[@]}"; do
|
||||||
|
case "$cmd" in
|
||||||
|
"$prefix"*) matches+=("$cmd") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#matches[@]}" -eq 0 ] && [ "${#prefix}" -ge 2 ]; then
|
||||||
|
# Subsequence fuzzy: every char of $prefix (after the leading '/') must
|
||||||
|
# appear in $cmd in order. Cheap, predictable, no scoring.
|
||||||
|
local needle="${prefix#/}"
|
||||||
|
for cmd in "${_LARRY_SLASH_CMDS[@]}"; do
|
||||||
|
local hay="${cmd#/}"
|
||||||
|
local i=0 nlen="${#needle}" ok=1
|
||||||
|
while [ "$i" -lt "$nlen" ]; do
|
||||||
|
local ch="${needle:i:1}"
|
||||||
|
case "$hay" in
|
||||||
|
*"$ch"*) hay="${hay#*"$ch"}" ;;
|
||||||
|
*) ok=0; break ;;
|
||||||
|
esac
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
[ "$ok" -eq 1 ] && matches+=("$cmd")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#matches[@]}" -eq 1 ]; then
|
||||||
|
# Replace the buffer with the matched command. Append a space so the user
|
||||||
|
# can immediately type the argument. (No-arg commands waste one keystroke
|
||||||
|
# — acceptable.)
|
||||||
|
READLINE_LINE="${matches[0]} "
|
||||||
|
READLINE_POINT=${#READLINE_LINE}
|
||||||
|
elif [ "${#matches[@]}" -gt 1 ]; then
|
||||||
|
# Multiple matches: print them on a new line, then let readline redisplay
|
||||||
|
# the prompt with the user's current buffer intact.
|
||||||
|
printf '\n'
|
||||||
|
printf ' %s' "${matches[@]}"
|
||||||
|
printf '\n'
|
||||||
|
# READLINE_LINE / READLINE_POINT stay as-is so the user sees their input.
|
||||||
|
fi
|
||||||
|
# Zero matches → silent no-op (the user's typo stays on screen so they can fix it).
|
||||||
|
}
|
||||||
|
|
||||||
|
# _install_readline_tab — wire TAB to the slash-completer for the lifetime
|
||||||
|
# of the REPL. Safe to call multiple times (bind is idempotent for the same
|
||||||
|
# key). No-op if `bind` isn't a builtin in this bash (e.g. non-interactive
|
||||||
|
# subshells, sh-mode invocations).
|
||||||
|
_install_readline_tab() {
|
||||||
|
# `bind -x` is bash 4.0+. The `2>/dev/null` swallows the warning bash
|
||||||
|
# emits on non-tty stdin ("bind: warning: line editing not enabled").
|
||||||
|
bind -x '"\t": __larry_complete_slash' 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
read_user_input() {
|
read_user_input() {
|
||||||
# Returns user input via global LARRY_INPUT.
|
# Returns user input via global LARRY_INPUT.
|
||||||
# If first line is "<<", read until line "EOF" (heredoc-style).
|
# If first line is "<<", read until line "EOF" (heredoc-style).
|
||||||
@ -1264,12 +1425,18 @@ read_user_input() {
|
|||||||
# correctly across terminals (MobaXterm/Cygwin in particular often has
|
# correctly across terminals (MobaXterm/Cygwin in particular often has
|
||||||
# stty erase mismatches that swallow plain `read`'s backspace). We pass
|
# stty erase mismatches that swallow plain `read`'s backspace). We pass
|
||||||
# the prompt via -p so readline knows the visible width.
|
# the prompt via -p so readline knows the visible width.
|
||||||
|
#
|
||||||
|
# v0.6.6: TAB on the first line is bound to __larry_complete_slash for
|
||||||
|
# slash-command completion. Continuation lines of a '<<' heredoc DO NOT
|
||||||
|
# get completion (we only `bind -x` ahead of the first-line read, not
|
||||||
|
# inside the heredoc loop), keeping the heredoc's "raw text" semantics.
|
||||||
LARRY_INPUT=""
|
LARRY_INPUT=""
|
||||||
local first
|
local first
|
||||||
if [ -t 0 ] && _readline_ok; then
|
if [ -t 0 ] && _readline_ok; then
|
||||||
local prompt; prompt=$(printf '%syou>%s ' "$C_GREEN" "$C_RESET")
|
local prompt; prompt=$(printf '%syou>%s ' "$C_GREEN" "$C_RESET")
|
||||||
# Clear the prompt the caller already printed, then re-emit via readline.
|
# Clear the prompt the caller already printed, then re-emit via readline.
|
||||||
printf '\r\033[K'
|
printf '\r\033[K'
|
||||||
|
_install_readline_tab
|
||||||
IFS= read -e -r -p "$prompt" first || return 1
|
IFS= read -e -r -p "$prompt" first || return 1
|
||||||
[ -n "$first" ] && history -s "$first"
|
[ -n "$first" ] && history -s "$first"
|
||||||
else
|
else
|
||||||
|
|||||||
82
lib/oauth.sh
82
lib/oauth.sh
@ -48,6 +48,31 @@ SCOPE="${LARRY_OAUTH_SCOPE:-org:create_api_key user:profile user:inference user:
|
|||||||
|
|
||||||
die() { printf 'oauth: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'oauth: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# secure_install SRC DST — atomically move SRC to DST and force mode 0600.
|
||||||
|
#
|
||||||
|
# Why this exists (v0.6.6): on Cygwin/MobaXterm the .oauth.json was landing
|
||||||
|
# at mode 0644 even though both cmd_login and cmd_refresh called `chmod 600`
|
||||||
|
# after the `mv`. Two compounding issues:
|
||||||
|
# 1) `chmod` order matters less than `umask` — the new file's mode is set
|
||||||
|
# at create() time; the chmod afterward only adjusts ACL entries Cygwin
|
||||||
|
# may or may not actually honour on NTFS-backed mounts (MobaXterm
|
||||||
|
# portable mounts don't set `noacl`, so POSIX perms are emulated via
|
||||||
|
# NTFS ACLs that can silently degrade to "Everyone:Read").
|
||||||
|
# 2) Prefer `install -m 600 SRC DST` when available: it sets the mode
|
||||||
|
# *atomically as part of the placement*, not as a post-hoc chmod.
|
||||||
|
#
|
||||||
|
# Fallback chain: install(1) → cp+chmod+rm → mv+chmod (legacy). Each branch
|
||||||
|
# ends with a redundant `chmod 600` so on a working POSIX FS the perms are
|
||||||
|
# always 0600 even if `install` silently no-ops the mode flag.
|
||||||
|
secure_install() {
|
||||||
|
local src="$1" dst="$2"
|
||||||
|
if command -v install >/dev/null 2>&1; then
|
||||||
|
install -m 600 "$src" "$dst" 2>/dev/null && rm -f "$src" && chmod 600 "$dst" 2>/dev/null && return 0
|
||||||
|
fi
|
||||||
|
mv "$src" "$dst" || return 1
|
||||||
|
chmod 600 "$dst" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
# Dependency check
|
# Dependency check
|
||||||
command -v curl >/dev/null 2>&1 || die "curl required"
|
command -v curl >/dev/null 2>&1 || die "curl required"
|
||||||
command -v jq >/dev/null 2>&1 || die "jq required"
|
command -v jq >/dev/null 2>&1 || die "jq required"
|
||||||
@ -60,10 +85,19 @@ b64url() { base64 | tr '/+' '_-' | tr -d '=' | tr -d '\n'; }
|
|||||||
# may be a Windows-native binary that doesn't understand Cygwin paths like
|
# may be a Windows-native binary that doesn't understand Cygwin paths like
|
||||||
# /home/mobaxterm/... when they come in as argv. Stdin redirection always
|
# /home/mobaxterm/... when they come in as argv. Stdin redirection always
|
||||||
# works because bash does the open() itself.
|
# works because bash does the open() itself.
|
||||||
|
#
|
||||||
|
# IMPORTANT (v0.6.6): jq output on MobaXterm/Cygwin can carry a trailing \r
|
||||||
|
# when the source JSON file was written with CRLF line endings, because jq -r
|
||||||
|
# preserves line endings byte-for-byte. A captured value of "1716826990\r"
|
||||||
|
# then crashes bash arithmetic with the cryptic:
|
||||||
|
# syntax error: invalid arithmetic operator (error token is "")
|
||||||
|
# (bash trying to print the embedded CR.) We strip \r at the source so EVERY
|
||||||
|
# caller is safe regardless of whether they use the value in arithmetic, string
|
||||||
|
# comparison, or printf. This is the central CR-defense; do not remove it.
|
||||||
# Usage: jqf <file> <jq-args...>
|
# Usage: jqf <file> <jq-args...>
|
||||||
jqf() {
|
jqf() {
|
||||||
local file="$1"; shift
|
local file="$1"; shift
|
||||||
jq "$@" < "$file"
|
jq "$@" < "$file" | tr -d '\r'
|
||||||
}
|
}
|
||||||
|
|
||||||
urlenc() {
|
urlenc() {
|
||||||
@ -186,8 +220,10 @@ EOF
|
|||||||
rm -f "$OAUTH_FILE.new"
|
rm -f "$OAUTH_FILE.new"
|
||||||
die "wrote oauth file is missing access_token/refresh_token/fetched_at — aborting"
|
die "wrote oauth file is missing access_token/refresh_token/fetched_at — aborting"
|
||||||
fi
|
fi
|
||||||
mv "$OAUTH_FILE.new" "$OAUTH_FILE"
|
# Pre-chmod the tempfile too — belt-and-suspenders in case secure_install's
|
||||||
chmod 600 "$OAUTH_FILE"
|
# `install` branch silently no-ops the mode flag on a hostile filesystem.
|
||||||
|
chmod 600 "$OAUTH_FILE.new" 2>/dev/null || true
|
||||||
|
secure_install "$OAUTH_FILE.new" "$OAUTH_FILE" || die "failed to install oauth file"
|
||||||
printf '\n✓ logged in. Tokens saved to %s (mode 0600).\n' "$OAUTH_FILE"
|
printf '\n✓ logged in. Tokens saved to %s (mode 0600).\n' "$OAUTH_FILE"
|
||||||
cmd_status
|
cmd_status
|
||||||
}
|
}
|
||||||
@ -212,6 +248,12 @@ cmd_refresh() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local now; now=$(date +%s)
|
local now; now=$(date +%s)
|
||||||
|
# Force tight perms on the sidecar at create() time — cmd_refresh historically
|
||||||
|
# didn't set umask before the tempfile write, so the .new file inherited the
|
||||||
|
# process default (often 0022 → 0644). On Cygwin where chmod-after-the-fact
|
||||||
|
# may degrade to NTFS-ACL approximation, the tempfile mode at creation is the
|
||||||
|
# only knob we can trust.
|
||||||
|
umask 077
|
||||||
# Pre-read the old refresh_token so we don't need --slurpfile (which would
|
# Pre-read the old refresh_token so we don't need --slurpfile (which would
|
||||||
# take a file path argv-style and break on MobaXterm's Windows-native jq).
|
# take a file path argv-style and break on MobaXterm's Windows-native jq).
|
||||||
local prev_refresh; prev_refresh=$(jqf "$OAUTH_FILE" -r '.refresh_token // empty')
|
local prev_refresh; prev_refresh=$(jqf "$OAUTH_FILE" -r '.refresh_token // empty')
|
||||||
@ -230,8 +272,12 @@ cmd_refresh() {
|
|||||||
printf 'refresh: new oauth file is missing required keys — keeping previous file intact\n' >&2
|
printf 'refresh: new oauth file is missing required keys — keeping previous file intact\n' >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
mv "$OAUTH_FILE.new" "$OAUTH_FILE"
|
chmod 600 "$OAUTH_FILE.new" 2>/dev/null || true
|
||||||
chmod 600 "$OAUTH_FILE"
|
if ! secure_install "$OAUTH_FILE.new" "$OAUTH_FILE"; then
|
||||||
|
rm -f "$OAUTH_FILE.new"
|
||||||
|
printf 'refresh: failed to install new oauth file\n' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
jqf "$OAUTH_FILE" -r '.access_token'
|
jqf "$OAUTH_FILE" -r '.access_token'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,6 +307,13 @@ cmd_ensure() {
|
|||||||
local fetched_at expires_in
|
local fetched_at expires_in
|
||||||
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
||||||
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
||||||
|
# Extra paranoia (v0.6.6): the v0.6.5 /oauth-debug surfaced "syntax error:
|
||||||
|
# invalid arithmetic operator" — diagnosed as CR contamination from CRLF
|
||||||
|
# JSON files (now stripped in jqf). Belt-and-suspenders: coerce via printf
|
||||||
|
# to a pure integer so ANY residual junk (whitespace, NULs, alpha glitches)
|
||||||
|
# falls back to 0 instead of crashing the arithmetic expression.
|
||||||
|
fetched_at=$(printf '%d' "${fetched_at:-0}" 2>/dev/null || echo 0)
|
||||||
|
expires_in=$(printf '%d' "${expires_in:-3600}" 2>/dev/null || echo 3600)
|
||||||
local now; now=$(date +%s)
|
local now; now=$(date +%s)
|
||||||
local expires_at=$((fetched_at + expires_in))
|
local expires_at=$((fetched_at + expires_in))
|
||||||
local left=$((expires_at - now))
|
local left=$((expires_at - now))
|
||||||
@ -297,6 +350,8 @@ cmd_status() {
|
|||||||
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
||||||
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
||||||
scope=$(jqf "$OAUTH_FILE" -r '.scope // "(unknown)"')
|
scope=$(jqf "$OAUTH_FILE" -r '.scope // "(unknown)"')
|
||||||
|
fetched_at=$(printf '%d' "${fetched_at:-0}" 2>/dev/null || echo 0)
|
||||||
|
expires_in=$(printf '%d' "${expires_in:-3600}" 2>/dev/null || echo 3600)
|
||||||
local now; now=$(date +%s)
|
local now; now=$(date +%s)
|
||||||
local expires_at=$((fetched_at + expires_in))
|
local expires_at=$((fetched_at + expires_in))
|
||||||
local left=$((expires_at - now))
|
local left=$((expires_at - now))
|
||||||
@ -363,12 +418,29 @@ cmd_debug() {
|
|||||||
fi
|
fi
|
||||||
printf ' status: parses as JSON\n'
|
printf ' status: parses as JSON\n'
|
||||||
|
|
||||||
|
# Line-ending check — Cygwin/MobaXterm tools sometimes write JSON with CRLF
|
||||||
|
# line endings (especially if the file gets edited by a Windows editor). The
|
||||||
|
# trailing \r on each line propagates through `jq -r` and crashes downstream
|
||||||
|
# bash arithmetic. Show byte counts of CR vs LF so we can spot CRLF taint.
|
||||||
|
local cr_count lf_count
|
||||||
|
cr_count=$(tr -dc '\r' < "$OAUTH_FILE" | wc -c | tr -d ' ')
|
||||||
|
lf_count=$(tr -dc '\n' < "$OAUTH_FILE" | wc -c | tr -d ' ')
|
||||||
|
printf ' line endings: CR=%s LF=%s' "$cr_count" "$lf_count"
|
||||||
|
if [ "$cr_count" -gt 0 ]; then
|
||||||
|
printf ' (CRLF detected — v0.6.6 jqf strips these defensively)\n'
|
||||||
|
else
|
||||||
|
printf ' (clean LF)\n'
|
||||||
|
fi
|
||||||
|
|
||||||
# Token math
|
# Token math
|
||||||
printf '\n[token math]\n'
|
printf '\n[token math]\n'
|
||||||
local fetched_at expires_in scope
|
local fetched_at expires_in scope
|
||||||
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
||||||
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
||||||
scope=$(jqf "$OAUTH_FILE" -r '.scope // "(missing)"')
|
scope=$(jqf "$OAUTH_FILE" -r '.scope // "(missing)"')
|
||||||
|
# Belt-and-suspenders integer coercion (see cmd_ensure for the why).
|
||||||
|
fetched_at=$(printf '%d' "${fetched_at:-0}" 2>/dev/null || echo 0)
|
||||||
|
expires_in=$(printf '%d' "${expires_in:-3600}" 2>/dev/null || echo 3600)
|
||||||
local now; now=$(date +%s)
|
local now; now=$(date +%s)
|
||||||
local expires_at=$((fetched_at + expires_in))
|
local expires_at=$((fetched_at + expires_in))
|
||||||
local left=$((expires_at - now))
|
local left=$((expires_at - now))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user