Files
hapi/ios/Packages/HapiKit/Sources/HapiProtocol/Models/FilesApi.swift
T
weishu 8417c8b33a fix(ios): decode fractional fs-mtime epoch fields (device-feedback parity)
Machine cli mtimes and file/directory modified are Double on the wire
(fs.stat mtimeMs carries sub-ms precision); integer decode threw on real
hub data. Mirrors the Android LenientEpochMs fix; 461 package tests green
in the Linux harness.
2026-08-18 16:27:32 +08:00

117 lines
3.6 KiB
Swift

import Foundation
// Wire types of the git & files surface (A-M4a):
// `docs/api/client-contract/rest.md` "Git & files"; shapes from
// `shared/src/apiTypes.ts` (`CommandResponse`, `FileReadResponse`,
// `FileSearchResponse`, `ListDirectoryResponse`), field-for-field in lockstep
// with the Android port (`app.hapi.protocol.wire.FilesApi`). All are
// RPC-wrapped — HTTP 200 with `success: false` is a normal failure, callers
// must check. The session `DirectoryEntry` row already lives in
// `ApiResponses.swift` (shared with the machine list-directory RPC family).
/// `GET /api/sessions/:id/git-status|git-diff-numstat|git-diff-file` —
/// `CommandResponse`: the hub relays raw git stdout verbatim; clients parse
/// it themselves (`HapiProtocol/Git/` twins `web/src/lib/gitParsers.ts`).
public struct GitCommandResponse: Codable, Equatable, Sendable {
public var success: Bool
public var stdout: String?
public var stderr: String?
public var exitCode: Int?
public var error: String?
public init(
success: Bool,
stdout: String? = nil,
stderr: String? = nil,
exitCode: Int? = nil,
error: String? = nil
) {
self.success = success
self.stdout = stdout
self.stderr = stderr
self.exitCode = exitCode
self.error = error
}
}
/// `GET /api/sessions/:id/file?path=` — `content` is **base64** (decode
/// before display).
public struct FileReadResponse: Codable, Equatable, Sendable {
public var success: Bool
public var content: String?
/// Bytes on disk.
public var size: Int?
/// Epoch ms.
public var modified: Double?
public var error: String?
public init(
success: Bool,
content: String? = nil,
size: Int? = nil,
modified: Double? = nil,
error: String? = nil
) {
self.success = success
self.content = content
self.size = size
self.modified = modified
self.error = error
}
}
/// `GET /api/sessions/:id/files?query=&limit=` (ripgrep-backed search).
public struct FileSearchResponse: Codable, Equatable, Sendable {
public var success: Bool
public var files: [FileSearchItem]?
public var error: String?
public init(success: Bool, files: [FileSearchItem]? = nil, error: String? = nil) {
self.success = success
self.files = files
self.error = error
}
}
public struct FileSearchItem: Codable, Equatable, Sendable {
public var fileName: String
/// Directory part; empty at the session root.
public var filePath: String
public var fullPath: String
/// `'file' | 'folder'` (search results are files in practice).
public var fileType: String
public var size: Int?
/// Epoch ms.
public var modified: Double?
public init(
fileName: String,
filePath: String,
fullPath: String,
fileType: String,
size: Int? = nil,
modified: Double? = nil
) {
self.fileName = fileName
self.filePath = filePath
self.fullPath = fullPath
self.fileType = fileType
self.size = size
self.modified = modified
}
}
/// `GET /api/sessions/:id/directory?path=` — empty path = session root.
/// Entries are the shared session `DirectoryEntry` rows (`ApiResponses.swift`).
public struct ListDirectoryResponse: Codable, Equatable, Sendable {
public var success: Bool
public var entries: [DirectoryEntry]?
public var error: String?
public init(success: Bool, entries: [DirectoryEntry]? = nil, error: String? = nil) {
self.success = success
self.entries = entries
self.error = error
}
}