@electerm/electerm-react 3.0.18 → 3.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/client/common/constants.js +4 -4
  2. package/client/common/db.js +20 -10
  3. package/client/components/file-transfer/conflict-resolve.jsx +38 -4
  4. package/client/components/file-transfer/transfer-queue.jsx +10 -4
  5. package/client/components/file-transfer/transfer.jsx +7 -4
  6. package/client/components/file-transfer/transports-action-store.jsx +14 -2
  7. package/client/components/footer/cmd-history.jsx +2 -4
  8. package/client/components/quick-commands/qm.styl +8 -0
  9. package/client/components/quick-commands/quick-command-item.jsx +4 -0
  10. package/client/components/quick-commands/quick-commands-box.jsx +10 -0
  11. package/client/components/quick-commands/quick-commands-form-elem.jsx +1 -1
  12. package/client/components/quick-commands/quick-commands-list-form.jsx +59 -4
  13. package/client/components/quick-commands/quick-commands-list.jsx +33 -3
  14. package/client/components/setting-panel/list.styl +5 -1
  15. package/client/components/setting-sync/server-data-status.jsx +2 -1
  16. package/client/components/setting-sync/setting-sync-form.jsx +93 -15
  17. package/client/components/setting-sync/setting-sync.jsx +5 -1
  18. package/client/components/sidebar/transfer-history-modal.jsx +2 -2
  19. package/client/components/tabs/tabs.styl +1 -0
  20. package/client/components/tabs/window-control.jsx +2 -0
  21. package/client/components/terminal/terminal-command-dropdown.jsx +1 -1
  22. package/client/components/terminal/terminal.jsx +14 -1
  23. package/client/components/terminal/transfer-client-base.js +38 -17
  24. package/client/components/terminal-info/network.jsx +0 -1
  25. package/client/components/tree-list/tree-list-item.jsx +5 -0
  26. package/client/components/tree-list/tree-list.jsx +17 -4
  27. package/client/components/tree-list/tree-list.styl +1 -1
  28. package/client/store/common.js +15 -15
  29. package/client/store/init-state.js +6 -31
  30. package/client/store/mcp-handler.js +315 -129
  31. package/client/store/store.js +1 -1
  32. package/client/store/sync.js +129 -3
  33. package/client/store/transfer-list.js +3 -2
  34. package/client/store/watch.js +1 -25
  35. package/package.json +1 -1
@@ -7,6 +7,10 @@ import uid from '../common/uid'
7
7
  import { settingMap } from '../common/constants'
8
8
  import { refs } from '../components/common/ref'
9
9
  import deepCopy from 'json-deep-copy'
10
+ import {
11
+ getLocalFileInfo,
12
+ getRemoteFileInfo
13
+ } from '../components/sftp/file-read'
10
14
 
11
15
  export default Store => {
12
16
  // Initialize MCP handler - called when MCP widget is started
@@ -56,8 +60,7 @@ export default Store => {
56
60
  case 'add_bookmark_group':
57
61
  result = await store.mcpAddBookmarkGroup(args)
58
62
  break
59
-
60
- // Quick command operations
63
+ /*
61
64
  case 'list_quick_commands':
62
65
  result = store.mcpListQuickCommands()
63
66
  break
@@ -70,7 +73,7 @@ export default Store => {
70
73
  case 'delete_quick_command':
71
74
  result = store.mcpDeleteQuickCommand(args)
72
75
  break
73
-
76
+ */
74
77
  // Tab operations
75
78
  case 'list_tabs':
76
79
  result = store.mcpListTabs()
@@ -105,20 +108,34 @@ export default Store => {
105
108
  result = store.mcpGetTerminalOutput(args)
106
109
  break
107
110
 
108
- // History operations
109
- case 'list_history':
110
- result = store.mcpListHistory(args)
111
+ // SFTP operations
112
+ case 'sftp_list':
113
+ result = await store.mcpSftpList(args)
114
+ break
115
+ case 'sftp_del':
116
+ result = await store.mcpSftpDel(args)
111
117
  break
112
- case 'clear_history':
113
- result = store.mcpClearHistory()
118
+ case 'sftp_stat':
119
+ result = await store.mcpSftpStat(args)
120
+ break
121
+ case 'sftp_read_file':
122
+ result = await store.mcpSftpReadFile(args)
114
123
  break
115
124
 
116
- // Transfer operations
117
- case 'list_transfers':
118
- result = store.mcpListTransfers()
125
+ // File transfer operations
126
+ case 'sftp_upload':
127
+ result = await store.mcpSftpUpload(args)
119
128
  break
120
- case 'list_transfer_history':
121
- result = store.mcpListTransferHistory(args)
129
+ case 'sftp_download':
130
+ result = await store.mcpSftpDownload(args)
131
+ break
132
+
133
+ // Zmodem (trzsz/rzsz) operations
134
+ case 'zmodem_upload':
135
+ result = store.mcpZmodemUpload(args)
136
+ break
137
+ case 'zmodem_download':
138
+ result = store.mcpZmodemDownload(args)
122
139
  break
123
140
 
124
141
  // Settings operations
@@ -249,58 +266,58 @@ export default Store => {
249
266
 
250
267
  // ==================== Quick Command APIs ====================
251
268
 
252
- Store.prototype.mcpListQuickCommands = function () {
253
- return deepCopy(window.store.quickCommands)
254
- }
255
-
256
- Store.prototype.mcpAddQuickCommand = function (args) {
257
- const { store } = window
258
- const qm = {
259
- id: uid(),
260
- name: args.name,
261
- commands: args.commands,
262
- inputOnly: args.inputOnly || false,
263
- labels: args.labels || []
264
- }
265
-
266
- store.addQuickCommand(qm)
267
-
268
- return {
269
- success: true,
270
- id: qm.id,
271
- message: `Quick command "${qm.name}" created`
272
- }
273
- }
274
-
275
- Store.prototype.mcpRunQuickCommand = function (args) {
276
- const { store } = window
277
- const qm = store.quickCommands.find(q => q.id === args.id)
278
- if (!qm) {
279
- throw new Error(`Quick command not found: ${args.id}`)
280
- }
281
-
282
- store.runQuickCommandItem(args.id)
283
-
284
- return {
285
- success: true,
286
- message: `Executed quick command "${qm.name}"`
287
- }
288
- }
289
-
290
- Store.prototype.mcpDeleteQuickCommand = function (args) {
291
- const { store } = window
292
- const qm = store.quickCommands.find(q => q.id === args.id)
293
- if (!qm) {
294
- throw new Error(`Quick command not found: ${args.id}`)
295
- }
296
-
297
- store.delQuickCommand({ id: args.id })
298
-
299
- return {
300
- success: true,
301
- message: `Deleted quick command "${qm.name}"`
302
- }
303
- }
269
+ // Store.prototype.mcpListQuickCommands = function () {
270
+ // return deepCopy(window.store.quickCommands)
271
+ // }
272
+
273
+ // Store.prototype.mcpAddQuickCommand = function (args) {
274
+ // const { store } = window
275
+ // const qm = {
276
+ // id: uid(),
277
+ // name: args.name,
278
+ // commands: args.commands,
279
+ // inputOnly: args.inputOnly || false,
280
+ // labels: args.labels || []
281
+ // }
282
+
283
+ // store.addQuickCommand(qm)
284
+
285
+ // return {
286
+ // success: true,
287
+ // id: qm.id,
288
+ // message: `Quick command "${qm.name}" created`
289
+ // }
290
+ // }
291
+
292
+ // Store.prototype.mcpRunQuickCommand = function (args) {
293
+ // const { store } = window
294
+ // const qm = store.quickCommands.find(q => q.id === args.id)
295
+ // if (!qm) {
296
+ // throw new Error(`Quick command not found: ${args.id}`)
297
+ // }
298
+
299
+ // store.runQuickCommandItem(args.id)
300
+
301
+ // return {
302
+ // success: true,
303
+ // message: `Executed quick command "${qm.name}"`
304
+ // }
305
+ // }
306
+
307
+ // Store.prototype.mcpDeleteQuickCommand = function (args) {
308
+ // const { store } = window
309
+ // const qm = store.quickCommands.find(q => q.id === args.id)
310
+ // if (!qm) {
311
+ // throw new Error(`Quick command not found: ${args.id}`)
312
+ // }
313
+
314
+ // store.delQuickCommand({ id: args.id })
315
+
316
+ // return {
317
+ // success: true,
318
+ // message: `Deleted quick command "${qm.name}"`
319
+ // }
320
+ // }
304
321
 
305
322
  // ==================== Tab APIs ====================
306
323
 
@@ -502,92 +519,261 @@ export default Store => {
502
519
  }
503
520
  }
504
521
 
505
- // ==================== History APIs ====================
522
+ // ==================== Settings APIs ====================
506
523
 
507
- Store.prototype.mcpListHistory = function (args = {}) {
524
+ Store.prototype.mcpGetSettings = function () {
508
525
  const { store } = window
509
- const limit = args.limit || 50
510
- const history = store.history.slice(0, limit)
511
-
512
- return history.map(h => ({
513
- id: h.id,
514
- title: h.title,
515
- host: h.host,
516
- type: h.type,
517
- time: h.time
518
- }))
526
+ // Return safe settings (no sensitive data)
527
+ const config = store.config
528
+ const excludeKeys = ['apiKeyAI', 'syncSetting']
529
+ const safeConfig = Object.fromEntries(
530
+ Object.entries(config).filter(([key]) => !excludeKeys.includes(key))
531
+ )
532
+ return safeConfig
519
533
  }
520
534
 
521
- Store.prototype.mcpClearHistory = function () {
535
+ // ==================== SFTP APIs ====================
536
+
537
+ Store.prototype.mcpGetSshSftpRef = function (tabId) {
522
538
  const { store } = window
523
- store.history = []
539
+ const resolvedTabId = tabId || store.activeTabId
540
+ if (!resolvedTabId) {
541
+ throw new Error('No active tab')
542
+ }
543
+ const tab = store.tabs.find(t => t.id === resolvedTabId)
544
+ if (!tab) {
545
+ throw new Error(`Tab not found: ${resolvedTabId}`)
546
+ }
547
+ if (tab.type !== 'ssh' && tab.type !== 'ftp') {
548
+ throw new Error(`Tab "${resolvedTabId}" is not an SSH/SFTP tab (type: ${tab.type || 'local'})`)
549
+ }
550
+ const sftpEntry = refs.get('sftp-' + resolvedTabId)
551
+ if (!sftpEntry || !sftpEntry.sftp) {
552
+ throw new Error(`SFTP not initialized for tab "${resolvedTabId}". Open the SFTP panel first.`)
553
+ }
554
+ return { sftp: sftpEntry.sftp, tab, tabId: resolvedTabId }
555
+ }
524
556
 
525
- return {
526
- success: true,
527
- message: 'History cleared'
557
+ Store.prototype.mcpSftpList = async function (args) {
558
+ const { sftp, tab, tabId } = window.store.mcpGetSshSftpRef(args.tabId)
559
+ const remotePath = args.remotePath
560
+ if (!remotePath) {
561
+ throw new Error('remotePath is required')
528
562
  }
563
+ const list = await sftp.list(remotePath)
564
+ return { tabId, host: tab.host, path: remotePath, list }
529
565
  }
530
566
 
531
- // ==================== Transfer APIs ====================
567
+ Store.prototype.mcpSftpStat = async function (args) {
568
+ const { sftp, tab, tabId } = window.store.mcpGetSshSftpRef(args.tabId)
569
+ const remotePath = args.remotePath
570
+ if (!remotePath) {
571
+ throw new Error('remotePath is required')
572
+ }
573
+ const stat = await sftp.stat(remotePath)
574
+ return { tabId, host: tab.host, path: remotePath, stat }
575
+ }
532
576
 
533
- Store.prototype.mcpListTransfers = function () {
534
- const { store } = window
535
- return store.fileTransfers.map(t => ({
536
- id: t.id,
537
- localPath: t.localPath,
538
- remotePath: t.remotePath,
539
- type: t.type,
540
- percent: t.percent,
541
- status: t.status
542
- }))
577
+ Store.prototype.mcpSftpReadFile = async function (args) {
578
+ const { sftp, tab, tabId } = window.store.mcpGetSshSftpRef(args.tabId)
579
+ const remotePath = args.remotePath
580
+ if (!remotePath) {
581
+ throw new Error('remotePath is required')
582
+ }
583
+ const content = await sftp.readFile(remotePath)
584
+ return { tabId, host: tab.host, path: remotePath, content }
543
585
  }
544
586
 
545
- Store.prototype.mcpListTransferHistory = function (args = {}) {
546
- const { store } = window
547
- const limit = args.limit || 50
548
- return store.transferHistory.slice(0, limit).map(t => ({
549
- id: t.id,
550
- localPath: t.localPath,
551
- remotePath: t.remotePath,
552
- type: t.type,
553
- status: t.status,
554
- time: t.time
555
- }))
587
+ Store.prototype.mcpSftpDel = async function (args) {
588
+ const { sftp, tab, tabId } = window.store.mcpGetSshSftpRef(args.tabId)
589
+ const remotePath = args.remotePath
590
+ if (!remotePath) {
591
+ throw new Error('remotePath is required')
592
+ }
593
+ // Use stat to determine if it's a file or directory
594
+ const stat = await sftp.stat(remotePath)
595
+ const isDirectory = typeof stat.isDirectory === 'function'
596
+ ? stat.isDirectory()
597
+ : !!stat.isDirectory
598
+ if (isDirectory) {
599
+ await sftp.rmdir(remotePath)
600
+ } else {
601
+ await sftp.rm(remotePath)
602
+ }
603
+ return { success: true, tabId, host: tab.host, path: remotePath, type: isDirectory ? 'directory' : 'file' }
556
604
  }
557
605
 
558
- // ==================== Settings APIs ====================
606
+ // ==================== File Transfer APIs ====================
559
607
 
560
- Store.prototype.mcpGetSettings = function () {
608
+ Store.prototype.mcpSftpUpload = async function (args) {
561
609
  const { store } = window
562
- // Return safe settings (no sensitive data)
563
- const config = store.config
564
- const safeConfig = {
565
- theme: config.theme,
566
- language: config.language,
567
- fontSize: config.fontSize,
568
- fontFamily: config.fontFamily,
569
- terminalType: config.terminalType,
570
- cursorStyle: config.cursorStyle,
571
- cursorBlink: config.cursorBlink,
572
- scrollback: config.scrollback
610
+ const { tab, tabId } = store.mcpGetSshSftpRef(args.tabId)
611
+ const localPath = args.localPath
612
+ const remotePath = args.remotePath
613
+ if (!localPath) {
614
+ throw new Error('localPath is required')
615
+ }
616
+ if (!remotePath) {
617
+ throw new Error('remotePath is required')
618
+ }
619
+
620
+ window._transferConflictPolicy = args.conflictPolicy || 'overwrite'
621
+
622
+ const fromFile = await getLocalFileInfo(localPath)
623
+ const transferItem = {
624
+ host: tab.host,
625
+ tabType: tab.type || 'ssh',
626
+ typeFrom: 'local',
627
+ typeTo: 'remote',
628
+ fromPath: localPath,
629
+ toPath: remotePath,
630
+ fromFile: {
631
+ ...fromFile,
632
+ host: tab.host,
633
+ tabType: tab.type || 'ssh',
634
+ tabId,
635
+ title: tab.title
636
+ },
637
+ id: uid(),
638
+ title: tab.title,
639
+ tabId,
640
+ operation: ''
641
+ }
642
+
643
+ store.addTransferList([transferItem])
644
+
645
+ return {
646
+ success: true,
647
+ message: `Upload started: ${localPath} → ${tab.host}:${remotePath}`,
648
+ transferId: transferItem.id,
649
+ tabId
573
650
  }
574
- return safeConfig
575
651
  }
576
652
 
577
- Store.prototype.mcpListTerminalThemes = function () {
653
+ Store.prototype.mcpSftpDownload = async function (args) {
578
654
  const { store } = window
579
- return store.terminalThemes.map(t => ({
580
- id: t.id,
581
- name: t.name,
582
- themeLight: t.themeLight
583
- }))
655
+ const { sftp, tab, tabId } = store.mcpGetSshSftpRef(args.tabId) // sftp used for getRemoteFileInfo
656
+ const remotePath = args.remotePath
657
+ const localPath = args.localPath
658
+ if (!remotePath) {
659
+ throw new Error('remotePath is required')
660
+ }
661
+ if (!localPath) {
662
+ throw new Error('localPath is required')
663
+ }
664
+
665
+ window._transferConflictPolicy = args.conflictPolicy || 'overwrite'
666
+
667
+ const fromFile = await getRemoteFileInfo(sftp, remotePath)
668
+ const transferItem = {
669
+ host: tab.host,
670
+ tabType: tab.type || 'ssh',
671
+ typeFrom: 'remote',
672
+ typeTo: 'local',
673
+ fromPath: remotePath,
674
+ toPath: localPath,
675
+ fromFile: {
676
+ ...fromFile,
677
+ id: uid(),
678
+ isSymbolicLink: false
679
+ },
680
+ id: uid(),
681
+ title: tab.title,
682
+ tabId
683
+ }
684
+
685
+ store.addTransferList([transferItem])
686
+
687
+ return {
688
+ success: true,
689
+ message: `Download started: ${tab.host}:${remotePath} → ${localPath}`,
690
+ transferId: transferItem.id,
691
+ tabId
692
+ }
584
693
  }
585
694
 
586
- Store.prototype.mcpListUiThemes = function () {
695
+ // ==================== Zmodem (trzsz/rzsz) APIs ====================
696
+
697
+ Store.prototype.mcpZmodemUpload = function (args) {
587
698
  const { store } = window
588
- return (store.uiThemes || []).map(t => ({
589
- id: t.id,
590
- name: t.name
591
- }))
699
+ const tabId = args.tabId || store.activeTabId
700
+ if (!tabId) {
701
+ throw new Error('No active tab')
702
+ }
703
+ const tab = store.tabs.find(t => t.id === tabId)
704
+ if (!tab) {
705
+ throw new Error(`Tab not found: ${tabId}`)
706
+ }
707
+
708
+ const files = args.files
709
+ if (!files || !Array.isArray(files) || files.length === 0) {
710
+ throw new Error('files array is required (list of local file paths to upload)')
711
+ }
712
+
713
+ const protocol = args.protocol || 'rzsz'
714
+ const uploadCmd = protocol === 'trzsz' ? 'trz' : 'rz'
715
+
716
+ // Set the control variable to bypass native file dialog
717
+ window._apiControlSelectFile = files
718
+
719
+ const term = refs.get('term-' + tabId)
720
+ if (!term) {
721
+ throw new Error(`Terminal not found for tab: ${tabId}`)
722
+ }
723
+ term.runQuickCommand(uploadCmd)
724
+
725
+ return {
726
+ success: true,
727
+ protocol,
728
+ command: uploadCmd,
729
+ message: `${uploadCmd} upload initiated for ${files.length} file(s)`,
730
+ files,
731
+ tabId
732
+ }
733
+ }
734
+
735
+ Store.prototype.mcpZmodemDownload = function (args) {
736
+ const { store } = window
737
+ const tabId = args.tabId || store.activeTabId
738
+ if (!tabId) {
739
+ throw new Error('No active tab')
740
+ }
741
+ const tab = store.tabs.find(t => t.id === tabId)
742
+ if (!tab) {
743
+ throw new Error(`Tab not found: ${tabId}`)
744
+ }
745
+
746
+ const saveFolder = args.saveFolder
747
+ if (!saveFolder) {
748
+ throw new Error('saveFolder is required (local folder to save downloaded files)')
749
+ }
750
+
751
+ const remoteFiles = args.remoteFiles
752
+ if (!remoteFiles || !Array.isArray(remoteFiles) || remoteFiles.length === 0) {
753
+ throw new Error('remoteFiles array is required (list of remote file paths to download)')
754
+ }
755
+
756
+ const protocol = args.protocol || 'rzsz'
757
+ const downloadCmd = protocol === 'trzsz' ? 'tsz' : 'sz'
758
+
759
+ // Set the control variable to bypass native folder dialog
760
+ window._apiControlSelectFolder = saveFolder
761
+
762
+ const term = refs.get('term-' + tabId)
763
+ if (!term) {
764
+ throw new Error(`Terminal not found for tab: ${tabId}`)
765
+ }
766
+ const quotedFiles = remoteFiles.map(f => `"${f}"`).join(' ')
767
+ term.runQuickCommand(`${downloadCmd} ${quotedFiles}`)
768
+
769
+ return {
770
+ success: true,
771
+ protocol,
772
+ command: downloadCmd,
773
+ message: `${downloadCmd} download initiated for ${remoteFiles.length} file(s) to ${saveFolder}`,
774
+ remoteFiles,
775
+ saveFolder,
776
+ tabId
777
+ }
592
778
  }
593
779
  }
@@ -176,7 +176,7 @@ class Store {
176
176
 
177
177
  get terminalCommandSuggestions () {
178
178
  const { store } = window
179
- const historyCommands = Array.from(store.terminalCommandHistory.keys())
179
+ const historyCommands = store.terminalCommandHistory.map(item => item.cmd)
180
180
  const batchInputCommands = store.batchInputs || []
181
181
  const quickCommands = (store.quickCommands || []).reduce(
182
182
  (p, q) => {