Quick and easy lists. Nothing complicated. Simple AF. Valkey.
  • Shell 92.1%
  • Makefile 7.9%
Find a file
Andrew Briscoe d3ff81d6c0
feat(vk-list): add vlist wrapper, literate Makefile, null-delimited IO & tests
**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`.

---
2025-12-26 11:16:06 +08:00
bin feat(vk-list): add vlist wrapper, literate Makefile, null-delimited IO & tests 2025-12-26 11:16:06 +08:00
tools feat(vk-list): add vlist wrapper, literate Makefile, null-delimited IO & tests 2025-12-26 11:16:06 +08:00
.gitignore feat(tooling): add agent contract, lint/report pipeline, and safer vk-list bootstrap 2025-12-26 06:05:36 +08:00
AGENTS.md feat(vk-list): add vlist wrapper, literate Makefile, null-delimited IO & tests 2025-12-26 11:16:06 +08:00
bootstrap-dev.sh feat(vk-list): add vlist wrapper, literate Makefile, null-delimited IO & tests 2025-12-26 11:16:06 +08:00
install-local.sh feat(vk-list): add vlist wrapper, literate Makefile, null-delimited IO & tests 2025-12-26 11:16:06 +08:00
Makefile feat(vk-list): add vlist wrapper, literate Makefile, null-delimited IO & tests 2025-12-26 11:16:06 +08:00
NOTES.md feat(todo,docs,ai): namespaced vk-todo CLI with edit/import/export and multi-view AI commit prompts 2025-12-25 20:32:43 +08:00
README.md feat(vk-list): add vlist wrapper, literate Makefile, null-delimited IO & tests 2025-12-26 11:16:06 +08:00
tests.sh feat(vk-list): add vlist wrapper, literate Makefile, null-delimited IO & tests 2025-12-26 11:16:06 +08:00

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

: litblock extracts 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.