Skip to content
Closed
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
8 changes: 4 additions & 4 deletions cmd/auth/login_scope_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"path/filepath"
"regexp"

"github.com/larksuite/cli/internal/appdir"
larkauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/internal/vfs"
)
Expand All @@ -25,7 +25,7 @@ type loginScopeCacheRecord struct {
// loginScopeCacheDir returns the directory used to persist auth login --no-wait
// requested scopes keyed by device_code.
func loginScopeCacheDir() string {
return filepath.Join(core.GetConfigDir(), "cache", "auth_login_scopes")
return filepath.Join(appdir.CacheDir(), "auth_login_scopes")
}

// loginScopeCachePath returns the cache file path for a given device_code.
Expand All @@ -44,14 +44,14 @@ func sanitizeLoginScopeCacheKey(deviceCode string) string {

// saveLoginRequestedScope persists the requested scope string for a device_code.
func saveLoginRequestedScope(deviceCode, requestedScope string) error {
if err := vfs.MkdirAll(loginScopeCacheDir(), 0700); err != nil {
if err := vfs.MkdirAll(loginScopeCacheDir(), 0o700); err != nil {
return err
}
data, err := json.Marshal(loginScopeCacheRecord{RequestedScope: requestedScope})
if err != nil {
return err
}
return validate.AtomicWrite(loginScopeCachePath(deviceCode), data, 0600)
return validate.AtomicWrite(loginScopeCachePath(deviceCode), data, 0o600)
Comment thread
JayYoung2021 marked this conversation as resolved.
}

// loadLoginRequestedScope loads the cached requested scope string for a device_code.
Expand Down
7 changes: 3 additions & 4 deletions cmd/config/init_interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import (
"net/http"

"github.com/charmbracelet/huh"
"github.com/larksuite/cli/internal/build"
qrcode "github.com/skip2/go-qrcode"

larkauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/build"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
qrcode "github.com/skip2/go-qrcode"
)

// configInitResult holds the result of the interactive config init flow.
Expand Down Expand Up @@ -169,7 +168,7 @@ func runCreateAppFlow(ctx context.Context, f *cmdutil.Factory, brandOverride cor

// Step 1: Request app registration (begin)
httpClient := &http.Client{}
authResp, err := larkauth.RequestAppRegistration(httpClient, larkBrand, f.IOStreams.ErrOut)
authResp, err := larkauth.RequestAppRegistration(httpClient, larkBrand)
if err != nil {
return nil, output.ErrAuth("app registration failed: %v", err)
}
Expand Down
180 changes: 180 additions & 0 deletions internal/appdir/appdir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package appdir

import (
"fmt"
"log"
"os"
"path/filepath"
"runtime"

"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/internal/vfs"
)

const appName = "lark-cli"

// ConfigDir returns the CLI config directory.
func ConfigDir() string {
if dir, ok := validatedEnvDir("LARKSUITE_CLI_CONFIG_DIR"); ok {
return dir
}
if dir, ok := xdgAppDir("XDG_CONFIG_HOME"); ok {
return dir
}
if dir, ok := legacyConfigDir(); ok {
return dir
}
return defaultConfigDir()
}

// ConfigPath returns the CLI config file path.
func ConfigPath() string {
return filepath.Join(ConfigDir(), "config.json")
}

// CacheDir returns the CLI cache directory.
func CacheDir() string {
if dir, ok := validatedEnvDir("LARKSUITE_CLI_CACHE_DIR"); ok {
return dir
}
if dir, ok := xdgAppDir("XDG_CACHE_HOME"); ok {
return dir
}
if dir, ok := legacyConfigDir(); ok {
return filepath.Join(dir, "cache")
}
return defaultCacheDir()
}

// StateDir returns the CLI state directory.
func StateDir() string {
if dir, ok := validatedEnvDir("LARKSUITE_CLI_STATE_DIR"); ok {
return dir
}
if dir, ok := xdgAppDir("XDG_STATE_HOME"); ok {
return dir
}
if dir, ok := legacyConfigDir(); ok {
return dir
}
return defaultStateDir()
}

Comment thread
greptile-apps[bot] marked this conversation as resolved.
// LogDir returns the CLI log directory.
func LogDir() string {
if dir, ok := validatedEnvDir("LARKSUITE_CLI_LOG_DIR"); ok {
return dir
}
return filepath.Join(StateDir(), "logs")
}

// DataDir returns the directory used for service-specific stored data.
// It returns an error if service contains path separators, traversal sequences,
// or other unsafe characters.
func DataDir(service string) (string, error) {
if err := validate.SafeServiceName(service); err != nil {
return "", fmt.Errorf("appdir.DataDir: invalid service name: %w", err)
}
if dir, ok := validatedEnvDir("LARKSUITE_CLI_DATA_DIR"); ok {
return filepath.Join(dir, service), nil
}
if dir, ok := xdgDataDir(service); ok {
return dir, nil
}
if dir, ok := legacyDataDir(service); ok {
return dir, nil
}
return defaultDataDir(service), nil
}

func validatedEnvDir(envName string) (string, bool) {
value := os.Getenv(envName)
if value == "" {
return "", false
}
dir, err := validate.SafeEnvDirPath(value, envName)
if err != nil {
fmt.Fprintf(log.Writer(), "warning: %s=%q is invalid (%v), using default\n", envName, value, err)
return "", false
}
return dir, true
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

func xdgAppDir(envName string) (string, bool) {
base, ok := validatedEnvDir(envName)
if !ok {
return "", false
}
return filepath.Join(base, appName), true
}

func xdgDataDir(service string) (string, bool) {
base, ok := validatedEnvDir("XDG_DATA_HOME")
if !ok {
return "", false
}
return filepath.Join(base, appName, service), true
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func legacyConfigDir() (string, bool) {
dir := filepath.Join(homeDir(), ".lark-cli")
return dir, dirExists(dir)
}

func legacyDataDir(service string) (string, bool) {
var dir string
switch runtime.GOOS {
case "darwin":
dir = filepath.Join(homeDir(), "Library", "Application Support", service)
default:
return "", false
}
return dir, dirExists(dir)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

func defaultConfigDir() string {
if runtime.GOOS == "windows" {
return filepath.Join(homeDir(), ".lark-cli")
}
return filepath.Join(homeDir(), ".config", appName)
}

func defaultCacheDir() string {
if runtime.GOOS == "windows" {
return filepath.Join(ConfigDir(), "cache")
}
return filepath.Join(homeDir(), ".cache", appName)
}

func defaultStateDir() string {
if runtime.GOOS == "windows" {
return ConfigDir()
}
return filepath.Join(homeDir(), ".local", "state", appName)
}

func defaultDataDir(service string) string {
if runtime.GOOS == "windows" {
return filepath.Join(homeDir(), ".lark-cli", "data", service)
}
return filepath.Join(homeDir(), ".local", "share", appName, service)
}

func dirExists(path string) bool {
info, err := vfs.Stat(path)
if err != nil {
return false
}
return info.IsDir()
}

func homeDir() string {
home, err := vfs.UserHomeDir()
if err != nil || home == "" {
fmt.Fprintf(log.Writer(), "warning: unable to determine home directory: %v\n", err)
}
return home
}
Loading
Loading