rds_ssm_connect 1.7.12 → 1.7.14
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.
- package/.github/workflows/release.yml +5 -1
- package/configLoader.js +86 -0
- package/connect.js +137 -6
- package/gui-adapter.js +60 -40
- package/package.json +1 -1
- package/scripts/generate-update-json.js +11 -0
- package/src/App.svelte +51 -1
- package/src/lib/SavedConnections.svelte +76 -11
- package/src/lib/Settings.svelte +433 -28
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/lib.rs +117 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/test/configLoader.test.js +150 -0
- package/envPortMapping.js +0 -54
- package/latest.json +0 -27
|
@@ -28,6 +28,10 @@ jobs:
|
|
|
28
28
|
rust_target: x86_64-unknown-linux-gnu
|
|
29
29
|
sidecar_target: node22-linux-x64
|
|
30
30
|
sidecar_triple: x86_64-unknown-linux-gnu
|
|
31
|
+
- platform: ubuntu-22.04-arm
|
|
32
|
+
rust_target: aarch64-unknown-linux-gnu
|
|
33
|
+
sidecar_target: node22-linux-arm64
|
|
34
|
+
sidecar_triple: aarch64-unknown-linux-gnu
|
|
31
35
|
- platform: windows-latest
|
|
32
36
|
rust_target: x86_64-pc-windows-msvc
|
|
33
37
|
sidecar_target: node22-win-x64
|
|
@@ -57,7 +61,7 @@ jobs:
|
|
|
57
61
|
if: startsWith(matrix.platform, 'ubuntu')
|
|
58
62
|
run: |
|
|
59
63
|
sudo apt-get update
|
|
60
|
-
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
|
64
|
+
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
|
61
65
|
|
|
62
66
|
- name: Install dependencies (macOS)
|
|
63
67
|
if: startsWith(matrix.platform, 'macos')
|
package/configLoader.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.rds-ssm-connect')
|
|
6
|
+
const CONFIG_FILE = 'projects.json'
|
|
7
|
+
|
|
8
|
+
export function getConfigPath() {
|
|
9
|
+
return path.join(CONFIG_DIR, CONFIG_FILE)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function loadProjectConfigs(configPath) {
|
|
13
|
+
const filePath = configPath || getConfigPath()
|
|
14
|
+
try {
|
|
15
|
+
const data = await fs.readFile(filePath, 'utf-8')
|
|
16
|
+
return JSON.parse(data)
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (err.code === 'ENOENT') return {}
|
|
19
|
+
throw err
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function saveProjectConfigs(configs, configPath) {
|
|
24
|
+
const filePath = configPath || getConfigPath()
|
|
25
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
26
|
+
await fs.writeFile(filePath, JSON.stringify(configs, null, 2) + '\n')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function saveProjectConfig(key, config, configPath) {
|
|
30
|
+
const configs = await loadProjectConfigs(configPath)
|
|
31
|
+
configs[key] = config
|
|
32
|
+
await saveProjectConfigs(configs, configPath)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function deleteProjectConfig(key, configPath) {
|
|
36
|
+
const configs = await loadProjectConfigs(configPath)
|
|
37
|
+
delete configs[key]
|
|
38
|
+
await saveProjectConfigs(configs, configPath)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const REQUIRED_FIELDS = [
|
|
42
|
+
'name',
|
|
43
|
+
'region',
|
|
44
|
+
'database',
|
|
45
|
+
'secretPrefix',
|
|
46
|
+
'rdsType',
|
|
47
|
+
'rdsPattern',
|
|
48
|
+
'envPortMapping',
|
|
49
|
+
'defaultPort',
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
const VALID_RDS_TYPES = ['cluster', 'instance']
|
|
53
|
+
const REGION_PATTERN = /^[a-z]{2}(-[a-z]+-\d+)$/
|
|
54
|
+
const PORT_PATTERN = /^\d+$/
|
|
55
|
+
|
|
56
|
+
export function validateProjectConfig(config) {
|
|
57
|
+
const errors = []
|
|
58
|
+
|
|
59
|
+
for (const field of REQUIRED_FIELDS) {
|
|
60
|
+
if (config[field] === undefined || config[field] === null || config[field] === '') {
|
|
61
|
+
errors.push(`Missing required field: ${field}`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (config.rdsType && !VALID_RDS_TYPES.includes(config.rdsType)) {
|
|
66
|
+
errors.push(`rdsType must be one of: ${VALID_RDS_TYPES.join(', ')}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (config.region && !REGION_PATTERN.test(config.region)) {
|
|
70
|
+
errors.push(`Invalid region format: ${config.region}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (config.defaultPort && !PORT_PATTERN.test(config.defaultPort)) {
|
|
74
|
+
errors.push(`defaultPort must be a numeric string: ${config.defaultPort}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (config.envPortMapping && typeof config.envPortMapping === 'object') {
|
|
78
|
+
for (const [key, value] of Object.entries(config.envPortMapping)) {
|
|
79
|
+
if (!PORT_PATTERN.test(value)) {
|
|
80
|
+
errors.push(`Port for "${key}" must be a numeric string: ${value}`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { valid: errors.length === 0, errors }
|
|
86
|
+
}
|
package/connect.js
CHANGED
|
@@ -7,10 +7,14 @@ import net from 'node:net'
|
|
|
7
7
|
import os from 'node:os'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import { promisify } from 'node:util'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
loadProjectConfigs,
|
|
12
|
+
saveProjectConfig,
|
|
13
|
+
validateProjectConfig,
|
|
14
|
+
} from './configLoader.js'
|
|
11
15
|
|
|
12
16
|
// Package info for version checking
|
|
13
|
-
const packageJson = { name: 'rds_ssm_connect', version: '1.7.
|
|
17
|
+
const packageJson = { name: 'rds_ssm_connect', version: '1.7.14' }
|
|
14
18
|
|
|
15
19
|
const execAsync = promisify(exec)
|
|
16
20
|
|
|
@@ -389,21 +393,34 @@ async function getRdsPort(ENV, projectConfig) {
|
|
|
389
393
|
}
|
|
390
394
|
|
|
391
395
|
function getProfilesForProject(allProfiles, projectConfig, allProjectConfigs) {
|
|
392
|
-
const { profileFilter } = projectConfig
|
|
396
|
+
const { profileFilter, envPortMapping } = projectConfig
|
|
393
397
|
|
|
398
|
+
let filtered
|
|
394
399
|
if (profileFilter) {
|
|
395
400
|
// Project has explicit filter - return profiles starting with filter
|
|
396
|
-
|
|
401
|
+
filtered = allProfiles.filter((env) => env.startsWith(profileFilter))
|
|
397
402
|
} else {
|
|
398
403
|
// No filter (legacy project like TLN) - return profiles that don't match any other project's filter
|
|
399
404
|
const otherFilters = Object.values(allProjectConfigs)
|
|
400
405
|
.filter((config) => config.profileFilter)
|
|
401
406
|
.map((config) => config.profileFilter)
|
|
402
407
|
|
|
403
|
-
|
|
408
|
+
filtered = allProfiles.filter(
|
|
404
409
|
(env) => !otherFilters.some((filter) => env.startsWith(filter)),
|
|
405
410
|
)
|
|
406
411
|
}
|
|
412
|
+
|
|
413
|
+
// Further restrict to profiles matching an envPortMapping suffix
|
|
414
|
+
if (envPortMapping && Object.keys(envPortMapping).length > 0) {
|
|
415
|
+
const suffixes = Object.keys(envPortMapping).sort(
|
|
416
|
+
(a, b) => b.length - a.length,
|
|
417
|
+
)
|
|
418
|
+
filtered = filtered.filter((env) =>
|
|
419
|
+
suffixes.some((suffix) => env.endsWith(suffix) || env === suffix),
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return filtered
|
|
407
424
|
}
|
|
408
425
|
|
|
409
426
|
// Get local port number based on environment suffix
|
|
@@ -480,6 +497,8 @@ async function getAvailableProjects() {
|
|
|
480
497
|
return []
|
|
481
498
|
}
|
|
482
499
|
|
|
500
|
+
const PROJECT_CONFIGS = await loadProjectConfigs()
|
|
501
|
+
|
|
483
502
|
return Object.entries(PROJECT_CONFIGS)
|
|
484
503
|
.filter(([_key, config]) => {
|
|
485
504
|
const matchingProfiles = getProfilesForProject(
|
|
@@ -498,6 +517,7 @@ async function getAvailableProjects() {
|
|
|
498
517
|
// Get profiles for a specific project
|
|
499
518
|
async function getProfilesForProjectKey(projectKey) {
|
|
500
519
|
const allProfiles = await readAwsConfig()
|
|
520
|
+
const PROJECT_CONFIGS = await loadProjectConfigs()
|
|
501
521
|
const projectConfig = PROJECT_CONFIGS[projectKey]
|
|
502
522
|
|
|
503
523
|
if (!projectConfig) {
|
|
@@ -511,6 +531,7 @@ async function getProfilesForProjectKey(projectKey) {
|
|
|
511
531
|
// Includes keepalive (prevents SSM idle timeout) and auto-reconnect
|
|
512
532
|
// (transparently reconnects on the same port if session drops unexpectedly).
|
|
513
533
|
async function connect(projectKey, profile, options = {}) {
|
|
534
|
+
const PROJECT_CONFIGS = await loadProjectConfigs()
|
|
514
535
|
const projectConfig = PROJECT_CONFIGS[projectKey]
|
|
515
536
|
if (!projectConfig) {
|
|
516
537
|
throw new Error(`Unknown project: ${projectKey}`)
|
|
@@ -674,6 +695,116 @@ async function main() {
|
|
|
674
695
|
await checkForUpdates()
|
|
675
696
|
|
|
676
697
|
try {
|
|
698
|
+
// Load project configs from user config file
|
|
699
|
+
let PROJECT_CONFIGS = await loadProjectConfigs()
|
|
700
|
+
|
|
701
|
+
// First-run wizard: if no projects configured, prompt to create one
|
|
702
|
+
if (Object.keys(PROJECT_CONFIGS).length === 0) {
|
|
703
|
+
const { setupNow } = await inquirer.default.prompt([
|
|
704
|
+
{
|
|
705
|
+
type: 'confirm',
|
|
706
|
+
name: 'setupNow',
|
|
707
|
+
message:
|
|
708
|
+
'No projects configured. Would you like to set up a project now?',
|
|
709
|
+
default: true,
|
|
710
|
+
},
|
|
711
|
+
])
|
|
712
|
+
|
|
713
|
+
if (!setupNow) {
|
|
714
|
+
console.log(
|
|
715
|
+
`\nTo configure manually, create ~/.rds-ssm-connect/projects.json\nSee the README for the config schema.\n`,
|
|
716
|
+
)
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const answers = await inquirer.default.prompt([
|
|
721
|
+
{
|
|
722
|
+
type: 'input',
|
|
723
|
+
name: 'key',
|
|
724
|
+
message: 'Project key (lowercase, hyphens):',
|
|
725
|
+
validate: (v) =>
|
|
726
|
+
/^[a-z][a-z0-9-]*$/.test(v) || 'Lowercase letters, digits, hyphens',
|
|
727
|
+
},
|
|
728
|
+
{ type: 'input', name: 'name', message: 'Display name:' },
|
|
729
|
+
{
|
|
730
|
+
type: 'input',
|
|
731
|
+
name: 'region',
|
|
732
|
+
message: 'AWS region:',
|
|
733
|
+
default: 'us-east-1',
|
|
734
|
+
},
|
|
735
|
+
{ type: 'input', name: 'database', message: 'Database name:' },
|
|
736
|
+
{
|
|
737
|
+
type: 'input',
|
|
738
|
+
name: 'secretPrefix',
|
|
739
|
+
message: 'Secret prefix (e.g. rds!cluster):',
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
type: 'select',
|
|
743
|
+
name: 'rdsType',
|
|
744
|
+
message: 'RDS type:',
|
|
745
|
+
choices: ['cluster', 'instance'],
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
type: 'input',
|
|
749
|
+
name: 'rdsPattern',
|
|
750
|
+
message: 'RDS identifier pattern:',
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
type: 'input',
|
|
754
|
+
name: 'profileFilter',
|
|
755
|
+
message: 'Profile filter prefix (leave empty for none):',
|
|
756
|
+
default: '',
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
type: 'input',
|
|
760
|
+
name: 'defaultPort',
|
|
761
|
+
message: 'Default local port:',
|
|
762
|
+
default: '5432',
|
|
763
|
+
},
|
|
764
|
+
])
|
|
765
|
+
|
|
766
|
+
// Collect port mappings
|
|
767
|
+
const envPortMappingInput = {}
|
|
768
|
+
let addMore = true
|
|
769
|
+
while (addMore) {
|
|
770
|
+
const mapping = await inquirer.default.prompt([
|
|
771
|
+
{ type: 'input', name: 'suffix', message: 'Environment suffix:' },
|
|
772
|
+
{ type: 'input', name: 'port', message: 'Local port:' },
|
|
773
|
+
{
|
|
774
|
+
type: 'confirm',
|
|
775
|
+
name: 'more',
|
|
776
|
+
message: 'Add another port mapping?',
|
|
777
|
+
default: false,
|
|
778
|
+
},
|
|
779
|
+
])
|
|
780
|
+
envPortMappingInput[mapping.suffix] = mapping.port
|
|
781
|
+
addMore = mapping.more
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const newConfig = {
|
|
785
|
+
name: answers.name,
|
|
786
|
+
region: answers.region,
|
|
787
|
+
database: answers.database,
|
|
788
|
+
secretPrefix: answers.secretPrefix,
|
|
789
|
+
rdsType: answers.rdsType,
|
|
790
|
+
rdsPattern: answers.rdsPattern,
|
|
791
|
+
profileFilter: answers.profileFilter || null,
|
|
792
|
+
envPortMapping: envPortMappingInput,
|
|
793
|
+
defaultPort: answers.defaultPort,
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const validation = validateProjectConfig(newConfig)
|
|
797
|
+
if (!validation.valid) {
|
|
798
|
+
console.log('\nValidation errors:')
|
|
799
|
+
validation.errors.forEach((e) => console.log(` - ${e}`))
|
|
800
|
+
return
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
await saveProjectConfig(answers.key, newConfig)
|
|
804
|
+
console.log(`\nProject "${answers.name}" saved!\n`)
|
|
805
|
+
PROJECT_CONFIGS = await loadProjectConfigs()
|
|
806
|
+
}
|
|
807
|
+
|
|
677
808
|
// Read all AWS profiles first
|
|
678
809
|
const allProfiles = await readAwsConfig()
|
|
679
810
|
|
|
@@ -834,6 +965,6 @@ export {
|
|
|
834
965
|
getLocalPort,
|
|
835
966
|
connect,
|
|
836
967
|
ipcEmitter,
|
|
837
|
-
|
|
968
|
+
loadProjectConfigs,
|
|
838
969
|
RETRY_CONFIG,
|
|
839
970
|
}
|
package/gui-adapter.js
CHANGED
|
@@ -22,8 +22,13 @@ import {
|
|
|
22
22
|
getAvailableProjects,
|
|
23
23
|
getLocalPort,
|
|
24
24
|
getProfilesForProjectKey,
|
|
25
|
-
|
|
25
|
+
loadProjectConfigs,
|
|
26
26
|
} from './connect.js'
|
|
27
|
+
import {
|
|
28
|
+
deleteProjectConfig,
|
|
29
|
+
saveProjectConfig,
|
|
30
|
+
validateProjectConfig,
|
|
31
|
+
} from './configLoader.js'
|
|
27
32
|
|
|
28
33
|
// Active connections Map - connectionId -> connection control object
|
|
29
34
|
const activeConnections = new Map()
|
|
@@ -51,19 +56,6 @@ async function isPortAvailable(port) {
|
|
|
51
56
|
})
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
// Find next available port starting from basePort
|
|
55
|
-
async function findAvailablePort(basePort, usedPorts = []) {
|
|
56
|
-
const usedSet = new Set(usedPorts.map((p) => parseInt(p, 10)))
|
|
57
|
-
|
|
58
|
-
for (let port = basePort; port < basePort + 100; port++) {
|
|
59
|
-
if (usedSet.has(port)) continue
|
|
60
|
-
if (await isPortAvailable(port)) {
|
|
61
|
-
return port.toString()
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
throw new Error(`No available ports found starting from ${basePort}`)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
59
|
// Handle incoming commands
|
|
68
60
|
async function handleCommand(command) {
|
|
69
61
|
const { id, action, ...params } = command
|
|
@@ -100,7 +92,8 @@ async function handleCommand(command) {
|
|
|
100
92
|
const connectionId = `conn_${randomUUID().slice(0, 8)}`
|
|
101
93
|
|
|
102
94
|
// Determine port to use
|
|
103
|
-
const
|
|
95
|
+
const configs = await loadProjectConfigs()
|
|
96
|
+
const projectConfig = configs[projectKey]
|
|
104
97
|
if (!projectConfig) {
|
|
105
98
|
sendResponse(id, 'error', {
|
|
106
99
|
message: `Unknown project: ${projectKey}`,
|
|
@@ -108,31 +101,21 @@ async function handleCommand(command) {
|
|
|
108
101
|
break
|
|
109
102
|
}
|
|
110
103
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Find next available port based on project's base port
|
|
127
|
-
const basePort = parseInt(getLocalPort(profile, projectConfig), 10)
|
|
128
|
-
// Combine usedPorts from params with currently active connections
|
|
129
|
-
const allUsedPorts = [
|
|
130
|
-
...usedPorts,
|
|
131
|
-
...Array.from(activeConnections.values()).map(
|
|
132
|
-
(c) => c.connectionInfo?.port,
|
|
133
|
-
),
|
|
134
|
-
].filter(Boolean)
|
|
135
|
-
portToUse = await findAvailablePort(basePort, allUsedPorts)
|
|
104
|
+
const portToUse = localPort || getLocalPort(profile, projectConfig)
|
|
105
|
+
const portNum = parseInt(portToUse, 10)
|
|
106
|
+
|
|
107
|
+
// Strict port check — never silently increment
|
|
108
|
+
const allUsedPorts = new Set([
|
|
109
|
+
...usedPorts.map((p) => parseInt(p, 10)),
|
|
110
|
+
...Array.from(activeConnections.values())
|
|
111
|
+
.map((c) => parseInt(c.connectionInfo?.port, 10))
|
|
112
|
+
.filter(Boolean),
|
|
113
|
+
])
|
|
114
|
+
if (allUsedPorts.has(portNum) || !(await isPortAvailable(portNum))) {
|
|
115
|
+
sendResponse(id, 'error', {
|
|
116
|
+
message: `Port ${portToUse} is not available. Close the application using it or change the port in project settings.`,
|
|
117
|
+
})
|
|
118
|
+
break
|
|
136
119
|
}
|
|
137
120
|
|
|
138
121
|
// Start new connection with the determined port
|
|
@@ -220,6 +203,43 @@ async function handleCommand(command) {
|
|
|
220
203
|
break
|
|
221
204
|
}
|
|
222
205
|
|
|
206
|
+
case 'list-project-configs': {
|
|
207
|
+
const configs = await loadProjectConfigs()
|
|
208
|
+
sendResponse(id, 'success', { configs })
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case 'save-project-config': {
|
|
213
|
+
const { key, config } = params
|
|
214
|
+
if (!key || !config) {
|
|
215
|
+
sendResponse(id, 'error', {
|
|
216
|
+
message: 'key and config are required',
|
|
217
|
+
})
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
const validation = validateProjectConfig(config)
|
|
221
|
+
if (!validation.valid) {
|
|
222
|
+
sendResponse(id, 'error', {
|
|
223
|
+
message: `Validation failed: ${validation.errors.join(', ')}`,
|
|
224
|
+
})
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
await saveProjectConfig(key, config)
|
|
228
|
+
sendResponse(id, 'success', { message: 'Project config saved' })
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case 'delete-project-config': {
|
|
233
|
+
const { key: deleteKey } = params
|
|
234
|
+
if (!deleteKey) {
|
|
235
|
+
sendResponse(id, 'error', { message: 'key is required' })
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
await deleteProjectConfig(deleteKey)
|
|
239
|
+
sendResponse(id, 'success', { message: 'Project config deleted' })
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
|
|
223
243
|
case 'ping': {
|
|
224
244
|
sendResponse(id, 'success', { message: 'pong' })
|
|
225
245
|
break
|
package/package.json
CHANGED
|
@@ -39,6 +39,7 @@ const TARGET_TO_PLATFORM = {
|
|
|
39
39
|
'aarch64-apple-darwin': 'darwin-aarch64',
|
|
40
40
|
'x86_64-apple-darwin': 'darwin-x86_64',
|
|
41
41
|
'x86_64-unknown-linux-gnu': 'linux-x86_64',
|
|
42
|
+
'aarch64-unknown-linux-gnu': 'linux-aarch64',
|
|
42
43
|
'x86_64-pc-windows-msvc': 'windows-x86_64',
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -97,3 +98,13 @@ const updateManifest = {
|
|
|
97
98
|
|
|
98
99
|
const outputPath = path.join(__dirname, '../latest.json')
|
|
99
100
|
fs.writeFileSync(outputPath, JSON.stringify(updateManifest, null, 2))
|
|
101
|
+
|
|
102
|
+
// Warn if latest.json is not gitignored (it should never be committed)
|
|
103
|
+
const gitignorePath = path.join(__dirname, '../.gitignore')
|
|
104
|
+
try {
|
|
105
|
+
const gitignore = fs.readFileSync(gitignorePath, 'utf-8')
|
|
106
|
+
if (!gitignore.includes('latest.json')) {
|
|
107
|
+
console.warn('WARNING: latest.json is not in .gitignore — add it to avoid accidental commits')
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
|
package/src/App.svelte
CHANGED
|
@@ -32,6 +32,7 @@ let showSavePrompt = $state(false)
|
|
|
32
32
|
let lastConnectedConfig = $state(null)
|
|
33
33
|
let saveConnectionName = $state('')
|
|
34
34
|
let showDeleteConfirm = $state(null)
|
|
35
|
+
let showCloseConfirm = $state(false)
|
|
35
36
|
let isCheckingUpdates = $state(false)
|
|
36
37
|
let updateCheckMessage = $state('')
|
|
37
38
|
|
|
@@ -42,10 +43,12 @@ let showSettings = $state(false)
|
|
|
42
43
|
|
|
43
44
|
let invoke = null
|
|
44
45
|
let listen = null
|
|
46
|
+
let appWindow = null
|
|
45
47
|
|
|
46
48
|
// Cleanup references
|
|
47
49
|
let cancelUpdateMsgTimeout = null
|
|
48
50
|
let unlistenSidecar = null
|
|
51
|
+
let unlistenCloseRequested = null
|
|
49
52
|
|
|
50
53
|
// Global keyboard shortcuts
|
|
51
54
|
function handleGlobalKeydown(e) {
|
|
@@ -83,21 +86,31 @@ async function initApp() {
|
|
|
83
86
|
loadingProjects = true
|
|
84
87
|
|
|
85
88
|
try {
|
|
86
|
-
const [core, event] = await withTimeout(
|
|
89
|
+
const [core, event, windowModule] = await withTimeout(
|
|
87
90
|
Promise.all([
|
|
88
91
|
import('@tauri-apps/api/core'),
|
|
89
92
|
import('@tauri-apps/api/event'),
|
|
93
|
+
import('@tauri-apps/api/window'),
|
|
90
94
|
]),
|
|
91
95
|
5000,
|
|
92
96
|
)
|
|
93
97
|
invoke = core.invoke
|
|
94
98
|
listen = event.listen
|
|
99
|
+
appWindow = windowModule.getCurrentWindow()
|
|
95
100
|
} catch (err) {
|
|
96
101
|
errorMessage = `Failed to load Tauri API: ${err}`
|
|
97
102
|
loadingProjects = false
|
|
98
103
|
return
|
|
99
104
|
}
|
|
100
105
|
|
|
106
|
+
// Intercept window close — prompt if there are active connections
|
|
107
|
+
unlistenCloseRequested = await appWindow.onCloseRequested(async (event) => {
|
|
108
|
+
if (activeConnections.length > 0) {
|
|
109
|
+
event.preventDefault()
|
|
110
|
+
showCloseConfirm = true
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
101
114
|
// Set up sidecar listener (non-blocking)
|
|
102
115
|
listen('sidecar-event', (ev) => {
|
|
103
116
|
const data = ev.payload
|
|
@@ -159,8 +172,23 @@ function retryInit() {
|
|
|
159
172
|
onDestroy(() => {
|
|
160
173
|
cancelUpdateMsgTimeout?.()
|
|
161
174
|
unlistenSidecar?.()
|
|
175
|
+
unlistenCloseRequested?.()
|
|
162
176
|
})
|
|
163
177
|
|
|
178
|
+
async function confirmClose() {
|
|
179
|
+
showCloseConfirm = false
|
|
180
|
+
try {
|
|
181
|
+
await invoke('disconnect_all')
|
|
182
|
+
} catch (_err) {
|
|
183
|
+
// Best-effort disconnect before closing
|
|
184
|
+
}
|
|
185
|
+
await invoke('quit_app')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function cancelClose() {
|
|
189
|
+
showCloseConfirm = false
|
|
190
|
+
}
|
|
191
|
+
|
|
164
192
|
async function checkForUpdates() {
|
|
165
193
|
if (isCheckingUpdates) return
|
|
166
194
|
isCheckingUpdates = true
|
|
@@ -213,6 +241,14 @@ async function loadProfiles() {
|
|
|
213
241
|
}
|
|
214
242
|
}
|
|
215
243
|
|
|
244
|
+
async function refreshProjects() {
|
|
245
|
+
try {
|
|
246
|
+
projects = await invoke('list_projects')
|
|
247
|
+
} catch (err) {
|
|
248
|
+
errorMessage = `Failed to refresh projects: ${err}`
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
216
252
|
async function handleConnect() {
|
|
217
253
|
if (!selectedProject || !selectedProfile) return
|
|
218
254
|
|
|
@@ -490,6 +526,7 @@ const isAlreadySaved = $derived(
|
|
|
490
526
|
{savedConnections}
|
|
491
527
|
{activeConnections}
|
|
492
528
|
{projects}
|
|
529
|
+
{connectingId}
|
|
493
530
|
onConnect={handleSavedConnectionConnect}
|
|
494
531
|
onDisconnect={handleDisconnectOne}
|
|
495
532
|
onDelete={handleDeleteSavedConnection}
|
|
@@ -586,6 +623,18 @@ const isAlreadySaved = $derived(
|
|
|
586
623
|
/>
|
|
587
624
|
{/if}
|
|
588
625
|
|
|
626
|
+
{#if showCloseConfirm}
|
|
627
|
+
<ConfirmDialog
|
|
628
|
+
title="Close Application"
|
|
629
|
+
message="All active connections will be closed. Are you sure you want to quit?"
|
|
630
|
+
confirmLabel="Quit"
|
|
631
|
+
cancelLabel="Cancel"
|
|
632
|
+
destructive={true}
|
|
633
|
+
onConfirm={confirmClose}
|
|
634
|
+
onCancel={cancelClose}
|
|
635
|
+
/>
|
|
636
|
+
{/if}
|
|
637
|
+
|
|
589
638
|
{#if showPrerequisites}
|
|
590
639
|
<PrerequisitesCheck
|
|
591
640
|
prerequisites={prerequisitesData}
|
|
@@ -598,6 +647,7 @@ const isAlreadySaved = $derived(
|
|
|
598
647
|
<Settings
|
|
599
648
|
onClose={() => showSettings = false}
|
|
600
649
|
{invoke}
|
|
650
|
+
onProjectsChanged={refreshProjects}
|
|
601
651
|
/>
|
|
602
652
|
{/if}
|
|
603
653
|
{/if}
|