Compare commits

..

2 Commits

Author SHA1 Message Date
Valentin Maerten
f9f2ecb8be chore: changelog for completion engine 2026-06-29 17:38:24 +02:00
Valentin Maerten
46201bcac9 feat(completion): unify shell wrappers behind task __complete 2026-06-29 17:38:24 +02:00
30 changed files with 1091 additions and 754 deletions

View File

@@ -19,21 +19,21 @@ jobs:
go-version: [1.25.10, 1.26.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: golangci-lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
with:
version: v2.12.2
lint-jsonschema:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14

View File

@@ -18,7 +18,7 @@ jobs:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: 1.26.x

View File

@@ -19,7 +19,7 @@ jobs:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: 1.26.x

View File

@@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{matrix.go-version}}

View File

@@ -10,11 +10,11 @@
by @Legimity).
- PowerShell completions now work with aliases of the `task` command, not just
the `task` binary itself (#2852 by @kojiishi).
- Fixed task and namespace aliases not being completed by the Zsh completion. A
`show-aliases` zstyle can turn this off (#2865, #2864 by @vmaerten).
- Fixed task and namespace aliases not being completed by the Zsh completion.
A `show-aliases` zstyle can turn this off (#2865, #2864 by @vmaerten).
- Fixed task names containing certain characters (e.g. `\`, `_`, `^`) leaking
into checksum/timestamp filenames, breaking `sources:`/`generates:` up-to-date
detection (#2886 by @s3onghyun).
into checksum/timestamp filenames, breaking `sources:`/`generates:`
up-to-date detection (#2886 by @s3onghyun).
- Fixed `for: matrix:` loops using `ref:` rows producing wrong values when the
same task was run concurrently (e.g. by parallel `deps`) with different vars
(#2890, #2894 by @amitmishra11).
@@ -23,15 +23,16 @@
- Added the `use_gitignore` setting (global or per-task) to skip files matched
by your `.gitignore` when fingerprinting `sources`/`generates` and when
watching (#2773 by @vmaerten).
- Added support for configuring output flags (`--output`,
`--output-group-begin`, `--output-group-end`, `--output-group-error-only`) via
the `TASK_OUTPUT*` environment variables (#2873 by @liiight).
- Added support for configuring output flags (`--output`, `--output-group-begin`,
`--output-group-end`, `--output-group-error-only`) via the `TASK_OUTPUT*`
environment variables (#2873 by @liiight).
- Added a `--temp-dir` flag (with `TASK_TEMP_DIR` env var and `temp-dir` taskrc
config) to customise the directory where Task stores temporary files such as
checksums. Relative paths are resolved against the root Taskfile (#2891 by
@kjasn).
- Defined environment variable behavior for remote taskfiles (#2267, #2847 by
@vmaerten).
- Unified Bash, Fish, Zsh and PowerShell completions behind a single `task
__complete` engine, so every shell offers the same suggestions: task names,
aliases, flags, flag values and per-task CLI variables (#2897 by @vmaerten).
## v3.51.1 - 2026-05-16

47
cmd/task/complete_cmd.go Normal file
View File

@@ -0,0 +1,47 @@
package main
import (
"io"
"os"
"github.com/spf13/pflag"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/complete"
)
func runComplete(args []string) error {
dir, entrypoint, global := extractTaskfileFlags(args)
e := task.NewExecutor(
task.WithDir(dir),
task.WithEntrypoint(entrypoint),
task.WithStdout(io.Discard),
task.WithStderr(io.Discard),
task.WithVersionCheck(false),
)
if global {
if home, err := os.UserHomeDir(); err == nil {
e.Options(task.WithDir(home))
}
}
// Best-effort: a missing or broken Taskfile must not break completion.
_ = e.Setup()
suggs, dirv := complete.Complete(e, pflag.CommandLine, args)
complete.Write(os.Stdout, suggs, dirv)
return nil
}
func extractTaskfileFlags(args []string) (dir, entrypoint string, global bool) {
fs := pflag.NewFlagSet("complete", pflag.ContinueOnError)
fs.SetOutput(io.Discard)
fs.ParseErrorsAllowlist.UnknownFlags = true
fs.Usage = func() {}
fs.StringVarP(&dir, "dir", "d", "", "")
fs.StringVarP(&entrypoint, "taskfile", "t", "", "")
fs.BoolVarP(&global, "global", "g", false, "")
_ = fs.Parse(args)
return
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/complete"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/flags"
"github.com/go-task/task/v3/internal/logger"
@@ -58,6 +59,12 @@ func emitCIErrorAnnotation(err error) {
}
func run() error {
// Dispatched before flag validation: the args after __complete are the
// user's command line, not Task's own flags.
if complete.IsActive() {
return runComplete(os.Args[2:])
}
log := &logger.Logger{
Stdout: os.Stdout,
Stderr: os.Stderr,

View File

@@ -15,7 +15,6 @@ import (
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -207,19 +206,10 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, e
// Use filepath.ToSlash for all paths to ensure consistent forward slashes
// across platforms. This prevents issues with backslashes being interpreted
// as escape sequences when paths are used in shell commands on Windows.
var rootTaskfile, rootDir string
if taskfile.IsRemoteEntrypoint(c.Entrypoint) {
rootTaskfile = c.Entrypoint
rootDir = ""
} else {
rootTaskfile = filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint))
rootDir = filepath.ToSlash(c.Dir)
}
allVars := map[string]string{
"TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": rootTaskfile,
"ROOT_DIR": rootDir,
"ROOT_TASKFILE": filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint)),
"ROOT_DIR": filepath.ToSlash(c.Dir),
"USER_WORKING_DIR": filepath.ToSlash(c.UserWorkingDir),
"TASK_VERSION": version.GetVersion(),
"PATH_LIST_SEPARATOR": string(os.PathListSeparator),
@@ -227,22 +217,9 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, e
}
if t != nil {
allVars["TASK"] = t.Task
if taskfile.IsRemoteEntrypoint(t.Location.Taskfile) {
allVars["TASKFILE"] = t.Location.Taskfile
allVars["TASKFILE_DIR"] = ""
switch {
case t.Dir == "":
allVars["TASK_DIR"] = filepath.ToSlash(c.UserWorkingDir)
case filepath.IsAbs(t.Dir):
allVars["TASK_DIR"] = filepath.ToSlash(t.Dir)
default:
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.UserWorkingDir, t.Dir))
}
} else {
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir))
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
}
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir))
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
} else {
allVars["TASK"] = ""
allVars["TASK_DIR"] = ""

View File

@@ -1,135 +0,0 @@
package task
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile/ast"
)
func TestGetSpecialVarsRemote(t *testing.T) {
t.Parallel()
uwd := t.TempDir()
uwdSlash := filepath.ToSlash(uwd)
localProj := filepath.Join(uwd, "proj")
localProjSlash := filepath.ToSlash(localProj)
localTaskfile := filepath.Join(localProj, "Taskfile.yml")
localTaskfileSlash := filepath.ToSlash(localTaskfile)
absTaskDir := filepath.Join(uwd, "opt", "work")
absTaskDirSlash := filepath.ToSlash(absTaskDir)
tests := []struct {
name string
entrypoint string
compilerDir string
taskDir string
taskfileLocation string
wantRootTaskfile string
wantRootDir string
wantTaskfile string
wantTaskfileDir string
wantTaskDir string
}{
{
name: "local entrypoint, local task",
entrypoint: localTaskfile,
compilerDir: localProj,
taskDir: "",
taskfileLocation: localTaskfile,
wantRootTaskfile: localTaskfileSlash,
wantRootDir: localProjSlash,
wantTaskfile: localTaskfileSlash,
wantTaskfileDir: localProjSlash,
wantTaskDir: localProjSlash,
},
{
name: "https entrypoint, empty task.dir",
entrypoint: "https://taskfile.dev/Taskfile.yml",
compilerDir: "",
taskDir: "",
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
wantRootDir: "",
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
wantTaskfileDir: "",
wantTaskDir: uwdSlash,
},
{
name: "https entrypoint, relative task.dir",
entrypoint: "https://taskfile.dev/Taskfile.yml",
compilerDir: "",
taskDir: "subdir",
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
wantRootDir: "",
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
wantTaskfileDir: "",
wantTaskDir: filepath.ToSlash(filepathext.SmartJoin(uwd, "subdir")),
},
{
name: "https entrypoint, absolute task.dir",
entrypoint: "https://taskfile.dev/Taskfile.yml",
compilerDir: "",
taskDir: absTaskDir,
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
wantRootDir: "",
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
wantTaskfileDir: "",
wantTaskDir: absTaskDirSlash,
},
{
name: "git entrypoint",
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
compilerDir: "",
taskDir: "",
taskfileLocation: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
wantRootTaskfile: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
wantRootDir: "",
wantTaskfile: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
wantTaskfileDir: "",
wantTaskDir: uwdSlash,
},
{
name: "local root, remote included task",
entrypoint: localTaskfile,
compilerDir: localProj,
taskDir: "",
taskfileLocation: "https://taskfile.dev/included.yml",
wantRootTaskfile: localTaskfileSlash,
wantRootDir: localProjSlash,
wantTaskfile: "https://taskfile.dev/included.yml",
wantTaskfileDir: "",
wantTaskDir: uwdSlash,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
c := &Compiler{
Dir: tt.compilerDir,
Entrypoint: tt.entrypoint,
UserWorkingDir: uwd,
}
task := &ast.Task{
Task: "mytask",
Dir: tt.taskDir,
Location: &ast.Location{Taskfile: tt.taskfileLocation},
}
vars, err := c.getSpecialVars(task, nil)
assert.NoError(t, err)
assert.Equal(t, tt.wantRootTaskfile, vars["ROOT_TASKFILE"], "ROOT_TASKFILE")
assert.Equal(t, tt.wantRootDir, vars["ROOT_DIR"], "ROOT_DIR")
assert.Equal(t, tt.wantTaskfile, vars["TASKFILE"], "TASKFILE")
assert.Equal(t, tt.wantTaskfileDir, vars["TASKFILE_DIR"], "TASKFILE_DIR")
assert.Equal(t, tt.wantTaskDir, vars["TASK_DIR"], "TASK_DIR")
})
}
}

View File

@@ -1,60 +1,69 @@
# vim: set tabstop=2 shiftwidth=2 expandtab:
#
# Thin wrapper around `task __complete`. All suggestion logic lives in the
# Go engine — do not add completion logic here.
_GO_TASK_COMPLETION_LIST_OPTION='--list-all'
TASK_CMD="${TASK_EXE:-task}"
function _task()
{
_task() {
local cur prev words cword
_init_completion -n : || return
# Check for `--` within command-line and quit or strip suffix.
local i
for i in "${!words[@]}"; do
if [ "${words[$i]}" == "--" ]; then
# Do not complete words following `--` passed to CLI_ARGS.
[ $cword -gt $i ] && return
# Remove the words following `--` to not put --list in CLI_ARGS.
words=( "${words[@]:0:$i}" )
break
fi
local -a args=()
if (( cword > 0 )); then
args=( "${words[@]:1:cword}" )
fi
if (( ${#args[@]} == 0 )); then
args=( "" )
fi
local output
output=$("$TASK_CMD" __complete "${args[@]}" 2>/dev/null)
if [[ -z "$output" ]]; then
_filedir
return
fi
local -a lines=()
local line
while IFS= read -r line; do
lines+=( "$line" )
done <<< "$output"
local last_idx=$(( ${#lines[@]} - 1 ))
local directive="${lines[$last_idx]#:}"
unset 'lines[$last_idx]'
if (( directive & 8 )); then
local exts=""
for line in "${lines[@]}"; do
exts+="${exts:+|}$line"
done
_filedir "@($exts)"
return
fi
if (( directive & 16 )); then
_filedir -d
return
fi
local -a values=()
for line in "${lines[@]}"; do
values+=( "${line%%$'\t'*}" )
done
# Handle special arguments of options.
case "$prev" in
-d|--dir|--remote-cache-dir)
_filedir -d
return $?
;;
--cacert|--cert|--cert-key)
_filedir
return $?
;;
-t|--taskfile)
_filedir yaml || return $?
_filedir yml
return $?
;;
-o|--output)
COMPREPLY=( $( compgen -W "interleaved group prefixed" -- $cur ) )
return 0
;;
esac
COMPREPLY=( $( compgen -W "${values[*]}" -- "$cur" ) )
# Handle normal options.
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "$(_parse_help $1)" -- $cur ) )
return 0
;;
esac
if (( directive & 2 )); then
compopt -o nospace 2>/dev/null
fi
# Prepare task name completions.
local tasks=( $( "${words[@]}" --silent $_GO_TASK_COMPLETION_LIST_OPTION 2> /dev/null ) )
COMPREPLY=( $( compgen -W "${tasks[*]}" -- "$cur" ) )
# Post-process because task names might contain colons.
__ltrim_colon_completions "$cur"
if (( ${#COMPREPLY[@]} == 0 )) && ! (( directive & 4 )); then
_filedir
fi
}
complete -F _task "$TASK_CMD"

View File

@@ -1,120 +1,46 @@
# Thin wrapper around `task __complete`. All suggestion logic lives in the
# Go engine — do not add completion logic here.
set -l GO_TASK_PROGNAME (if set -q GO_TASK_PROGNAME; echo $GO_TASK_PROGNAME; else if set -q TASK_EXE; echo $TASK_EXE; else; echo task; end)
# Cache variables for experiments (global)
set -g __task_experiments_cache ""
set -g __task_experiments_cache_time 0
function __task_complete --inherit-variable GO_TASK_PROGNAME
set -l tokens (commandline -opc)
set -l current (commandline -ct)
set -l args
if test (count $tokens) -gt 1
set args $tokens[2..-1]
end
set args $args $current
# Helper function to get experiments with 1-second cache
function __task_get_experiments --inherit-variable GO_TASK_PROGNAME
set -l now (date +%s)
set -l ttl 1 # Cache for 1 second only
# Return cached value if still valid
if test (math "$now - $__task_experiments_cache_time") -lt $ttl
printf '%s\n' $__task_experiments_cache
set -l output ($GO_TASK_PROGNAME __complete $args 2>/dev/null)
set -l count (count $output)
if test $count -eq 0
return
end
# Refresh cache
set -g __task_experiments_cache ($GO_TASK_PROGNAME --experiments 2>/dev/null)
set -g __task_experiments_cache_time $now
printf '%s\n' $__task_experiments_cache
end
# Helper function to check if an experiment is enabled
function __task_is_experiment_enabled
set -l experiment $argv[1]
__task_get_experiments | string match -qr "^\* $experiment:.*on"
end
function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME
# Check if the global task is requested
set -l global_task false
commandline --current-process | read --tokenize --list --local cmd_args
for arg in $cmd_args
if test "_$arg" = "_--"
break # ignore arguments to be passed to the task
set -l last $output[$count]
if not string match -q ':*' -- $last
# Protocol violation: emit raw lines as a fallback.
for line in $output
echo $line
end
return
end
if test "_$arg" = "_--global" -o "_$arg" = "_-g"
set global_task true
break
set -l directive (string replace -r '^:' '' -- $last)
# FilterFileExt / FilterDirs are handled by fish's native file completion
# via the separate `complete` registrations below.
if test (math "$directive & 8") -ne 0; or test (math "$directive & 16") -ne 0
return
end
end
# Read the list of tasks (and potential errors)
if $global_task
$GO_TASK_PROGNAME --global --list-all
else
$GO_TASK_PROGNAME --list-all
end 2>&1 | read -lz rawOutput
# Return on non-zero exit code (for cases when there is no Taskfile found or etc.)
if test $status -ne 0
return
end
# Grab names and descriptions (if any) of the tasks
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):[[:space:]]\{2,\}\(.*\)[[:space:]]\{2,\}(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):[[:space:]]\{2,\}\(.*\)/\1\t\2/'| string split0)
if test $output
echo $output
end
if test $count -gt 1
for line in $output[1..(math $count - 1)]
echo $line
end
end
end
complete -c $GO_TASK_PROGNAME \
-d 'Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was specified.' \
-xa "(__task_get_tasks)" \
-n "not __fish_seen_subcommand_from --"
# Standard flags
complete -c $GO_TASK_PROGNAME -s a -l list-all -d 'list all tasks'
complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)'
complete -c $GO_TASK_PROGNAME -s C -l concurrency -d 'limit number of concurrent tasks'
complete -c $GO_TASK_PROGNAME -l completion -d 'generate shell completion script' -xa "bash zsh fish powershell"
complete -c $GO_TASK_PROGNAME -s d -l dir -d 'set directory of execution'
complete -c $GO_TASK_PROGNAME -l disable-fuzzy -d 'disable fuzzy matching for task names'
complete -c $GO_TASK_PROGNAME -s n -l dry -d 'compile and print tasks without executing'
complete -c $GO_TASK_PROGNAME -s x -l exit-code -d 'pass-through exit code of task command'
complete -c $GO_TASK_PROGNAME -l experiments -d 'list available experiments'
complete -c $GO_TASK_PROGNAME -s F -l failfast -d 'when running tasks in parallel, stop all tasks if one fails'
complete -c $GO_TASK_PROGNAME -s f -l force -d 'force execution even when up-to-date'
complete -c $GO_TASK_PROGNAME -s g -l global -d 'run global Taskfile from home directory'
complete -c $GO_TASK_PROGNAME -s h -l help -d 'show help'
complete -c $GO_TASK_PROGNAME -s i -l init -d 'create new Taskfile'
complete -c $GO_TASK_PROGNAME -l insecure -d 'allow insecure Taskfile downloads'
complete -c $GO_TASK_PROGNAME -s I -l interval -d 'interval to watch for changes'
complete -c $GO_TASK_PROGNAME -s j -l json -d 'format task list as JSON'
complete -c $GO_TASK_PROGNAME -s l -l list -d 'list tasks with descriptions'
complete -c $GO_TASK_PROGNAME -l nested -d 'nest namespaces when listing as JSON'
complete -c $GO_TASK_PROGNAME -l no-status -d 'ignore status when listing as JSON'
complete -c $GO_TASK_PROGNAME -l interactive -d 'prompt for missing required variables'
complete -c $GO_TASK_PROGNAME -s o -l output -d 'set output style' -xa "interleaved group prefixed"
complete -c $GO_TASK_PROGNAME -l output-group-begin -d 'message template before grouped output'
complete -c $GO_TASK_PROGNAME -l output-group-end -d 'message template after grouped output'
complete -c $GO_TASK_PROGNAME -l output-group-error-only -d 'hide output from successful tasks'
complete -c $GO_TASK_PROGNAME -s p -l parallel -d 'execute tasks in parallel'
complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disable echoing'
complete -c $GO_TASK_PROGNAME -l sort -d 'set task sorting order' -xa "default alphanumeric none"
complete -c $GO_TASK_PROGNAME -l status -d 'exit non-zero if tasks not up-to-date'
complete -c $GO_TASK_PROGNAME -l summary -d 'show task summary'
complete -c $GO_TASK_PROGNAME -s t -l taskfile -d 'choose Taskfile to run'
complete -c $GO_TASK_PROGNAME -s v -l verbose -d 'verbose output'
complete -c $GO_TASK_PROGNAME -l version -d 'show version'
complete -c $GO_TASK_PROGNAME -s w -l watch -d 'watch mode, re-run on changes'
complete -c $GO_TASK_PROGNAME -s y -l yes -d 'assume yes to all prompts'
# Experimental flags (dynamically checked at completion time via -n condition)
# GentleForce experiment
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled GENTLE_FORCE" -l force-all -d 'force execution of task and all dependencies'
# RemoteTaskfiles experiment - Options
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l offline -d 'use only local or cached Taskfiles'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l timeout -d 'timeout for remote Taskfile downloads'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l expiry -d 'cache expiry duration'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa "(__fish_complete_directories)"
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cacert -d 'custom CA certificate for TLS' -r
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert -d 'client certificate for mTLS' -r
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert-key -d 'client certificate private key' -r
# RemoteTaskfiles experiment - Operations
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l download -d 'download remote Taskfile'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l clear-cache -d 'clear remote Taskfile cache'
complete -c $GO_TASK_PROGNAME --no-files -a "(__task_complete)"
complete -c $GO_TASK_PROGNAME -s t -l taskfile -r -k -a "(__fish_complete_suffix .yml .yaml)"
complete -c $GO_TASK_PROGNAME -s d -l dir -xa "(__fish_complete_directories)"

View File

@@ -1,94 +1,61 @@
using namespace System.Management.Automation
# Thin wrapper around `task __complete`. All suggestion logic lives in the
# Go engine — do not add completion logic here.
$cmdNames = @('task') + (Get-Alias -Definition task,task.exe,*\task,*\task.exe -ErrorAction SilentlyContinue).Name | Select-Object -Unique
Register-ArgumentCompleter -CommandName $cmdNames -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
Register-ArgumentCompleter -Native -CommandName $cmdNames -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
if ($commandName.StartsWith('-')) {
$completions = @(
# Standard flags (alphabetical order)
[CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'list all tasks'),
[CompletionResult]::new('--list-all', '--list-all', [CompletionResultType]::ParameterName, 'list all tasks'),
[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'colored output'),
[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'colored output'),
[CompletionResult]::new('-C', '-C', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
[CompletionResult]::new('--concurrency', '--concurrency', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
[CompletionResult]::new('--completion', '--completion', [CompletionResultType]::ParameterName, 'generate shell completion'),
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'set directory'),
[CompletionResult]::new('--dir', '--dir', [CompletionResultType]::ParameterName, 'set directory'),
[CompletionResult]::new('--disable-fuzzy', '--disable-fuzzy', [CompletionResultType]::ParameterName, 'disable fuzzy matching'),
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'dry run'),
[CompletionResult]::new('--dry', '--dry', [CompletionResultType]::ParameterName, 'dry run'),
[CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'pass-through exit code'),
[CompletionResult]::new('--exit-code', '--exit-code', [CompletionResultType]::ParameterName, 'pass-through exit code'),
[CompletionResult]::new('--experiments', '--experiments', [CompletionResultType]::ParameterName, 'list experiments'),
[CompletionResult]::new('-F', '-F', [CompletionResultType]::ParameterName, 'fail fast on pallalel tasks'),
[CompletionResult]::new('--failfast', '--failfast', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('--force', '--force', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('-g', '-g', [CompletionResultType]::ParameterName, 'run global Taskfile'),
[CompletionResult]::new('--global', '--global', [CompletionResultType]::ParameterName, 'run global Taskfile'),
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'show help'),
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'show help'),
[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'create new Taskfile'),
[CompletionResult]::new('--init', '--init', [CompletionResultType]::ParameterName, 'create new Taskfile'),
[CompletionResult]::new('--insecure', '--insecure', [CompletionResultType]::ParameterName, 'allow insecure downloads'),
[CompletionResult]::new('-I', '-I', [CompletionResultType]::ParameterName, 'watch interval'),
[CompletionResult]::new('--interval', '--interval', [CompletionResultType]::ParameterName, 'watch interval'),
[CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'format as JSON'),
[CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, 'format as JSON'),
[CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'list tasks'),
[CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'list tasks'),
[CompletionResult]::new('--nested', '--nested', [CompletionResultType]::ParameterName, 'nest namespaces in JSON'),
[CompletionResult]::new('--no-status', '--no-status', [CompletionResultType]::ParameterName, 'ignore status in JSON'),
[CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'prompt for missing required variables'),
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'set output style'),
[CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'set output style'),
[CompletionResult]::new('--output-group-begin', '--output-group-begin', [CompletionResultType]::ParameterName, 'template before group'),
[CompletionResult]::new('--output-group-end', '--output-group-end', [CompletionResultType]::ParameterName, 'template after group'),
[CompletionResult]::new('--output-group-error-only', '--output-group-error-only', [CompletionResultType]::ParameterName, 'hide successful output'),
[CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'execute in parallel'),
[CompletionResult]::new('--parallel', '--parallel', [CompletionResultType]::ParameterName, 'execute in parallel'),
[CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'silent mode'),
[CompletionResult]::new('--silent', '--silent', [CompletionResultType]::ParameterName, 'silent mode'),
[CompletionResult]::new('--sort', '--sort', [CompletionResultType]::ParameterName, 'task sorting order'),
[CompletionResult]::new('--status', '--status', [CompletionResultType]::ParameterName, 'check task status'),
[CompletionResult]::new('--summary', '--summary', [CompletionResultType]::ParameterName, 'show task summary'),
[CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'choose Taskfile'),
[CompletionResult]::new('--taskfile', '--taskfile', [CompletionResultType]::ParameterName, 'choose Taskfile'),
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'verbose output'),
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'verbose output'),
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'show version'),
[CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'watch mode'),
[CompletionResult]::new('--watch', '--watch', [CompletionResultType]::ParameterName, 'watch mode'),
[CompletionResult]::new('-y', '-y', [CompletionResultType]::ParameterName, 'assume yes'),
[CompletionResult]::new('--yes', '--yes', [CompletionResultType]::ParameterName, 'assume yes')
)
$TaskExe = if ($env:TASK_EXE) { $env:TASK_EXE } else { 'task' }
# Experimental flags (dynamically added based on enabled experiments)
$experiments = & task --experiments 2>$null | Out-String
if ($experiments -match '\* GENTLE_FORCE:.*on') {
$completions += [CompletionResult]::new('--force-all', '--force-all', [CompletionResultType]::ParameterName, 'force all dependencies')
# Words after the program name, truncated to the cursor.
$argsToPass = @()
$elements = $commandAst.CommandElements
if ($elements.Count -gt 1) {
for ($i = 1; $i -lt $elements.Count; $i++) {
$el = $elements[$i]
if ($el.Extent.StartOffset -ge $cursorPosition) { break }
$argsToPass += $el.ToString()
}
if ($experiments -match '\* REMOTE_TASKFILES:.*on') {
# Options
$completions += [CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'use cached Taskfiles')
$completions += [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout')
$completions += [CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry')
$completions += [CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory')
$completions += [CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate')
$completions += [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate')
$completions += [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key')
# Operations
$completions += [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile')
$completions += [CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache')
}
return $completions.Where{ $_.CompletionText.StartsWith($commandName) }
}
# The trailing word (possibly empty) must reach the engine so it knows
# the cursor sits on a fresh word.
if ($argsToPass.Count -gt 0 -and $argsToPass[-1] -eq $wordToComplete) {
$argsToPass[-1] = $wordToComplete
} else {
$argsToPass += $wordToComplete
}
return $(task --list-all --silent) | Where-Object { $_.StartsWith($commandName) } | ForEach-Object { return $_ + " " }
$output = & $TaskExe __complete @argsToPass 2>$null
if (-not $output) { return }
$lines = @($output)
if ($lines.Count -eq 0) { return }
$last = $lines[-1]
if (-not $last.StartsWith(':')) { return }
$directive = [int]($last.Substring(1))
$data = if ($lines.Count -gt 1) { $lines[0..($lines.Count - 2)] } else { @() }
# FilterFileExt
if ($directive -band 8) {
$patterns = $data | ForEach-Object { "*.$_" }
return Get-ChildItem -Path . -Include $patterns -File -ErrorAction SilentlyContinue |
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderItem, $_.Name) }
}
# FilterDirs
if ($directive -band 16) {
return Get-ChildItem -Path . -Directory -ErrorAction SilentlyContinue |
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderContainer, $_.Name) }
}
return $data | ForEach-Object {
$parts = $_ -split "`t", 2
$value = $parts[0]
$desc = if ($parts.Count -gt 1 -and $parts[1]) { $parts[1] } else { $value }
[CompletionResult]::new($value, $value, [CompletionResultType]::ParameterValue, $desc)
}
}

View File

@@ -1,171 +1,65 @@
#compdef task
typeset -A opt_args
#
# Thin wrapper around `task __complete`. All suggestion logic lives in the
# Go engine — do not add completion logic here.
TASK_CMD="${TASK_EXE:-task}"
compdef _task "$TASK_CMD"
_GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}"
# Check if an experiment is enabled
function __task_is_experiment_enabled() {
local experiment=$1
task --experiments 2>/dev/null | grep -q "^\* ${experiment}:.*on"
}
# Listing commands from Taskfile.yml
function __task_list() {
local -a scripts cmd task_aliases match mbegin mend
local -i enabled=0
local taskfile item task desc task_alias
cmd=($TASK_CMD)
taskfile=${(Qv)opt_args[(i)-t|--taskfile]}
taskfile=${taskfile//\~/$HOME}
for arg in "${words[@]:0:$CURRENT}"; do
if [[ "$arg" = "--" ]]; then
# Use default completion for words after `--` as they are CLI_ARGS.
_default
return 0
fi
done
if [[ -n "$taskfile" && -f "$taskfile" ]]; then
cmd+=(--taskfile "$taskfile")
fi
# Check if global flag is set
if (( ${+opt_args[-g]} || ${+opt_args[--global]} )); then
cmd+=(--global)
fi
if output=$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION 2>/dev/null); then
enabled=1
fi
(( enabled )) || return 0
scripts=()
# Read zstyle verbose option (default = true via -T)
local show_desc
zstyle -T ":completion:${curcontext}:" verbose && show_desc=true || show_desc=false
# Read zstyle show-aliases option (default = true via -T)
local show_aliases
zstyle -T ":completion:${curcontext}:" show-aliases && show_aliases=true || show_aliases=false
for item in "${(@)${(f)output}[2,-1]#\* }"; do
task="${item%%:[[:space:]]*}"
# Extract the aliases listed in the trailing "(aliases: a, b)" column.
# NB: `aliases` is a reserved zsh parameter, so use a different name.
task_aliases=()
if [[ "$show_aliases" == "true" && "$item" == (#b)*'(aliases: '(*)')' ]]; then
task_aliases=( "${(@s:, :)match[1]}" )
fi
if [[ "$show_desc" == "true" ]]; then
local desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" )
for task_alias in $task_aliases; do
scripts+=( "${task_alias//:/\\:}:$desc (alias of $task)" )
done
else
scripts+=( "$task" )
for task_alias in $task_aliases; do
scripts+=( "$task_alias" )
done
fi
done
if [[ "$show_desc" == "true" ]]; then
_describe 'Task to run' scripts
else
compadd -Q -a scripts
fi
}
_task() {
local -a standard_args operation_args
local -a args lines completions opts
local output directive line
standard_args=(
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: '
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]'
'(-F --failfast)'{-F,--failfast}'[when running tasks in parallel, stop all tasks if one fails]'
'(-f --force)'{-f,--force}'[run even if task is up-to-date]'
'(-c --color)'{-c,--color}'[colored output]'
'(--completion)--completion[generate shell completion script]:shell:(bash zsh fish powershell)'
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs'
'(--disable-fuzzy)--disable-fuzzy[disable fuzzy matching for task names]'
'(-n --dry)'{-n,--dry}'[compiles and prints tasks without executing]'
'(--dry)--dry[dry-run mode, compile and print tasks only]'
'(-x --exit-code)'{-x,--exit-code}'[pass-through exit code of task command]'
'(--experiments)--experiments[list available experiments]'
'(-g --global)'{-g,--global}'[run global Taskfile from home directory]'
'(--insecure)--insecure[allow insecure Taskfile downloads]'
'(-I --interval)'{-I,--interval}'[interval to watch for changes]:duration: '
'(-j --json)'{-j,--json}'[format task list as JSON]'
'(--nested)--nested[nest namespaces when listing as JSON]'
'(--no-status)--no-status[ignore status when listing as JSON]'
'(--interactive)--interactive[prompt for missing required variables]'
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)'
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: '
'(--output-group-end)--output-group-end[message template after grouped output]:template text: '
'(--output-group-error-only)--output-group-error-only[hide output from successful tasks]'
'(-s --silent)'{-s,--silent}'[disable echoing]'
'(--sort)--sort[set task sorting order]:order:(default alphanumeric none)'
'(--status)--status[exit non-zero if supplied tasks not up-to-date]'
'(--summary)--summary[show summary\: field from tasks instead of running them]'
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files'
'(-v --verbose)'{-v,--verbose}'[verbose mode]'
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]'
'(-y --yes)'{-y,--yes}'[assume yes to all prompts]'
)
# (@) preserves a trailing empty string, which the engine relies on to
# know the cursor is on a fresh word.
args=("${(@)words[2,CURRENT]}")
(( ${#args} == 0 )) && args=("")
# Experimental flags (dynamically added based on enabled experiments)
# Options (modify behavior)
if __task_is_experiment_enabled "GENTLE_FORCE"; then
standard_args+=('(--force-all)--force-all[force execution of task and all dependencies]')
output=$("$TASK_CMD" __complete "${args[@]}" 2>/dev/null)
if [[ -z "$output" ]]; then
_files
return
fi
if __task_is_experiment_enabled "REMOTE_TASKFILES"; then
standard_args+=(
'(--offline --download)--offline[use only local or cached Taskfiles]'
'(--timeout)--timeout[timeout for remote Taskfile downloads]:duration: '
'(--expiry)--expiry[cache expiry duration]:duration: '
'(--remote-cache-dir)--remote-cache-dir[directory to cache remote Taskfiles]:cache dir:_dirs'
'(--cacert)--cacert[custom CA certificate for TLS]:file:_files'
'(--cert)--cert[client certificate for mTLS]:file:_files'
'(--cert-key)--cert-key[client certificate private key]:file:_files'
)
lines=("${(f)output}")
directive="${lines[-1]#:}"
lines=("${(@)lines[1,-2]}")
if (( directive & 8 )); then
local -a globs
for line in "${lines[@]}"; do
globs+=("*.${line}")
done
_files -g "(${(j:|:)globs})"
return
fi
operation_args=(
# Task names completion (can be specified multiple times)
'(operation)*: :__task_list'
# Operational args completion (mutually exclusive)
+ '(operation)'
'(*)'{-l,--list}'[list describable tasks]'
'(*)'{-a,--list-all}'[list all tasks]'
'(*)'{-i,--init}'[create new Taskfile.yml]'
'(- *)'{-h,--help}'[show help]'
'(- *)--version[show version and exit]'
)
# Experimental operations (dynamically added based on enabled experiments)
if __task_is_experiment_enabled "REMOTE_TASKFILES"; then
standard_args+=(
'(--offline --clear-cache)--download[download remote Taskfile]'
)
operation_args+=(
'(* --download)--clear-cache[clear remote Taskfile cache]'
)
if (( directive & 16 )); then
_path_files -/
return
fi
_arguments -S $standard_args $operation_args
# `:` inside the value must be escaped: _describe splits on the first
# unescaped colon (e.g. "docs:serve" would otherwise become value "docs").
local value desc
for line in "${lines[@]}"; do
if [[ "$line" == *$'\t'* ]]; then
value="${line%%$'\t'*}"
desc="${line#*$'\t'}"
completions+=("${value//:/\\:}:$desc")
else
completions+=("${line//:/\\:}")
fi
done
(( directive & 2 )) && opts+=(-S '')
(( directive & 32 )) && opts+=(-V)
if (( ${#completions} > 0 )); then
_describe -t tasks 'task' completions "${opts[@]}"
fi
(( directive & 4 )) && return
_files
}
# don't run the completion function when being source-ed or eval-ed
if [ "$funcstack[1]" = "_task" ]; then
_task "$@"
fi
compdef _task "$TASK_CMD"

View File

@@ -0,0 +1,30 @@
// Package complete implements the `task __complete` protocol consumed by the
// shell completion wrappers. The protocol mirrors cobra v2 so a future
// migration stays cheap.
package complete
import "os"
const CommandName = "__complete"
func IsActive() bool {
return len(os.Args) >= 2 && os.Args[1] == CommandName
}
// Directive mirrors cobra's ShellCompDirective bitfield.
type Directive int
const (
DirectiveDefault Directive = 0
DirectiveError Directive = 1 << 0
DirectiveNoSpace Directive = 1 << 1
DirectiveNoFileComp Directive = 1 << 2
DirectiveFilterFileExt Directive = 1 << 3
DirectiveFilterDirs Directive = 1 << 4
DirectiveKeepOrder Directive = 1 << 5
)
type Suggestion struct {
Value string
Description string
}

View File

@@ -0,0 +1,279 @@
package complete_test
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/complete"
)
func newTestFlagSet() *pflag.FlagSet {
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
var b bool
var s string
fs.BoolVarP(&b, "list-all", "a", false, "Lists all tasks")
fs.BoolVarP(&b, "list", "l", false, "Lists tasks with descriptions")
fs.BoolVarP(&b, "verbose", "v", false, "Verbose mode")
fs.StringVarP(&s, "taskfile", "t", "", "Taskfile path")
fs.StringVarP(&s, "dir", "d", "", "Run dir")
fs.StringVarP(&s, "output", "o", "", "Output style")
fs.StringVar(&s, "sort", "", "Sort order")
fs.StringVar(&s, "cacert", "", "CA cert path")
return fs
}
const testTaskfile = `version: '3'
vars:
ALLOWED_ENVS:
- dev
- staging
- prod
tasks:
deploy:
desc: Deploy the application
aliases: [dep, ship]
requires:
vars:
- name: ENV
enum:
- dev
- staging
- prod
- REGION
cmds:
- 'echo {{.ENV}} {{.REGION}}'
build:
desc: Build it
cmds:
- 'echo build'
dynenum:
desc: Dynamic enum
requires:
vars:
- name: ENV
enum:
ref: .ALLOWED_ENVS
cmds:
- 'echo {{.ENV}}'
docs:serve:
desc: Serve docs locally
cmds:
- 'echo serving'
`
func setupExecutor(t *testing.T) *task.Executor {
t.Helper()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte(testTaskfile), 0o644))
e := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(io.Discard),
task.WithStderr(io.Discard),
task.WithVersionCheck(false),
)
require.NoError(t, e.Setup())
return e
}
func TestComplete_TaskNames(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{""})
require.ElementsMatch(t,
[]string{"build", "deploy", "dep", "ship", "dynenum", "docs:serve"},
values(suggs),
)
require.Equal(t, complete.DirectiveNoFileComp, dir)
require.Contains(t, descriptions(suggs), "Deploy the application")
}
func TestComplete_AliasResolvesToTaskVars(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"dep", ""})
require.Equal(t, []string{"ENV=dev", "ENV=staging", "ENV=prod", "REGION="}, values(suggs))
require.Equal(t, complete.DirectiveNoSpace|complete.DirectiveNoFileComp, dir)
}
func TestComplete_StaticEnum(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"deploy", ""})
require.Equal(t, []string{"ENV=dev", "ENV=staging", "ENV=prod", "REGION="}, values(suggs))
require.Equal(t, complete.DirectiveNoSpace|complete.DirectiveNoFileComp, dir)
}
func TestComplete_EnumRef(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, _ := complete.Complete(e, newTestFlagSet(), []string{"dynenum", ""})
require.Equal(t, []string{"ENV=dev", "ENV=staging", "ENV=prod"}, values(suggs))
}
func TestComplete_NoRequires(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"build", ""})
require.Empty(t, suggs)
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_FlagValueNotConfusedWithTaskName(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--dir", "deploy", ""})
require.ElementsMatch(t,
[]string{"build", "deploy", "dep", "ship", "dynenum", "docs:serve"},
values(suggs),
)
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_NamespacedTaskName(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"docs:serve", ""})
require.Empty(t, suggs)
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_FlagValueInlineEquals(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--output="})
require.Equal(t, []string{"interleaved", "group", "prefixed"}, values(suggs))
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_AfterDash(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"deploy", "--", ""})
require.Empty(t, suggs)
require.Equal(t, complete.DirectiveDefault, dir)
}
func TestComplete_FlagNames(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"-"})
require.NotEmpty(t, suggs)
require.Equal(t, complete.DirectiveNoFileComp, dir)
vals := values(suggs)
require.Contains(t, vals, "--list-all")
require.Contains(t, vals, "--taskfile")
require.Contains(t, vals, "-a")
}
func TestComplete_EnumFlagValue_Output(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--output", ""})
require.Equal(t, []string{"interleaved", "group", "prefixed"}, values(suggs))
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_EnumFlagValue_Sort(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, _ := complete.Complete(e, newTestFlagSet(), []string{"--sort", ""})
require.Equal(t, []string{"default", "alphanumeric", "none"}, values(suggs))
}
func TestComplete_PathFlag_Taskfile(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--taskfile", ""})
require.Equal(t, []string{"yml", "yaml"}, values(suggs))
require.Equal(t, complete.DirectiveFilterFileExt, dir)
}
func TestComplete_PathFlag_Dir(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--dir", ""})
require.Empty(t, suggs)
require.Equal(t, complete.DirectiveFilterDirs, dir)
}
func TestComplete_PathFlag_Cacert(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--cacert", ""})
require.Empty(t, suggs)
require.Equal(t, complete.DirectiveDefault, dir)
}
func TestComplete_NilExecutor(t *testing.T) {
t.Parallel()
suggs, dir := complete.Complete(nil, newTestFlagSet(), []string{"-"})
require.NotEmpty(t, suggs)
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestWrite_Format(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
complete.Write(&buf, []complete.Suggestion{
{Value: "deploy", Description: "Deploy the app"},
{Value: "build"},
}, complete.DirectiveNoSpace|complete.DirectiveNoFileComp)
require.Equal(t, "deploy\tDeploy the app\nbuild\n:6\n", buf.String())
}
func TestWrite_EmptyWithDirective(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
complete.Write(&buf, nil, complete.DirectiveFilterDirs)
require.Equal(t, ":16\n", buf.String())
}
func values(suggs []complete.Suggestion) []string {
out := make([]string, 0, len(suggs))
for _, s := range suggs {
out = append(out, s.Value)
}
return out
}
func descriptions(suggs []complete.Suggestion) []string {
out := make([]string, 0, len(suggs))
for _, s := range suggs {
out = append(out, s.Description)
}
return out
}

View File

@@ -0,0 +1,65 @@
package complete
import (
"strings"
"github.com/spf13/pflag"
)
type completionContext struct {
toComplete string
prev string
taskName string
afterDash bool
}
// parseContext infers the cursor position from args. fs is needed to skip the
// word following a value-taking flag, otherwise `task --dir deploy` would
// mistake "deploy" (the directory) for a task name.
func parseContext(args []string, knownTasks []string, fs *pflag.FlagSet) completionContext {
ctx := completionContext{}
if len(args) == 0 {
return ctx
}
ctx.toComplete = args[len(args)-1]
if len(args) >= 2 {
ctx.prev = args[len(args)-2]
}
known := make(map[string]struct{}, len(knownTasks))
for _, t := range knownTasks {
known[t] = struct{}{}
}
skipNext := false
for _, w := range args[:len(args)-1] {
if skipNext {
skipNext = false
continue
}
if w == "--" {
ctx.afterDash = true
continue
}
if ctx.afterDash {
continue
}
if strings.HasPrefix(w, "-") {
if !strings.Contains(w, "=") {
if f := matchFlagName(fs, w); f != nil && flagTakesValue(f) {
skipNext = true
}
}
continue
}
if strings.Contains(w, "=") {
continue
}
if _, ok := known[w]; ok {
ctx.taskName = w
}
}
return ctx
}

171
internal/complete/engine.go Normal file
View File

@@ -0,0 +1,171 @@
package complete
import (
"strings"
"github.com/spf13/pflag"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast"
)
// Complete is the single entry point used by `task __complete`. e may be nil
// when the Taskfile failed to load; flag completion still works in that case.
func Complete(e *task.Executor, fs *pflag.FlagSet, args []string) ([]Suggestion, Directive) {
knownTasks := taskNames(e)
ctx := parseContext(args, knownTasks, fs)
if ctx.afterDash {
return nil, DirectiveDefault
}
if ctx.prev != "" {
if flag := matchFlagName(fs, ctx.prev); flag != nil && flagTakesValue(flag) {
return completeFlagValue(flag.Name, ctx.toComplete)
}
}
if strings.HasPrefix(ctx.toComplete, "-") {
if eqIdx := strings.Index(ctx.toComplete, "="); eqIdx != -1 {
flagWord := ctx.toComplete[:eqIdx]
partial := ctx.toComplete[eqIdx+1:]
if f := matchFlagName(fs, flagWord); f != nil && flagTakesValue(f) {
return completeFlagValue(f.Name, partial)
}
}
return listFlags(fs), DirectiveNoFileComp
}
if ctx.taskName != "" && e != nil && e.Taskfile != nil {
return completeTaskVars(e, ctx.taskName, ctx.toComplete)
}
return completeTaskNames(e), DirectiveNoFileComp
}
func taskNames(e *task.Executor) []string {
if e == nil || e.Taskfile == nil {
return nil
}
var out []string
for t := range e.Taskfile.Tasks.Values(nil) {
if t.Internal {
continue
}
out = append(out, strings.TrimSuffix(t.Task, ":"))
for _, alias := range t.Aliases {
out = append(out, strings.TrimSuffix(alias, ":"))
}
}
return out
}
func completeTaskNames(e *task.Executor) []Suggestion {
if e == nil || e.Taskfile == nil {
return nil
}
tasks, err := e.GetTaskList(task.FilterOutInternal)
if err != nil {
return nil
}
out := make([]Suggestion, 0, len(tasks))
for _, t := range tasks {
out = append(out, Suggestion{
Value: strings.TrimSuffix(t.Task, ":"),
Description: t.Desc,
})
for _, alias := range t.Aliases {
out = append(out, Suggestion{
Value: strings.TrimSuffix(alias, ":"),
Description: t.Desc,
})
}
}
return out
}
func completeFlagValue(flagName, toComplete string) ([]Suggestion, Directive) {
if dir, ok := flagDirective[flagName]; ok {
switch dir {
case DirectiveFilterFileExt:
suggs := make([]Suggestion, 0, len(taskfileExtensions))
for _, ext := range taskfileExtensions {
suggs = append(suggs, Suggestion{Value: ext})
}
return suggs, DirectiveFilterFileExt
case DirectiveFilterDirs:
return nil, DirectiveFilterDirs
default:
return nil, DirectiveDefault
}
}
if values, ok := flagEnums[flagName]; ok {
out := make([]Suggestion, 0, len(values))
for _, v := range values {
out = append(out, Suggestion{Value: v})
}
_ = toComplete
return out, DirectiveNoFileComp
}
return nil, DirectiveDefault
}
func completeTaskVars(e *task.Executor, taskName, toComplete string) ([]Suggestion, Directive) {
compiled, err := e.FastCompiledTask(&task.Call{Task: taskName})
if err != nil || compiled == nil || compiled.Requires == nil {
return nil, DirectiveNoFileComp
}
cache := &templater.Cache{Vars: compiled.Vars}
out := make([]Suggestion, 0, 8)
for _, v := range compiled.Requires.Vars {
if v == nil || v.Name == "" {
continue
}
values := enumValues(v.Enum, cache)
if len(values) == 0 {
out = append(out, Suggestion{Value: v.Name + "="})
continue
}
for _, val := range values {
out = append(out, Suggestion{Value: v.Name + "=" + val})
}
}
_ = toComplete
if len(out) == 0 {
return nil, DirectiveNoFileComp
}
return out, DirectiveNoSpace | DirectiveNoFileComp
}
func enumValues(enum *ast.Enum, cache *templater.Cache) []string {
if enum == nil {
return nil
}
if len(enum.Value) > 0 {
return enum.Value
}
if enum.Ref == "" {
return nil
}
resolved := templater.ResolveRef(enum.Ref, cache)
if cache.Err() != nil {
return nil
}
arr, ok := resolved.([]any)
if !ok {
return nil
}
out := make([]string, 0, len(arr))
for _, item := range arr {
s, ok := item.(string)
if !ok {
return nil
}
out = append(out, s)
}
return out
}

View File

@@ -0,0 +1,71 @@
package complete
import (
"sort"
"strings"
"github.com/spf13/pflag"
)
// flagEnums lists allowed values for enum-style flags. Keep in sync with the
// help strings in internal/flags/flags.go.
var flagEnums = map[string][]string{
"output": {"interleaved", "group", "prefixed"},
"sort": {"default", "alphanumeric", "none"},
"completion": {"bash", "zsh", "fish", "powershell"},
}
var flagDirective = map[string]Directive{
"taskfile": DirectiveFilterFileExt,
"dir": DirectiveFilterDirs,
"remote-cache-dir": DirectiveFilterDirs,
"cacert": DirectiveDefault,
"cert": DirectiveDefault,
"cert-key": DirectiveDefault,
}
var taskfileExtensions = []string{"yml", "yaml"}
// flagTakesValue is false for boolean switches (NoOptDefVal == "true").
func flagTakesValue(f *pflag.Flag) bool {
return f.NoOptDefVal == ""
}
// listFlags walks fs at call time so experiment-gated flags appear or
// disappear based on the active experiments.
func listFlags(fs *pflag.FlagSet) []Suggestion {
if fs == nil {
return nil
}
out := make([]Suggestion, 0, 64)
fs.VisitAll(func(f *pflag.Flag) {
if f.Hidden || f.Deprecated != "" {
return
}
out = append(out, Suggestion{
Value: "--" + f.Name,
Description: f.Usage,
})
if f.Shorthand != "" {
out = append(out, Suggestion{
Value: "-" + f.Shorthand,
Description: f.Usage,
})
}
})
sort.Slice(out, func(i, j int) bool { return out[i].Value < out[j].Value })
return out
}
func matchFlagName(fs *pflag.FlagSet, word string) *pflag.Flag {
if fs == nil {
return nil
}
switch {
case strings.HasPrefix(word, "--"):
return fs.Lookup(strings.TrimPrefix(word, "--"))
case strings.HasPrefix(word, "-") && len(word) == 2:
return fs.ShorthandLookup(word[1:])
}
return nil
}

View File

@@ -0,0 +1,28 @@
package complete
import (
"fmt"
"io"
"strings"
)
// Write emits the cobra-v2 completion protocol: one `value\tdescription` (or
// bare `value`) per suggestion, followed by a trailing `:<directive>` line
// that shell wrappers split off even when there are zero suggestions.
func Write(w io.Writer, suggs []Suggestion, dir Directive) {
for _, s := range suggs {
value := sanitize(s.Value)
desc := sanitize(s.Description)
if desc == "" {
fmt.Fprintln(w, value)
continue
}
fmt.Fprintf(w, "%s\t%s\n", value, desc)
}
fmt.Fprintf(w, ":%d\n", dir)
}
func sanitize(s string) string {
r := strings.NewReplacer("\n", " ", "\r", " ", "\t", " ")
return r.Replace(s)
}

View File

@@ -13,13 +13,18 @@ type (
}
// Task describes a single task
Task struct {
Name string `json:"name"`
Task string `json:"task"`
Desc string `json:"desc"`
Summary string `json:"summary"`
Aliases []string `json:"aliases"`
UpToDate *bool `json:"up_to_date,omitempty"`
Location *Location `json:"location"`
Name string `json:"name"`
Task string `json:"task"`
Desc string `json:"desc"`
Summary string `json:"summary"`
Aliases []string `json:"aliases"`
UpToDate *bool `json:"up_to_date,omitempty"`
Location *Location `json:"location"`
Requires []RequiredVar `json:"requires,omitempty"`
}
RequiredVar struct {
Name string `json:"name"`
Enum []string `json:"enum,omitempty"`
}
// Location describes a task's location in a taskfile
Location struct {
@@ -45,9 +50,28 @@ func NewTask(task *ast.Task) Task {
Column: task.Location.Column,
Taskfile: task.Location.Taskfile,
},
Requires: newRequiredVars(task.Requires),
}
}
func newRequiredVars(requires *ast.Requires) []RequiredVar {
if requires == nil || len(requires.Vars) == 0 {
return nil
}
out := make([]RequiredVar, 0, len(requires.Vars))
for _, v := range requires.Vars {
if v == nil {
continue
}
rv := RequiredVar{Name: v.Name}
if v.Enum != nil && len(v.Enum.Value) > 0 {
rv.Enum = append([]string{}, v.Enum.Value...)
}
out = append(out, rv)
}
return out
}
func (parent *Namespace) AddNamespace(namespacePath []string, task Task) {
if len(namespacePath) == 0 {
return

View File

@@ -14,6 +14,7 @@ import (
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/complete"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile/ast"
@@ -177,6 +178,13 @@ func init() {
pflag.StringVar(&Cert, "cert", getConfig(config, "REMOTE_CERT", func() *string { return config.Remote.Cert }, ""), "Path to a client certificate for HTTPS connections.")
pflag.StringVar(&CertKey, "cert-key", getConfig(config, "REMOTE_CERT_KEY", func() *string { return config.Remote.CertKey }, ""), "Path to a client certificate key for HTTPS connections.")
}
// In completion mode the user's `--flag` words must reach the engine
// untouched. The BoolVar/StringVar calls above already populated
// pflag.CommandLine, which is all the engine needs.
if complete.IsActive() {
return
}
pflag.Parse()
// Auto-detect color based on environment when not explicitly configured

View File

@@ -2,7 +2,7 @@
# Runtimes
go = "1.26.4"
node = "24"
pnpm = "11.9.0"
pnpm = "11.8.0"
# Dev tools
golangci-lint = "2.12.2"

View File

@@ -73,7 +73,7 @@ func NewNode(
return node, err
}
func IsRemoteEntrypoint(entrypoint string) bool {
func isRemoteEntrypoint(entrypoint string) bool {
scheme, _ := getScheme(entrypoint)
switch scheme {
case "git", "http", "https":

View File

@@ -60,7 +60,7 @@ func (node *FileNode) Read() ([]byte, error) {
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if IsRemoteEntrypoint(entrypoint) {
if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -188,7 +188,7 @@ func (node *GitNode) ReadContext(ctx context.Context) ([]byte, error) {
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if IsRemoteEntrypoint(entrypoint) {
if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -42,7 +42,7 @@ func (node *StdinNode) Read() ([]byte, error) {
func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if IsRemoteEntrypoint(entrypoint) {
if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -23,5 +23,5 @@
"vitepress-plugin-llms": "^1.9.1",
"vue": "^3.5.18"
},
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b"
"packageManager": "pnpm@11.8.0+sha512.c1f5e7c4cb241c8f174b743851d82f42b802324afc8b0f116b96adb15aa06664948dde36960a3ba1079ba5b4b29dd0140135b94b5b5f5263592249d68e555f26"
}

286
website/pnpm-lock.yaml generated
View File

@@ -16,25 +16,25 @@ importers:
version: 24.13.2
netlify-cli:
specifier: ^26.0.0
version: 26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)(supports-color@10.2.2)
version: 26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)
prettier:
specifier: ^3.6.2
version: 3.9.4
version: 3.8.4
vitepress:
specifier: ^1.6.3
version: 1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.16)(search-insights@2.17.3)(typescript@5.9.3)
version: 1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3)
vitepress-plugin-group-icons:
specifier: ^1.6.1
version: 1.7.5(vite@5.4.21(@types/node@24.13.2))
vitepress-plugin-llms:
specifier: ^1.9.1
version: 1.13.2
version: 1.13.1
vitepress-plugin-tabs:
specifier: ^0.9.0
version: 0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.16)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.39(typescript@5.9.3))
version: 0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.38(typescript@5.9.3))
vue:
specifier: ^3.5.18
version: 3.5.39(typescript@5.9.3)
version: 3.5.38(typescript@5.9.3)
packages:
@@ -1480,17 +1480,17 @@ packages:
vite: ^5.0.0 || ^6.0.0
vue: ^3.2.25
'@vue/compiler-core@3.5.39':
resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==}
'@vue/compiler-core@3.5.38':
resolution: {integrity: sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==}
'@vue/compiler-dom@3.5.39':
resolution: {integrity: sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==}
'@vue/compiler-dom@3.5.38':
resolution: {integrity: sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==}
'@vue/compiler-sfc@3.5.39':
resolution: {integrity: sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==}
'@vue/compiler-sfc@3.5.38':
resolution: {integrity: sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==}
'@vue/compiler-ssr@3.5.39':
resolution: {integrity: sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==}
'@vue/compiler-ssr@3.5.38':
resolution: {integrity: sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==}
'@vue/devtools-api@7.7.7':
resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
@@ -1501,25 +1501,25 @@ packages:
'@vue/devtools-shared@7.7.7':
resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
'@vue/reactivity@3.5.39':
resolution: {integrity: sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==}
'@vue/reactivity@3.5.38':
resolution: {integrity: sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==}
'@vue/runtime-core@3.5.39':
resolution: {integrity: sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==}
'@vue/runtime-core@3.5.38':
resolution: {integrity: sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==}
'@vue/runtime-dom@3.5.39':
resolution: {integrity: sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==}
'@vue/runtime-dom@3.5.38':
resolution: {integrity: sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==}
'@vue/server-renderer@3.5.39':
resolution: {integrity: sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==}
'@vue/server-renderer@3.5.38':
resolution: {integrity: sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==}
peerDependencies:
vue: 3.5.39
vue: 3.5.38
'@vue/shared@3.5.18':
resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==}
'@vue/shared@3.5.39':
resolution: {integrity: sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==}
'@vue/shared@3.5.38':
resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==}
'@vueuse/core@12.8.2':
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
@@ -3232,8 +3232,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.15.0:
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
js-yaml@3.14.2:
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
hasBin: true
json-schema-ref-resolver@3.0.0:
@@ -3624,8 +3624,8 @@ packages:
nan@2.27.0:
resolution: {integrity: sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==}
nanoid@3.3.15:
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@@ -4006,8 +4006,8 @@ packages:
peerDependencies:
postcss: ^8.2.9
postcss@8.5.16:
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
postcss@8.5.15:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
@@ -4042,8 +4042,8 @@ packages:
resolution: {integrity: sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==}
engines: {node: '>= 0.6'}
prettier@3.9.4:
resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==}
prettier@3.8.4:
resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==}
engines: {node: '>=14'}
hasBin: true
@@ -4971,8 +4971,8 @@ packages:
vite:
optional: true
vitepress-plugin-llms@1.13.2:
resolution: {integrity: sha512-2O4s0I5pjEZzgnoWgBPCZCyhah9FH5uQB6lGADazMoyF1URJshtG04ZnmX+cbmQmniN3T5JzdJO9B4q8JHDKOQ==}
vitepress-plugin-llms@1.13.1:
resolution: {integrity: sha512-m+rxyghF5INi8hBw0huFPx6+VvaX1tDGvw1H7FdXowaZJ3dcRY5ShgbmK1AQlmeOFMdd16H8WarhSHLPXF/2OA==}
engines: {node: '>=18'}
vitepress-plugin-tabs@0.9.0:
@@ -4993,8 +4993,8 @@ packages:
postcss:
optional: true
vue@3.5.39:
resolution: {integrity: sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==}
vue@3.5.38:
resolution: {integrity: sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@@ -5131,10 +5131,6 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yargs@17.7.3:
resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
engines: {node: '>=12'}
yauzl@2.10.0:
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
@@ -5903,7 +5899,7 @@ snapshots:
semver: 7.8.4
tmp-promise: 3.0.3
'@netlify/dev@4.18.7(rollup@4.46.2)(supports-color@10.2.2)':
'@netlify/dev@4.18.7(rollup@4.46.2)':
dependencies:
'@netlify/ai': 0.4.1
'@netlify/blobs': 10.7.9(supports-color@10.2.2)
@@ -5911,9 +5907,9 @@ snapshots:
'@netlify/database-dev': 0.10.1
'@netlify/dev-utils': 4.4.6
'@netlify/edge-functions-dev': 1.0.20
'@netlify/functions-dev': 1.3.0(rollup@4.46.2)(supports-color@10.2.2)
'@netlify/functions-dev': 1.3.0(rollup@4.46.2)
'@netlify/headers': 2.1.11
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9(supports-color@10.2.2))
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9)
'@netlify/redirects': 3.1.13
'@netlify/runtime': 4.1.25
'@netlify/static': 3.1.10
@@ -5986,7 +5982,7 @@ snapshots:
dependencies:
'@netlify/types': 2.8.0
'@netlify/functions-dev@1.3.0(rollup@4.46.2)(supports-color@10.2.2)':
'@netlify/functions-dev@1.3.0(rollup@4.46.2)':
dependencies:
'@netlify/blobs': 10.7.9(supports-color@10.2.2)
'@netlify/dev-utils': 4.4.6
@@ -5994,7 +5990,7 @@ snapshots:
'@netlify/zip-it-and-ship-it': 14.7.1(rollup@4.46.2)(supports-color@10.2.2)
cron-parser: 4.9.0
decache: 4.6.2
extract-zip: 2.0.1(supports-color@10.2.2)
extract-zip: 2.0.1
is-stream: 4.0.1
jwt-decode: 4.0.0
lambda-local: 2.2.0
@@ -6046,9 +6042,9 @@ snapshots:
dependencies:
'@netlify/headers-parser': 9.0.3
'@netlify/images@1.3.10(@netlify/blobs@10.7.9(supports-color@10.2.2))':
'@netlify/images@1.3.10(@netlify/blobs@10.7.9)':
dependencies:
ipx: 3.1.1(@netlify/blobs@10.7.9(supports-color@10.2.2))
ipx: 3.1.1(@netlify/blobs@10.7.9)
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -6427,7 +6423,7 @@ snapshots:
'@pnpm/network.ca-file': 1.0.2
config-chain: 1.1.13
'@pnpm/tabtab@0.5.4(supports-color@10.2.2)':
'@pnpm/tabtab@0.5.4':
dependencies:
debug: 4.4.3(supports-color@10.2.2)
enquirer: 2.4.1
@@ -6680,40 +6676,40 @@ snapshots:
- rollup
- supports-color
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.39(typescript@5.9.3))':
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.38(typescript@5.9.3))':
dependencies:
vite: 5.4.21(@types/node@24.13.2)
vue: 3.5.39(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
'@vue/compiler-core@3.5.39':
'@vue/compiler-core@3.5.38':
dependencies:
'@babel/parser': 7.29.7
'@vue/shared': 3.5.39
'@vue/shared': 3.5.38
entities: 7.0.1
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-dom@3.5.39':
'@vue/compiler-dom@3.5.38':
dependencies:
'@vue/compiler-core': 3.5.39
'@vue/shared': 3.5.39
'@vue/compiler-core': 3.5.38
'@vue/shared': 3.5.38
'@vue/compiler-sfc@3.5.39':
'@vue/compiler-sfc@3.5.38':
dependencies:
'@babel/parser': 7.29.7
'@vue/compiler-core': 3.5.39
'@vue/compiler-dom': 3.5.39
'@vue/compiler-ssr': 3.5.39
'@vue/shared': 3.5.39
'@vue/compiler-core': 3.5.38
'@vue/compiler-dom': 3.5.38
'@vue/compiler-ssr': 3.5.38
'@vue/shared': 3.5.38
estree-walker: 2.0.2
magic-string: 0.30.21
postcss: 8.5.16
postcss: 8.5.15
source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.39':
'@vue/compiler-ssr@3.5.38':
dependencies:
'@vue/compiler-dom': 3.5.39
'@vue/shared': 3.5.39
'@vue/compiler-dom': 3.5.38
'@vue/shared': 3.5.38
'@vue/devtools-api@7.7.7':
dependencies:
@@ -6733,38 +6729,38 @@ snapshots:
dependencies:
rfdc: 1.4.1
'@vue/reactivity@3.5.39':
'@vue/reactivity@3.5.38':
dependencies:
'@vue/shared': 3.5.39
'@vue/shared': 3.5.38
'@vue/runtime-core@3.5.39':
'@vue/runtime-core@3.5.38':
dependencies:
'@vue/reactivity': 3.5.39
'@vue/shared': 3.5.39
'@vue/reactivity': 3.5.38
'@vue/shared': 3.5.38
'@vue/runtime-dom@3.5.39':
'@vue/runtime-dom@3.5.38':
dependencies:
'@vue/reactivity': 3.5.39
'@vue/runtime-core': 3.5.39
'@vue/shared': 3.5.39
'@vue/reactivity': 3.5.38
'@vue/runtime-core': 3.5.38
'@vue/shared': 3.5.38
csstype: 3.2.3
'@vue/server-renderer@3.5.39(vue@3.5.39(typescript@5.9.3))':
'@vue/server-renderer@3.5.38(vue@3.5.38(typescript@5.9.3))':
dependencies:
'@vue/compiler-ssr': 3.5.39
'@vue/shared': 3.5.39
vue: 3.5.39(typescript@5.9.3)
'@vue/compiler-ssr': 3.5.38
'@vue/shared': 3.5.38
vue: 3.5.38(typescript@5.9.3)
'@vue/shared@3.5.18': {}
'@vue/shared@3.5.39': {}
'@vue/shared@3.5.38': {}
'@vueuse/core@12.8.2(typescript@5.9.3)':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 12.8.2
'@vueuse/shared': 12.8.2(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
transitivePeerDependencies:
- typescript
@@ -6772,7 +6768,7 @@ snapshots:
dependencies:
'@vueuse/core': 12.8.2(typescript@5.9.3)
'@vueuse/shared': 12.8.2(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
optionalDependencies:
focus-trap: 7.6.5
jwt-decode: 4.0.0
@@ -6783,7 +6779,7 @@ snapshots:
'@vueuse/shared@12.8.2(typescript@5.9.3)':
dependencies:
vue: 3.5.39(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
transitivePeerDependencies:
- typescript
@@ -7062,7 +7058,7 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
body-parser@2.3.0(supports-color@10.2.2):
body-parser@2.3.0:
dependencies:
bytes: 3.1.2
content-type: 2.0.0
@@ -7459,11 +7455,11 @@ snapshots:
dependencies:
node-source-walk: 7.0.2
detective-postcss@8.0.4(postcss@8.5.16):
detective-postcss@8.0.4(postcss@8.5.15):
dependencies:
is-url-superb: 4.0.0
postcss: 8.5.16
postcss-values-parser: 6.0.2(postcss@8.5.16)
postcss: 8.5.15
postcss-values-parser: 6.0.2(postcss@8.5.15)
detective-sass@6.0.2:
dependencies:
@@ -7489,7 +7485,7 @@ snapshots:
detective-vue2@2.3.0(supports-color@10.2.2)(typescript@5.9.3):
dependencies:
'@dependents/detective-less': 5.0.3
'@vue/compiler-sfc': 3.5.39
'@vue/compiler-sfc': 3.5.38
detective-es6: 5.0.2
detective-sass: 6.0.2
detective-scss: 5.0.2
@@ -7812,10 +7808,10 @@ snapshots:
dependencies:
on-headers: 1.1.0
express@5.2.1(supports-color@10.2.2):
express@5.2.1:
dependencies:
accepts: 2.0.0
body-parser: 2.3.0(supports-color@10.2.2)
body-parser: 2.3.0
content-disposition: 1.1.0
content-type: 1.0.5
cookie: 0.7.2
@@ -7825,7 +7821,7 @@ snapshots:
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 2.1.1(supports-color@10.2.2)
finalhandler: 2.1.1
fresh: 2.0.0
http-errors: 2.0.1
merge-descriptors: 2.0.0
@@ -7836,8 +7832,8 @@ snapshots:
proxy-addr: 2.0.7
qs: 6.15.2
range-parser: 1.2.1
router: 2.2.0(supports-color@10.2.2)
send: 1.2.1(supports-color@10.2.2)
router: 2.2.0
send: 1.2.1
serve-static: 2.2.1
statuses: 2.0.2
type-is: 2.1.0
@@ -7851,7 +7847,7 @@ snapshots:
extend@3.0.2: {}
extract-zip@2.0.1(supports-color@10.2.2):
extract-zip@2.0.1:
dependencies:
debug: 4.4.3(supports-color@10.2.2)
get-stream: 5.2.0
@@ -7957,7 +7953,7 @@ snapshots:
filter-obj@6.1.0: {}
finalhandler@2.1.1(supports-color@10.2.2):
finalhandler@2.1.1:
dependencies:
debug: 4.4.3(supports-color@10.2.2)
encodeurl: 2.0.0
@@ -7997,7 +7993,7 @@ snapshots:
dependencies:
from2: 2.3.0
follow-redirects@1.16.0(debug@4.4.3(supports-color@10.2.2)):
follow-redirects@1.16.0(debug@4.4.3):
optionalDependencies:
debug: 4.4.3(supports-color@10.2.2)
@@ -8155,7 +8151,7 @@ snapshots:
gray-matter@4.0.3:
dependencies:
js-yaml: 3.15.0
js-yaml: 3.14.2
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
@@ -8250,21 +8246,21 @@ snapshots:
statuses: 2.0.2
toidentifier: 1.0.1
http-proxy-middleware@3.0.7(supports-color@10.2.2):
http-proxy-middleware@3.0.7:
dependencies:
'@types/http-proxy': 1.17.17
debug: 4.4.3(supports-color@10.2.2)
http-proxy: 1.18.1(debug@4.4.3(supports-color@10.2.2))
http-proxy: 1.18.1(debug@4.4.3)
is-glob: 4.0.3
is-plain-object: 5.0.0
micromatch: 4.0.8
transitivePeerDependencies:
- supports-color
http-proxy@1.18.1(debug@4.4.3(supports-color@10.2.2)):
http-proxy@1.18.1(debug@4.4.3):
dependencies:
eventemitter3: 4.0.7
follow-redirects: 1.16.0(debug@4.4.3(supports-color@10.2.2))
follow-redirects: 1.16.0(debug@4.4.3)
requires-port: 1.0.0
transitivePeerDependencies:
- debug
@@ -8278,7 +8274,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@8.0.0(supports-color@10.2.2):
https-proxy-agent@8.0.0:
dependencies:
agent-base: 8.0.0
debug: 4.4.3(supports-color@10.2.2)
@@ -8359,7 +8355,7 @@ snapshots:
ipaddr.js@2.4.0: {}
ipx@3.1.1(@netlify/blobs@10.7.9(supports-color@10.2.2)):
ipx@3.1.1(@netlify/blobs@10.7.9):
dependencies:
'@fastify/accept-negotiator': 2.0.1
citty: 0.1.6
@@ -8375,7 +8371,7 @@ snapshots:
sharp: 0.34.5
svgo: 4.0.1
ufo: 1.6.4
unstorage: 1.17.5(@netlify/blobs@10.7.9(supports-color@10.2.2))
unstorage: 1.17.5(@netlify/blobs@10.7.9)
xss: 1.0.15
transitivePeerDependencies:
- '@azure/app-configuration'
@@ -8602,7 +8598,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.15.0:
js-yaml@3.14.2:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
@@ -9036,7 +9032,7 @@ snapshots:
millify@6.1.0:
dependencies:
yargs: 17.7.3
yargs: 17.7.2
mime-db@1.54.0: {}
@@ -9109,7 +9105,7 @@ snapshots:
nan@2.27.0:
optional: true
nanoid@3.3.15: {}
nanoid@3.3.12: {}
nanospinner@1.2.2:
dependencies:
@@ -9117,7 +9113,7 @@ snapshots:
negotiator@1.0.0: {}
netlify-cli@26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)(supports-color@10.2.2):
netlify-cli@26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2):
dependencies:
'@fastify/static': 9.1.3
'@netlify/ai': 0.4.1
@@ -9126,19 +9122,19 @@ snapshots:
'@netlify/build': 35.15.0(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)
'@netlify/build-info': 10.5.1
'@netlify/config': 24.6.0
'@netlify/dev': 4.18.7(rollup@4.46.2)(supports-color@10.2.2)
'@netlify/dev': 4.18.7(rollup@4.46.2)
'@netlify/dev-utils': 4.4.6
'@netlify/edge-bundler': 14.10.3
'@netlify/edge-functions': 3.0.8
'@netlify/edge-functions-bootstrap': 2.17.1
'@netlify/headers-parser': 9.0.3
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9(supports-color@10.2.2))
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9)
'@netlify/local-functions-proxy': 2.0.3
'@netlify/redirect-parser': 15.0.4
'@netlify/zip-it-and-ship-it': 14.7.1(rollup@4.46.2)(supports-color@10.2.2)
'@octokit/rest': 22.0.1
'@opentelemetry/api': 1.9.1
'@pnpm/tabtab': 0.5.4(supports-color@10.2.2)
'@pnpm/tabtab': 0.5.4
ansi-escapes: 7.3.0
ansi-to-html: 0.7.2
ascii-table: 0.0.9
@@ -9161,7 +9157,7 @@ snapshots:
envinfo: 7.21.0
etag: 1.8.1
execa: 5.1.1
express: 5.2.1(supports-color@10.2.2)
express: 5.2.1
express-logging: 1.1.1
fastest-levenshtein: 1.0.16
fastify: 5.8.5
@@ -9171,9 +9167,9 @@ snapshots:
get-port: 5.1.1
git-repo-info: 2.1.1
gitconfiglocal: 2.1.0
http-proxy: 1.18.1(debug@4.4.3(supports-color@10.2.2))
http-proxy-middleware: 3.0.7(supports-color@10.2.2)
https-proxy-agent: 8.0.0(supports-color@10.2.2)
http-proxy: 1.18.1(debug@4.4.3)
http-proxy-middleware: 3.0.7
https-proxy-agent: 8.0.0
inquirer: 8.2.7(@types/node@24.13.2)
inquirer-autocomplete-prompt: 1.4.0(inquirer@8.2.7(@types/node@24.13.2))
is-docker: 4.0.0
@@ -9608,16 +9604,16 @@ snapshots:
possible-typed-array-names@1.1.0: {}
postcss-values-parser@6.0.2(postcss@8.5.16):
postcss-values-parser@6.0.2(postcss@8.5.15):
dependencies:
color-name: 1.1.4
is-url-superb: 4.0.0
postcss: 8.5.16
postcss: 8.5.15
quote-unquote: 1.0.0
postcss@8.5.16:
postcss@8.5.15:
dependencies:
nanoid: 3.3.15
nanoid: 3.3.12
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -9642,7 +9638,7 @@ snapshots:
detective-amd: 6.1.0
detective-cjs: 6.1.1
detective-es6: 5.0.2
detective-postcss: 8.0.4(postcss@8.5.16)
detective-postcss: 8.0.4(postcss@8.5.15)
detective-sass: 6.0.2
detective-scss: 5.0.2
detective-stylus: 5.0.1
@@ -9650,14 +9646,14 @@ snapshots:
detective-vue2: 2.3.0(supports-color@10.2.2)(typescript@5.9.3)
module-definition: 6.0.2
node-source-walk: 7.0.2
postcss: 8.5.16
postcss: 8.5.15
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
precond@0.2.3: {}
prettier@3.9.4: {}
prettier@3.8.4: {}
pretty-bytes@7.1.0: {}
@@ -9942,7 +9938,7 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.46.2
fsevents: 2.3.3
router@2.2.0(supports-color@10.2.2):
router@2.2.0:
dependencies:
debug: 4.4.3(supports-color@10.2.2)
depd: 2.0.0
@@ -10016,7 +10012,7 @@ snapshots:
semver@7.8.4: {}
send@1.2.1(supports-color@10.2.2):
send@1.2.1:
dependencies:
debug: 4.4.3(supports-color@10.2.2)
encodeurl: 2.0.0
@@ -10037,7 +10033,7 @@ snapshots:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 1.2.1(supports-color@10.2.2)
send: 1.2.1
transitivePeerDependencies:
- supports-color
@@ -10594,7 +10590,7 @@ snapshots:
unpipe@1.0.0: {}
unstorage@1.17.5(@netlify/blobs@10.7.9(supports-color@10.2.2)):
unstorage@1.17.5(@netlify/blobs@10.7.9):
dependencies:
anymatch: 3.1.3
chokidar: 5.0.0
@@ -10660,7 +10656,7 @@ snapshots:
vite@5.4.21(@types/node@24.13.2):
dependencies:
esbuild: 0.21.5
postcss: 8.5.16
postcss: 8.5.15
rollup: 4.46.2
optionalDependencies:
'@types/node': 24.13.2
@@ -10674,7 +10670,7 @@ snapshots:
optionalDependencies:
vite: 5.4.21(@types/node@24.13.2)
vitepress-plugin-llms@1.13.2:
vitepress-plugin-llms@1.13.1:
dependencies:
gray-matter: 4.0.3
markdown-it: 14.2.0
@@ -10693,12 +10689,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
vitepress-plugin-tabs@0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.16)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.39(typescript@5.9.3)):
vitepress-plugin-tabs@0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.38(typescript@5.9.3)):
dependencies:
vitepress: 1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.16)(search-insights@2.17.3)(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
vitepress: 1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.16)(search-insights@2.17.3)(typescript@5.9.3):
vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3):
dependencies:
'@docsearch/css': 3.8.2
'@docsearch/js': 3.8.2(@algolia/client-search@5.35.0)(search-insights@2.17.3)
@@ -10707,7 +10703,7 @@ snapshots:
'@shikijs/transformers': 2.5.0
'@shikijs/types': 2.5.0
'@types/markdown-it': 14.1.2
'@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.39(typescript@5.9.3))
'@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.38(typescript@5.9.3))
'@vue/devtools-api': 7.7.7
'@vue/shared': 3.5.18
'@vueuse/core': 12.8.2(typescript@5.9.3)
@@ -10717,9 +10713,9 @@ snapshots:
minisearch: 7.1.2
shiki: 2.5.0
vite: 5.4.21(@types/node@24.13.2)
vue: 3.5.39(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
optionalDependencies:
postcss: 8.5.16
postcss: 8.5.15
transitivePeerDependencies:
- '@algolia/client-search'
- '@types/node'
@@ -10747,13 +10743,13 @@ snapshots:
- typescript
- universal-cookie
vue@3.5.39(typescript@5.9.3):
vue@3.5.38(typescript@5.9.3):
dependencies:
'@vue/compiler-dom': 3.5.39
'@vue/compiler-sfc': 3.5.39
'@vue/runtime-dom': 3.5.39
'@vue/server-renderer': 3.5.39(vue@3.5.39(typescript@5.9.3))
'@vue/shared': 3.5.39
'@vue/compiler-dom': 3.5.38
'@vue/compiler-sfc': 3.5.38
'@vue/runtime-dom': 3.5.38
'@vue/server-renderer': 3.5.38(vue@3.5.38(typescript@5.9.3))
'@vue/shared': 3.5.38
optionalDependencies:
typescript: 5.9.3
@@ -10916,16 +10912,6 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
yargs@17.7.3:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
yauzl@2.10.0:
dependencies:
buffer-crc32: 0.2.13

View File

@@ -190,21 +190,6 @@ includes:
my-remote-namespace: https://{{.TOKEN}}@raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml
```
## Special Variables
The file-path [special variables](../reference/templating.md#file-paths) behave
differently when a Taskfile is loaded from a remote source, because there is no
local file or directory that corresponds 1:1 to the Taskfile:
| Variable | Value when loaded remotely |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `TASKFILE` / `ROOT_TASKFILE` | The original URL, unchanged |
| `TASKFILE_DIR` / `ROOT_DIR` | Empty string — a directory variable cannot point to a URL |
| `TASK_DIR` | Resolved against `USER_WORKING_DIR` (relative `dir:` → joined with `USER_WORKING_DIR`, empty `dir:` → `USER_WORKING_DIR`, absolute `dir:` → kept as-is) |
If a remote Taskfile includes a local Taskfile (or vice-versa), each variable
reflects the source of the Taskfile it refers to.
## Security
### Automatic checksums

View File

@@ -565,9 +565,6 @@ tasks:
- echo "{{.MULTILINE | catLines}}" # Replace newlines with spaces
```
In pipeline form, `join` receives the list from the left-hand side. The
equivalent non-pipeline form is `{{join " " .WORDS}}`.
#### Shell Argument Parsing
```yaml