Skip to content

Commit 4e72f56

Browse files
committed
release: v2.3.0 — Docker runtime-agnostic, updated README
- Docker tool now supports Docker, Podman, nerdctl, OrbStack, Rancher Desktop (PR #1) - README: added Releasing section, updated to 33 tools / 300 tests - 300 tests, 0 failures
2 parents 30ae668 + badd682 commit 4e72f56

5 files changed

Lines changed: 189 additions & 24 deletions

File tree

DXTools/Services/DockerService.swift

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,87 @@ struct DockerService {
1111
var isRunning: Bool { status.lowercased().contains("up") }
1212
}
1313

14+
enum Runtime: String, CaseIterable {
15+
case docker = "docker"
16+
case podman = "podman"
17+
case nerdctl = "nerdctl"
18+
19+
var displayName: String {
20+
switch self {
21+
case .docker: return "Docker"
22+
case .podman: return "Podman"
23+
case .nerdctl: return "nerdctl"
24+
}
25+
}
26+
}
27+
28+
/// Detected runtime name for display (e.g. "Docker", "Podman")
29+
static var detectedRuntimeName: String {
30+
detectRuntime()?.displayName ?? "Container runtime"
31+
}
32+
33+
/// Search common paths for a container runtime binary, returning the first one found.
34+
static func detectRuntime() -> Runtime? {
35+
let searchPaths = [
36+
"/usr/local/bin",
37+
"/opt/homebrew/bin",
38+
"/usr/bin",
39+
NSHomeDirectory() + "/.docker/bin",
40+
NSHomeDirectory() + "/.rd/bin", // Rancher Desktop
41+
"/Applications/OrbStack.app/Contents/MacOS", // OrbStack
42+
]
43+
44+
for runtime in Runtime.allCases {
45+
// First try `which` to respect the user's PATH
46+
let whichResult = shell("/usr/bin/which \(runtime.rawValue) 2>/dev/null")
47+
if !whichResult.isEmpty && FileManager.default.isExecutableFile(atPath: whichResult) {
48+
return runtime
49+
}
50+
// Then check common install locations
51+
for dir in searchPaths {
52+
let path = "\(dir)/\(runtime.rawValue)"
53+
if FileManager.default.isExecutableFile(atPath: path) {
54+
return runtime
55+
}
56+
}
57+
}
58+
return nil
59+
}
60+
61+
/// Resolve the full path to the runtime binary.
62+
private static func runtimePath() -> String? {
63+
guard let runtime = detectRuntime() else { return nil }
64+
let searchPaths = [
65+
"/usr/local/bin",
66+
"/opt/homebrew/bin",
67+
"/usr/bin",
68+
NSHomeDirectory() + "/.docker/bin",
69+
NSHomeDirectory() + "/.rd/bin",
70+
"/Applications/OrbStack.app/Contents/MacOS",
71+
]
72+
73+
let whichResult = shell("/usr/bin/which \(runtime.rawValue) 2>/dev/null")
74+
if !whichResult.isEmpty && FileManager.default.isExecutableFile(atPath: whichResult) {
75+
return whichResult
76+
}
77+
for dir in searchPaths {
78+
let path = "\(dir)/\(runtime.rawValue)"
79+
if FileManager.default.isExecutableFile(atPath: path) {
80+
return path
81+
}
82+
}
83+
return nil
84+
}
85+
86+
/// Run a container runtime command using the detected binary.
87+
private static func run(_ arguments: String) -> String {
88+
guard let bin = runtimePath() else { return "" }
89+
return shell("\(bin) \(arguments)")
90+
}
91+
1492
static func listContainers(all: Bool = true) -> [Container] {
1593
let flag = all ? "-a" : ""
16-
let output = shell("docker ps \(flag) --format '{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}|{{.CreatedAt}}' 2>/dev/null")
94+
let output = run("ps \(flag) --format '{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}|{{.CreatedAt}}' 2>/dev/null")
1795
guard !output.isEmpty else { return [] }
1896

1997
return output.split(separator: "\n").compactMap { line in
@@ -31,32 +109,34 @@ struct DockerService {
31109
}
32110

33111
static func start(_ containerId: String) -> Bool {
34-
let output = shell("docker start \(containerId) 2>&1")
112+
let output = run("start \(containerId) 2>&1")
35113
return !output.isEmpty
36114
}
37115

38116
static func stop(_ containerId: String) -> Bool {
39-
let output = shell("docker stop \(containerId) 2>&1")
117+
let output = run("stop \(containerId) 2>&1")
40118
return !output.isEmpty
41119
}
42120

43121
static func restart(_ containerId: String) -> Bool {
44-
let output = shell("docker restart \(containerId) 2>&1")
122+
let output = run("restart \(containerId) 2>&1")
45123
return !output.isEmpty
46124
}
47125

48126
static func remove(_ containerId: String) -> Bool {
49-
let output = shell("docker rm -f \(containerId) 2>&1")
127+
let output = run("rm -f \(containerId) 2>&1")
50128
return !output.isEmpty
51129
}
52130

53131
static func logs(_ containerId: String, lines: Int = 100) -> String {
54-
shell("docker logs --tail \(lines) \(containerId) 2>&1")
132+
run("logs --tail \(lines) \(containerId) 2>&1")
55133
}
56134

57135
static func isDockerRunning() -> Bool {
58-
let output = shell("docker info 2>&1")
136+
guard runtimePath() != nil else { return false }
137+
let output = run("info 2>&1")
59138
return output.contains("Server Version") || output.contains("Containers:")
139+
|| output.contains("host") // podman info output
60140
}
61141

62142
private static func shell(_ command: String) -> String {

DXTools/ViewModels/DockerViewModel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class DockerViewModel {
99
var logs: String = ""
1010
var isDockerAvailable: Bool = false
1111
var isLoading: Bool = false
12+
var runtimeName: String = "Docker"
1213

1314
var filtered: [DockerService.Container] {
1415
if searchQuery.isEmpty { return containers }
@@ -23,9 +24,11 @@ class DockerViewModel {
2324
func refresh() {
2425
isLoading = true
2526
DispatchQueue.global(qos: .userInitiated).async { [self] in
27+
let name = DockerService.detectedRuntimeName
2628
let available = DockerService.isDockerRunning()
2729
let list = available ? DockerService.listContainers(all: showAll) : []
2830
DispatchQueue.main.async {
31+
self.runtimeName = name
2932
self.isDockerAvailable = available
3033
self.containers = list
3134
self.isLoading = false

DXTools/Views/Tools/DockerView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ struct DockerView: View {
2626
VStack(spacing: 10) {
2727
Spacer()
2828
Image(systemName: "shippingbox").font(.system(size: 30, weight: .ultraLight)).foregroundStyle(t.textGhost)
29-
Text("Docker not running").font(.system(size: 12, weight: .semibold, design: .rounded)).foregroundStyle(t.textTertiary)
30-
Text("Start Docker Desktop and refresh").font(.system(size: 10, weight: .medium)).foregroundStyle(t.textGhost)
29+
Text("No container runtime found").font(.system(size: 12, weight: .semibold, design: .rounded)).foregroundStyle(t.textTertiary)
30+
Text("Install Docker, Podman, or OrbStack and refresh").font(.system(size: 10, weight: .medium)).foregroundStyle(t.textGhost)
3131
Spacer()
3232
}
3333
.frame(maxWidth: .infinity)

README.md

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
<p align="center">
22
<img src="https://img.shields.io/badge/macOS-14%2B-000?logo=apple&logoColor=white" />
33
<img src="https://img.shields.io/badge/Swift-5.9-F05138?logo=swift&logoColor=white" />
4-
<img src="https://img.shields.io/badge/tools-23-FF8C42" />
5-
<img src="https://img.shields.io/badge/tests-245-4ADE80" />
4+
<img src="https://img.shields.io/badge/tools-33-FF8C42" />
5+
<img src="https://img.shields.io/badge/tests-300-4ADE80" />
66
<img src="https://img.shields.io/badge/dependencies-0-blue" />
7-
<img src="https://img.shields.io/badge/size-2.3MB-purple" />
7+
<img src="https://img.shields.io/badge/size-~3MB-purple" />
88
<img src="https://img.shields.io/github/license/OpenStruct/dx-tools" />
9+
<img src="https://img.shields.io/github/v/release/OpenStruct/dx-tools?color=FF8C42" />
910
</p>
1011

1112
# ⚡ DX Tools
1213

13-
**23 developer tools in one native macOS app.** JSON formatting, JWT decoding, port killing, DNS lookups, regex testing — all offline, instant, keyboard-first.
14+
**33 developer tools in one native macOS app.** JSON formatting, JWT decoding, port killing, QR codes, SQL formatting, API testing — all offline, instant, keyboard-first.
1415

15-
> No Electron. No web wrapper. Pure SwiftUI. 2.3 MB.
16+
> No Electron. No web wrapper. Pure SwiftUI. ~3 MB.
1617
1718
---
1819

@@ -61,6 +62,7 @@ xcodebuild -project DXTools.xcodeproj -scheme DXTools -configuration Release bui
6162
| **JSON → Swift** | Convert JSON to Swift Codable models |
6263
| **JSON → TypeScript** | Convert JSON to TypeScript interfaces |
6364
| **JSON Diff** | Side-by-side structural diff with path tracking |
65+
| **JSON Schema Validator** | Validate JSON against a schema — type, required, min/max, pattern |
6466

6567
### Encoding
6668
| Tool | Description |
@@ -69,32 +71,41 @@ xcodebuild -project DXTools.xcodeproj -scheme DXTools -configuration Release bui
6971
| **Base64** | Encode/decode, standard and URL-safe |
7072
| **Hash Generator** | MD5, SHA-1, SHA-256, SHA-512 |
7173
| **URL Encoder** | Percent-encode/decode, parse components |
74+
| **Image Base64** | Encode images to base64/data URI, decode back to image |
7275

7376
### Generators
7477
| Tool | Description |
7578
|------|-------------|
7679
| **UUID Generator** | v4 UUIDs, bulk generation, one-click copy |
7780
| **Color Converter** | HEX ↔ RGB ↔ HSL, code gen for Swift/CSS/Flutter/Android/Tailwind |
78-
| **Epoch Converter** | Unix timestamp ↔ human date, 6 world clocks |
81+
| **Epoch Converter** | Unix timestamp ↔ human date, 8 world clocks |
7982
| **Password Generator** | Secure passwords & passphrases, strength meter |
8083
| **Unix Permissions** | chmod calculator, numeric ↔ symbolic, interactive checkboxes |
8184
| **Cron Parser** | Human-readable descriptions, next 10 run times |
85+
| **QR Code Generator** | Generate from text/URL, 4 error correction levels, copy/save PNG |
86+
| **SSH Key Generator** | RSA, Ed25519, ECDSA with custom comments, copy/export |
87+
| **Timestamp Converter** | Unix ↔ ISO 8601 ↔ RFC 2822, timezone support |
8288

8389
### DevOps
8490
| Tool | Description |
8591
|------|-------------|
8692
| **Port Manager** | List listening ports, kill processes, search & sort |
8793
| **Network Info** | Local/public IP, DNS lookup (A/AAAA/CNAME/MX/NS/TXT) |
88-
| **API Request Builder** | Send HTTP requests, headers, body, response viewer |
94+
| **API Request Builder** | Send HTTP requests, headers, query params, body, response viewer |
8995
| **Env Manager** | Parse .env files, mask secrets, diff two envs |
9096
| **cURL → Code** | Paste cURL, get Swift/Go/Python/JS/Ruby |
97+
| **Docker Manager** | Container list, start/stop/remove, image management |
98+
| **Git Stats** | Repository stats, branch info, recent commits |
99+
| **HTTP Status Codes** | Searchable reference, 35+ codes, color-coded categories |
91100

92101
### Text
93102
| Tool | Description |
94103
|------|-------------|
95104
| **Regex Tester** | Live matching, capture groups, replace mode |
96105
| **Markdown Preview** | Side-by-side editor and rendered preview |
97106
| **Lorem Generator** | Words, sentences, paragraphs, fake data, JSON |
107+
| **Text Diff** | LCS-based diff, unified output, side-by-side view |
108+
| **SQL Formatter** | Format & minify SQL, 3 indent styles, JOINs/subqueries |
98109

99110
---
100111

@@ -140,10 +151,12 @@ dx env diff .env .env.production
140151
- **Menu Bar** — Quick UUID, Epoch, Password without opening the app
141152
- **Favorites** — Right-click to pin tools to sidebar
142153
- **Dark / Light / System** — Theme toggle in Settings
143-
- **Global Hotkey** — ⌘⇧Space to summon the app
154+
- **Syntax Highlighting** — Live JSON/SQL coloring in editors
155+
- **History** — Per-tool history of recent operations
156+
- **URL Handler**`dx://tool-name` opens app to specific tool
157+
- **Auto-Update** — Checks GitHub Releases on launch
144158
- **Drag & Drop** — Drop files onto editors
145159
- **Export** — Save output to file (⌘S)
146-
- **History** — Per-tool history of recent operations
147160

148161
### Keyboard Shortcuts
149162

@@ -165,16 +178,16 @@ dx env diff .env .env.production
165178
```
166179
DXTools/
167180
├── Models/ # Tool enum, Theme
168-
├── Services/ # Pure logic (23 service files)
181+
├── Services/ # Pure logic (28 service files)
169182
├── ViewModels/ # @Observable view models
170183
├── Views/
171-
│ ├── Components/ # CodeEditor, SplitLayout, Toast
184+
│ ├── Components/ # CodeEditor, SplitLayout, ToolHeader, HistoryPanel
172185
│ ├── Sidebar/ # Navigation, Welcome
173-
│ └── Tools/ # 23 tool views
186+
│ └── Tools/ # 33 tool views
174187
└── DXToolsApp.swift # App entry, menu bar, clipboard
175188
176189
Sources/ # CLI (swift-argument-parser)
177-
DXToolsTests/ # 245 tests across 21 suites
190+
DXToolsTests/ # 300 tests across 25 suites
178191
```
179192

180193
- **SwiftUI** with `@Observable` (macOS 14+)
@@ -193,7 +206,7 @@ xcodegen generate
193206
# Build & run
194207
xcodebuild -project DXTools.xcodeproj -scheme DXTools build
195208

196-
# Run tests (245 tests)
209+
# Run tests (300 tests)
197210
xcodebuild -project DXTools.xcodeproj -scheme DXToolsTests test
198211

199212
# Build CLI
@@ -205,6 +218,75 @@ swift build -c release
205218

206219
---
207220

221+
## Releasing
222+
223+
Releases follow a `dev``main` → tag workflow. CI builds, tests, and publishes automatically.
224+
225+
### Quick Release
226+
227+
```bash
228+
# 1. Make sure you're on dev with everything committed
229+
git checkout dev
230+
231+
# 2. Bump version in project.yml
232+
# MARKETING_VERSION: "X.Y.Z"
233+
234+
# 3. Regenerate Xcode project
235+
xcodegen generate
236+
237+
# 4. Commit and push
238+
git add -A && git commit -m "chore: bump version to X.Y.Z"
239+
git push
240+
241+
# 5. Merge to main
242+
git checkout main
243+
git merge dev --no-ff -m "release: vX.Y.Z — description"
244+
git push
245+
246+
# 6. Tag and push — this triggers CI release
247+
git tag vX.Y.Z
248+
git push origin vX.Y.Z
249+
250+
# 7. Go back to dev
251+
git checkout dev
252+
```
253+
254+
### What CI Does on Tag Push
255+
256+
1. **Builds CLI**`swift build -c release` → uploads `dx` binary
257+
2. **Builds macOS app** — XcodeGen → xcodebuild → `DX Tools.app`
258+
3. **Runs 300 tests** — all must pass
259+
4. **Packages DMG** — app + Applications symlink + volume icon
260+
5. **Creates GitHub Release** — attaches `DXTools.dmg` + `dx` CLI binary
261+
6. **Deploys website** — if `web/` changed, GitHub Pages updates automatically
262+
263+
### Release Checklist
264+
265+
```
266+
[ ] Version bumped in project.yml (MARKETING_VERSION)
267+
[ ] xcodegen generate ran
268+
[ ] All 300 tests pass locally
269+
[ ] Committed and pushed to dev
270+
[ ] Merged dev → main
271+
[ ] Tag pushed (vX.Y.Z)
272+
[ ] CI green — check https://github.com/OpenStruct/dx-tools/actions
273+
[ ] Release has both DXTools.dmg and dx assets
274+
[ ] Website updated (if web/ changed)
275+
```
276+
277+
### Hotfix
278+
279+
```bash
280+
# Fix directly on dev, then follow normal release flow
281+
git checkout dev
282+
# ... make fix ...
283+
git add -A && git commit -m "fix: description"
284+
git push
285+
# Then merge + tag as above
286+
```
287+
288+
---
289+
208290
## License
209291

210292
MIT — see [LICENSE](LICENSE)

project.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ settings:
1010
base:
1111
SWIFT_VERSION: "5.9"
1212
MACOSX_DEPLOYMENT_TARGET: "14.0"
13-
MARKETING_VERSION: "2.2.1"
13+
MARKETING_VERSION: "2.3.0"
1414
CURRENT_PROJECT_VERSION: 1
1515

1616
targets:

0 commit comments

Comments
 (0)