Skip to content
Draft
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
36 changes: 34 additions & 2 deletions Sources/SlayNodeMenuBar/CommandParsing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ enum CommandParser {
continue
}

if let systemPropertyPort = extractSystemPropertyPort(from: token) {
collected.insert(systemPropertyPort)
continue
}

if let inlinePort = extractInlinePort(from: token) {
collected.insert(inlinePort)
continue
Expand Down Expand Up @@ -297,12 +302,37 @@ enum CommandParser {
return port
}

if let port = extractURLPort(from: normalizedValue) {
return port
}

// Some shell snippets end values with punctuation (e.g. "PORT=3000,")
// and should still resolve to the intended port value.
guard !normalizedValue.contains(":") else { return nil }
return parsePortPrefix(normalizedValue)
}

private static func extractSystemPropertyPort(from token: String) -> Int? {
guard token.hasPrefix("-D"),
let separator = token.firstIndex(of: "=") else { return nil }

let keyStart = token.index(token.startIndex, offsetBy: 2)
guard keyStart < separator else { return nil }

let key = token[keyStart..<separator]
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
let rawValue = String(token[token.index(after: separator)...])
guard isPortEnvironmentKey(key.replacingOccurrences(of: ".", with: "_")) else { return nil }

let trimmedValue = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
let normalizedValue = unwrappedQuotedValue(trimmedValue)

return extractPortCandidate(from: normalizedValue)
?? extractURLPort(from: normalizedValue)
?? parsePortPrefix(normalizedValue)
}

private static func isPortEnvironmentKey(_ key: String) -> Bool {
guard !key.isEmpty else { return false }
let parts = key.split { character in
Expand All @@ -327,7 +357,7 @@ enum CommandParser {

private static func extractURLPort(from token: String) -> Int? {
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
let sanitized = trimmed.trimmingCharacters(in: CharacterSet(charactersIn: ",;.)]"))
let sanitized = trimmed.trimmingCharacters(in: CharacterSet(charactersIn: ",;.)]}\"'"))
guard let components = URLComponents(string: sanitized),
let port = components.port,
isValidPort(port) else {
Expand Down Expand Up @@ -415,7 +445,9 @@ enum CommandParser {
guard let range = expression.range(of: separator) else { continue }
let candidate = String(expression[range.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines)
let unwrappedCandidate = unwrappedQuotedValue(candidate)
return extractPortCandidate(from: unwrappedCandidate) ?? parsePortPrefix(unwrappedCandidate)
return extractPortCandidate(from: unwrappedCandidate)
?? extractURLPort(from: unwrappedCandidate)
?? parsePortPrefix(unwrappedCandidate)
}

return nil
Expand Down
3 changes: 3 additions & 0 deletions Sources/SlayNodeMenuBar/ServiceHistoryStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ enum WorkspaceHistoryHeuristics {
".bin",
".cache",
".claude",
".build",
".next",
".nuxt",
".pnpm-store",
".swiftpm",
".svelte-kit",
".turbo",
".yarn",
"build",
"cache",
"coverage",
"deriveddata",
"dist",
"node_modules",
"out",
Expand Down
39 changes: 38 additions & 1 deletion Tests/SlayNodeMenuBarTests/CommandParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,20 +148,50 @@ final class CommandParserTests: XCTestCase {
XCTAssertEqual(ports, [3000, 4173, 9229])
}

func testInferPortsFromEnvironmentAssignmentsWithURLValues() {
let tokens = [
"PORT=http://localhost:3000",
"WEB_PORT=https://127.0.0.1:4173/graphql",
"npm",
"run",
"dev"
]
let ports = CommandParser.inferPorts(from: tokens)

XCTAssertEqual(ports, [3000, 4173])
}

func testInferPortsFromEnvironmentAssignmentsWithShellDefaults() {
let tokens = [
"PORT=${PORT:-3000}",
"WEB_PORT=${WEB_PORT-4173}",
"APP_PORT=${APP_PORT:-\"8080\"}",
"ALT_PORT=${ALT_PORT:=9229}",
"FALLBACK_PORT=${FALLBACK_PORT=9333}",
"URL_PORT=${URL_PORT:-http://localhost:5050}",
"GRAPH_PORT=${GRAPH_PORT:=https://127.0.0.1:6060/graphql}",
"npm",
"run",
"dev"
]
let ports = CommandParser.inferPorts(from: tokens)

XCTAssertEqual(ports, [3000, 4173, 8080, 9229, 9333])
XCTAssertEqual(ports, [3000, 4173, 5050, 6060, 8080, 9229, 9333])
}

func testInferPortsFromSystemPropertyAssignments() {
let tokens = [
"java",
"-Dserver.port=8080",
"-Dquarkus.http.port=127.0.0.1:8181",
"-Dmanagement.server.port=https://localhost:9090",
"-Dreport=1234",
"-jar",
"app.jar"
]
let ports = CommandParser.inferPorts(from: tokens)

XCTAssertEqual(ports, [8080, 8181, 9090])
}

func testInferPortsFromSocketAddressFlags() {
Expand Down Expand Up @@ -214,6 +244,13 @@ final class CommandParserTests: XCTestCase {
XCTAssertEqual(ports, [5443])
}

func testInferPortsFromURLTokenWithClosingBracePunctuation() {
let tokens = ["node", "server.js", "https://localhost:7443/graphql}"]
let ports = CommandParser.inferPorts(from: tokens)

XCTAssertEqual(ports, [7443])
}

func testInferPortsDoesNotTreatBareIPv6AddressAsPort() {
let ports = CommandParser.inferPorts(from: ["node", "server.js", "[::1]"])

Expand Down
15 changes: 14 additions & 1 deletion Tests/SlayNodeMenuBarTests/WorkspaceHistoryHeuristicsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,20 @@ final class WorkspaceHistoryHeuristicsTests: XCTestCase {
let tempRoot = try makeTempDirectory()
defer { try? FileManager.default.removeItem(at: tempRoot) }

for name in ["coverage", "out", "storybook-static", ".next", ".turbo", ".pnpm-store", ".omx", ".codex", ".claude"] {
for name in [
"coverage",
"out",
"storybook-static",
".next",
".turbo",
".pnpm-store",
".omx",
".codex",
".claude",
".build",
".swiftpm",
"DerivedData"
] {
let workspace = WorkspaceIdentity(
id: tempRoot.appendingPathComponent(name).path.lowercased(),
name: name,
Expand Down
Loading