vscode extensions
This commit is contained in:
+85
-2
@@ -61,6 +61,7 @@ type Service struct {
|
||||
auditLog *audit.Logger
|
||||
prefs *preferences.Manager
|
||||
ctx context.Context
|
||||
eventSink func(string, interface{})
|
||||
|
||||
// secretChans maps request IDs to channels awaiting user secret input.
|
||||
secretMu sync.Mutex
|
||||
@@ -107,6 +108,12 @@ func (s *Service) SetContext(ctx context.Context) {
|
||||
s.RegisterSecretResponseListener()
|
||||
}
|
||||
|
||||
// SetEventSink registers an alternate UI event sink for non-Wails hosts such
|
||||
// as the VS Code bridge.
|
||||
func (s *Service) SetEventSink(sink func(string, interface{})) {
|
||||
s.eventSink = sink
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Profile operations
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1026,6 +1033,26 @@ func (s *Service) DownloadGitHubRepoArchive(sessionID string, fullName string, r
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// DownloadGitHubRepoArchiveToPath downloads a repository zip archive to an
|
||||
// explicit local destination. Non-Wails hosts provide the save path through
|
||||
// their own file picker.
|
||||
func (s *Service) DownloadGitHubRepoArchiveToPath(sessionID string, fullName string, ref string, destPath string) (string, error) {
|
||||
if strings.TrimSpace(fullName) == "" {
|
||||
return "", fmt.Errorf("repository name cannot be empty")
|
||||
}
|
||||
destPath = strings.TrimSpace(destPath)
|
||||
if destPath == "" {
|
||||
return "", fmt.Errorf("destination path cannot be empty")
|
||||
}
|
||||
if filepath.Ext(destPath) != ".zip" {
|
||||
destPath += ".zip"
|
||||
}
|
||||
if err := s.sessions.DownloadGitHubArchive(sessionID, fullName, ref, destPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return destPath, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VNC
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1410,6 +1437,19 @@ func (s *Service) SFTPDownload(sessionID string, remotePath string) error {
|
||||
return s.sessions.SFTPDownload(sessionID, remotePath, localPath)
|
||||
}
|
||||
|
||||
// SFTPDownloadToPath downloads a remote file to an explicit local path. This
|
||||
// is used by non-Wails hosts, where the UI owns the save dialog.
|
||||
func (s *Service) SFTPDownloadToPath(sessionID string, remotePath string, localPath string) error {
|
||||
if strings.TrimSpace(remotePath) == "" {
|
||||
return fmt.Errorf("remote path cannot be empty")
|
||||
}
|
||||
localPath = strings.TrimSpace(localPath)
|
||||
if localPath == "" {
|
||||
return fmt.Errorf("local path cannot be empty")
|
||||
}
|
||||
return s.sessions.SFTPDownload(sessionID, remotePath, localPath)
|
||||
}
|
||||
|
||||
// SFTPUpload uploads a local file to a remote directory. Opens a native file
|
||||
// dialog to select the local file.
|
||||
func (s *Service) SFTPUpload(sessionID string, remotePath string) error {
|
||||
@@ -1431,6 +1471,29 @@ func (s *Service) SFTPUpload(sessionID string, remotePath string) error {
|
||||
return s.sessions.SFTPUpload(sessionID, localPath, destPath)
|
||||
}
|
||||
|
||||
// SFTPUploadPath uploads an explicit local file path to a remote directory.
|
||||
// The remote filename is derived from the local basename and sanitised through
|
||||
// the same upload-name validation used by browser uploads.
|
||||
func (s *Service) SFTPUploadPath(sessionID string, remoteDir string, localPath string) error {
|
||||
localPath = strings.TrimSpace(localPath)
|
||||
if localPath == "" {
|
||||
return fmt.Errorf("local path cannot be empty")
|
||||
}
|
||||
info, err := os.Stat(localPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading local file metadata: %w", err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return fmt.Errorf("local path is a directory")
|
||||
}
|
||||
safeName, err := safeSFTPUploadName(filepath.Base(localPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destPath := path.Join(remoteDir, safeName)
|
||||
return s.sessions.SFTPUpload(sessionID, localPath, destPath)
|
||||
}
|
||||
|
||||
// SFTPUploadData uploads raw file data to a remote directory.
|
||||
// Data is expected as base64-encoded bytes from the frontend.
|
||||
func (s *Service) SFTPUploadData(sessionID string, remoteDir string, fileName string, base64Data string) error {
|
||||
@@ -1676,7 +1739,7 @@ func (s *Service) TestConnection(host string, port int) (map[string]interface{},
|
||||
// This bridges the synchronous Go SSH auth callback with the asynchronous
|
||||
// frontend prompt UI.
|
||||
func (s *Service) promptSecret(prompt string) (string, error) {
|
||||
if s.ctx == nil {
|
||||
if s.ctx == nil && s.eventSink == nil {
|
||||
return "", fmt.Errorf("application context not available")
|
||||
}
|
||||
|
||||
@@ -1693,7 +1756,7 @@ func (s *Service) promptSecret(prompt string) (string, error) {
|
||||
s.secretMu.Unlock()
|
||||
|
||||
// Emit the prompt event to the frontend.
|
||||
wailsRuntime.EventsEmit(s.ctx, "session:secret:prompt", map[string]interface{}{
|
||||
s.emitEvent("session:secret:prompt", map[string]interface{}{
|
||||
"requestID": requestID,
|
||||
"message": prompt,
|
||||
"title": "Credentials Required",
|
||||
@@ -1751,6 +1814,26 @@ func (s *Service) RegisterSecretResponseListener() {
|
||||
})
|
||||
}
|
||||
|
||||
// RespondSecret supplies a credential prompt response for non-Wails hosts.
|
||||
func (s *Service) RespondSecret(requestID string, value string, cancelled bool) {
|
||||
s.secretMu.Lock()
|
||||
ch, exists := s.secretChans[requestID]
|
||||
s.secretMu.Unlock()
|
||||
|
||||
if exists {
|
||||
ch <- secretResponse{value: value, cancelled: cancelled}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) emitEvent(eventName string, data interface{}) {
|
||||
if s.eventSink != nil {
|
||||
s.eventSink(eventName, data)
|
||||
}
|
||||
if s.ctx != nil {
|
||||
wailsRuntime.EventsEmit(s.ctx, eventName, data)
|
||||
}
|
||||
}
|
||||
|
||||
// derefProfiles converts a slice of profile pointers to values.
|
||||
func derefProfiles(ptrs []*models.Profile) []models.Profile {
|
||||
result := make([]models.Profile, len(ptrs))
|
||||
|
||||
@@ -218,6 +218,23 @@ func (s *Service) LeviathanSaveWorking() (string, error) {
|
||||
return path, writeLeviathanDocumentFile(path, doc)
|
||||
}
|
||||
|
||||
// LeviathanSaveWorkingToPath writes the patched bytes to an explicit path.
|
||||
// This is used by non-Wails hosts, where file picking is owned by the host UI.
|
||||
func (s *Service) LeviathanSaveWorkingToPath(path string) (string, error) {
|
||||
engine := s.leviathanEngine()
|
||||
engine.mu.Lock()
|
||||
if engine.doc == nil {
|
||||
engine.mu.Unlock()
|
||||
return "", errors.New("open a file first")
|
||||
}
|
||||
doc := engine.doc
|
||||
engine.mu.Unlock()
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return "", errors.New("save path is required")
|
||||
}
|
||||
return path, writeLeviathanDocumentFile(path, doc)
|
||||
}
|
||||
|
||||
// LeviathanExportJSON writes a tool result to disk.
|
||||
func (s *Service) LeviathanExportJSON(name string, value map[string]any) (string, error) {
|
||||
if s.ctx == nil {
|
||||
|
||||
@@ -70,9 +70,12 @@ type Manager struct {
|
||||
auditLog *audit.Logger
|
||||
recordStore *recording.Store
|
||||
ctx context.Context // Wails app context for event emission
|
||||
eventSink func(string, interface{})
|
||||
hostKeyMu sync.Mutex
|
||||
hostKeyChans map[string]chan bool
|
||||
hostKeyListenerRegistered bool
|
||||
secretMu sync.Mutex
|
||||
secretChans map[string]chan string
|
||||
}
|
||||
|
||||
// NewManager creates a Manager wired to the given backing services.
|
||||
@@ -93,6 +96,16 @@ func (m *Manager) SetContext(ctx context.Context) {
|
||||
m.registerHostKeyResponseListener()
|
||||
}
|
||||
|
||||
// SetEventSink registers an alternate UI event sink for non-Wails hosts.
|
||||
// Wails remains supported through SetContext; this hook is used by the VS Code
|
||||
// bridge so existing protocol/session code can be reused without a webview
|
||||
// runtime context.
|
||||
func (m *Manager) SetEventSink(sink func(string, interface{})) {
|
||||
m.mu.Lock()
|
||||
m.eventSink = sink
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Connect creates a new session from a profile and connects it.
|
||||
// The getSecret callback is used to prompt the user for credentials when
|
||||
// required. Returns the session ID on success.
|
||||
@@ -1706,7 +1719,7 @@ func (m *Manager) hostKeyCallback(sessionID string, policy models.HostKeyPolicy)
|
||||
}
|
||||
|
||||
func (m *Manager) promptHostKey(sessionID string, host string, keyType string, fingerprint string) (bool, error) {
|
||||
if m.ctx == nil {
|
||||
if m.ctx == nil && m.eventSink == nil {
|
||||
return false, fmt.Errorf("host key prompt unavailable")
|
||||
}
|
||||
|
||||
@@ -1784,6 +1797,20 @@ func (m *Manager) handleHostKeyResponse(optionalData ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// RespondHostKey supplies a host-key prompt response for non-Wails hosts.
|
||||
func (m *Manager) RespondHostKey(requestID string, accepted bool) {
|
||||
m.hostKeyMu.Lock()
|
||||
ch, exists := m.hostKeyChans[requestID]
|
||||
m.hostKeyMu.Unlock()
|
||||
|
||||
if exists {
|
||||
select {
|
||||
case ch <- accepted:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildSecretCallback returns a func() (string, error) that resolves a
|
||||
// credential according to the profile's SecretMode:
|
||||
// - "saved": check keychain/vault first, prompt if missing, then persist
|
||||
@@ -2240,25 +2267,30 @@ func (m *Manager) scheduleReconnect(sess *Session, sessionID string) {
|
||||
// original callback is no longer available.
|
||||
func (m *Manager) buildInteractiveSecretCallback(sessionID string) func(prompt string) (string, error) {
|
||||
return func(prompt string) (string, error) {
|
||||
// Use the same event-based flow as the initial connection.
|
||||
requestID := uuid.New().String()
|
||||
|
||||
ch := make(chan string, 1)
|
||||
m.mu.Lock()
|
||||
// Store a temporary secret channel keyed by requestID.
|
||||
if m.ctx != nil {
|
||||
wailsRuntime.EventsEmit(m.ctx, "session:secret:prompt", map[string]interface{}{
|
||||
"requestID": requestID,
|
||||
"sessionID": sessionID,
|
||||
"title": "Reconnection Credentials",
|
||||
"message": prompt,
|
||||
})
|
||||
m.secretMu.Lock()
|
||||
if m.secretChans == nil {
|
||||
m.secretChans = make(map[string]chan string)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
m.secretChans[requestID] = ch
|
||||
m.secretMu.Unlock()
|
||||
defer func() {
|
||||
m.secretMu.Lock()
|
||||
delete(m.secretChans, requestID)
|
||||
m.secretMu.Unlock()
|
||||
}()
|
||||
|
||||
m.emitEvent("session:secret:prompt", map[string]interface{}{
|
||||
"requestID": requestID,
|
||||
"sessionID": sessionID,
|
||||
"title": "Reconnection Credentials",
|
||||
"message": prompt,
|
||||
})
|
||||
|
||||
// Wait for the response with a timeout.
|
||||
var response string
|
||||
cancel := make(chan struct{})
|
||||
|
||||
if m.ctx != nil {
|
||||
wailsRuntime.EventsOn(m.ctx, "session:secret:response", func(optionalData ...interface{}) {
|
||||
@@ -2282,7 +2314,6 @@ func (m *Manager) buildInteractiveSecretCallback(sessionID string) func(prompt s
|
||||
select {
|
||||
case response = <-ch:
|
||||
case <-time.After(60 * time.Second):
|
||||
close(cancel)
|
||||
return "", fmt.Errorf("credential prompt timed out during reconnect")
|
||||
}
|
||||
|
||||
@@ -2290,6 +2321,21 @@ func (m *Manager) buildInteractiveSecretCallback(sessionID string) func(prompt s
|
||||
}
|
||||
}
|
||||
|
||||
// RespondSecret supplies a credential prompt response for reconnect prompts in
|
||||
// non-Wails hosts.
|
||||
func (m *Manager) RespondSecret(requestID string, value string) {
|
||||
m.secretMu.Lock()
|
||||
ch, exists := m.secretChans[requestID]
|
||||
m.secretMu.Unlock()
|
||||
|
||||
if exists {
|
||||
select {
|
||||
case ch <- value:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CancelReconnect cancels any in-progress auto-reconnect for a session.
|
||||
func (m *Manager) CancelReconnect(sessionID string) {
|
||||
m.mu.Lock()
|
||||
@@ -2324,6 +2370,12 @@ func (m *Manager) createTabName(profile *models.Profile) string {
|
||||
|
||||
// emitEvent sends an event to the Wails frontend via the runtime context.
|
||||
func (m *Manager) emitEvent(eventName string, data interface{}) {
|
||||
m.mu.RLock()
|
||||
sink := m.eventSink
|
||||
m.mu.RUnlock()
|
||||
if sink != nil {
|
||||
sink(eventName, data)
|
||||
}
|
||||
if m.ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user