File size: 21,472 Bytes
4fc4790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
import Foundation

enum CommandResolver {
    private static let projectRootDefaultsKey = "openclaw.gatewayProjectRootPath"
    private static let helperName = "openclaw"

    static func gatewayEntrypoint(in root: URL) -> String? {
        let distEntry = root.appendingPathComponent("dist/index.js").path
        if FileManager().isReadableFile(atPath: distEntry) { return distEntry }
        let openclawEntry = root.appendingPathComponent("openclaw.mjs").path
        if FileManager().isReadableFile(atPath: openclawEntry) { return openclawEntry }
        let binEntry = root.appendingPathComponent("bin/openclaw.js").path
        if FileManager().isReadableFile(atPath: binEntry) { return binEntry }
        return nil
    }

    static func runtimeResolution() -> Result<RuntimeResolution, RuntimeResolutionError> {
        RuntimeLocator.resolve(searchPaths: self.preferredPaths())
    }

    static func runtimeResolution(searchPaths: [String]?) -> Result<RuntimeResolution, RuntimeResolutionError> {
        RuntimeLocator.resolve(searchPaths: searchPaths ?? self.preferredPaths())
    }

    static func makeRuntimeCommand(
        runtime: RuntimeResolution,
        entrypoint: String,
        subcommand: String,
        extraArgs: [String]) -> [String]
    {
        [runtime.path, entrypoint, subcommand] + extraArgs
    }

    static func runtimeErrorCommand(_ error: RuntimeResolutionError) -> [String] {
        let message = RuntimeLocator.describeFailure(error)
        return self.errorCommand(with: message)
    }

    static func errorCommand(with message: String) -> [String] {
        let script = """
        cat <<'__OPENCLAW_ERR__' >&2
        \(message)
        __OPENCLAW_ERR__
        exit 1
        """
        return ["/bin/sh", "-c", script]
    }

    static func projectRoot() -> URL {
        if let stored = UserDefaults.standard.string(forKey: self.projectRootDefaultsKey),
           let url = self.expandPath(stored),
           FileManager().fileExists(atPath: url.path)
        {
            return url
        }
        let fallback = FileManager().homeDirectoryForCurrentUser
            .appendingPathComponent("Projects/openclaw")
        if FileManager().fileExists(atPath: fallback.path) {
            return fallback
        }
        return FileManager().homeDirectoryForCurrentUser
    }

    static func setProjectRoot(_ path: String) {
        UserDefaults.standard.set(path, forKey: self.projectRootDefaultsKey)
    }

    static func projectRootPath() -> String {
        self.projectRoot().path
    }

    static func preferredPaths() -> [String] {
        let current = ProcessInfo.processInfo.environment["PATH"]?
            .split(separator: ":").map(String.init) ?? []
        let home = FileManager().homeDirectoryForCurrentUser
        let projectRoot = self.projectRoot()
        return self.preferredPaths(home: home, current: current, projectRoot: projectRoot)
    }

    static func preferredPaths(home: URL, current: [String], projectRoot: URL) -> [String] {
        var extras = [
            home.appendingPathComponent("Library/pnpm").path,
            "/opt/homebrew/bin",
            "/usr/local/bin",
            "/usr/bin",
            "/bin",
        ]
        #if DEBUG
        // Dev-only convenience. Avoid project-local PATH hijacking in release builds.
        extras.insert(projectRoot.appendingPathComponent("node_modules/.bin").path, at: 0)
        #endif
        let openclawPaths = self.openclawManagedPaths(home: home)
        if !openclawPaths.isEmpty {
            extras.insert(contentsOf: openclawPaths, at: 1)
        }
        extras.insert(contentsOf: self.nodeManagerBinPaths(home: home), at: 1 + openclawPaths.count)
        var seen = Set<String>()
        // Preserve order while stripping duplicates so PATH lookups remain deterministic.
        return (extras + current).filter { seen.insert($0).inserted }
    }

    private static func openclawManagedPaths(home: URL) -> [String] {
        let bases = [
            home.appendingPathComponent(".openclaw"),
        ]
        var paths: [String] = []
        for base in bases {
            let bin = base.appendingPathComponent("bin")
            let nodeBin = base.appendingPathComponent("tools/node/bin")
            if FileManager().fileExists(atPath: bin.path) {
                paths.append(bin.path)
            }
            if FileManager().fileExists(atPath: nodeBin.path) {
                paths.append(nodeBin.path)
            }
        }
        return paths
    }

    private static func nodeManagerBinPaths(home: URL) -> [String] {
        var bins: [String] = []

        // Volta
        let volta = home.appendingPathComponent(".volta/bin")
        if FileManager().fileExists(atPath: volta.path) {
            bins.append(volta.path)
        }

        // asdf
        let asdf = home.appendingPathComponent(".asdf/shims")
        if FileManager().fileExists(atPath: asdf.path) {
            bins.append(asdf.path)
        }

        // fnm
        bins.append(contentsOf: self.versionedNodeBinPaths(
            base: home.appendingPathComponent(".local/share/fnm/node-versions"),
            suffix: "installation/bin"))

        // nvm
        bins.append(contentsOf: self.versionedNodeBinPaths(
            base: home.appendingPathComponent(".nvm/versions/node"),
            suffix: "bin"))

        return bins
    }

    private static func versionedNodeBinPaths(base: URL, suffix: String) -> [String] {
        guard FileManager().fileExists(atPath: base.path) else { return [] }
        let entries: [String]
        do {
            entries = try FileManager().contentsOfDirectory(atPath: base.path)
        } catch {
            return []
        }

        func parseVersion(_ name: String) -> [Int] {
            let trimmed = name.hasPrefix("v") ? String(name.dropFirst()) : name
            return trimmed.split(separator: ".").compactMap { Int($0) }
        }

        let sorted = entries.sorted { a, b in
            let va = parseVersion(a)
            let vb = parseVersion(b)
            let maxCount = max(va.count, vb.count)
            for i in 0..<maxCount {
                let ai = i < va.count ? va[i] : 0
                let bi = i < vb.count ? vb[i] : 0
                if ai != bi { return ai > bi }
            }
            // If identical numerically, keep stable ordering.
            return a > b
        }

        var paths: [String] = []
        for entry in sorted {
            let binDir = base.appendingPathComponent(entry).appendingPathComponent(suffix)
            let node = binDir.appendingPathComponent("node")
            if FileManager().isExecutableFile(atPath: node.path) {
                paths.append(binDir.path)
            }
        }
        return paths
    }

    static func findExecutable(named name: String, searchPaths: [String]? = nil) -> String? {
        for dir in searchPaths ?? self.preferredPaths() {
            let candidate = (dir as NSString).appendingPathComponent(name)
            if FileManager().isExecutableFile(atPath: candidate) {
                return candidate
            }
        }
        return nil
    }

    static func openclawExecutable(searchPaths: [String]? = nil) -> String? {
        self.findExecutable(named: self.helperName, searchPaths: searchPaths)
    }

    static func projectOpenClawExecutable(projectRoot: URL? = nil) -> String? {
        #if DEBUG
        let root = projectRoot ?? self.projectRoot()
        let candidate = root.appendingPathComponent("node_modules/.bin").appendingPathComponent(self.helperName).path
        return FileManager().isExecutableFile(atPath: candidate) ? candidate : nil
        #else
        return nil
        #endif
    }

    static func nodeCliPath() -> String? {
        let root = self.projectRoot()
        let candidates = [
            root.appendingPathComponent("openclaw.mjs").path,
            root.appendingPathComponent("bin/openclaw.js").path,
        ]
        for candidate in candidates where FileManager().isReadableFile(atPath: candidate) {
            return candidate
        }
        return nil
    }

    static func hasAnyOpenClawInvoker(searchPaths: [String]? = nil) -> Bool {
        if self.openclawExecutable(searchPaths: searchPaths) != nil { return true }
        if self.findExecutable(named: "pnpm", searchPaths: searchPaths) != nil { return true }
        if self.findExecutable(named: "node", searchPaths: searchPaths) != nil,
           self.nodeCliPath() != nil
        {
            return true
        }
        return false
    }

    static func openclawNodeCommand(
        subcommand: String,
        extraArgs: [String] = [],
        defaults: UserDefaults = .standard,
        configRoot: [String: Any]? = nil,
        searchPaths: [String]? = nil) -> [String]
    {
        let settings = self.connectionSettings(defaults: defaults, configRoot: configRoot)
        if settings.mode == .remote, let ssh = self.sshNodeCommand(
            subcommand: subcommand,
            extraArgs: extraArgs,
            settings: settings)
        {
            return ssh
        }

        let runtimeResult = self.runtimeResolution(searchPaths: searchPaths)

        switch runtimeResult {
        case let .success(runtime):
            let root = self.projectRoot()
            if let openclawPath = self.projectOpenClawExecutable(projectRoot: root) {
                return [openclawPath, subcommand] + extraArgs
            }

            if let entry = self.gatewayEntrypoint(in: root) {
                return self.makeRuntimeCommand(
                    runtime: runtime,
                    entrypoint: entry,
                    subcommand: subcommand,
                    extraArgs: extraArgs)
            }
            if let pnpm = self.findExecutable(named: "pnpm", searchPaths: searchPaths) {
                // Use --silent to avoid pnpm lifecycle banners that would corrupt JSON outputs.
                return [pnpm, "--silent", "openclaw", subcommand] + extraArgs
            }
            if let openclawPath = self.openclawExecutable(searchPaths: searchPaths) {
                return [openclawPath, subcommand] + extraArgs
            }

            let missingEntry = """
            openclaw entrypoint missing (looked for dist/index.js or openclaw.mjs); run pnpm build.
            """
            return self.errorCommand(with: missingEntry)

        case let .failure(error):
            return self.runtimeErrorCommand(error)
        }
    }

    static func openclawCommand(
        subcommand: String,
        extraArgs: [String] = [],
        defaults: UserDefaults = .standard,
        configRoot: [String: Any]? = nil,
        searchPaths: [String]? = nil) -> [String]
    {
        self.openclawNodeCommand(
            subcommand: subcommand,
            extraArgs: extraArgs,
            defaults: defaults,
            configRoot: configRoot,
            searchPaths: searchPaths)
    }

    // MARK: - SSH helpers

    private static func sshNodeCommand(subcommand: String, extraArgs: [String], settings: RemoteSettings) -> [String]? {
        guard !settings.target.isEmpty else { return nil }
        guard let parsed = self.parseSSHTarget(settings.target) else { return nil }

        // Run the real openclaw CLI on the remote host.
        let exportedPath = [
            "/opt/homebrew/bin",
            "/usr/local/bin",
            "/usr/bin",
            "/bin",
            "/usr/sbin",
            "/sbin",
            "$HOME/Library/pnpm",
            "$PATH",
        ].joined(separator: ":")
        let quotedArgs = ([subcommand] + extraArgs).map(self.shellQuote).joined(separator: " ")
        let userPRJ = settings.projectRoot.trimmingCharacters(in: .whitespacesAndNewlines)
        let userCLI = settings.cliPath.trimmingCharacters(in: .whitespacesAndNewlines)

        let projectSection = if userPRJ.isEmpty {
            """
            DEFAULT_PRJ="$HOME/Projects/openclaw"
            if [ -d "$DEFAULT_PRJ" ]; then
              PRJ="$DEFAULT_PRJ"
              cd "$PRJ" || { echo "Project root not found: $PRJ"; exit 127; }
            fi
            """
        } else {
            """
            PRJ=\(self.shellQuote(userPRJ))
            cd "$PRJ" || { echo "Project root not found: $PRJ"; exit 127; }
            """
        }

        let cliSection = if userCLI.isEmpty {
            ""
        } else {
            """
            CLI_HINT=\(self.shellQuote(userCLI))
            if [ -n "$CLI_HINT" ]; then
              if [ -x "$CLI_HINT" ]; then
                CLI="$CLI_HINT"
                "$CLI_HINT" \(quotedArgs);
                exit $?;
              elif [ -f "$CLI_HINT" ]; then
                if command -v node >/dev/null 2>&1; then
                  CLI="node $CLI_HINT"
                  node "$CLI_HINT" \(quotedArgs);
                  exit $?;
                fi
              fi
            fi
            """
        }

        let scriptBody = """
        PATH=\(exportedPath);
        CLI="";
        \(cliSection)
        \(projectSection)
        if command -v openclaw >/dev/null 2>&1; then
          CLI="$(command -v openclaw)"
          openclaw \(quotedArgs);
        elif [ -n "${PRJ:-}" ] && [ -f "$PRJ/dist/index.js" ]; then
          if command -v node >/dev/null 2>&1; then
            CLI="node $PRJ/dist/index.js"
            node "$PRJ/dist/index.js" \(quotedArgs);
          else
            echo "Node >=22 required on remote host"; exit 127;
          fi
        elif [ -n "${PRJ:-}" ] && [ -f "$PRJ/openclaw.mjs" ]; then
          if command -v node >/dev/null 2>&1; then
            CLI="node $PRJ/openclaw.mjs"
            node "$PRJ/openclaw.mjs" \(quotedArgs);
          else
            echo "Node >=22 required on remote host"; exit 127;
          fi
        elif [ -n "${PRJ:-}" ] && [ -f "$PRJ/bin/openclaw.js" ]; then
          if command -v node >/dev/null 2>&1; then
            CLI="node $PRJ/bin/openclaw.js"
            node "$PRJ/bin/openclaw.js" \(quotedArgs);
          else
            echo "Node >=22 required on remote host"; exit 127;
          fi
        elif command -v pnpm >/dev/null 2>&1; then
          CLI="pnpm --silent openclaw"
          pnpm --silent openclaw \(quotedArgs);
        else
          echo "openclaw CLI missing on remote host"; exit 127;
        fi
        """
        let options: [String] = [
            "-o", "BatchMode=yes",
            "-o", "StrictHostKeyChecking=accept-new",
            "-o", "UpdateHostKeys=yes",
        ]
        let args = self.sshArguments(
            target: parsed,
            identity: settings.identity,
            options: options,
            remoteCommand: ["/bin/sh", "-c", scriptBody])
        return ["/usr/bin/ssh"] + args
    }

    struct RemoteSettings {
        let mode: AppState.ConnectionMode
        let target: String
        let identity: String
        let projectRoot: String
        let cliPath: String
    }

    static func connectionSettings(
        defaults: UserDefaults = .standard,
        configRoot: [String: Any]? = nil) -> RemoteSettings
    {
        let root = configRoot ?? OpenClawConfigFile.loadDict()
        let mode = ConnectionModeResolver.resolve(root: root, defaults: defaults).mode
        let target = defaults.string(forKey: remoteTargetKey) ?? ""
        let identity = defaults.string(forKey: remoteIdentityKey) ?? ""
        let projectRoot = defaults.string(forKey: remoteProjectRootKey) ?? ""
        let cliPath = defaults.string(forKey: remoteCliPathKey) ?? ""
        return RemoteSettings(
            mode: mode,
            target: self.sanitizedTarget(target),
            identity: identity,
            projectRoot: projectRoot,
            cliPath: cliPath)
    }

    static func connectionModeIsRemote(defaults: UserDefaults = .standard) -> Bool {
        self.connectionSettings(defaults: defaults).mode == .remote
    }

    private static func sanitizedTarget(_ raw: String) -> String {
        let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.hasPrefix("ssh ") {
            return trimmed.replacingOccurrences(of: "ssh ", with: "").trimmingCharacters(in: .whitespacesAndNewlines)
        }
        return trimmed
    }

    struct SSHParsedTarget {
        let user: String?
        let host: String
        let port: Int
    }

    static func parseSSHTarget(_ target: String) -> SSHParsedTarget? {
        let trimmed = self.normalizeSSHTargetInput(target)
        guard !trimmed.isEmpty else { return nil }
        if trimmed.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines.union(.controlCharacters)) != nil {
            return nil
        }
        let userHostPort: String
        let user: String?
        if let atRange = trimmed.range(of: "@") {
            user = String(trimmed[..<atRange.lowerBound])
            userHostPort = String(trimmed[atRange.upperBound...])
        } else {
            user = nil
            userHostPort = trimmed
        }

        let host: String
        let port: Int
        if let colon = userHostPort.lastIndex(of: ":"), colon != userHostPort.startIndex {
            host = String(userHostPort[..<colon])
            let portStr = String(userHostPort[userHostPort.index(after: colon)...])
            guard let parsedPort = Int(portStr), parsedPort > 0, parsedPort <= 65535 else {
                return nil
            }
            port = parsedPort
        } else {
            host = userHostPort
            port = 22
        }

        return self.makeSSHTarget(user: user, host: host, port: port)
    }

    static func sshTargetValidationMessage(_ target: String) -> String? {
        let trimmed = self.normalizeSSHTargetInput(target)
        guard !trimmed.isEmpty else { return nil }
        if trimmed.hasPrefix("-") {
            return "SSH target cannot start with '-'"
        }
        if trimmed.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines.union(.controlCharacters)) != nil {
            return "SSH target cannot contain spaces"
        }
        if self.parseSSHTarget(trimmed) == nil {
            return "SSH target must look like user@host[:port]"
        }
        return nil
    }

    private static func shellQuote(_ text: String) -> String {
        if text.isEmpty { return "''" }
        let escaped = text.replacingOccurrences(of: "'", with: "'\\''")
        return "'\(escaped)'"
    }

    private static func expandPath(_ path: String) -> URL? {
        var expanded = path
        if expanded.hasPrefix("~") {
            let home = FileManager().homeDirectoryForCurrentUser.path
            expanded.replaceSubrange(expanded.startIndex...expanded.startIndex, with: home)
        }
        return URL(fileURLWithPath: expanded)
    }

    private static func normalizeSSHTargetInput(_ target: String) -> String {
        var trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.hasPrefix("ssh ") {
            trimmed = trimmed.replacingOccurrences(of: "ssh ", with: "")
                .trimmingCharacters(in: .whitespacesAndNewlines)
        }
        return trimmed
    }

    private static func isValidSSHComponent(_ value: String, allowLeadingDash: Bool = false) -> Bool {
        if value.isEmpty { return false }
        if !allowLeadingDash, value.hasPrefix("-") { return false }
        let invalid = CharacterSet.whitespacesAndNewlines.union(.controlCharacters)
        return value.rangeOfCharacter(from: invalid) == nil
    }

    static func makeSSHTarget(user: String?, host: String, port: Int) -> SSHParsedTarget? {
        let trimmedHost = host.trimmingCharacters(in: .whitespacesAndNewlines)
        guard self.isValidSSHComponent(trimmedHost) else { return nil }
        let trimmedUser = user?.trimmingCharacters(in: .whitespacesAndNewlines)
        let normalizedUser: String?
        if let trimmedUser {
            guard self.isValidSSHComponent(trimmedUser) else { return nil }
            normalizedUser = trimmedUser.isEmpty ? nil : trimmedUser
        } else {
            normalizedUser = nil
        }
        guard port > 0, port <= 65535 else { return nil }
        return SSHParsedTarget(user: normalizedUser, host: trimmedHost, port: port)
    }

    private static func sshTargetString(_ target: SSHParsedTarget) -> String {
        target.user.map { "\($0)@\(target.host)" } ?? target.host
    }

    static func sshArguments(
        target: SSHParsedTarget,
        identity: String,
        options: [String],
        remoteCommand: [String] = []) -> [String]
    {
        var args = options
        if target.port > 0 {
            args.append(contentsOf: ["-p", String(target.port)])
        }
        let trimmedIdentity = identity.trimmingCharacters(in: .whitespacesAndNewlines)
        if !trimmedIdentity.isEmpty {
            // Only use IdentitiesOnly when an explicit identity file is provided.
            // This allows 1Password SSH agent and other SSH agents to provide keys.
            args.append(contentsOf: ["-o", "IdentitiesOnly=yes"])
            args.append(contentsOf: ["-i", trimmedIdentity])
        }
        args.append("--")
        args.append(self.sshTargetString(target))
        args.append(contentsOf: remoteCommand)
        return args
    }

    #if SWIFT_PACKAGE
    static func _testNodeManagerBinPaths(home: URL) -> [String] {
        self.nodeManagerBinPaths(home: home)
    }
    #endif
}