-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.swift
309 lines (253 loc) · 11.3 KB
/
main.swift
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
import Foundation
import CryptoKit
struct RepoInitializer {
static let tokenFile = "\(FileManager.default.homeDirectoryForCurrentUser.path)/.mr_token"
static func getDeviceIdentifier() -> String {
// Try to get persistent machine ID first
if let id = try? String(contentsOf: URL(fileURLWithPath: "/etc/machine-id"), encoding: .utf8) {
return id.trimmingCharacters(in: .whitespacesAndNewlines)
}
// On macOS, get IOPlatformUUID which is persistent
let task = Process()
task.launchPath = "/usr/sbin/ioreg"
task.arguments = ["-d2", "-c", "IOPlatformExpertDevice"]
let pipe = Pipe()
task.standardOutput = pipe
do {
try task.run()
task.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
if let range = output.range(of: "IOPlatformUUID\" = \"([^\"]+)\"", options: .regularExpression),
let uuidRange = output[range].range(of: "\"([^\"]+)\"$", options: .regularExpression) {
return String(output[uuidRange]).replacingOccurrences(of: "\"", with: "")
}
} catch {
// If we can't get a persistent ID, we should fail rather than use a random one
fatalError("Could not get persistent machine identifier")
}
// If we couldn't get a persistent ID, we should fail rather than risk inconsistent encryption
fatalError("Could not get persistent machine identifier")
}
static func encryptToken(_ token: String) throws -> String {
let deviceId = getDeviceIdentifier()
let key = SymmetricKey(data: SHA256.hash(data: deviceId.data(using: .utf8)!))
let tokenData = token.data(using: .utf8)!
let sealedBox = try AES.GCM.seal(tokenData, using: key)
return sealedBox.combined!.base64EncodedString()
}
static func decryptToken(_ encrypted: String) throws -> String {
let deviceId = getDeviceIdentifier()
let key = SymmetricKey(data: SHA256.hash(data: deviceId.data(using: .utf8)!))
let data = Data(base64Encoded: encrypted)!
let sealedBox = try AES.GCM.SealedBox(combined: data)
let decryptedData = try AES.GCM.open(sealedBox, using: key)
return String(data: decryptedData, encoding: .utf8)!
}
static func getStoredToken() throws -> String? {
guard FileManager.default.fileExists(atPath: tokenFile),
let encrypted = try? String(contentsOfFile: tokenFile, encoding: .utf8) else {
return nil
}
return try decryptToken(encrypted)
}
static func validateToken(_ token: String) -> (isValid: Bool, message: String?) {
// Check if token starts with expected prefix
if !token.starts(with: "ghp_") {
return (false, "Token must start with 'ghp_'")
}
// Check for valid characters (alphanumeric)
if !token.dropFirst(4).allSatisfy({ $0.isLetter || $0.isNumber }) {
return (false, "Token should only contain letters and numbers after 'ghp_'")
}
return (true, nil)
}
static func storeToken(_ token: String) throws {
// Get the directory path
let tokenDir = (tokenFile as NSString).deletingLastPathComponent
// Create directory if it doesn't exist
if !FileManager.default.fileExists(atPath: tokenDir) {
try FileManager.default.createDirectory(
atPath: tokenDir,
withIntermediateDirectories: true,
attributes: [FileAttributeKey.posixPermissions: 0o700]
)
}
// Encrypt and store token
let encrypted = try encryptToken(token)
try encrypted.write(toFile: tokenFile, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: tokenFile)
}
static func promptForNewToken() throws -> String {
var attempts = 0
let maxAttempts = 3
while attempts < maxAttempts {
print("""
🔑 GitHub Personal Access Token (PAT) required.
To generate a new PAT:
1. Visit: https://github.com/settings/tokens
2. Click "Generate new token" (classic)
3. Give it a name (e.g., "mr-tool")
4. Select scopes: 'repo' and 'workflow'
5. Click "Generate token"
6. Copy the generated token and paste it below
""")
print("\nPAT: ", terminator: "")
guard let input = readLine()?.trimmingCharacters(in: .whitespacesAndNewlines),
!input.isEmpty else {
attempts += 1
print("\n❌ Token cannot be empty")
continue
}
let validation = validateToken(input)
if !validation.isValid {
attempts += 1
print("\n❌ Invalid token format: \(validation.message ?? "unknown error")")
if attempts < maxAttempts {
print("Please try again (\(maxAttempts - attempts) attempts remaining)\n")
}
continue
}
try storeToken(input)
return input
}
throw NSError(domain: "TokenError", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Maximum token entry attempts exceeded"
])
}
static func getToken() throws -> String {
if let token = try getStoredToken() {
return token
}
return try promptForNewToken()
}
static func isGitHubAuthenticated() -> Bool {
let task = Process()
task.launchPath = "/opt/homebrew/bin/gh"
task.arguments = ["auth", "status"]
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
do {
try task.run()
task.waitUntilExit()
return task.terminationStatus == 0
} catch {
return false
}
}
static func clearGitHubAuth() {
// Attempt to logout, ignore any errors
let task = Process()
task.launchPath = "/opt/homebrew/bin/gh"
task.arguments = ["auth", "logout", "--hostname", "github.com"]
try? task.run()
task.waitUntilExit()
}
static func clearScreen() {
print("\u{001B}[2J\u{001B}[H", terminator: "")
}
static func run() {
guard CommandLine.arguments.count > 1 else {
print("Usage: mr <repo-name>")
exit(1)
}
let repoName = CommandLine.arguments[1]
do {
if !isGitHubAuthenticated() {
clearGitHubAuth()
let token = try getToken()
// Instead of trying to login, just set the token in environment
var environment = ProcessInfo.processInfo.environment
environment["GH_TOKEN"] = token
print("📦 Creating repository '\(repoName)' on GitHub...")
let task = Process()
task.environment = environment
task.launchPath = "/opt/homebrew/bin/gh"
task.arguments = [
"repo", "create", repoName,
"--private",
"--clone"
]
try task.run()
task.waitUntilExit()
if task.terminationStatus != 0 {
print("\n❌ Authentication failed: Invalid GitHub token")
try? FileManager.default.removeItem(atPath: tokenFile)
exit(1)
}
}
FileManager.default.changeCurrentDirectoryPath(repoName)
let readmeContent = "# \(repoName)\n\nProject repository created with mr"
try readmeContent.write(
toFile: "README.md",
atomically: true,
encoding: .utf8
)
print("📝 Creating initial commit...")
try runCommand(command: "/usr/bin/git", arguments: ["add", "."])
try runCommand(
command: "/usr/bin/git",
arguments: ["commit", "-m", "Initial commit"]
)
print("🚀 Pushing to GitHub...")
try runCommand(command: "/usr/bin/git", arguments: ["push", "-u", "origin", "main"])
print("✅ Successfully created repository '\(repoName)'!")
print(" Local path: \(FileManager.default.currentDirectoryPath)")
print(" GitHub URL: https://github.com/\(try getGitHubUser())/\(repoName)")
} catch {
print("Error: \(error.localizedDescription)")
exit(1)
}
}
static func getGitHubUser() throws -> String {
let task = Process()
task.launchPath = "/opt/homebrew/bin/gh"
task.arguments = ["api", "user", "--jq", ".login"]
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
try task.run()
task.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let username = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw NSError(domain: "GitHubError", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Could not get GitHub username"
])
}
return username
}
static func runCommand(command: String, arguments: [String], input: String? = nil) throws {
let task = Process()
task.launchPath = command
task.arguments = arguments
let outputPipe = Pipe()
task.standardOutput = outputPipe
task.standardError = outputPipe
if let input = input {
let inputPipe = Pipe()
task.standardInput = inputPipe
// Write the input with a newline and flush immediately
let inputString = input + "\n"
try inputPipe.fileHandleForWriting.write(contentsOf: inputString.data(using: .utf8)!)
try inputPipe.fileHandleForWriting.synchronize()
try inputPipe.fileHandleForWriting.close()
}
try task.run()
task.waitUntilExit()
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
if task.terminationStatus != 0 {
if let errorMessage = String(data: outputData, encoding: .utf8) {
throw NSError(domain: "CommandError", code: Int(task.terminationStatus), userInfo: [
NSLocalizedDescriptionKey: errorMessage.trimmingCharacters(in: .whitespacesAndNewlines)
])
}
}
// Print output for debugging
if let output = String(data: outputData, encoding: .utf8), !output.isEmpty {
print(output)
}
}
}
// Entry point
RepoInitializer.run()