@@ -484,6 +484,97 @@ class AppDelegate: NSObject, NSApplicationDelegate {
484484 }
485485 }
486486
487+ private func showUpdateProgressWindow( ) -> ( NSWindow , NSTextView ) {
488+ let window = NSWindow (
489+ contentRect: NSRect ( x: 0 , y: 0 , width: 720 , height: 480 ) ,
490+ styleMask: [ . titled, . closable] ,
491+ backing: . buffered,
492+ defer: false
493+ )
494+ window. title = L ( " Updating Codex Discord " , " Codex Discord 업데이트 중 " )
495+ window. center ( )
496+
497+ let contentView = NSView ( frame: window. contentView? . bounds ?? . zero)
498+ contentView. autoresizingMask = [ . width, . height]
499+ window. contentView = contentView
500+
501+ let titleLabel = NSTextField ( labelWithString: L ( " Update in progress... " , " 업데이트 진행 중... " ) )
502+ titleLabel. font = NSFont . boldSystemFont ( ofSize: 15 )
503+ titleLabel. frame = NSRect ( x: 20 , y: 438 , width: 680 , height: 22 )
504+ contentView. addSubview ( titleLabel)
505+
506+ let descLabel = NSTextField ( labelWithString: L (
507+ " The log below shows each update step and command output. " ,
508+ " 아래 로그에 업데이트 단계와 명령 출력이 표시됩니다. "
509+ ) )
510+ descLabel. textColor = . secondaryLabelColor
511+ descLabel. frame = NSRect ( x: 20 , y: 416 , width: 680 , height: 18 )
512+ contentView. addSubview ( descLabel)
513+
514+ let scrollView = NSScrollView ( frame: NSRect ( x: 20 , y: 20 , width: 680 , height: 388 ) )
515+ scrollView. hasVerticalScroller = true
516+ scrollView. autohidesScrollers = true
517+
518+ let textView = NSTextView ( frame: scrollView. bounds)
519+ textView. isEditable = false
520+ textView. isSelectable = true
521+ textView. font = NSFont . monospacedSystemFont ( ofSize: 11 , weight: . regular)
522+ textView. textContainerInset = NSSize ( width: 8 , height: 8 )
523+ if #available( macOS 10 . 14 , * ) {
524+ textView. backgroundColor = . textBackgroundColor
525+ }
526+ scrollView. documentView = textView
527+ contentView. addSubview ( scrollView)
528+
529+ window. makeKeyAndOrderFront ( nil )
530+ NSApp . activate ( ignoringOtherApps: true )
531+ return ( window, textView)
532+ }
533+
534+ private func appendUpdateLog( _ textView: NSTextView , _ text: String ) {
535+ let normalized = text. replacingOccurrences ( of: " \r \n " , with: " \n " )
536+ DispatchQueue . main. async {
537+ let prefix = textView. string. hasSuffix ( " \n " ) || textView. string. isEmpty ? " " : " \n "
538+ textView. textStorage? . append ( NSAttributedString ( string: prefix + normalized) )
539+ if !normalized. hasSuffix ( " \n " ) {
540+ textView. textStorage? . append ( NSAttributedString ( string: " \n " ) )
541+ }
542+ textView. scrollToEndOfDocument ( nil )
543+ }
544+ }
545+
546+ @discardableResult
547+ private func runShellStreaming( _ command: String , onOutput: @escaping ( String ) -> Void ) -> Int32 {
548+ let task = Process ( )
549+ task. launchPath = " /bin/bash "
550+ task. arguments = [ " -c " , command]
551+
552+ let pipe = Pipe ( )
553+ task. standardOutput = pipe
554+ task. standardError = pipe
555+
556+ let handle = pipe. fileHandleForReading
557+ handle. readabilityHandler = { fileHandle in
558+ let data = fileHandle. availableData
559+ if data. isEmpty { return }
560+ if let text = String ( data: data, encoding: . utf8) , !text. isEmpty {
561+ onOutput ( text)
562+ }
563+ }
564+
565+ do {
566+ try task. run ( )
567+ task. waitUntilExit ( )
568+ } catch {
569+ onOutput ( error. localizedDescription + " \n " )
570+ handle. readabilityHandler = nil
571+ return - 1
572+ }
573+
574+ handle. readabilityHandler = nil
575+ return task. terminationStatus
576+ }
577+
487578 @objc private func performUpdate( ) {
488579 NSApp . activate ( ignoringOtherApps: true )
489580 let alert = NSAlert ( )
@@ -516,84 +607,207 @@ class AppDelegate: NSObject, NSApplicationDelegate {
516607 }
517608
518609 if alert. runModal ( ) == . alertFirstButtonReturn {
519- let wasRunning = isRunning ( )
520- if wasRunning {
521- runShell ( " launchctl unload ' \( plistDst) ' 2>/dev/null " )
522- }
610+ let ( progressWindow, logView) = showUpdateProgressWindow ( )
611+ appendUpdateLog ( logView, L ( " Starting update... " , " 업데이트를 시작합니다... " ) )
523612
524- let hasLocalChanges = !runShell( " cd ' \( botDir) ' && git status --porcelain 2>/dev/null " )
525- . trimmingCharacters ( in: . whitespacesAndNewlines)
526- . isEmpty
527- if hasLocalChanges {
528- _ = runShell ( " cd ' \( botDir) ' && git stash push -u -m codex-discord-auto-update >/dev/null 2>&1 " )
529- }
530-
531- runShell ( " cd ' \( botDir) ' && git fetch origin main --tags " )
532- let pullOutput = runShell ( " cd ' \( botDir) ' && git reset --hard origin/main 2>&1 " )
613+ DispatchQueue . global ( qos: . userInitiated) . async {
614+ let wasRunning = self . isRunning ( )
615+ if wasRunning {
616+ self . appendUpdateLog ( logView, self . L ( " Stopping running bot... " , " 실행 중인 봇을 중지합니다... " ) )
617+ _ = self . runShellStreaming ( " launchctl unload ' \( self . plistDst) ' 2>/dev/null " ) {
618+ self . appendUpdateLog ( logView, $0)
619+ }
620+ }
533621
534- let afterPull = runShell ( " cd ' \( botDir) ' && git rev-parse HEAD " ) . trimmingCharacters ( in : . whitespacesAndNewlines )
535- let remote = runShell ( " cd ' \( botDir ) ' && git rev-parse origin/main " ) . trimmingCharacters ( in: . whitespacesAndNewlines)
536- if !afterPull . isEmpty && !remote . isEmpty && afterPull != remote {
622+ let hasLocalChanges = ! self . runShell ( " cd ' \( self . botDir) ' && git status --porcelain 2>/dev/null " )
623+ . trimmingCharacters ( in: . whitespacesAndNewlines)
624+ . isEmpty
537625 if hasLocalChanges {
538- _ = runShell ( " cd ' \( botDir) ' && git stash pop >/dev/null 2>&1 " )
626+ self . appendUpdateLog ( logView, self . L ( " Stashing local changes... " , " 로컬 변경사항을 stash 합니다... " ) )
627+ _ = self . runShellStreaming ( " cd ' \( self . botDir) ' && git stash push -u -m codex-discord-auto-update 2>&1 " ) {
628+ self . appendUpdateLog ( logView, $0)
629+ }
539630 }
540- let errAlert = NSAlert ( )
541- errAlert. messageText = L ( " Update Failed " , " 업데이트 실패 " )
542- errAlert. informativeText = L ( " git pull failed: \n " , " git pull 실패: \n " ) + pullOutput
543- errAlert. alertStyle = . critical
544- errAlert. runModal ( )
545- if wasRunning {
546- generatePlist ( )
547- runShell ( " launchctl load ' \( plistDst) ' " )
631+
632+ self . appendUpdateLog ( logView, self . L ( " Fetching latest changes... " , " 최신 변경사항을 가져옵니다... " ) )
633+ let fetchStatus = self . runShellStreaming ( " cd ' \( self . botDir) ' && git fetch origin main --tags 2>&1 " ) {
634+ self . appendUpdateLog ( logView, $0)
548635 }
549- return
550- }
551636
552- let output = runShell ( " cd ' \( botDir) ' && npm install && npm rebuild better-sqlite3 && npm run build 2>&1 " )
637+ self . appendUpdateLog ( logView, self . L ( " Resetting to origin/main... " , " origin/main 기준으로 맞춥니다... " ) )
638+ let resetStatus = self . runShellStreaming ( " cd ' \( self . botDir) ' && git reset --hard origin/main 2>&1 " ) {
639+ self . appendUpdateLog ( logView, $0)
640+ }
553641
554- if hasLocalChanges {
555- _ = runShell ( " cd ' \( botDir) ' && git stash pop >/dev/null 2>&1 " )
556- }
642+ let afterPull = self . runShell ( " cd ' \( self . botDir) ' && git rev-parse HEAD " ) . trimmingCharacters ( in: . whitespacesAndNewlines)
643+ let remote = self . runShell ( " cd ' \( self . botDir) ' && git rev-parse origin/main " ) . trimmingCharacters ( in: . whitespacesAndNewlines)
644+ if fetchStatus != 0 || resetStatus != 0 || ( !afterPull. isEmpty && !remote. isEmpty && afterPull != remote) {
645+ if hasLocalChanges {
646+ self . appendUpdateLog ( logView, self . L ( " Restoring stashed changes... " , " stash 변경사항을 복원합니다... " ) )
647+ _ = self . runShellStreaming ( " cd ' \( self . botDir) ' && git stash pop 2>&1 " ) {
648+ self . appendUpdateLog ( logView, $0)
649+ }
650+ }
651+ if wasRunning {
652+ self . generatePlist ( )
653+ _ = self . runShell ( " launchctl load ' \( self . plistDst) ' " )
654+ }
655+ DispatchQueue . main. async {
656+ let errAlert = NSAlert ( )
657+ errAlert. messageText = self . L ( " Update Failed " , " 업데이트 실패 " )
658+ errAlert. informativeText = self . L (
659+ " git sync failed. Check the update log window for details. " ,
660+ " git 동기화가 실패했습니다. 자세한 내용은 업데이트 로그 창을 확인하세요. "
661+ )
662+ errAlert. alertStyle = . critical
663+ errAlert. runModal ( )
664+ progressWindow. makeKeyAndOrderFront ( nil )
665+ }
666+ return
667+ }
557668
558- currentVersion = getVersion ( )
559- updateAvailable = false
560-
561- // Rebuild the menu bar app itself and restart
562- let swiftSrc = " \( botDir) /menubar/CodexBotMenu.swift "
563- let swiftBin = " \( botDir) /menubar/CodexBotMenu "
564- if FileManager . default. fileExists ( atPath: swiftSrc) {
565- // Accept Xcode license if needed before compiling
566- let xcrunCheck = runShell ( " xcrun --find swiftc 2>&1 " )
567- if xcrunCheck. contains ( " license " ) || xcrunCheck. contains ( " error " ) {
568- runShell ( " osascript -e 'do shell script \" xcodebuild -license accept \" with administrator privileges' 2>/dev/null " )
669+ if hasLocalChanges {
670+ self . appendUpdateLog ( logView, self . L ( " Restoring stashed changes... " , " stash 변경사항을 복원합니다... " ) )
671+ _ = self . runShellStreaming ( " cd ' \( self . botDir) ' && git stash pop 2>&1 " ) {
672+ self . appendUpdateLog ( logView, $0)
673+ }
569674 }
570- runShell ( " swiftc -o ' \( swiftBin) ' ' \( swiftSrc) ' -framework Cocoa 2>&1 " )
571675
572- if wasRunning {
573- generatePlist ( )
574- runShell ( " launchctl load ' \( plistDst) ' " )
676+ self . appendUpdateLog ( logView, self . L ( " Installing npm dependencies... " , " npm 의존성을 설치합니다... " ) )
677+ let installStatus = self . runShellStreaming ( " cd ' \( self . botDir) ' && npm install 2>&1 " ) {
678+ self . appendUpdateLog ( logView, $0)
679+ }
680+ if installStatus != 0 {
681+ if wasRunning {
682+ self . generatePlist ( )
683+ _ = self . runShell ( " launchctl load ' \( self . plistDst) ' " )
684+ }
685+ DispatchQueue . main. async {
686+ let errAlert = NSAlert ( )
687+ errAlert. messageText = self . L ( " Update Failed " , " 업데이트 실패 " )
688+ errAlert. informativeText = self . L (
689+ " npm install failed. Check the update log window for details. " ,
690+ " npm install이 실패했습니다. 자세한 내용은 업데이트 로그 창을 확인하세요. "
691+ )
692+ errAlert. alertStyle = . critical
693+ errAlert. runModal ( )
694+ progressWindow. makeKeyAndOrderFront ( nil )
695+ }
696+ return
697+ }
698+
699+ self . appendUpdateLog ( logView, self . L ( " Rebuilding better-sqlite3... " , " better-sqlite3를 다시 빌드합니다... " ) )
700+ let rebuildStatus = self . runShellStreaming ( " cd ' \( self . botDir) ' && npm rebuild better-sqlite3 2>&1 " ) {
701+ self . appendUpdateLog ( logView, $0)
702+ }
703+ if rebuildStatus != 0 {
704+ if wasRunning {
705+ self . generatePlist ( )
706+ _ = self . runShell ( " launchctl load ' \( self . plistDst) ' " )
707+ }
708+ DispatchQueue . main. async {
709+ let errAlert = NSAlert ( )
710+ errAlert. messageText = self . L ( " Update Failed " , " 업데이트 실패 " )
711+ errAlert. informativeText = self . L (
712+ " Native rebuild failed. Check the update log window for details. " ,
713+ " 네이티브 재빌드가 실패했습니다. 자세한 내용은 업데이트 로그 창을 확인하세요. "
714+ )
715+ errAlert. alertStyle = . critical
716+ errAlert. runModal ( )
717+ progressWindow. makeKeyAndOrderFront ( nil )
718+ }
719+ return
575720 }
576721
577- // Restart menu bar app (detached from terminal)
578- runShell ( " nohup bash -c 'sleep 1 && \" \( swiftBin) \" ' > /dev/null 2>&1 & " )
722+ self . appendUpdateLog ( logView, self . L ( " Building project... " , " 프로젝트를 빌드합니다... " ) )
723+ let buildStatus = self . runShellStreaming ( " cd ' \( self . botDir) ' && npm run build 2>&1 " ) {
724+ self . appendUpdateLog ( logView, $0)
725+ }
726+ if buildStatus != 0 {
727+ if wasRunning {
728+ self . generatePlist ( )
729+ _ = self . runShell ( " launchctl load ' \( self . plistDst) ' " )
730+ }
731+ DispatchQueue . main. async {
732+ let errAlert = NSAlert ( )
733+ errAlert. messageText = self . L ( " Update Failed " , " 업데이트 실패 " )
734+ errAlert. informativeText = self . L (
735+ " Build failed. Check the update log window for details. " ,
736+ " 빌드가 실패했습니다. 자세한 내용은 업데이트 로그 창을 확인하세요. "
737+ )
738+ errAlert. alertStyle = . critical
739+ errAlert. runModal ( )
740+ progressWindow. makeKeyAndOrderFront ( nil )
741+ }
742+ return
743+ }
579744
580- NSApplication . shared. terminate ( nil )
581- return
582- }
745+ self . currentVersion = self . getVersion ( )
746+ self . updateAvailable = false
747+ self . appendUpdateLog ( logView, self . L ( " Updated to version: " , " 업데이트된 버전: " ) + self . currentVersion)
748+
749+ let swiftSrc = " \( self . botDir) /menubar/CodexBotMenu.swift "
750+ let swiftBin = " \( self . botDir) /menubar/CodexBotMenu "
751+ if FileManager . default. fileExists ( atPath: swiftSrc) {
752+ self . appendUpdateLog ( logView, self . L ( " Rebuilding menu bar app... " , " 메뉴바 앱을 다시 빌드합니다... " ) )
753+ let xcrunCheck = self . runShell ( " xcrun --find swiftc 2>&1 " )
754+ if xcrunCheck. contains ( " license " ) || xcrunCheck. contains ( " error " ) {
755+ self . appendUpdateLog ( logView, self . L ( " Requesting Xcode license approval... " , " Xcode 라이선스 승인을 요청합니다... " ) )
756+ _ = self . runShell ( " osascript -e 'do shell script \" xcodebuild -license accept \" with administrator privileges' 2>/dev/null " )
757+ }
583758
584- if wasRunning {
585- generatePlist ( )
586- runShell ( " launchctl load ' \( plistDst) ' " )
587- }
759+ let swiftStatus = self . runShellStreaming ( " swiftc -o ' \( swiftBin) ' ' \( swiftSrc) ' -framework Cocoa 2>&1 " ) {
760+ self . appendUpdateLog ( logView, $0)
761+ }
762+ if swiftStatus != 0 {
763+ if wasRunning {
764+ self . generatePlist ( )
765+ _ = self . runShell ( " launchctl load ' \( self . plistDst) ' " )
766+ }
767+ DispatchQueue . main. async {
768+ let errAlert = NSAlert ( )
769+ errAlert. messageText = self . L ( " Update Failed " , " 업데이트 실패 " )
770+ errAlert. informativeText = self . L (
771+ " Menu bar app rebuild failed. Check the update log window for details. " ,
772+ " 메뉴바 앱 재빌드가 실패했습니다. 자세한 내용은 업데이트 로그 창을 확인하세요. "
773+ )
774+ errAlert. alertStyle = . critical
775+ errAlert. runModal ( )
776+ progressWindow. makeKeyAndOrderFront ( nil )
777+ }
778+ return
779+ }
588780
589- let doneAlert = NSAlert ( )
590- doneAlert. messageText = L ( " Update Complete " , " 업데이트 완료 " )
591- doneAlert. informativeText = L ( " Updated to version: " , " 업데이트된 버전: " ) + currentVersion + " \n \n " + output
592- doneAlert. alertStyle = . informational
593- doneAlert. runModal ( )
781+ if wasRunning {
782+ self . generatePlist ( )
783+ _ = self . runShell ( " launchctl load ' \( self . plistDst) ' " )
784+ }
594785
595- updateStatus ( )
596- buildMenu ( )
786+ self . appendUpdateLog ( logView, self . L ( " Restarting menu bar app... " , " 메뉴바 앱을 재시작합니다... " ) )
787+ _ = self . runShell ( " nohup bash -c 'sleep 1 && \" \( swiftBin) \" ' > /dev/null 2>&1 & " )
788+ DispatchQueue . main. async {
789+ NSApplication . shared. terminate ( nil )
790+ }
791+ return
792+ }
793+
794+ if wasRunning {
795+ self . generatePlist ( )
796+ _ = self . runShell ( " launchctl load ' \( self . plistDst) ' " )
797+ }
798+
799+ DispatchQueue . main. async {
800+ progressWindow. close ( )
801+ let doneAlert = NSAlert ( )
802+ doneAlert. messageText = self . L ( " Update Complete " , " 업데이트 완료 " )
803+ doneAlert. informativeText = self . L ( " Updated to version: " , " 업데이트된 버전: " ) + self . currentVersion
804+ doneAlert. alertStyle = . informational
805+ doneAlert. runModal ( )
806+
807+ self . updateStatus ( )
808+ self . buildMenu ( )
809+ }
810+ }
597811 }
598812 }
599813
0 commit comments