diff --git a/.gitignore b/.gitignore index c06b3a7..a7bb246 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ DerivedData/ .swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc -.zeplin/ +.zeplin-cli/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1932b91..4da520f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.5.0 + +- Move global config from `~/.zeplin/` to `~/.config/zeplin-cli/` (XDG Base Directory) +- Move update cache from `~/.zeplin/` to `~/.cache/zeplin-cli/` +- Move project-local config from `.zeplin/` to `.zeplin-cli/` +- Support `XDG_CONFIG_HOME` and `XDG_CACHE_HOME` environment variables + ## 0.4.3 - Fix `--limit` not returning more than 100 results (now paginates automatically) diff --git a/README.md b/README.md index 19ddc46..214751f 100644 --- a/README.md +++ b/README.md @@ -221,11 +221,11 @@ zeplin-cli user profile zeplin-cli auth init ``` -This prompts for your token, optionally a default organization ID, and saves credentials to `~/.zeplin/config.json` with restricted permissions (600). It also verifies the token works. +This prompts for your token, optionally a default organization ID, and saves credentials to `~/.config/zeplin-cli/config.json` with restricted permissions (600). It also verifies the token works. ### Manual Config File -Create `~/.zeplin/config.json`: +Create `~/.config/zeplin-cli/config.json`: ```json { @@ -252,14 +252,24 @@ zeplin-cli projects list zeplin-cli projects list --token "your-personal-access-token" ``` +### Configuration Paths + +zeplin-cli follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/latest/): + +| Purpose | Default Path | Override | +|---------|-------------|----------| +| Config | `~/.config/zeplin-cli/config.json` | `$XDG_CONFIG_HOME` | +| Cache | `~/.cache/zeplin-cli/update-check.json` | `$XDG_CACHE_HOME` | +| Project | `.zeplin-cli/config.json` | — | + ### Credential Resolution Order Credentials are resolved in this order (first match wins): 1. `--token` command-line flag 2. `ZEPLIN_TOKEN` environment variable -3. Project-local config (`.zeplin/config.json`) -4. Global config (`~/.zeplin/config.json`) +3. Project-local config (`.zeplin-cli/config.json`) +4. Global config (`~/.config/zeplin-cli/config.json`) ### Multiple Profiles @@ -588,7 +598,7 @@ zeplin-cli design-tokens get --project --pretty > tokens.json Run `zeplin-cli auth init` to set up credentials interactively. In interactive mode (`zeplin-cli` with no arguments), you'll be prompted to set up credentials automatically. -You can also check that your config file exists at `~/.zeplin/config.json`. +You can also check that your config file exists at `~/.config/zeplin-cli/config.json`. ### "Unauthorized" errors diff --git a/Sources/ZeplinCLI/CLI.swift b/Sources/ZeplinCLI/CLI.swift index f348d54..1484641 100644 --- a/Sources/ZeplinCLI/CLI.swift +++ b/Sources/ZeplinCLI/CLI.swift @@ -16,8 +16,8 @@ public struct Zeplin: ParsableCommand { Credentials can be provided via: 1. Command-line flag (--token) 2. Environment variable (ZEPLIN_TOKEN) - 3. Project-local config (.zeplin/config.json) - 4. Global config (~/.zeplin/config.json) + 3. Project-local config (.zeplin-cli/config.json) + 4. Global config (~/.config/zeplin-cli/config.json) Run 'zeplin-cli auth init' to set up credentials interactively. Get a personal access token at https://app.zeplin.io/profile/developer @@ -45,7 +45,7 @@ public struct Zeplin: ParsableCommand { DOCUMENTATION https://docs.zeplin.dev/reference/introduction """, - version: "0.4.3", + version: "0.5.0", subcommands: [ InteractiveCommand.self, AuthCommand.self, diff --git a/Sources/ZeplinCLI/Commands/Auth/AuthCommand.swift b/Sources/ZeplinCLI/Commands/Auth/AuthCommand.swift index dc904fb..ae38a07 100644 --- a/Sources/ZeplinCLI/Commands/Auth/AuthCommand.swift +++ b/Sources/ZeplinCLI/Commands/Auth/AuthCommand.swift @@ -9,7 +9,7 @@ struct AuthCommand: ParsableCommand { discussion: """ Set up and manage Zeplin API credentials. - Credentials are stored in ~/.zeplin/config.json with restricted permissions (600). + Credentials are stored in ~/.config/zeplin-cli/config.json with restricted permissions (600). You can configure multiple profiles for different accounts. GETTING A TOKEN @@ -155,7 +155,7 @@ struct AuthProfilesCommand: ParsableCommand { let resolver = CredentialResolver() if let config = try resolver.loadConfig(from: CredentialResolver.globalConfigPath) { - print("Global config (~/.zeplin/config.json):") + print("Global config (\(CredentialResolver.globalConfigPath)):") print(" Default: \(config.defaultProfile ?? "(none)")") print(" Profiles:") for (name, profile) in config.profiles.sorted(by: { $0.key < $1.key }) { @@ -166,11 +166,11 @@ struct AuthProfilesCommand: ParsableCommand { } } } else { - print("No global config found at ~/.zeplin/config.json") + print("No global config found at \(CredentialResolver.globalConfigPath)") } if let config = try resolver.loadConfig(from: CredentialResolver.localConfigPath) { - print("\nLocal config (.zeplin/config.json):") + print("\nLocal config (\(CredentialResolver.localConfigPath)):") print(" Default: \(config.defaultProfile ?? "(none)")") print(" Profiles:") for (name, _) in config.profiles.sorted(by: { $0.key < $1.key }) { diff --git a/Sources/ZeplinCLI/UpdateChecker.swift b/Sources/ZeplinCLI/UpdateChecker.swift index c305922..38c654b 100644 --- a/Sources/ZeplinCLI/UpdateChecker.swift +++ b/Sources/ZeplinCLI/UpdateChecker.swift @@ -1,4 +1,5 @@ import Foundation +import ZeplinKit public enum UpdateChecker { @@ -9,9 +10,7 @@ public enum UpdateChecker { private static let fetchTimeout: TimeInterval = 3 private static var cacheURL: URL { - FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent(".zeplin") - .appendingPathComponent("update-check.json") + Paths.updateCheckCacheFile } public struct Cache: Codable { diff --git a/Sources/ZeplinKit/Auth/CredentialResolver.swift b/Sources/ZeplinKit/Auth/CredentialResolver.swift index e26d7d9..6912762 100644 --- a/Sources/ZeplinKit/Auth/CredentialResolver.swift +++ b/Sources/ZeplinKit/Auth/CredentialResolver.swift @@ -13,8 +13,8 @@ public struct CredentialOptions: Sendable { } public struct CredentialResolver: Sendable { - public static let globalConfigPath = "~/.zeplin/config.json" - public static let localConfigPath = ".zeplin/config.json" + public static var globalConfigPath: String { Paths.globalConfigPath } + public static var localConfigPath: String { Paths.localConfigPath } public static let envToken = "ZEPLIN_TOKEN" public static let envOrganizationId = "ZEPLIN_ORGANIZATION_ID" @@ -49,7 +49,7 @@ public struct CredentialResolver: Sendable { Credentials can also be provided via: - Command-line flag (--token) - Environment variable (ZEPLIN_TOKEN) - - Config file (~/.zeplin/config.json) + - Config file (\(Paths.globalConfigPath)) Get a personal access token at: https://app.zeplin.io/profile/developer diff --git a/Sources/ZeplinKit/Paths.swift b/Sources/ZeplinKit/Paths.swift new file mode 100644 index 0000000..642ace1 --- /dev/null +++ b/Sources/ZeplinKit/Paths.swift @@ -0,0 +1,48 @@ +import Foundation + +public enum Paths { + + // MARK: - Config + + public static var configDirectory: URL { + let base: URL + if let xdg = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"], !xdg.isEmpty { + base = URL(fileURLWithPath: xdg) + } else { + base = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".config") + } + return base.appendingPathComponent("zeplin-cli") + } + + public static var globalConfigFile: URL { + configDirectory.appendingPathComponent("config.json") + } + + public static var globalConfigPath: String { + if let xdg = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"], !xdg.isEmpty { + return URL(fileURLWithPath: xdg) + .appendingPathComponent("zeplin-cli/config.json").path + } + return "~/.config/zeplin-cli/config.json" + } + + // MARK: - Cache + + public static var cacheDirectory: URL { + let base: URL + if let xdg = ProcessInfo.processInfo.environment["XDG_CACHE_HOME"], !xdg.isEmpty { + base = URL(fileURLWithPath: xdg) + } else { + base = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".cache") + } + return base.appendingPathComponent("zeplin-cli") + } + + public static var updateCheckCacheFile: URL { + cacheDirectory.appendingPathComponent("update-check.json") + } + + // MARK: - Local (project-scoped) + + public static let localConfigPath = ".zeplin-cli/config.json" +} diff --git a/Tests/ZeplinKitTests/CredentialsTests.swift b/Tests/ZeplinKitTests/CredentialsTests.swift index 0e82f57..ffbafea 100644 --- a/Tests/ZeplinKitTests/CredentialsTests.swift +++ b/Tests/ZeplinKitTests/CredentialsTests.swift @@ -132,7 +132,7 @@ struct CredentialsTests { @Test func missingProfileThrows() throws { let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - let subDir = tempDir.appendingPathComponent(".zeplin") + let subDir = tempDir.appendingPathComponent(".zeplin-cli") try FileManager.default.createDirectory(at: subDir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: tempDir) }