From d4530a9667631d736be5335795aaed578a43da78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 8 Mar 2026 16:14:11 +0000 Subject: [PATCH] add script task templates and refactor convert-to-mp3 to use template pattern Co-Authored-By: Claude Opus 4.6 --- seed/tasks/convert-to-mp3/run.sh | 48 ++++---- seed/templates/SCRIPTS.md | 188 +++++++++++++++++++++++++++++++ seed/templates/file-processor.sh | 54 +++++++++ seed/templates/standalone.sh | 12 ++ 4 files changed, 279 insertions(+), 23 deletions(-) create mode 100644 seed/templates/SCRIPTS.md create mode 100644 seed/templates/file-processor.sh create mode 100644 seed/templates/standalone.sh diff --git a/seed/tasks/convert-to-mp3/run.sh b/seed/tasks/convert-to-mp3/run.sh index c3b6c87f..1b3732d4 100755 --- a/seed/tasks/convert-to-mp3/run.sh +++ b/seed/tasks/convert-to-mp3/run.sh @@ -1,20 +1,11 @@ #!/usr/bin/env bash set -euo pipefail -# Accepts path as $1 or $INPUT_FILE_PATH -TARGET="${1:-${INPUT_FILE_PATH:-}}" +# ── Task: Convert To MP3 ── -if [[ -z "$TARGET" ]]; then - echo "Error: No file or directory path provided" >&2 - exit 1 -fi +EXTENSIONS="flac|wav|ogg|wma|aac|m4a|opus|aiff|aif|ape|wv|alac|dsf|dff" -if [[ ! -e "$TARGET" ]]; then - echo "Error: Path does not exist: $TARGET" >&2 - exit 1 -fi - -convert_file() { +process_file() { local input="$1" local ext="${input##*.}" local dir @@ -34,37 +25,48 @@ convert_file() { fi echo "Converting: $input → $output" - ffmpeg -i "$input" -codec:a libmp3lame -b:a 320k -map_metadata 0 -id3v2_version 3 -y "$output" 2>/dev/null - - if [[ $? -eq 0 ]]; then - echo " ✓ Done" + if ffmpeg -i "$input" -codec:a libmp3lame -b:a 320k -map_metadata 0 -id3v2_version 3 -y "$output" 2>/dev/null; then + echo " Done" else - echo " ✗ Failed" >&2 + echo " Failed" >&2 return 1 fi } -AUDIO_EXTS="flac|wav|ogg|wma|aac|m4a|opus|aiff|aif|ape|wv|alac|dsf|dff" +# ── Template: File Processor ── + +TARGET="${1:-${INPUT_FILE_PATH:-}}" + +if [[ -z "$TARGET" ]]; then + echo "Error: No file or directory path provided" >&2 + exit 1 +fi + +if [[ ! -e "$TARGET" ]]; then + echo "Error: Path does not exist: $TARGET" >&2 + exit 1 +fi + converted=0 failed=0 if [[ -f "$TARGET" ]]; then - if convert_file "$TARGET"; then + if process_file "$TARGET"; then converted=$((converted + 1)) else failed=$((failed + 1)) fi elif [[ -d "$TARGET" ]]; then while IFS= read -r -d '' file; do - if convert_file "$file"; then + if process_file "$file"; then converted=$((converted + 1)) else failed=$((failed + 1)) fi - done < <(find "$TARGET" -type f -regextype posix-extended -iregex ".*\.($AUDIO_EXTS)" -print0 | sort -z) + done < <(find "$TARGET" -type f -regextype posix-extended -iregex ".*\.($EXTENSIONS)" -print0 | sort -z) if [[ $converted -eq 0 && $failed -eq 0 ]]; then - echo "No audio files found in: $TARGET" + echo "No matching files found in: $TARGET" exit 0 fi else @@ -73,4 +75,4 @@ else fi echo "" -echo "Summary: $converted converted, $failed failed" +echo "Summary: $converted processed, $failed failed" diff --git a/seed/templates/SCRIPTS.md b/seed/templates/SCRIPTS.md new file mode 100644 index 00000000..77626e08 --- /dev/null +++ b/seed/templates/SCRIPTS.md @@ -0,0 +1,188 @@ +# Script Task Templates + +When creating a script-mode task, always use one of these templates. The template handles all boilerplate (input parsing, validation, iteration, error handling, summary). You only write the task-specific logic. + +## Inputs + +All task inputs defined in the TASK.md frontmatter are available to the script in two ways: + +1. **Environment variables**: `INPUT_` (uppercase) — e.g. `file_path` → `$INPUT_FILE_PATH` +2. **Positional arguments**: if `args: [file_path]` is set in TASK.md, the value is passed as `$1` + +## Templates + +### 1. File Processor + +Use when the task operates on a file or directory of files. + +**When to use**: converting files, transcoding, extracting metadata, batch renaming, any per-file operation. + +**Structure**: Define `EXTENSIONS` and `process_file()`, then the template handles discovery, iteration, and summary. + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# ── Task: ── + +EXTENSIONS="ext1|ext2|ext3" + +process_file() { + local input="$1" + # ... do work on $input ... + # return 0 on success, 1 on failure +} + +# ── Template: File Processor ── + +TARGET="${1:-${INPUT_FILE_PATH:-}}" + +if [[ -z "$TARGET" ]]; then + echo "Error: No file or directory path provided" >&2 + exit 1 +fi + +if [[ ! -e "$TARGET" ]]; then + echo "Error: Path does not exist: $TARGET" >&2 + exit 1 +fi + +converted=0 +failed=0 + +if [[ -f "$TARGET" ]]; then + if process_file "$TARGET"; then + converted=$((converted + 1)) + else + failed=$((failed + 1)) + fi +elif [[ -d "$TARGET" ]]; then + while IFS= read -r -d '' file; do + if process_file "$file"; then + converted=$((converted + 1)) + else + failed=$((failed + 1)) + fi + done < <(find "$TARGET" -type f -regextype posix-extended -iregex ".*\.($EXTENSIONS)" -print0 | sort -z) + + if [[ $converted -eq 0 && $failed -eq 0 ]]; then + echo "No matching files found in: $TARGET" + exit 0 + fi +else + echo "Error: Not a file or directory: $TARGET" >&2 + exit 1 +fi + +echo "" +echo "Summary: $converted processed, $failed failed" +``` + +**Slots to fill**: +- `EXTENSIONS` — pipe-separated list of file extensions to match when processing a directory +- `process_file()` — receives one argument: the full path to the file. Print progress to stdout, errors to stderr. Return 0 on success, 1 on failure. + +**Example** (convert audio to MP3): +```bash +EXTENSIONS="flac|wav|ogg|wma|aac|m4a|opus" + +process_file() { + local input="$1" + local output="${input%.*}.mp3" + + if [[ -f "$output" ]]; then + echo "Skipping (exists): $output" + return 0 + fi + + echo "Converting: $input" + if ffmpeg -i "$input" -codec:a libmp3lame -b:a 320k -y "$output" 2>/dev/null; then + echo " Done" + else + echo " Failed" >&2 + return 1 + fi +} +``` + +### 2. Standalone + +Use when the task doesn't operate on a specific file target — it uses inputs from environment variables or takes no input at all. + +**When to use**: fetching data from APIs, generating reports, system tasks, anything that isn't per-file processing. + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# ── Task: ── + +main() { + # Access inputs via $INPUT_* environment variables + # e.g. $INPUT_COUNTRY, $INPUT_LIMIT, $INPUT_OUTPUT_DIR + + # ... do work ... +} + +# ── Template: Standalone ── +main +``` + +**Slots to fill**: +- `main()` — the entire task logic. Use `$INPUT_*` env vars for parameters. + +**Example** (download RSS feed): +```bash +main() { + local url="$INPUT_FEED_URL" + local output="${INPUT_OUTPUT_DIR:-$HOME}/feed.xml" + + echo "Fetching: $url" + if curl -sSL "$url" -o "$output"; then + echo "Saved to: $output" + else + echo "Failed to fetch feed" >&2 + exit 1 + fi +} +``` + +## TASK.md Frontmatter + +Every script task needs a TASK.md with this structure: + +```yaml +--- +name: Task Name +description: What the task does +version: 1 +mode: script +language: bash +triggers: # optional — enables context menu on matching files + - type: file + extensions: + - ext1 + - ext2 + - type: directory +inputs: + input_name: + type: string # string, number, boolean + description: What this input is +args: [input_name] # optional — pass inputs as positional args +--- + +# Task Name + +Brief description of what the task does. +``` + +## Rules + +1. **Always use `set -euo pipefail`** — fail fast on errors. +2. **Print progress to stdout** — the user sees this streamed in the UI. +3. **Print errors to stderr** — they appear in red in the UI. +4. **Return proper exit codes** — 0 = success, non-zero = failure. The UI shows "Task complete" or "Task failed" based on exit code. +5. **Don't assume tools are installed** — check with `command -v` before using optional dependencies. +6. **Use `$INPUT_*` env vars** — never hardcode paths or values that should come from inputs. +7. **The script runs inside a bwrap sandbox** for non-admin users — it has access to `/data/home/` (user's home), `/tmp`, and standard system tools. It cannot access other users' data. +8. **Keep scripts self-contained** — the entire script is stored in the database as a single text blob. No external file dependencies. diff --git a/seed/templates/file-processor.sh b/seed/templates/file-processor.sh new file mode 100644 index 00000000..3ead8676 --- /dev/null +++ b/seed/templates/file-processor.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ── File Processor Template ── +# Handles: input resolution, validation, file discovery, iteration, summary. +# The task only needs to define: EXTENSIONS and process_file(). +# +# Slots to fill before this template: +# EXTENSIONS="flac|wav|ogg" — pipe-separated list of file extensions to match +# process_file() { ... } — receives $1 (input path), returns 0 on success + +# ── Input resolution ── +TARGET="${1:-${INPUT_FILE_PATH:-}}" + +if [[ -z "$TARGET" ]]; then + echo "Error: No file or directory path provided" >&2 + exit 1 +fi + +if [[ ! -e "$TARGET" ]]; then + echo "Error: Path does not exist: $TARGET" >&2 + exit 1 +fi + +# ── Execution ── +converted=0 +failed=0 + +if [[ -f "$TARGET" ]]; then + if process_file "$TARGET"; then + converted=$((converted + 1)) + else + failed=$((failed + 1)) + fi +elif [[ -d "$TARGET" ]]; then + while IFS= read -r -d '' file; do + if process_file "$file"; then + converted=$((converted + 1)) + else + failed=$((failed + 1)) + fi + done < <(find "$TARGET" -type f -regextype posix-extended -iregex ".*\.($EXTENSIONS)" -print0 | sort -z) + + if [[ $converted -eq 0 && $failed -eq 0 ]]; then + echo "No matching files found in: $TARGET" + exit 0 + fi +else + echo "Error: Not a file or directory: $TARGET" >&2 + exit 1 +fi + +echo "" +echo "Summary: $converted processed, $failed failed" diff --git a/seed/templates/standalone.sh b/seed/templates/standalone.sh new file mode 100644 index 00000000..e6e3b6e9 --- /dev/null +++ b/seed/templates/standalone.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ── Standalone Template ── +# For tasks that don't operate on a file/directory target. +# All inputs are available as INPUT_ environment variables. +# +# Slot to fill before this template: +# main() { ... } — the task logic, using $INPUT_* env vars + +# ── Execution ── +main