- Shell 92.1%
- Makefile 7.9%
**Summary**
Introduce `vlist` as a namespaced wrapper over `todo`, add a literate `Makefile`-based build/test workflow, support null-delimited import/export, and grow the README into a coherent spec with an executable test suite.
**Capabilities**
- New `vlist` CLI:
- `vlist <LIST_NAME> [COMMAND...]` delegates to `todo` with `VK_LIST=<LIST_NAME>`.
- `todo` gains:
- `clear` command to delete a list.
- `VK_NULL_DELIM` mode:
- `VK_NULL_DELIM=1 todo export -` → `\0`-separated entries.
- `VK_NULL_DELIM=1 todo import -` uses `parallel -0` to accept null-delimited input.
- `Makefile` (generated from README) provides:
- `bootstrap`, `build`, `install`, `test`, `check`, `clean`, `help`.
**Operations / Workflows**
- Development:
- `make bootstrap` → generate `bootstrap-dev.sh`, `install-local.sh`, `Makefile`, `tests.sh`.
- `make build` → build `bin/todo` + `bin/vlist`.
- `make install` → symlink `todo` and `vlist` into `$HOME/.local/bin`.
- `make test` → run shell-based test suite against `vk-list-tests:*` keys.
- Usage:
- Plain: `todo`, `todo add "task"`, `todo remove 1`, `todo clear`.
- Namespaced: `vlist ideas add "new idea"`, `vlist project:files export -`.
- Composition:
- Line mode: `todo export - | VK_LIST=backup todo import -`.
- Null mode: `VK_NULL_DELIM=1 vlist project:files import -`.
**Systems & Integration**
- Tools:
- `litblock` now generates `todo`, `vlist`, `Makefile`, `install-local.sh`, `tests.sh` from README.
- `parallel` used for both line and null-delimited imports.
- `fd` + `shellcheck` via `make check`.
- Env:
- `VK_LIST`, `VK_PREFIX`, `VK_APPNAME` (existing),
- New `VK_NULL_DELIM`,
- `TODO_BIN` to point tests/wrapper at the correct `todo` binary.
**Data & Information**
- Valkey keys remain `${VK_PREFIX}:${VK_LIST}`, with explicit test namespaces (`vk-list-tests:test-a`, etc.).
- README now includes:
- Core spec, wrapper spec, build/install steps, test suite, backlog, and Valkey command appendix.
**Project Impact**
- Strengthens literate, self-bootstrapping architecture: all scripts and build logic sourced from README.
- Establishes a stable `make` interface for CI and tooling.
- Promotes lists as first-class, composable contexts via `vlist` and null-delimited IO.
**Issues / Risks**
- `parallel` is now a hard dependency for `import`.
- `tests.sh`’s per-test selection likely misuses `command -v` for shell functions and should be refined.
- Minor typo: help text uses `VL_LIST` instead of `VK_LIST`.
---
|
||
|---|---|---|
| bin | ||
| tools | ||
| .gitignore | ||
| AGENTS.md | ||
| bootstrap-dev.sh | ||
| install-local.sh | ||
| Makefile | ||
| NOTES.md | ||
| README.md | ||
| tests.sh | ||
vk-list: Manage Lists in Valkey
Quick and easy lists. Nothing complicated. Simple AF
Usage
> todo add "some activity"
$ 1 some activity
> todo add "another"
$ 1 another
$ 2 some activity
> todo remove 1
$ 1 some activity
Design
The most recent thing is at the top, because as you work on tasks, you need to do something quickly—you can jot down what you were doing when you switch tasks.
Code focuses on readability, minimal features, and pipeline operations through simplicity. Simple AF
Note
:
litblockextracts labeled code fences. Multiple«label»blocks compose in reading order.
Setup Project
Neovim Tip: Setup files with
:!litblock "setup project" | sh -
litblock "setup project" README.md | sh -
Or without litblock:
cat README.md | sed -n '/^```sh «setup project»$/,/^```$/{/^```/d;p}'
#!/usr/bin/env sh
# File: bootstrap-dev.sh
# Generated in README.md «setup project»
BASE_PATH="$(git worktree list --porcelain | grep '^worktree' | cut -f2- -d\ )"
export BASE_PATH
export LITBLOCK_SRC="${BASE_PATH}/README.md"
litblock "setup project" > "${BASE_PATH}/bootstrap-dev.sh"
chmod +x "${BASE_PATH}/bootstrap-dev.sh"
litblock "install-local.sh" > "${BASE_PATH}/install-local.sh"
chmod +x "${BASE_PATH}/install-local.sh"
litblock "Makefile" > "${BASE_PATH}/Makefile"
litblock "test suite" > "${BASE_PATH}/tests.sh"
chmod +x "${BASE_PATH}/tests.sh"
"${BASE_PATH}/install-local.sh"
Build Instructions
#!/usr/bin/env sh
# File: install-local.sh
# Generated in README.md «install-local.sh»
BASE_PATH="$(git worktree list --porcelain | grep '^worktree' | cut -f2- -d\ )"
export BASE_PATH
export LITBLOCK_SRC="${BASE_PATH}/README.md"
mkdir -p "${BASE_PATH}/bin"
litblock "vk-list" > "${BASE_PATH}/bin/todo"
chmod +x "${BASE_PATH}/bin/todo"
litblock "vk-list wrapper" > "${BASE_PATH}/bin/vlist"
chmod +x "${BASE_PATH}/bin/vlist"
ln -sf "$(realpath "${BASE_PATH}/bin/todo")" "${HOME}/.local/bin/todo"
ln -sf "$(realpath "${BASE_PATH}/bin/vlist")" "${HOME}/.local/bin/vlist"
todo
Makefile
# File: Makefile
# Generated from README.md «Makefile»
SHELL := /bin/sh
.SHELLFLAGS := -eu -c
.ONESHELL:
BASE_PATH := $(shell git worktree list --porcelain | grep '^worktree' | cut -f2- -d\ )
LITBLOCK_SRC := $(BASE_PATH)/README.md
BIN_DIR := $(BASE_PATH)/bin
INSTALL_DIR := $(HOME)/.local/bin
export LITBLOCK_SRC
.PHONY: all bootstrap build install test clean check help
all: build
help:
@echo "vk-list literate build system"
@echo ""
@echo "Targets:"
@echo " bootstrap Generate bootstrap-dev.sh and install-local.sh"
@echo " build Build bin/todo and bin/vlist"
@echo " install Install to ~/.local/bin"
@echo " test Run test suite"
@echo " check Run shellcheck on all executables"
@echo " clean Remove generated files"
bootstrap: $(BASE_PATH)/bootstrap-dev.sh $(BASE_PATH)/install-local.sh $(BASE_PATH)/Makefile
$(BASE_PATH)/bootstrap-dev.sh: $(LITBLOCK_SRC)
litblock "setup project" > $@
chmod +x $@
$(BASE_PATH)/install-local.sh: $(LITBLOCK_SRC)
litblock "install-local.sh" > $@
chmod +x $@
$(BASE_PATH)/Makefile: $(LITBLOCK_SRC)
litblock "Makefile" > $@
build: $(BIN_DIR)/todo $(BIN_DIR)/vlist
$(BIN_DIR)/todo: $(LITBLOCK_SRC)
mkdir -p $(BIN_DIR)
litblock "vk-list" > $@
chmod +x $@
$(BIN_DIR)/vlist: $(LITBLOCK_SRC)
mkdir -p $(BIN_DIR)
litblock "vk-list wrapper" > $@
chmod +x $@
install: build
ln -sf $(BASE_PATH)/bin/todo $(INSTALL_DIR)/todo
ln -sf $(BASE_PATH)/bin/vlist $(INSTALL_DIR)/vlist
@echo "Installed to $(INSTALL_DIR)"
test: build
@export VK_PREFIX="vk-list-tests" TODO_BIN="$(BIN_DIR)/todo"; \
litblock "test suite" | sh -
check:
@fd -t x . "$(BASE_PATH)" -x shellcheck
clean:
rm -f $(BIN_DIR)/todo $(BIN_DIR)/vlist
rm -f $(BASE_PATH)/bootstrap-dev.sh $(BASE_PATH)/install-local.sh
valkey-cli --raw keys 'vk-list-tests:*' | parallel -j1 valkey-cli del
Literate Specification
Default Configuration
#!/usr/bin/env sh
# File: bin/todo
# Generated from README.md «vk-list»
set -eu
DEFAULT_APP_NAME="todo"
DEFAULT_VK_PREFIX="vk-list"
DEFAULT_VK_LIST="todo"
Core Functions
tasks() {
start="${1:-0}"
len="$(valkey-cli --raw llen "$VALKEY_LIST_NAME" 2>/dev/null || echo 0)"
[ "$len" -eq 0 ] 2>/dev/null && return 0
end=$((len - 1))
valkey-cli --raw lrange "$VALKEY_LIST_NAME" "$start" "$end" | nl -ba
}
clear_tasks() {
valkey-cli --raw del "$VALKEY_LIST_NAME" >/dev/null
}
add_task() {
task="$*"
if [ -n "$task" ]; then
valkey-cli --raw lpush "$VALKEY_LIST_NAME" "$task" >/dev/null
else
echo "No task" >&2
return 1
fi
}
get_task() {
task_id="$1"
if [ -z "$task_id" ] || [ "$task_id" -le 0 ] 2>/dev/null; then
echo "Provide a task: $APP_NAME get <TASK_NUM>" >&2
return 1
fi
idx=$((task_id - 1))
valkey-cli --raw lindex "$VALKEY_LIST_NAME" "$idx"
}
edit_task() {
task_id="$1"
shift
task="$*"
if [ -z "$task_id" ] || [ "$task_id" -le 0 ] 2>/dev/null; then
echo "Provide a task: $APP_NAME edit <TASK_NUM> <TASK>" >&2
return 1
fi
idx=$((task_id - 1))
valkey-cli --raw lset "${VALKEY_LIST_NAME}" "$idx" "$task" >/dev/null
}
remove_task() {
task_id="$1"
if [ -z "$task_id" ] || [ "$task_id" -le 0 ] 2>/dev/null; then
echo "Provide a task: $APP_NAME remove <TASK_NUM>" >&2
return 1
fi
task="$(get_task "$task_id" | tr -d '\r\n')"
[ -n "$task" ] || return 0
valkey-cli --raw lrem "$VALKEY_LIST_NAME" 1 "$task" >/dev/null
}
Import/Export with Null Delimiter Support
export_tasks() {
file="${1:--}"
if [ "${VK_NULL_DELIM:-0}" = "1" ]; then
# Null-delimited: read each item and output with null separator
len="$(valkey-cli --raw llen "$VALKEY_LIST_NAME" 2>/dev/null || echo 0)"
[ "$len" -eq 0 ] && return 0
i=0
while [ "$i" -lt "$len" ]; do
item="$(valkey-cli --raw lindex "$VALKEY_LIST_NAME" "$i")"
printf '%s\0' "$item"
i=$((i + 1))
done | if [ "$file" = "-" ]; then cat; else cat > "$file"; fi
else
# Line-delimited: standard export
if [ "$file" = "-" ]; then
valkey-cli --raw lrange "$VALKEY_LIST_NAME" 0 -1
else
valkey-cli --raw lrange "$VALKEY_LIST_NAME" 0 -1 > "$file"
fi
fi
}
import_tasks() {
file="${1:--}"
if [ "${VK_NULL_DELIM:-0}" = "1" ]; then
# Null-delimited import using parallel
if [ "$file" = "-" ]; then
parallel -0 --will-cite -j1 "$0" add
else
parallel -0 --will-cite -j1 "$0" add < "$file"
fi
else
# Line-delimited import using parallel
if [ "$file" = "-" ]; then
parallel --will-cite -j1 "$0" add
else
parallel --will-cite -j1 "$0" add < "$file"
fi
fi
}
Help and Dispatch
help() {
cat <<-HELP
Usage: ${APP_NAME} [COMMAND] [ARGS...]
Commands
${APP_NAME} List all tasks
${APP_NAME} add <TASK> Add a task
${APP_NAME} remove <N> Remove task N
${APP_NAME} edit <N> <TASK> Edit task N
${APP_NAME} get <N> Get task N
${APP_NAME} clear Clear all tasks
${APP_NAME} export [FILE] Export tasks (default: stdout)
${APP_NAME} import [FILE] Import tasks (default: stdin)
${APP_NAME} help This help
Environment Variables
VK_LIST List name (default: todo)
VK_PREFIX Key prefix (default: vk-list)
VK_APPNAME App name (default: todo)
VK_NULL_DELIM Use \\0 delimiters (1=yes, 0=no)
Examples
${APP_NAME} add "my task"
VK_LIST=example:ideas ${APP_NAME} add "new idea"
${APP_NAME} export - | VK_LIST=example:backup ${APP_NAME} import -
VK_NULL_DELIM=1 fd -0 . -e md | VL_LIST=example ${APP_NAME} import -
HELP
}
APP_NAME="${VK_APPNAME:-$DEFAULT_APP_NAME}"
VALKEY_PREFIX="${VK_PREFIX:-$DEFAULT_VK_PREFIX}"
VK_LIST="${VK_LIST:-$DEFAULT_VK_LIST}"
VALKEY_LIST_NAME="${VALKEY_PREFIX}:${VK_LIST}"
if [ -z "${1:-}" ]; then
tasks
exit 0
fi
cmd="$1"
shift
case "$cmd" in
add) add_task "$@"; tasks ;;
get) get_task "$@" ;;
remove) remove_task "$@"; tasks ;;
edit) edit_task "$@"; tasks ;;
clear) clear_tasks; tasks ;;
export) export_tasks "$@" ;;
import) import_tasks "$@"; tasks ;;
help) help ;;
*) echo "Unknown command: $cmd" >&2; help >&2; exit 1 ;;
esac
vlist Wrapper
#!/usr/bin/env sh
# File: bin/vlist
# Generated from README.md «vk-list wrapper»
set -eu
BASE_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
TODO_BIN="${TODO_BIN:-${BASE_DIR}/bin/todo}"
if [ $# -lt 1 ]; then
echo "Usage: vlist <LIST_NAME> [COMMAND ...]" >&2
exit 1
fi
list="$1"
shift
VK_APPNAME="vlist:${list}" VK_LIST="$list" exec "$TODO_BIN" "$@"
Test Suite
This needs to be run as a first class citizen, and be extracted on bootstrap. It also should run all tests, so the test status of all the commands should be returned. This needs a bit of refinement.
Run all tests:
make test
# or
export LITBLOCK_SRC="README.md"
litblock "test suite" | sh -
Run individual test:
export VK_PREFIX="vk-list-tests"
export TODO_BIN="bin/todo"
export LITBLOCK_SRC="README.md"
litblock "test suite" | sh -s - test_clear_command
litblock "test suite" | sh -s - "sanity test"
#!/usr/bin/env -S sh
# Generated from «test suite» in README.md
set -e
export VK_PREFIX="vk-list-tests"
todo_bin="${TODO_BIN:-bin/todo}"
cleanup_test_lists() {
for list in test-a test-b test-c test-null-a test-null-b; do
VK_LIST="$list" "$todo_bin" clear >/dev/null 2>&1 || true
done
rm -f TEST TEST_NULL
}
trap cleanup_test_lists EXIT
sanity_tests() {
# Test environment variable precedence with xargs
foobar="$(A="bar" echo "$A" | A="foo" xargs printf "%s %s\n" "$A")"
if [ "$foobar" = "foo bar" ]; then
echo "✓ Sanity test passed"
else
echo "✗ Sanity test FAILED: got '$foobar'" >&2
exit 1
fi
}
test_basic_import_export() {
echo "Running: basic import/export"
printf "foo\nbar\n" > TEST
VK_LIST="test-a" "$todo_bin" import TEST >/dev/null
VK_LIST="test-a" "$todo_bin" export - > /tmp/test-a-export
if diff -q TEST /tmp/test-a-export >/dev/null; then
echo "✓ Basic import/export passed"
else
echo "✗ Import/export mismatch" >&2
diff TEST /tmp/test-a-export >&2
exit 1
fi
rm -f /tmp/test-a-export
}
test_namespace_isolation() {
echo "Running: namespace isolation"
printf "foo\nbar\n" > TEST
VK_LIST="test-a" "$todo_bin" import TEST >/dev/null
VK_LIST="test-b" "$todo_bin" import TEST >/dev/null
if ! diff -q <(VK_LIST="test-a" "$todo_bin" export -) \
<(VK_LIST="test-b" "$todo_bin" export -) >/dev/null; then
echo "✗ Lists should match initially" >&2
exit 1
fi
VK_LIST="test-a" "$todo_bin" add "unique item" >/dev/null
if diff -q <(VK_LIST="test-a" "$todo_bin" export -) \
<(VK_LIST="test-b" "$todo_bin" export -) >/dev/null; then
echo "✗ Lists should differ after mutation" >&2
exit 1
fi
echo "✓ Namespace isolation passed"
}
test_pipe_composition() {
echo "Running: pipe composition"
printf "foo\nbar\n" > TEST
VK_LIST="test-b" "$todo_bin" import TEST >/dev/null
VK_LIST="test-b" "$todo_bin" export - | VK_LIST="test-c" "$todo_bin" import - >/dev/null
if diff -q <(VK_LIST="test-b" "$todo_bin" export -) \
<(VK_LIST="test-c" "$todo_bin" export -) >/dev/null; then
echo "✓ Pipe composition passed"
else
echo "✗ Piped lists should match" >&2
exit 1
fi
}
test_clear_command() {
echo "Running: clear command"
VK_LIST="test-a" "$todo_bin" add "item1" >/dev/null
VK_LIST="test-a" "$todo_bin" add "item2" >/dev/null
count_before=$(VK_LIST="test-a" "$todo_bin" export - | wc -l)
if [ "$count_before" -lt 2 ]; then
echo "✗ Expected at least 2 items before clear" >&2
exit 1
fi
VK_LIST="test-a" "$todo_bin" clear >/dev/null
count_after=$(VK_LIST="test-a" "$todo_bin" export - | wc -l)
if [ "$count_after" -eq 0 ]; then
echo "✓ Clear command passed"
else
echo "✗ List should be empty after clear (got $count_after items)" >&2
exit 1
fi
}
test_null_delimited() {
echo "Running: null-delimited mode"
# Create entries with null separators
printf "line one\nline two\0entry with\nnewline\0final entry\0" > TEST_NULL
VK_NULL_DELIM=1 VK_LIST="test-null-a" "$todo_bin" import TEST_NULL >/dev/null
VK_NULL_DELIM=1 VK_LIST="test-null-a" "$todo_bin" export - > /tmp/test-null-export
if diff -q TEST_NULL /tmp/test-null-export >/dev/null; then
echo "✓ Null-delimited import/export passed"
else
echo "✗ Null-delimited mismatch" >&2
echo "Expected:"
xxd TEST_NULL | head -20 >&2
echo "Got:"
xxd /tmp/test-null-export | head -20 >&2
exit 1
fi
rm -f /tmp/test-null-export
}
main() {
# If specific tests requested, run only those
if [ $# -gt 0 ]; then
for test_name in "$@"; do
if command -v "$test_name" >/dev/null 2>&1; then
"$test_name"
else
echo "Unknown test: $test_name" >&2
exit 1
fi
done
else
# Run all tests
sanity_tests
test_basic_import_export
test_namespace_isolation
test_pipe_composition
test_clear_command
test_null_delimited
fi
echo ""
echo "All tests passed."
}
main "$@"
Backlog
- Create MAN page
- Add range operations [START|END|RANGE]
- Rollback: snapshot to XDG_CACHE_HOME before destructive edits
- Keep snapshots in XDG_DATA_HOME as diffs
- Auto-completion for list discovery
- Streaming lists with ring structures
Future Ideas
List as functor:
fd . -x vlist context add {}
fd . -e md | parallel vlist files add {}
Composed processing:
VK_NULL_DELIM=1 vlist project:files export - | \
parallel -0 terminal-ai --input-file="{}" -- "describe {}" | \
VK_NULL_DELIM=1 vlist project:summaries import -
Cross-list operations:
paste -z <(vlist files export -0) <(vlist summaries export -0) | \
vlist merged import -0
Appendix: Valkey Commands
Generated with: valkey-cli help @list | sed -r 's/\x1B\[[0-9;]*[A-Za-z]//g'
LPUSH key element [element ...]
Prepends elements to a list. Creates key if needed.
LRANGE key start stop
Returns range of elements from a list.
LLEN key
Returns length of a list.
LINDEX key index
Returns element from list by index.
LSET key index element
Sets value of element in list by index.
LREM key count element
Removes elements from list.
LTRIM key start stop
Removes elements from both ends.