Skip to content

Commit cbe00fb

Browse files
release: v0.7.15
fix(updater): let electron-builder publish assets directly (--publish always) instead of softprops/action-gh-release to ensure latest.yml filenames match actual asset names — fixes Windows auto-update failure fix(ci): consolidate macOS x64+arm64 into single build job so latest-mac.yml includes both architectures feat(ui): add custom minimize/maximize/close buttons for Windows/Linux fix(security): block sensitive keys from store:get-setting fix(security): remove exposed window.electron ipcRenderer bypass fix(ssh): tear down port forwards on session disconnect fix(db): encrypt credentials during JSON migration fix(ui): only show AI summary toast for active session fix(terminal): re-init highlighter after device auto-detection
1 parent c5b9250 commit cbe00fb

14 files changed

Lines changed: 115 additions & 46 deletions

File tree

.github/workflows/release.yml

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,16 @@ jobs:
2828
include:
2929
- os: macos-latest
3030
platform: mac
31-
arch: arm64
32-
build_cmd: npm run build && npx electron-builder --mac --arm64
33-
- os: macos-latest
34-
platform: mac
35-
arch: x64
36-
build_cmd: npm run build && npx electron-builder --mac --x64
31+
build_cmd: npm run build && npx electron-builder --mac --x64 --arm64 --publish always
3732
- os: windows-latest
3833
platform: win
39-
build_cmd: npm run build:win
34+
build_cmd: npm run build && npx electron-builder --win --publish always
4035
- os: ubuntu-latest
4136
platform: linux
42-
build_cmd: npm run build:linux
37+
build_cmd: npm run build && npx electron-builder --linux --publish always
4338

4439
runs-on: ${{ matrix.os }}
45-
name: ${{ matrix.os }} ${{ matrix.arch || '' }}
40+
name: ${{ matrix.os }}
4641

4742
steps:
4843
- name: Checkout
@@ -126,23 +121,10 @@ jobs:
126121
Write-Host "No Windows certificate found — building unsigned."
127122
}
128123
129-
- name: Build & package
124+
- name: Build, package & publish
130125
env:
131126
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
132127
APPLE_ID: ${{ secrets.APPLE_ID }}
133128
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
134129
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
135130
run: ${{ matrix.build_cmd }}
136-
137-
- name: Upload to GitHub Release
138-
uses: softprops/action-gh-release@v3
139-
with:
140-
fail_on_unmatched_files: false
141-
files: |
142-
dist/*.dmg
143-
dist/*.zip
144-
dist/*.exe
145-
dist/*.AppImage
146-
dist/*.deb
147-
dist/*.yml
148-
token: ${{ secrets.GITHUB_TOKEN }}

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "netcopilot",
3-
"version": "0.7.14",
3+
"version": "0.7.15",
44
"description": "NetCopilot – AI-powered SSH/Telnet terminal for network engineers",
55
"main": "./out/main/index.js",
66
"author": {

src/main/db.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Database from 'better-sqlite3-multiple-ciphers'
2-
import { app } from 'electron'
2+
import { app, safeStorage } from 'electron'
33
import path from 'path'
44
import { existsSync, readFileSync } from 'fs'
55
import { Connection, ConnectionGroup, SSHKey } from '../types/shared'
@@ -187,7 +187,12 @@ function migrateFromJson(db: Database.Database): void {
187187
"INSERT OR IGNORE INTO settings (key, value) VALUES (@key, @value)"
188188
)
189189
for (const [k, v] of Object.entries(credData.credentials ?? {})) {
190-
insertCred.run({ key: `cred:${k}`, value: JSON.stringify(v) })
190+
if (safeStorage.isEncryptionAvailable()) {
191+
const encrypted = safeStorage.encryptString(v)
192+
insertCred.run({ key: `cred:${k}`, value: JSON.stringify(encrypted.toString('base64')) })
193+
} else {
194+
insertCred.run({ key: `cred:${k}`, value: JSON.stringify(v) })
195+
}
191196
}
192197
} catch {/* ignore credential migration errors */}
193198
}

src/main/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ function createWindow(): void {
5353
mainWindow?.show()
5454
})
5555

56+
mainWindow.on('maximize', () => {
57+
mainWindow?.webContents.send('window:maximized-change', true)
58+
})
59+
mainWindow.on('unmaximize', () => {
60+
mainWindow?.webContents.send('window:maximized-change', false)
61+
})
62+
5663
// Zoom in/out/reset via Cmd+= / Cmd+- / Cmd+0
5764
mainWindow.webContents.on('before-input-event', (event, input) => {
5865
if (input.type !== 'keyDown') return
@@ -121,6 +128,15 @@ app.whenReady().then(() => {
121128

122129
ipcMain.handle('app:get-version', () => app.getVersion())
123130

131+
// Window controls for custom titlebar (Windows/Linux)
132+
ipcMain.handle('window:minimize', () => mainWindow?.minimize())
133+
ipcMain.handle('window:maximize', () => {
134+
if (mainWindow?.isMaximized()) mainWindow.unmaximize()
135+
else mainWindow?.maximize()
136+
})
137+
ipcMain.handle('window:close', () => mainWindow?.close())
138+
ipcMain.handle('window:is-maximized', () => mainWindow?.isMaximized() ?? false)
139+
124140
setupStoreHandlers(ipcMain)
125141
setupSshHandlers(ipcMain, () => mainWindow)
126142
setupTelnetHandlers(ipcMain, () => mainWindow)

src/main/ssh.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ function teardownSession(sessionId: string): void {
2121
try { session.jumpClient.end() } catch { /* already closed */ }
2222
}
2323
activeSessions.delete(sessionId)
24+
25+
// Close any port forwards tied to this session
26+
for (const [id, fwd] of activeForwards) {
27+
if (fwd.sessionId === sessionId) {
28+
fwd.server.close()
29+
activeForwards.delete(id)
30+
}
31+
}
2432
}
2533

2634
const activeSessions = new Map<string, ActiveSession>()

src/main/store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ export function setupStoreHandlers(ipcMain: IpcMain): void {
118118
// ── Settings ────────────────────────────────────────────────────────────────
119119

120120
ipcMain.handle('store:get-setting', (_, key: string) => {
121+
const forbidden = ['license.key', 'masterPasswordHash', 'dbKey']
122+
if (forbidden.includes(key) || key.startsWith('cred:')) return undefined
121123
const row = getDb()
122124
.prepare('SELECT value FROM settings WHERE key = ?')
123125
.get(key) as { value: string } | undefined

src/preload/index.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { contextBridge, ipcRenderer } from 'electron'
2-
import { electronAPI } from '@electron-toolkit/preload'
32

43
const api = {
54
// Store
@@ -96,6 +95,19 @@ const api = {
9695
getVersion: (): Promise<string> => ipcRenderer.invoke('app:get-version'),
9796
},
9897

98+
// Window controls (custom titlebar for Windows/Linux)
99+
window: {
100+
minimize: () => ipcRenderer.invoke('window:minimize'),
101+
maximize: () => ipcRenderer.invoke('window:maximize'),
102+
close: () => ipcRenderer.invoke('window:close'),
103+
isMaximized: () => ipcRenderer.invoke('window:is-maximized'),
104+
onMaximizedChange: (cb: (maximized: boolean) => void) => {
105+
const handler = (_: unknown, maximized: boolean) => cb(maximized)
106+
ipcRenderer.on('window:maximized-change', handler)
107+
return () => ipcRenderer.removeListener('window:maximized-change', handler)
108+
},
109+
},
110+
99111
// Auto-updater
100112
updater: {
101113
check: () => ipcRenderer.invoke('updater:check'),
@@ -216,14 +228,11 @@ const api = {
216228

217229
if (process.contextIsolated) {
218230
try {
219-
contextBridge.exposeInMainWorld('electron', electronAPI)
220231
contextBridge.exposeInMainWorld('api', api)
221232
} catch (error) {
222233
console.error(error)
223234
}
224235
} else {
225-
// @ts-ignore
226-
window.electron = electronAPI
227236
// @ts-ignore
228237
window.api = api
229238
}

src/renderer/src/components/TitleBar.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Settings, Zap, Network, HelpCircle, BookOpen, Keyboard } from 'lucide-react'
1+
import { Settings, Zap, Network, HelpCircle, BookOpen, Keyboard, Minus, Square, X, Copy } from 'lucide-react'
22
import { useState, useRef, useEffect } from 'react'
33
import { useAppStore } from '../store'
44

@@ -12,8 +12,15 @@ export function TitleBar({ onShortcuts, onWelcome }: Props): JSX.Element {
1212
const isMac = navigator.userAgent.includes('Mac')
1313
const activeSessions = sessions.filter(s => s.status === 'connected').length
1414
const [helpOpen, setHelpOpen] = useState(false)
15+
const [isMaximized, setIsMaximized] = useState(false)
1516
const helpRef = useRef<HTMLDivElement>(null)
1617

18+
useEffect(() => {
19+
if (isMac) return
20+
window.api.window.isMaximized().then(setIsMaximized)
21+
return window.api.window.onMaximizedChange(setIsMaximized)
22+
}, [isMac])
23+
1724
useEffect(() => {
1825
if (!helpOpen) return
1926
const handler = (e: MouseEvent) => {
@@ -26,7 +33,7 @@ export function TitleBar({ onShortcuts, onWelcome }: Props): JSX.Element {
2633
return (
2734
<div
2835
className="drag-region h-11 flex items-center justify-between border-b border-border bg-sidebar shrink-0"
29-
style={{ paddingLeft: isMac ? '80px' : '16px', paddingRight: '12px' }}
36+
style={{ paddingLeft: isMac ? '80px' : '16px', paddingRight: isMac ? '12px' : '0px' }}
3037
>
3138
{/* Logo */}
3239
<div className="flex items-center gap-2 no-drag">
@@ -100,6 +107,36 @@ export function TitleBar({ onShortcuts, onWelcome }: Props): JSX.Element {
100107
<Settings className="w-4 h-4" />
101108
</button>
102109
</div>
110+
111+
{/* Window controls — Windows/Linux only */}
112+
{!isMac && (
113+
<div className="flex items-center no-drag ml-2">
114+
<button
115+
onClick={() => window.api.window.minimize()}
116+
className="w-11 h-11 flex items-center justify-center text-muted-foreground hover:bg-white/10 hover:text-foreground transition-colors"
117+
title="Minimize"
118+
>
119+
<Minus className="w-4 h-4" />
120+
</button>
121+
<button
122+
onClick={() => window.api.window.maximize()}
123+
className="w-11 h-11 flex items-center justify-center text-muted-foreground hover:bg-white/10 hover:text-foreground transition-colors"
124+
title={isMaximized ? 'Restore' : 'Maximize'}
125+
>
126+
{isMaximized
127+
? <Copy className="w-3.5 h-3.5 rotate-180" />
128+
: <Square className="w-3 h-3" />
129+
}
130+
</button>
131+
<button
132+
onClick={() => window.api.window.close()}
133+
className="w-11 h-11 flex items-center justify-center text-muted-foreground hover:bg-red-500 hover:text-white transition-colors"
134+
title="Close"
135+
>
136+
<X className="w-4 h-4" />
137+
</button>
138+
</div>
139+
)}
103140
</div>
104141
)
105142
}

src/renderer/src/components/terminal/TabBar.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,17 @@ export function TabBar(): JSX.Element {
5454
onActivate={() => setActiveSession(session.id)}
5555
onClose={() => {
5656
if (session.id === splitSessionId) setSplitSession(null)
57-
// Generate session summary from ARIA messages before closing
58-
const { aiMessages } = useAppStore.getState()
59-
const cmds = aiMessages.filter(m => m.toolCalls?.length).flatMap(m => m.toolCalls ?? []).filter(t => t.status === 'done')
60-
if (cmds.length > 0) {
61-
const names = [...new Set(cmds.map(t => t.command.split(' ').slice(0, 3).join(' ')))].slice(0, 3)
62-
toast.info(`Session closed — ${session.connection.name}`, {
63-
description: `ARIA ran ${cmds.length} command${cmds.length > 1 ? 's' : ''}: ${names.join(', ')}${cmds.length > 3 ? '…' : ''}`,
64-
duration: 5000,
65-
})
57+
// Only summarize AI messages when closing the active session (aiMessages is global)
58+
if (session.id === activeSessionId) {
59+
const { aiMessages } = useAppStore.getState()
60+
const cmds = aiMessages.filter(m => m.toolCalls?.length).flatMap(m => m.toolCalls ?? []).filter(t => t.status === 'done')
61+
if (cmds.length > 0) {
62+
const names = [...new Set(cmds.map(t => t.command.split(' ').slice(0, 3).join(' ')))].slice(0, 3)
63+
toast.info(`Session closed — ${session.connection.name}`, {
64+
description: `ARIA ran ${cmds.length} command${cmds.length > 1 ? 's' : ''}: ${names.join(', ')}${cmds.length > 3 ? '…' : ''}`,
65+
duration: 5000,
66+
})
67+
}
6668
}
6769
closeSession(session.id)
6870
}}

0 commit comments

Comments
 (0)