Skip to content

Commit 168728b

Browse files
authored
Invoke CLI extension (#889)
<!--- Note to EXTERNAL Contributors --> <!-- Thanks for opening a PR! If it is a significant code change, please **make sure there is an open issue** for this. We work best with you when we have accepted the idea first before you code. --> <!--- For ALL Contributors 👇 --> ## What was changed <!-- Describe what has changed in this PR --> CLI now invokes extensions in accordance with [CLI Extensions proposal](https://github.com/temporalio/proposals/blob/master/cli/cli-extensions.md). Note that the help command integration part is going to be in a follow-up PR. ## Why? Allow Temporal CLI Extensions. ## Checklist <!--- add/delete as needed ---> 1. Closes <!-- add issue number here --> 2. How was this tested: <!--- Please describe how you tested your changes/how we can test them --> 3. Any docs updates needed? <!--- update README if applicable or point out where to update docs.temporal.io -->
1 parent 90bebb4 commit 168728b

6 files changed

Lines changed: 530 additions & 31 deletions

File tree

go.mod

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ require (
2020
go.temporal.io/sdk v1.37.0
2121
go.temporal.io/sdk/contrib/envconfig v0.1.0
2222
go.temporal.io/server v1.29.1
23-
golang.org/x/term v0.32.0
23+
golang.org/x/term v0.38.0
24+
golang.org/x/tools v0.40.0
2425
google.golang.org/grpc v1.72.2
2526
google.golang.org/protobuf v1.36.6
2627
gopkg.in/yaml.v3 v3.0.1
@@ -153,13 +154,14 @@ require (
153154
go.uber.org/mock v0.5.0 // indirect
154155
go.uber.org/multierr v1.11.0 // indirect
155156
go.uber.org/zap v1.27.0 // indirect
156-
golang.org/x/crypto v0.38.0 // indirect
157+
golang.org/x/crypto v0.46.0 // indirect
157158
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
158-
golang.org/x/net v0.40.0 // indirect
159+
golang.org/x/mod v0.31.0 // indirect
160+
golang.org/x/net v0.48.0 // indirect
159161
golang.org/x/oauth2 v0.30.0 // indirect
160-
golang.org/x/sync v0.14.0 // indirect
161-
golang.org/x/sys v0.33.0 // indirect
162-
golang.org/x/text v0.25.0 // indirect
162+
golang.org/x/sync v0.19.0 // indirect
163+
golang.org/x/sys v0.39.0 // indirect
164+
golang.org/x/text v0.32.0 // indirect
163165
golang.org/x/time v0.11.0 // indirect
164166
google.golang.org/api v0.228.0 // indirect
165167
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect

go.sum

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -417,8 +417,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
417417
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
418418
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
419419
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
420-
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
421-
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
420+
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
421+
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
422422
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
423423
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
424424
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -438,8 +438,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
438438
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
439439
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
440440
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
441-
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
442-
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
441+
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
442+
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
443443
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
444444
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
445445
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -451,8 +451,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
451451
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
452452
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
453453
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
454-
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
455-
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
454+
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
455+
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
456456
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
457457
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
458458
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -461,8 +461,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
461461
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
462462
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
463463
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
464-
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
465-
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
464+
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
465+
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
466466
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
467467
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
468468
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -481,24 +481,24 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
481481
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
482482
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
483483
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
484-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
485-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
484+
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
485+
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
486486
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
487487
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
488488
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
489489
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
490490
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
491-
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
492-
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
491+
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
492+
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
493493
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
494494
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
495495
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
496496
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
497497
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
498498
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
499499
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
500-
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
501-
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
500+
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
501+
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
502502
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
503503
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
504504
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -515,8 +515,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
515515
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
516516
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
517517
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
518-
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
519-
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
518+
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
519+
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
520520
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
521521
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
522522
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package temporalcli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os/exec"
7+
"slices"
8+
"strings"
9+
10+
"github.com/spf13/cobra"
11+
"github.com/spf13/pflag"
12+
)
13+
14+
const (
15+
extensionPrefix = "temporal-"
16+
extensionSeparator = "-" // separates command parts in extension name
17+
argDashReplacement = "_" // dashes in args are replaced to avoid ambiguity
18+
)
19+
20+
// cliArgsToParseForExtension lists CLI flags that should be parsed (validated).
21+
var cliArgsToParseForExtension = map[string]bool{
22+
"command-timeout": true,
23+
}
24+
25+
// tryExecuteExtension tries to execute an extension command if the command is not a built-in command.
26+
// It returns an error if the extension command fails, and a boolean indicating whether an extension was executed.
27+
func tryExecuteExtension(cctx *CommandContext, tcmd *TemporalCommand) (error, bool) {
28+
// Find the deepest matching built-in command and remaining args.
29+
foundCmd, remainingArgs, findErr := tcmd.Command.Find(cctx.Options.Args)
30+
31+
// Check if remaining args include positional args (not just flags).
32+
// If not, a built-in command fully handles this - no extension needed.
33+
hasPosArgs := slices.ContainsFunc(remainingArgs, isPosArg)
34+
if findErr == nil && !hasPosArgs {
35+
return nil, false
36+
}
37+
38+
// Group args into these lists:
39+
// - cliParseArgs: args to validate (subset of cliPassArgs)
40+
// - cliPassArgs: known CLI args to pass to extension
41+
// - extArgs: args to pass to extension and use for extension lookup
42+
cliParseArgs, cliPassArgs, extArgs := groupArgs(foundCmd, remainingArgs)
43+
44+
// Search for an extension executable.
45+
cmdPrefix := strings.Split(foundCmd.CommandPath(), " ")[1:]
46+
extPath, extArgs := lookupExtension(cmdPrefix, extArgs)
47+
48+
// Parse CLI args that need validation.
49+
if len(cliParseArgs) > 0 {
50+
if err := foundCmd.Flags().Parse(cliParseArgs); err != nil {
51+
return err, false
52+
}
53+
}
54+
55+
if extPath == "" {
56+
return nil, false
57+
}
58+
59+
// Apply --command-timeout if set.
60+
ctx := cctx.Context
61+
if timeout := tcmd.CommandTimeout.Duration(); timeout > 0 {
62+
var cancel context.CancelFunc
63+
ctx, cancel = context.WithTimeout(ctx, timeout)
64+
defer cancel()
65+
}
66+
67+
cmd := exec.CommandContext(ctx, extPath, append(cliPassArgs, extArgs...)...)
68+
cmd.Stdin, cmd.Stdout, cmd.Stderr = cctx.Options.Stdin, cctx.Options.Stdout, cctx.Options.Stderr
69+
if err := cmd.Run(); err != nil {
70+
if ctx.Err() != nil {
71+
return fmt.Errorf("program interrupted"), true
72+
}
73+
if _, ok := err.(*exec.ExitError); ok {
74+
return nil, true
75+
}
76+
return fmt.Errorf("extension %s failed: %w", extPath, err), true
77+
}
78+
79+
return nil, true
80+
}
81+
82+
func groupArgs(foundCmd *cobra.Command, args []string) (cliParseArgs, cliPassArgs, extArgs []string) {
83+
seenPos := false
84+
for i := 0; i < len(args); i++ {
85+
arg := args[i]
86+
87+
if isPosArg(arg) {
88+
seenPos = true
89+
extArgs = append(extArgs, arg)
90+
continue
91+
}
92+
93+
name, hasInline := parseFlagArg(arg)
94+
if f, takesValue := lookupFlag(foundCmd, name); f != nil {
95+
// Known CLI flag: goes to cliPassArgs.
96+
// Flags in cliArgsToParseForExtension also go to cliParseArgs.
97+
shouldParse := cliArgsToParseForExtension[f.Name]
98+
cliPassArgs = append(cliPassArgs, arg)
99+
if shouldParse {
100+
cliParseArgs = append(cliParseArgs, arg)
101+
}
102+
if takesValue && !hasInline && i+1 < len(args) {
103+
i++
104+
cliPassArgs = append(cliPassArgs, args[i])
105+
if shouldParse {
106+
cliParseArgs = append(cliParseArgs, args[i])
107+
}
108+
}
109+
} else {
110+
// Unknown flag: before first positional goes to cliParseArgs (to fail validation),
111+
// after first positional goes to extArgs (passed to extension).
112+
if seenPos {
113+
extArgs = append(extArgs, arg)
114+
} else {
115+
cliParseArgs = append(cliParseArgs, arg)
116+
}
117+
}
118+
}
119+
return
120+
}
121+
122+
func isPosArg(arg string) bool {
123+
return !strings.HasPrefix(arg, "-")
124+
}
125+
126+
// parseFlagArg extracts the flag name from a flag argument.
127+
// Handles both --flag=value and --flag forms, returning the name and whether it has an inline value.
128+
func parseFlagArg(arg string) (name string, hasInline bool) {
129+
name, _, hasInline = strings.Cut(strings.TrimLeft(arg, "-"), "=")
130+
return
131+
}
132+
133+
// lookupFlag finds a flag by name on cmd and all parents.
134+
// It resolves aliases and considers shorthand flags.
135+
func lookupFlag(cmd *cobra.Command, name string) (*pflag.Flag, bool) {
136+
if normalize := cmd.Flags().GetNormalizeFunc(); normalize != nil {
137+
name = string(normalize(cmd.Flags(), name))
138+
}
139+
for c := cmd; c != nil; c = c.Parent() {
140+
if f := c.Flags().Lookup(name); f != nil {
141+
return f, f.NoOptDefVal == ""
142+
}
143+
if len(name) == 1 {
144+
if f := c.Flags().ShorthandLookup(name); f != nil {
145+
return f, f.NoOptDefVal == ""
146+
}
147+
}
148+
}
149+
return nil, false
150+
}
151+
152+
// lookupExtension finds an extension executable and returns its path along with
153+
// extArgs with matched positional args removed.
154+
func lookupExtension(cmdPrefix, extArgs []string) (string, []string) {
155+
// Extract positional args from extArgs until we hit an unknown flag.
156+
// We stop at unknown flags because we can't tell if subsequent args are flag values or positionals.
157+
var posArgs []string
158+
for _, arg := range extArgs {
159+
if !isPosArg(arg) {
160+
break
161+
}
162+
// Dashes are converted to underscores so "foo bar-baz" finds "temporal-foo-bar_baz".
163+
posArgs = append(posArgs, strings.ReplaceAll(arg, extensionSeparator, argDashReplacement))
164+
}
165+
166+
// Try most-specific to least-specific.
167+
parts := append(cmdPrefix, posArgs...)
168+
for n := len(parts); n > len(cmdPrefix); n-- {
169+
path, err := exec.LookPath(extensionPrefix + strings.Join(parts[:n], extensionSeparator))
170+
if err != nil {
171+
continue
172+
}
173+
// Remove matched positionals from extArgs (they come first).
174+
matched := n - len(cmdPrefix)
175+
return path, extArgs[matched:]
176+
}
177+
178+
return "", extArgs
179+
}

0 commit comments

Comments
 (0)