Skip to content

Commit ba0b2dc

Browse files
committed
feat(doc,sheets,slides,wiki): emit typed error envelopes across ccm domains
Classify doc, sheets, slides, and wiki shortcut failures with typed errors so CLI users and automation receive more specific, actionable diagnostics. Keep the migration scoped to the four CCM shortcut domains and their error-contract guards; shared common helper behavior remains unchanged.
1 parent 33de28f commit ba0b2dc

54 files changed

Lines changed: 826 additions & 794 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.golangci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,20 @@ linters:
6565
- forbidigo
6666
# errs-typed-only enforced on paths already migrated to errs.NewXxxError.
6767
# Add a path when its migration is complete.
68-
- path-except: (internal/auth/|internal/errcompat/|internal/errclass/|internal/client/|internal/cmdutil/factory\.go|cmd/auth/|cmd/config/|cmd/service/|shortcuts/common/mcp_client\.go|shortcuts/calendar/helpers\.go|shortcuts/drive/)
68+
- path-except: (internal/auth/|internal/errcompat/|internal/errclass/|internal/client/|internal/cmdutil/factory\.go|cmd/auth/|cmd/config/|cmd/service/|shortcuts/common/mcp_client\.go|shortcuts/calendar/helpers\.go|shortcuts/drive/|shortcuts/doc/|shortcuts/sheets/|shortcuts/slides/|shortcuts/wiki/)
6969
text: errs-typed-only
7070
linters:
7171
- forbidigo
7272
# errs-no-bare-wrap enforced on paths fully migrated to typed final
7373
# errors. Scoped separately from errs-typed-only because cmd/auth/,
7474
# cmd/config/ still have residual fmt.Errorf and must not be caught.
75-
- path-except: (shortcuts/drive/|shortcuts/calendar/helpers\.go|shortcuts/common/mcp_client\.go)
75+
- path-except: (shortcuts/drive/|shortcuts/doc/|shortcuts/sheets/|shortcuts/slides/|shortcuts/wiki/|shortcuts/calendar/helpers\.go|shortcuts/common/mcp_client\.go)
7676
text: errs-no-bare-wrap
7777
linters:
7878
- forbidigo
7979
# errs-no-legacy-helper is drive-only: the shared helpers it bans are
8080
# still used by other domains until their later migration phase.
81-
- path-except: (shortcuts/drive/)
81+
- path-except: (shortcuts/drive/|shortcuts/doc/|shortcuts/sheets/|shortcuts/slides/|shortcuts/wiki/)
8282
text: errs-no-legacy-helper
8383
linters:
8484
- forbidigo

lint/errscontract/rule_no_legacy_envelope_literal.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import (
1616
// call sites must return a typed errs.* error instead. Future domains opt in by
1717
// appending their path prefix here.
1818
var migratedEnvelopePaths = []string{
19+
"shortcuts/doc/",
1920
"shortcuts/drive/",
21+
"shortcuts/sheets/",
22+
"shortcuts/slides/",
23+
"shortcuts/wiki/",
2024
}
2125

2226
// legacyOutputImportPath is the import path of the package that declares the

shortcuts/doc/clipboard.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"regexp"
1212
"runtime"
1313
"strings"
14+
15+
"github.com/larksuite/cli/errs"
1416
)
1517

1618
// readClipboardImageBytes reads the current clipboard image and returns the
@@ -35,13 +37,13 @@ func readClipboardImageBytes() ([]byte, error) {
3537
case "linux":
3638
data, err = readClipboardLinux()
3739
default:
38-
return nil, fmt.Errorf("clipboard image upload is not supported on %s", runtime.GOOS)
40+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard image upload is not supported on %s", runtime.GOOS)
3941
}
4042
if err != nil {
4143
return nil, err
4244
}
4345
if len(data) == 0 {
44-
return nil, fmt.Errorf("clipboard contains no image data")
46+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data")
4547
}
4648
return data, nil
4749
}
@@ -91,9 +93,9 @@ func readClipboardDarwin() ([]byte, error) {
9193
}
9294

9395
if stderrText != "" {
94-
return nil, fmt.Errorf("clipboard contains no image data (osascript: %s)", stderrText)
96+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data (osascript: %s)", stderrText)
9597
}
96-
return nil, fmt.Errorf("clipboard contains no image data")
98+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data")
9799
}
98100

99101
// runOsascript invokes osascript with a single AppleScript expression and
@@ -188,14 +190,14 @@ func decodeOsascriptData(s string) ([]byte, error) {
188190
// decodeHex decodes an uppercase hex string (as produced by osascript) to bytes.
189191
func decodeHex(h string) ([]byte, error) {
190192
if len(h)%2 != 0 {
191-
return nil, fmt.Errorf("odd hex length")
193+
return nil, fmt.Errorf("odd hex length") //nolint:forbidigo // intermediate decode helper; result discarded by caller on error
192194
}
193195
b := make([]byte, len(h)/2)
194196
for i := 0; i < len(h); i += 2 {
195197
hi := hexVal(h[i])
196198
lo := hexVal(h[i+1])
197199
if hi < 0 || lo < 0 {
198-
return nil, fmt.Errorf("invalid hex char at %d", i)
200+
return nil, fmt.Errorf("invalid hex char at %d", i) //nolint:forbidigo // intermediate decode helper; result discarded by caller on error
199201
}
200202
b[i/2] = byte(hi<<4 | lo)
201203
}
@@ -237,12 +239,12 @@ $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
237239
if msg == "" {
238240
msg = err.Error()
239241
}
240-
return nil, fmt.Errorf("clipboard read failed (%s)", msg)
242+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard read failed (%s)", msg)
241243
}
242244
b64 := strings.TrimSpace(string(out))
243245
data, decErr := base64.StdEncoding.DecodeString(b64)
244246
if decErr != nil {
245-
return nil, fmt.Errorf("clipboard image decode failed: %w", decErr)
247+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard image decode failed: %s", decErr).WithCause(decErr)
246248
}
247249
return data, nil
248250
}
@@ -325,15 +327,15 @@ func readClipboardLinux() ([]byte, error) {
325327
foundTool = true
326328
out, err := exec.Command(t.name, t.args...).Output()
327329
if err != nil {
328-
lastErr = fmt.Errorf("clipboard image read failed via %s: %w", t.name, err)
330+
lastErr = errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard image read failed via %s: %s", t.name, err).WithCause(err)
329331
continue
330332
}
331333
if len(out) == 0 {
332-
lastErr = fmt.Errorf("clipboard contains no image data (%s returned empty output)", t.name)
334+
lastErr = errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data (%s returned empty output)", t.name)
333335
continue
334336
}
335337
if t.validatePNG && !hasPNGMagic(out) {
336-
lastErr = fmt.Errorf("clipboard contains no PNG image data (%s output is not a PNG)", t.name)
338+
lastErr = errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no PNG image data (%s output is not a PNG)", t.name)
337339
continue
338340
}
339341
return out, nil
@@ -342,8 +344,8 @@ func readClipboardLinux() ([]byte, error) {
342344
if foundTool && lastErr != nil {
343345
return nil, lastErr
344346
}
345-
return nil, fmt.Errorf(
346-
"clipboard image read failed: no supported tool found. " +
347-
"Install one of xclip, wl-clipboard, or xsel via your distro's package manager " +
347+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition,
348+
"clipboard image read failed: no supported tool found. "+
349+
"Install one of xclip, wl-clipboard, or xsel via your distro's package manager "+
348350
"(apt, dnf, pacman, apk, brew, etc.).")
349351
}

shortcuts/doc/doc_errors.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package doc
5+
6+
import (
7+
"errors"
8+
9+
"github.com/larksuite/cli/errs"
10+
"github.com/larksuite/cli/extension/fileio"
11+
)
12+
13+
// wrapDocNetworkErr returns err unchanged when it is already a typed errs.*
14+
// error (preserving its subtype / code / log_id from the runtime boundary),
15+
// and only wraps a raw, unclassified error as a transport-level network error.
16+
func wrapDocNetworkErr(err error, format string, args ...any) error {
17+
if _, ok := errs.ProblemOf(err); ok {
18+
return err
19+
}
20+
return errs.NewNetworkError(errs.SubtypeNetworkTransport, format, args...).WithCause(err)
21+
}
22+
23+
// docInputStatError maps a FileIO.Stat/Open error for input file validation to
24+
// a typed validation error:
25+
// - Path validation failures → "unsafe file path: ..."
26+
// - Other errors → "<msg>: ..." (the caller's context, e.g. "file not found")
27+
func docInputStatError(err error, msg string) error {
28+
if err == nil {
29+
return nil
30+
}
31+
if errors.Is(err, fileio.ErrPathValidation) {
32+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe file path: %s", err).WithCause(err)
33+
}
34+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s: %s", msg, err).WithCause(err)
35+
}
36+
37+
// docSaveError maps a FileIO.Save error to a typed error. Path validation
38+
// failures are validation errors (exit code 2); mkdir / write failures are
39+
// internal file-I/O errors (exit code 5).
40+
func docSaveError(err error) error {
41+
if err == nil {
42+
return nil
43+
}
44+
var me *fileio.MkdirError
45+
switch {
46+
case errors.Is(err, fileio.ErrPathValidation):
47+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe output path: %s", err).WithCause(err)
48+
case errors.As(err, &me):
49+
return errs.NewInternalError(errs.SubtypeFileIO, "cannot create parent directory: %s", err).WithCause(err)
50+
default:
51+
return errs.NewInternalError(errs.SubtypeFileIO, "cannot create file: %s", err).WithCause(err)
52+
}
53+
}

shortcuts/doc/doc_media_download.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010

1111
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
1212

13+
"github.com/larksuite/cli/errs"
1314
"github.com/larksuite/cli/extension/fileio"
14-
"github.com/larksuite/cli/internal/output"
1515
"github.com/larksuite/cli/internal/validate"
1616
"github.com/larksuite/cli/shortcuts/common"
1717
)
@@ -51,10 +51,10 @@ var DocMediaDownload = common.Shortcut{
5151
overwrite := runtime.Bool("overwrite")
5252

5353
if err := validate.ResourceName(token, "--token"); err != nil {
54-
return output.ErrValidation("%s", err)
54+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err).WithParam("--token")
5555
}
5656
if _, err := runtime.ResolveSavePath(outputPath); err != nil {
57-
return output.ErrValidation("unsafe output path: %s", err)
57+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe output path: %s", err).WithParam("--output").WithCause(err)
5858
}
5959

6060
fmt.Fprintf(runtime.IO().ErrOut, "Downloading: %s %s\n", mediaType, common.MaskToken(token))
@@ -73,7 +73,7 @@ var DocMediaDownload = common.Shortcut{
7373
ApiPath: apiPath,
7474
})
7575
if err != nil {
76-
return output.ErrNetwork("download failed: %v", err)
76+
return wrapDocNetworkErr(err, "download failed: %v", err)
7777
}
7878
defer resp.Body.Close()
7979

@@ -86,14 +86,14 @@ var DocMediaDownload = common.Shortcut{
8686
// Validate final path after extension append
8787
if finalPath != outputPath {
8888
if _, err := runtime.ResolveSavePath(finalPath); err != nil {
89-
return output.ErrValidation("unsafe output path: %s", err)
89+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe output path: %s", err).WithParam("--output").WithCause(err)
9090
}
9191
}
9292

9393
// Overwrite check on final path (after extension detection)
9494
if !overwrite {
9595
if _, statErr := runtime.FileIO().Stat(finalPath); statErr == nil {
96-
return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", finalPath)
96+
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "output file already exists: %s (use --overwrite to replace)", finalPath).WithParam("--output")
9797
}
9898
}
9999

@@ -102,7 +102,7 @@ var DocMediaDownload = common.Shortcut{
102102
ContentLength: resp.ContentLength,
103103
}, resp.Body)
104104
if err != nil {
105-
return common.WrapSaveErrorByCategory(err, "io")
105+
return docSaveError(err)
106106
}
107107

108108
savedPath, _ := runtime.ResolveSavePath(finalPath)

0 commit comments

Comments
 (0)