@k-leumas/queque-cli 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/app.d.ts +3 -0
- package/dist/cli/app.js +60269 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/cli/main.d.ts +1 -2
- package/dist/cli/main.js +31 -1746
- package/dist/cli/main.js.map +1 -1
- package/package.json +3 -2
- package/shell/zsh/queque.zsh +214 -35
package/shell/zsh/queque.zsh
CHANGED
|
@@ -22,37 +22,181 @@ _qq_cleanup() {
|
|
|
22
22
|
trap - EXIT ERR INT
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Installed-tool handoff (spike 001, D-01, D-19).
|
|
27
|
+
# Writes the names zsh can resolve into the session dir ($1) next to
|
|
28
|
+
# request.json, so the qq client can flag candidates that use missing tools:
|
|
29
|
+
# commands.txt — keys of $commands (external commands on PATH)
|
|
30
|
+
# builtins.txt — keys of $builtins (never flagged)
|
|
31
|
+
# functions.txt — function names (minus `_`-prefixed internals) AND alias
|
|
32
|
+
# names. One file on purpose (D-01 amended) — do not split it.
|
|
33
|
+
# `rehash` runs on every enabled call so a tool installed earlier in this
|
|
34
|
+
# shell session is listed (no mtime fingerprint, no background job — D-19).
|
|
35
|
+
# Side effect, accepted: `rehash` also drops manual `hash name=path` entries.
|
|
36
|
+
# Names only: one expansion per file, no per-entry loop, no ordering in zsh
|
|
37
|
+
# (both cost 100+ ms; sort in Node if ever needed).
|
|
38
|
+
# Every line is guarded: the widget's ERR trap deletes the session dir
|
|
39
|
+
# (request.json included) on any failing command, so nothing here may leave a
|
|
40
|
+
# non-zero status, and stderr stays off the ZLE line. A failed write removes
|
|
41
|
+
# all three files, so failure means "no files" and the client shows no flags.
|
|
42
|
+
# Kill switch: QQ_TOOL_AWARENESS=0 in the shell writes nothing.
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
_qq_write_tool_handoff() {
|
|
45
|
+
local dir="$1"
|
|
46
|
+
if [[ "${QQ_TOOL_AWARENESS:-}" == 0 ]]; then
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
zmodload zsh/parameter 2>/dev/null || return 0
|
|
50
|
+
rehash 2>/dev/null || true
|
|
51
|
+
{
|
|
52
|
+
print -rl -- ${(k)commands} > "$dir/commands.txt" &&
|
|
53
|
+
print -rl -- ${(k)builtins} > "$dir/builtins.txt" &&
|
|
54
|
+
print -rl -- ${(k)functions:#_*} ${(k)aliases} > "$dir/functions.txt"
|
|
55
|
+
} 2>/dev/null || {
|
|
56
|
+
rm -f -- "$dir/commands.txt" "$dir/builtins.txt" "$dir/functions.txt" 2>/dev/null || true
|
|
57
|
+
}
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
26
60
|
|
|
27
|
-
|
|
28
|
-
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Instant first frame (quick 261011-jbh, LD-1).
|
|
63
|
+
# The moment `??` fires, the widget paints the `queque › <lbuffer>` header and
|
|
64
|
+
# a `thinking…` row into the modal surface, before Node starts:
|
|
65
|
+
# inline — `_qq_paint_inline_instant_frame` prints the scroll reserve, a
|
|
66
|
+
# fresh cursor save, an erase, the frame, and a restore, so the
|
|
67
|
+
# cursor ends on the reserve top. The reserve is byte-identical to
|
|
68
|
+
# the client's inlineScrollReserve(MODAL_VIEWPORT_LINES)
|
|
69
|
+
# (src/client/instant-frame.ts; tests/zsh-widget.test.ts z5 guards
|
|
70
|
+
# the drift), so the client's own reserve moves nothing.
|
|
71
|
+
# Zellij — `_qq_write_pane_instant_frame` writes the frame to
|
|
72
|
+
# `$tmpdir/instant-frame.txt` (0700 session dir) and the pane
|
|
73
|
+
# command becomes `/bin/sh -c "$QQ_PANE_PAINT_SCRIPT"`, which cats
|
|
74
|
+
# the file and execs the unchanged client argv. The lbuffer never
|
|
75
|
+
# reaches the zellij or sh argv.
|
|
76
|
+
# The Ink client clears the frame in its first visible write (takeover). The
|
|
77
|
+
# rows mirror Ink's loading frame: header (only for a non-empty lbuffer), the
|
|
78
|
+
# FILTER row and the content margin (blank here), then the loading text.
|
|
79
|
+
# Control characters are stripped from the lbuffer before it reaches the
|
|
80
|
+
# terminal. Plain stdout only (no tty-device redirects; ZLE tracks it).
|
|
81
|
+
# Every line is guarded like the handoff helper: the widget's ERR trap deletes
|
|
82
|
+
# the session dir on any failing command, so every list ends in status 0 and
|
|
83
|
+
# stderr stays off the ZLE line. No subprocesses.
|
|
84
|
+
# Kill switch: QQ_INSTANT_FRAME=0 in the shell paints nothing and launches the
|
|
85
|
+
# client exactly as before.
|
|
86
|
+
# Constants use `typeset -g`, not `-r`: the widget is re-sourced in dev.
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
typeset -g QQ_MODAL_VIEWPORT_LINES=18 # mirrors MODAL_VIEWPORT_LINES (modal-layout.ts)
|
|
89
|
+
typeset -g QQ_MODAL_MAX_COLUMNS=80 # mirrors MODAL_MAX_WIDTH (CandidateSelect / Modal)
|
|
90
|
+
typeset -g QQ_MODAL_PADDING_COLUMNS=1 # Modal paddingX on each side
|
|
91
|
+
typeset -g QQ_ZELLIJ_FRAME_COLUMNS=2 # floating pane border columns
|
|
92
|
+
typeset -g QQ_INSTANT_FRAME_ROWS_ABOVE_LOADING=2 # FILTER row + content marginTop
|
|
93
|
+
typeset -g QQ_INSTANT_FRAME_LOADING_TEXT='thinking…' # LoadingSpinner FRAMES[0]
|
|
94
|
+
typeset -g QQ_INSTANT_FRAME_FILE='instant-frame.txt'
|
|
95
|
+
typeset -g QQ_PANE_PAINT_SCRIPT='cat "$1" 2>/dev/null; shift; exec "$@"'
|
|
96
|
+
typeset -g QQ_INSTANT_FRAME_PAINTED="${QQ_INSTANT_FRAME_PAINTED:-0}"
|
|
97
|
+
|
|
98
|
+
# _qq_instant_frame lbuffer columns — sets REPLY to the frame rows joined by
|
|
99
|
+
# CR LF (empty with QQ_INSTANT_FRAME=0). Non-numeric or zero columns mean 80.
|
|
100
|
+
_qq_instant_frame() {
|
|
101
|
+
REPLY=''
|
|
102
|
+
if [[ "${QQ_INSTANT_FRAME:-}" == 0 ]]; then
|
|
29
103
|
return 0
|
|
30
104
|
fi
|
|
31
|
-
local
|
|
32
|
-
|
|
105
|
+
local lbuffer="${1//[[:cntrl:]]/}"
|
|
106
|
+
local columns="${2:-}"
|
|
107
|
+
if [[ "$columns" != <1-> ]] || (( columns > QQ_MODAL_MAX_COLUMNS )); then
|
|
108
|
+
columns=$QQ_MODAL_MAX_COLUMNS
|
|
109
|
+
fi
|
|
110
|
+
local width=$(( columns - 2 * QQ_MODAL_PADDING_COLUMNS ))
|
|
111
|
+
if (( width < 1 )); then
|
|
112
|
+
width=1
|
|
113
|
+
fi
|
|
114
|
+
local pad="${(l:QQ_MODAL_PADDING_COLUMNS:)}"
|
|
115
|
+
local dim=$'\e[2m' undim=$'\e[22m'
|
|
116
|
+
local -a rows
|
|
117
|
+
if [[ -n "${1:-}" ]]; then
|
|
118
|
+
local text="queque › ${lbuffer}"
|
|
119
|
+
if (( ${#text} > width )); then
|
|
120
|
+
text="${text[1,width-1]}…"
|
|
121
|
+
fi
|
|
122
|
+
rows+=("${pad}${dim}${text}${undim}")
|
|
123
|
+
fi
|
|
124
|
+
repeat $QQ_INSTANT_FRAME_ROWS_ABOVE_LOADING rows+=('')
|
|
125
|
+
rows+=("${pad}${dim}${QQ_INSTANT_FRAME_LOADING_TEXT}${undim}")
|
|
126
|
+
REPLY="${(pj:\r\n:)rows}"
|
|
127
|
+
return 0
|
|
33
128
|
}
|
|
34
129
|
|
|
35
|
-
|
|
36
|
-
|
|
130
|
+
# _qq_inline_instant_frame_bytes lbuffer columns — sets REPLY to the inline
|
|
131
|
+
# paint: reserve (newlines, CSI <n> A, CR, ESC 7) + CSI J + frame + ESC 8.
|
|
132
|
+
# Empty when the frame is empty.
|
|
133
|
+
_qq_inline_instant_frame_bytes() {
|
|
134
|
+
_qq_instant_frame "${1:-}" "${2:-}"
|
|
135
|
+
if [[ -z "$REPLY" ]]; then
|
|
136
|
+
return 0
|
|
137
|
+
fi
|
|
138
|
+
local frame="$REPLY"
|
|
139
|
+
REPLY="${(pl:QQ_MODAL_VIEWPORT_LINES::\n:)}"$'\e['"${QQ_MODAL_VIEWPORT_LINES}A"$'\r\e7\e[J'"${frame}"$'\e8'
|
|
140
|
+
return 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# _qq_paint_inline_instant_frame lbuffer — prints the inline paint in one
|
|
144
|
+
# write when stdout is a terminal. QQ_INSTANT_FRAME_PAINTED=1 only when that
|
|
145
|
+
# print succeeded (the widget then clears it if the client exits non-zero).
|
|
146
|
+
_qq_paint_inline_instant_frame() {
|
|
147
|
+
QQ_INSTANT_FRAME_PAINTED=0
|
|
148
|
+
if [[ ! -t 1 ]]; then
|
|
149
|
+
return 0
|
|
150
|
+
fi
|
|
151
|
+
_qq_inline_instant_frame_bytes "${1:-}" "${COLUMNS:-}"
|
|
152
|
+
if [[ -z "$REPLY" ]]; then
|
|
37
153
|
return 0
|
|
38
154
|
fi
|
|
155
|
+
if print -rn -- "$REPLY" 2>/dev/null; then
|
|
156
|
+
QQ_INSTANT_FRAME_PAINTED=1
|
|
157
|
+
fi
|
|
158
|
+
return 0
|
|
159
|
+
}
|
|
39
160
|
|
|
40
|
-
|
|
161
|
+
# _qq_write_pane_instant_frame dir lbuffer pane_columns — writes the frame +
|
|
162
|
+
# CSI H to dir/instant-frame.txt for the Zellij pane wrapper. A failed write
|
|
163
|
+
# removes the file, so failure means "no file" and the pane launches as before.
|
|
164
|
+
_qq_write_pane_instant_frame() {
|
|
165
|
+
local dir="$1"
|
|
166
|
+
_qq_instant_frame "${2:-}" "${3:-}"
|
|
167
|
+
if [[ -z "$REPLY" ]]; then
|
|
41
168
|
return 0
|
|
42
169
|
fi
|
|
170
|
+
{
|
|
171
|
+
print -rn -- "${REPLY}"$'\e[H' > "$dir/$QQ_INSTANT_FRAME_FILE"
|
|
172
|
+
} 2>/dev/null || {
|
|
173
|
+
rm -f -- "$dir/$QQ_INSTANT_FRAME_FILE" 2>/dev/null || true
|
|
174
|
+
}
|
|
175
|
+
return 0
|
|
176
|
+
}
|
|
43
177
|
|
|
44
|
-
|
|
45
|
-
|
|
178
|
+
typeset -g QQ_DAEMON_PREWARMED="${QQ_DAEMON_PREWARMED:-0}"
|
|
179
|
+
|
|
180
|
+
_qq_log() {
|
|
181
|
+
if [[ -z "$QQ_DEBUG_LOG_FILE" ]]; then
|
|
46
182
|
return 0
|
|
47
183
|
fi
|
|
184
|
+
local message="$1"
|
|
185
|
+
print -r -- "$(date '+%Y-%m-%dT%H:%M:%S%z') [zsh] $message" >> "$QQ_DEBUG_LOG_FILE" 2>/dev/null
|
|
186
|
+
}
|
|
48
187
|
|
|
49
|
-
|
|
188
|
+
_qq_prewarm_daemon() {
|
|
189
|
+
if [[ "$QQ_DAEMON_PREWARMED" == 1 ]]; then
|
|
50
190
|
return 0
|
|
51
191
|
fi
|
|
52
192
|
|
|
53
193
|
QQ_DAEMON_PREWARMED=1
|
|
54
194
|
(
|
|
55
|
-
|
|
195
|
+
if [[ -n "$QQ_DEV_ROOT" && -f "$QQ_DEV_ROOT/dist/cli/main.js" ]] && command -v node >/dev/null 2>&1; then
|
|
196
|
+
command node "$QQ_DEV_ROOT/dist/cli/main.js" daemon --ensure
|
|
197
|
+
elif command -v qq >/dev/null 2>&1; then
|
|
198
|
+
command qq daemon --ensure
|
|
199
|
+
fi
|
|
56
200
|
) >/dev/null 2>&1 &!
|
|
57
201
|
}
|
|
58
202
|
#
|
|
@@ -188,10 +332,40 @@ qq-question-widget() {
|
|
|
188
332
|
|
|
189
333
|
# Second `?` detected — consume trigger and capture buffers
|
|
190
334
|
_qq_log "trigger fired lbuffer=${LBUFFER} rbuffer=${RBUFFER}"
|
|
335
|
+
QQ_INSTANT_FRAME_PAINTED=0
|
|
191
336
|
_qq_capture_buffers
|
|
192
337
|
LBUFFER="$QQ_LBUFFER"
|
|
193
338
|
RBUFFER="$QQ_RBUFFER"
|
|
194
339
|
|
|
340
|
+
# Build the qq invocation.
|
|
341
|
+
# qq may be a shell function (dev mode) or an installed binary. zellij run
|
|
342
|
+
# uses exec — it does not start a shell, so shell functions are invisible.
|
|
343
|
+
# Use node + the script path directly when QQ_DEV_ROOT is set; otherwise
|
|
344
|
+
# fall back to a bare qq (assumes a globally-installed binary).
|
|
345
|
+
local -a qq_cmd
|
|
346
|
+
if [[ -n "${QQ_DEV_ROOT:-}" && -f "${QQ_DEV_ROOT}/dist/cli/main.js" ]]; then
|
|
347
|
+
qq_cmd=("node" "${QQ_DEV_ROOT}/dist/cli/main.js")
|
|
348
|
+
else
|
|
349
|
+
# whence -p searches PATH only, bypassing shell functions — prevents the dev
|
|
350
|
+
# qq() function from shadowing the installed Homebrew binary.
|
|
351
|
+
local _qq_bin
|
|
352
|
+
_qq_bin=$(whence -p qq 2>/dev/null)
|
|
353
|
+
if [[ -z "$_qq_bin" ]]; then
|
|
354
|
+
zle -M "queque: qq not found — run: qq init zsh >> ~/.zshrc && source ~/.zshrc"
|
|
355
|
+
return 0
|
|
356
|
+
fi
|
|
357
|
+
qq_cmd=("$_qq_bin")
|
|
358
|
+
fi
|
|
359
|
+
_qq_log "qq_cmd=${qq_cmd[*]}"
|
|
360
|
+
|
|
361
|
+
# Instant first frame (LD-1): paint the header and loading row now, before
|
|
362
|
+
# mktemp, jq and the tool handoff, so the modal shows something before Node
|
|
363
|
+
# starts. The Zellij pane gets its frame through a file further down.
|
|
364
|
+
if [[ -z "$ZELLIJ" ]]; then
|
|
365
|
+
_qq_paint_inline_instant_frame "$QQ_LBUFFER"
|
|
366
|
+
_qq_log "instant frame painted=${QQ_INSTANT_FRAME_PAINTED}"
|
|
367
|
+
fi
|
|
368
|
+
|
|
195
369
|
# Create a private session directory (CR-006 fix).
|
|
196
370
|
# Using mktemp -d + chmod 700 prevents symlink-redirect attacks on temp paths.
|
|
197
371
|
local tmpdir req_file
|
|
@@ -220,27 +394,8 @@ qq-question-widget() {
|
|
|
220
394
|
"rbuffer": $(printf '%s' "$QQ_RBUFFER" | jq -Rs .)
|
|
221
395
|
}
|
|
222
396
|
JSON
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
# qq may be a shell function (dev mode) or an installed binary. zellij run
|
|
226
|
-
# uses exec — it does not start a shell, so shell functions are invisible.
|
|
227
|
-
# Use node + the script path directly when QQ_DEV_ROOT is set; otherwise
|
|
228
|
-
# fall back to a bare qq (assumes a globally-installed binary).
|
|
229
|
-
local -a qq_cmd
|
|
230
|
-
if [[ -n "${QQ_DEV_ROOT:-}" && -f "${QQ_DEV_ROOT}/dist/cli/main.js" ]]; then
|
|
231
|
-
qq_cmd=("node" "${QQ_DEV_ROOT}/dist/cli/main.js")
|
|
232
|
-
else
|
|
233
|
-
# whence -p searches PATH only, bypassing shell functions — prevents the dev
|
|
234
|
-
# qq() function from shadowing the installed Homebrew binary.
|
|
235
|
-
local _qq_bin
|
|
236
|
-
_qq_bin=$(whence -p qq 2>/dev/null)
|
|
237
|
-
if [[ -z "$_qq_bin" ]]; then
|
|
238
|
-
zle -M "queque: qq not found — run: qq init zsh >> ~/.zshrc && source ~/.zshrc"
|
|
239
|
-
return 0
|
|
240
|
-
fi
|
|
241
|
-
qq_cmd=("$_qq_bin")
|
|
242
|
-
fi
|
|
243
|
-
_qq_log "qq_cmd=${qq_cmd[*]}"
|
|
397
|
+
_qq_write_tool_handoff "$tmpdir"
|
|
398
|
+
_qq_log "tool handoff written"
|
|
244
399
|
|
|
245
400
|
if [[ -n "$ZELLIJ" ]]; then
|
|
246
401
|
# ---- Zellij path: FIFO + floating pane ----
|
|
@@ -258,10 +413,26 @@ JSON
|
|
|
258
413
|
export QQ_PANE_WIDTH="$qq_pane_width"
|
|
259
414
|
export QQ_PANE_HEIGHT="$qq_pane_height"
|
|
260
415
|
|
|
416
|
+
# Instant first frame (LD-1): the frame goes to a file in the 0700 session
|
|
417
|
+
# dir; the pane wrapper cats it, then execs the unchanged client argv. A
|
|
418
|
+
# non-numeric pane width (e.g. a percentage) falls back to the modal width.
|
|
419
|
+
local qq_frame_columns=0
|
|
420
|
+
if [[ "$qq_pane_width" == <1-> ]]; then
|
|
421
|
+
qq_frame_columns=$(( qq_pane_width - QQ_ZELLIJ_FRAME_COLUMNS ))
|
|
422
|
+
fi
|
|
423
|
+
_qq_write_pane_instant_frame "$tmpdir" "$QQ_LBUFFER" "$qq_frame_columns"
|
|
424
|
+
local -a pane_cmd
|
|
425
|
+
if [[ -s "$tmpdir/$QQ_INSTANT_FRAME_FILE" ]]; then
|
|
426
|
+
pane_cmd=(/bin/sh -c "$QQ_PANE_PAINT_SCRIPT" queque-instant-frame "$tmpdir/$QQ_INSTANT_FRAME_FILE" "${qq_cmd[@]}")
|
|
427
|
+
else
|
|
428
|
+
pane_cmd=("${qq_cmd[@]}")
|
|
429
|
+
fi
|
|
430
|
+
_qq_log "pane instant frame=${#pane_cmd[@]}"
|
|
431
|
+
|
|
261
432
|
# Launch the floating pane backgrounded+disowned (D-06).
|
|
262
433
|
# Launch at config height; qq client shrinks the pane once content is rendered.
|
|
263
434
|
zellij run --floating --close-on-exit --width "$qq_pane_width" --height "$qq_pane_height" --name "qq" -- \
|
|
264
|
-
"${
|
|
435
|
+
"${pane_cmd[@]}" client --request-file "$req_file" --result-file "$fifo_path" \
|
|
265
436
|
2>>"${QQ_DEBUG_LOG_FILE:-/tmp/qq-${UID}-debug.log}" &!
|
|
266
437
|
|
|
267
438
|
# Block on FIFO read with a 30 s timeout (D-05).
|
|
@@ -279,8 +450,16 @@ JSON
|
|
|
279
450
|
|
|
280
451
|
# Run the client as a foreground child. ZLE yields the terminal so Ink
|
|
281
452
|
# can open /dev/tty and render the selection UI inline.
|
|
453
|
+
local qq_client_status=0
|
|
282
454
|
"${qq_cmd[@]}" client --request-file "$req_file" --result-file "$result_file" \
|
|
283
|
-
2>>"${QQ_DEBUG_LOG_FILE:-/tmp/qq-${UID}-debug.log}"
|
|
455
|
+
2>>"${QQ_DEBUG_LOG_FILE:-/tmp/qq-${UID}-debug.log}" || qq_client_status=$?
|
|
456
|
+
|
|
457
|
+
# A client that crashed (before or after Ink) cannot clear the instant
|
|
458
|
+
# frame: restore the widget's own fresh save (the reserve top) and erase.
|
|
459
|
+
if [[ "$QQ_INSTANT_FRAME_PAINTED" == 1 && "$qq_client_status" != 0 ]]; then
|
|
460
|
+
print -rn -- $'\e8\e[J' 2>/dev/null || true
|
|
461
|
+
_qq_log "instant frame cleared after client status=${qq_client_status}"
|
|
462
|
+
fi
|
|
284
463
|
|
|
285
464
|
_qq_apply_result "$result_file" || true
|
|
286
465
|
fi
|