add script task templates and refactor convert-to-mp3 to use template pattern
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Accepts path as $1 or $INPUT_FILE_PATH
|
# ── Task: Convert To MP3 ──
|
||||||
TARGET="${1:-${INPUT_FILE_PATH:-}}"
|
|
||||||
|
|
||||||
if [[ -z "$TARGET" ]]; then
|
EXTENSIONS="flac|wav|ogg|wma|aac|m4a|opus|aiff|aif|ape|wv|alac|dsf|dff"
|
||||||
echo "Error: No file or directory path provided" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -e "$TARGET" ]]; then
|
process_file() {
|
||||||
echo "Error: Path does not exist: $TARGET" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
convert_file() {
|
|
||||||
local input="$1"
|
local input="$1"
|
||||||
local ext="${input##*.}"
|
local ext="${input##*.}"
|
||||||
local dir
|
local dir
|
||||||
@@ -34,37 +25,48 @@ convert_file() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Converting: $input → $output"
|
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 ffmpeg -i "$input" -codec:a libmp3lame -b:a 320k -map_metadata 0 -id3v2_version 3 -y "$output" 2>/dev/null; then
|
||||||
|
echo " Done"
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
echo " ✓ Done"
|
|
||||||
else
|
else
|
||||||
echo " ✗ Failed" >&2
|
echo " Failed" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
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
|
converted=0
|
||||||
failed=0
|
failed=0
|
||||||
|
|
||||||
if [[ -f "$TARGET" ]]; then
|
if [[ -f "$TARGET" ]]; then
|
||||||
if convert_file "$TARGET"; then
|
if process_file "$TARGET"; then
|
||||||
converted=$((converted + 1))
|
converted=$((converted + 1))
|
||||||
else
|
else
|
||||||
failed=$((failed + 1))
|
failed=$((failed + 1))
|
||||||
fi
|
fi
|
||||||
elif [[ -d "$TARGET" ]]; then
|
elif [[ -d "$TARGET" ]]; then
|
||||||
while IFS= read -r -d '' file; do
|
while IFS= read -r -d '' file; do
|
||||||
if convert_file "$file"; then
|
if process_file "$file"; then
|
||||||
converted=$((converted + 1))
|
converted=$((converted + 1))
|
||||||
else
|
else
|
||||||
failed=$((failed + 1))
|
failed=$((failed + 1))
|
||||||
fi
|
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
|
if [[ $converted -eq 0 && $failed -eq 0 ]]; then
|
||||||
echo "No audio files found in: $TARGET"
|
echo "No matching files found in: $TARGET"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -73,4 +75,4 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Summary: $converted converted, $failed failed"
|
echo "Summary: $converted processed, $failed failed"
|
||||||
|
|||||||
@@ -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_<NAME>` (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: <Task Name> ──
|
||||||
|
|
||||||
|
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: <Task Name> ──
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -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"
|
||||||
@@ -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_<NAME> environment variables.
|
||||||
|
#
|
||||||
|
# Slot to fill before this template:
|
||||||
|
# main() { ... } — the task logic, using $INPUT_* env vars
|
||||||
|
|
||||||
|
# ── Execution ──
|
||||||
|
main
|
||||||
Reference in New Issue
Block a user