feat: v0.11.0 VS Code-like explorer, markdown preview, search in files
build-deb / Lint, test, package (push) Successful in 2m29s
build-deb / Deploy to CT 126 (push) Successful in 7s
build-deb / Notify on failure (push) Skipped

- Explorer: context menus, multi-select, inline rename/new, drag to move
  or copy, desktop drag-and-drop uploads (folders too), cut/copy/paste,
  duplicate, download, delete to a 7-day trash with Undo. Server side in
  libexec/devdesk-fsops.sh with bats tests. Closes #54
- Markdown preview in a shadow root: headings with anchors and outline,
  highlighted code with copy, callouts, task lists, front matter, links
  that open files, live split view. Editor status bar, multi-cursor,
  closeBrackets, Ctrl+G, tab menus and reordering. Closes #55
- Search in files (Ctrl+Shift+F) via git grep / grep. Closes #56
- Fix: nested $(...) path checks now fail hard (set -e does not reach
  them; an escape created /x in testing); newest folder listing wins;
  sessionAction returns the reply JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHRLCCdXYm41Qx5kNhehaP
This commit is contained in:
2026-10-22 21:25:43 -04:00
co-authored by Claude Opus 5.5
parent e60f1ad066
commit 65114165e7
15 changed files with 2166 additions and 167 deletions
+143
View File
@@ -1195,3 +1195,146 @@ JSONL
run devdesk attach --unsplit alpha
[ "$(jq -r .closed <<<"$output")" = 0 ]
}
fs_put() { # SESSION PATH MODE CONTENT-FILE
local size
size=$(stat -c %s "$4")
base64 -w0 "$4" | devdesk attach --fs-put "$1" "$2" "$size" "$3"
}
last_json() { tail -n1 <<<"$output"; }
@test "fs-mkdir, fs-move and fs-copy stay inside the folder and never overwrite (#54)" {
p="$DEVDESK_PROJECTS_ROOT/alpha"
mkdir -p "$p/src" && printf 'a\n' >"$p/src/a.txt" && printf 'b\n' >"$p/b.txt"
ln -s "$TMP/outside" "$p/out"
ln -s b.txt "$p/link"
run devdesk attach --fs-mkdir alpha docs/guides
[ "$status" -eq 0 ] && [ -d "$p/docs/guides" ]
[ "$(last_json | jq -r .path)" = docs/guides ]
run devdesk attach --fs-mkdir alpha docs
[[ $output == *"already exists"* ]]
for bad in ../x /abs out/x 'a/../b' . '' docs/; do
run devdesk attach --fs-mkdir alpha "$bad"
[ "$status" -ne 0 ]
done
[ -z "$(ls -A "$TMP/outside")" ]
# out/x once escaped to /x: set -e doesn't reach into $(...) (#54)
[ ! -e /x ] && [ ! -e /abs ]
# rename a file, move a folder into another, never onto something
run devdesk attach --fs-move alpha src/a.txt src/renamed.txt
[ "$status" -eq 0 ] && [ -f "$p/src/renamed.txt" ] && [ ! -e "$p/src/a.txt" ]
run devdesk attach --fs-move alpha b.txt src/renamed.txt
[ "$status" -ne 0 ] && [[ $output == *"already exists"* ]]
[ "$(cat "$p/b.txt")" = b ]
run devdesk attach --fs-move alpha src docs/src
[ "$status" -eq 0 ] && [ -f "$p/docs/src/renamed.txt" ]
run devdesk attach --fs-move alpha docs docs/guides/docs
[ "$status" -ne 0 ] && [[ $output == *"inside itself"* ]]
run devdesk attach --fs-move alpha b.txt out/b.txt
[ "$status" -ne 0 ]
run devdesk attach --fs-move alpha ../outside x
[ "$status" -ne 0 ]
# a symlink moves as a link
run devdesk attach --fs-move alpha link link2
[ "$status" -eq 0 ] && [ -L "$p/link2" ] && [ -f "$p/b.txt" ]
# copy: folders recursively, missing parents created
run devdesk attach --fs-copy alpha docs/src new/place/src
[ "$status" -eq 0 ] && [ -f "$p/new/place/src/renamed.txt" ] && [ -f "$p/docs/src/renamed.txt" ]
run devdesk attach --fs-copy alpha b.txt docs/src/renamed.txt
[ "$status" -ne 0 ]
}
@test "fs-delete moves to the trash and fs-restore puts it back (#54)" {
p="$DEVDESK_PROJECTS_ROOT/alpha"
mkdir -p "$p/dir/sub" && printf 'x\n' >"$p/dir/sub/x" && printf 'y\n' >"$p/y.txt"
ln -s "$TMP/outside" "$p/out"
touch "$TMP/outside/keep"
run devdesk attach --fs-delete alpha dir y.txt out
[ "$status" -eq 0 ]
id=$(last_json | jq -r .trash)
[ "$(last_json | jq -c .paths)" = '["dir","y.txt","out"]' ]
[ ! -e "$p/dir" ] && [ ! -e "$p/y.txt" ] && [ ! -L "$p/out" ] && [ -f "$TMP/outside/keep" ]
# nothing deleted when one path is bad
printf 'z\n' >"$p/z.txt"
run devdesk attach --fs-delete alpha z.txt ../outside
[ "$status" -ne 0 ] && [ -f "$p/z.txt" ]
run devdesk attach --fs-delete alpha .
[ "$status" -ne 0 ]
# restore; a path taken meanwhile is refused
printf 'new\n' >"$p/y.txt"
run devdesk attach --fs-restore alpha "$id"
[ "$status" -ne 0 ] && [[ $output == *"already exists: y.txt"* ]]
rm "$p/y.txt"
run devdesk attach --fs-restore alpha "$id"
[ "$status" -eq 0 ]
[ "$(cat "$p/dir/sub/x" "$p/y.txt")" = "$(printf 'x\ny')" ] && [ -L "$p/out" ]
[ ! -e "$DEVDESK_STATE_DIR/trash/alpha/$id" ]
run devdesk attach --fs-restore alpha "../../etc"
[ "$status" -ne 0 ]
}
@test "fs-put uploads any file into a folder; fs-download returns it (#54)" {
p="$DEVDESK_PROJECTS_ROOT/alpha"
head -c 3000 /dev/urandom >"$TMP/bin"
run fs_put alpha assets/img/logo.bin new "$TMP/bin"
[ "$status" -eq 0 ] && cmp -s "$TMP/bin" "$p/assets/img/logo.bin"
[ "$(stat -c %a "$p/assets/img/logo.bin")" = 644 ]
run fs_put alpha assets/img/logo.bin new "$TMP/bin"
[ "$status" -ne 0 ] && [[ $output == *"already exists"* ]]
printf 'v2' >"$TMP/v2"
chmod 600 "$p/assets/img/logo.bin"
run fs_put alpha assets/img/logo.bin replace "$TMP/v2"
[ "$status" -eq 0 ] && [ "$(cat "$p/assets/img/logo.bin")" = v2 ] && [ "$(stat -c %a "$p/assets/img/logo.bin")" = 600 ]
: >"$TMP/empty"
run fs_put alpha empty.txt new "$TMP/empty"
[ "$status" -eq 0 ] && [ -f "$p/empty.txt" ] && [ ! -s "$p/empty.txt" ]
ln -s "$TMP/outside" "$p/out"
for bad in out/x ../x /x assets; do
run fs_put alpha "$bad" new "$TMP/v2"
[ "$status" -ne 0 ]
done
[ -z "$(ls -A "$TMP/outside")" ]
run fs_put alpha assets replace "$TMP/v2"
[ "$status" -ne 0 ] && [ -d "$p/assets" ]
# short stream: nothing left behind
run bash -c "printf aGVs | '$REPO/bin/devdesk' attach --fs-put alpha short.txt 6 new"
[ "$status" -ne 0 ] && [ ! -e "$p/short.txt" ] && [ -z "$(find "$p" -name '.*devdesk-*')" ]
# download: binary body comes back whole
run devdesk attach --fs-download alpha assets/img/logo.bin
[ "$status" -eq 0 ]
[ "$(sed -n '/DEVDESK-BLOB-BEGIN/{n;p}' <<<"$output" | base64 -d)" = v2 ]
head -c 3000 /dev/urandom >"$p/rand"
run devdesk attach --fs-download alpha rand
sed -n '/DEVDESK-BLOB-BEGIN/{n;p}' <<<"$output" | base64 -d | cmp -s - "$p/rand"
run devdesk attach --fs-download alpha out
[ "$status" -ne 0 ]
}
@test "fs-grep searches files, literally or by regex, git or not (#56)" {
p="$DEVDESK_PROJECTS_ROOT/alpha"
mkdir -p "$p/src" "$p/node_modules/x"
printf 'const Answer = 42;\nlet b = "a:b";\n' >"$p/src/a.js"
printf 'answer here\n' >"$p/node_modules/x/i.js"
printf '\x00\x01answer' >"$p/bin.dat"
run devdesk attach --fs-grep alpha - 'Answer'
[ "$(jq -c '[.matches[] | [.p, .l, .t]]' <<<"$output")" = '[["src/a.js",1,"const Answer = 42;"]]' ]
run devdesk attach --fs-grep alpha i 'answer'
[ "$(jq -r '[.matches[].p] | join(",")' <<<"$output")" = src/a.js ]
run devdesk attach --fs-grep alpha - '"a:b"'
[ "$(jq -r '.matches[0].t' <<<"$output")" = 'let b = "a:b";' ]
run devdesk attach --fs-grep alpha r 'A[n]swer = [0-9]+'
[ "$(jq -r '.matches | length' <<<"$output")" = 1 ]
run devdesk attach --fs-grep alpha - '[n]'
[ "$(jq -r '.matches | length' <<<"$output")" = 0 ]
# in a repository: git grep, .gitignore honoured, untracked files included
git -C "$p" init -q
printf 'node_modules/\n' >"$p/.gitignore"
printf 'Answer again\n' >"$p/untracked.txt"
run devdesk attach --fs-grep alpha w 'Answer'
[ "$(jq -c '[.matches[] | [.p, .l]] | sort' <<<"$output")" = '[["src/a.js",1],["untracked.txt",1]]' ]
run devdesk attach --fs-grep alpha x 'Answer'
[ "$status" -ne 0 ]
run devdesk attach --fs-grep alpha - ''
[ "$status" -ne 0 ]
}