Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ The Go toolchain version comes from `go.mod` (`go 1.26.3`). Tests run with
`TestFallbackDoesNotWalkUp`) for a `Taskfile.yml`, `mise.toml`, or
`Makefile` whose runner is on `PATH`, and shells out. Order is fixed by `foreignRunners`
and the `make` arm intentionally drops `default` and skips the `--`
separator (since `make` doesn't understand it). Tests stub the package-
separator (since `make` doesn't understand it). `--list` follows the
same path via `tryForeignListFallback`, delegating to the runner's
native listing (`task --list`, `mise tasks ls`); runners without a
native listing (e.g. `make`) are skipped. Tests stub the package-
level `fallbackLookPath` / `fallbackRun` hooks rather than the real exec.
- **Internal tasks** — names whose local segment starts with `_`
(e.g. `_helper`, `cli:_fmt`) are excluded from `--list` and `--complete`
Expand Down
37 changes: 33 additions & 4 deletions fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,29 @@ var (
)

// foreignRunners lists, in priority order, the foreign task files we know
// how to delegate to.
// how to delegate to. `listArgs` is the runner's native `--list`
// invocation; leave it nil when the runner has no equivalent.
var foreignRunners = []struct {
bin string
files []string
build func(tasks, cliArgs []string) []string
bin string
files []string
build func(tasks, cliArgs []string) []string
listArgs []string
}{
{
bin: "task",
files: []string{"Taskfile.yml", "taskfile.yml", "Taskfile.yaml", "taskfile.yaml"},
build: func(tasks, cliArgs []string) []string {
return foreignArgs(nil, tasks, cliArgs)
},
listArgs: []string{"--list"},
},
{
bin: "mise",
files: []string{"mise.toml"},
build: func(tasks, cliArgs []string) []string {
return foreignArgs([]string{"run"}, tasks, cliArgs)
},
listArgs: []string{"tasks", "ls"},
},
{
// `make` doesn't understand a `--` separator, so trailing args are
Expand Down Expand Up @@ -109,3 +113,28 @@ func (a *App) tryForeignFallback(ctx context.Context, parsed *args) (bool, error
}
return false, nil
}

// tryForeignListFallback delegates `--list` to a colocated foreign task
// file's runner. Same cwd-only stance as tryForeignFallback; runners
// without a native listing command (listArgs == nil) are skipped.
func (a *App) tryForeignListFallback(ctx context.Context) (bool, error) {
dir, err := a.Getwd()
if err != nil {
return false, nil
}

for _, r := range foreignRunners {
if r.listArgs == nil {
continue
}
if _, err := fallbackLookPath(r.bin); err != nil {
continue
}
for _, name := range r.files {
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
return true, fallbackRun(ctx, r.bin, r.listArgs, dir, a)
}
}
}
return false, nil
}
66 changes: 66 additions & 0 deletions fallback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,72 @@ tasks:
assert.False(t, fr.called)
}

func TestFallbackListRunsTaskWhenTaskfilePresent(t *testing.T) {
// `gogo --list` with a Taskfile but no gogo.yaml delegates to
// `task --list` so users get one listing UX across both tools.
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte("version: '3'\n"), 0o644))

fr := &fakeRun{}
withForeignHooks(t, allFound, fr.run)

app, _, _ := newTestApp(t, dir, "--list")
require.NoError(t, app.Run(t.Context()))
assert.Equal(t, "task", fr.name)
assert.Equal(t, []string{"--list"}, fr.argv)
assert.Equal(t, dir, fr.dir)
}

func TestFallbackListRunsMise(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "mise.toml"), []byte("[tasks.build]\nrun='true'\n"), 0o644))

fr := &fakeRun{}
withForeignHooks(t, allFound, fr.run)

app, _, _ := newTestApp(t, dir, "--list")
require.NoError(t, app.Run(t.Context()))
assert.Equal(t, "mise", fr.name)
assert.Equal(t, []string{"tasks", "ls"}, fr.argv)
}

func TestFallbackListSkipsMakefile(t *testing.T) {
// `make` has no native task listing, so `gogo --list` must surface
// the original "no gogo.yaml" error rather than shelling out.
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "Makefile"), []byte("build:\n\t@true\n"), 0o644))

fr := &fakeRun{}
withForeignHooks(t, allFound, fr.run)

app, _, _ := newTestApp(t, dir, "--list")
err := app.Run(t.Context())
require.Error(t, err)
assert.Contains(t, err.Error(), "no gogo.yaml")
assert.False(t, fr.called)
}

func TestFallbackListGogoYamlWins(t *testing.T) {
// A real gogo.yaml is listed by gogo itself even when a Taskfile
// sits alongside it.
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "gogo.yaml"), []byte(`version: "1"
tasks:
# Build the project
build:
cmd: go build
`), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte("version: '3'\n"), 0o644))

fr := &fakeRun{}
withForeignHooks(t, allFound, fr.run)

app, stdout, _ := newTestApp(t, dir, "--list")
require.NoError(t, app.Run(t.Context()))
assert.False(t, fr.called)
assert.Contains(t, stdout.String(), "Build the project")
}

func TestFallbackPropagatesError(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte("version: '3'\n"), 0o644))
Expand Down
10 changes: 8 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func (a *App) Run(ctx context.Context) error {
}

if parsed.List {
return a.listTasks()
return a.listTasks(ctx)
}

dir, tf, err := a.loadConfig()
Expand Down Expand Up @@ -311,9 +311,15 @@ func (a *App) printTaskNames() {
}

// listTasks renders the colorized, column-aligned task index for `--list`.
func (a *App) listTasks() error {
// When no gogo.yaml is found, listing is delegated to a colocated foreign
// task file's runner (e.g. `task --list`) so users don't have to learn a
// second listing flag per tool.
func (a *App) listTasks(ctx context.Context) error {
_, tf, err := a.loadConfig()
if err != nil {
if handled, fbErr := a.tryForeignListFallback(ctx); handled {
return fbErr
}
return err
}
writeTaskListings(a.Stdout, gatherTaskListings(tf))
Expand Down
Loading