dsh-ssh-manage 0.3.2 → 0.3.4

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/README.en.md CHANGED
@@ -25,7 +25,7 @@
25
25
  - **Host management** — scan the config, groups, tags, favorites, notes, latency and host inspection;
26
26
  - **Interactive terminal** — ssh2 allocates a PTY, xterm.js renders it, with colors, arrow keys, full-screen programs such as `vim`/`top`, and window resizing;
27
27
  - **Day-to-day operations** — batch commands, SFTP browse/mkdir/rename/delete/upload/download, connection tests;
28
- - **Credentials** — one AES-256-GCM encrypted password per host, never written to `~/.ssh/config` and never returned to the browser;
28
+ - **Credentials** — one AES-256-GCM encrypted password per host, never written to `~/.ssh/config`; passwords are not returned to the browser by default, but Show password decrypts one on demand in the current page and auto-hides it after 30 seconds;
29
29
  - **Agent tool** — `ssh_manager` exposes the same capabilities to the model.
30
30
 
31
31
  No more jumping between a terminal and DSH, and no need to remember where each host lives in the config file.
@@ -60,7 +60,7 @@ No more jumping between a terminal and DSH, and no need to remember where each h
60
60
  ### Credentials and data
61
61
 
62
62
  - Passwords are AES-256-GCM encrypted in the plugin state directory and unlocked automatically at startup; no master password, no system Keychain.
63
- - Passwords are only used for SSH password authentication (also compatible with hosts that only offer keyboard-interactive). They are never written to `~/.ssh/config`, command arguments, logs or responses.
63
+ - Passwords are only used for SSH password authentication (also compatible with hosts that only offer keyboard-interactive). They are never written to `~/.ssh/config`, command arguments or logs. They are not returned by default; Show password explicitly decrypts one into the current page for 30 seconds and it is never exposed through the Agent tool.
64
64
  - Ciphertext and key live in the same user directory, so this mainly protects against accidental file reads, not against malicious processes running as the same user.
65
65
  - Groups, tags, favorites, notes and inspection data live in `hosts.json`; passwords live in `vault.json`; configuration changes are snapshotted to the backup directory.
66
66
 
@@ -96,16 +96,16 @@ Creating a host requires alias, hostname and password. The password is not writt
96
96
 
97
97
  ### From npm (recommended)
98
98
 
99
- npm package: [`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage) (current version `0.3.2`).
99
+ npm package: [`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage) (current version `0.3.4`).
100
100
 
101
101
  ```text
102
- plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.2
102
+ plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.4
103
103
  ```
104
104
 
105
105
  Or with the DSH CLI:
106
106
 
107
107
  ```bash
108
- dsh plugin add dsh-ssh-manage@0.3.2
108
+ dsh plugin add dsh-ssh-manage@0.3.4
109
109
  ```
110
110
 
111
111
  After installation, refresh the Web page; the sidebar shows an "SSH Hosts" entry with id `dsh-ssh-manage`.
package/README.md CHANGED
@@ -25,7 +25,7 @@
25
25
  - **主机管理**:扫描配置、分组、标签、收藏、备注、连接延迟、主机巡检;
26
26
  - **交互式终端**:ssh2 分配 PTY,xterm.js 渲染,支持彩色输出、方向键、`vim`/`top` 等全屏程序和窗口自适应;
27
27
  - **日常运维**:批量命令、SFTP 目录浏览 / 新建 / 重命名 / 删除 / 上传 / 下载、测试连接;
28
- - **凭据管理**:每台主机保存一份 AES-256-GCM 加密密码,不写进 `~/.ssh/config`,也不返回给浏览器;
28
+ - **凭据管理**:每台主机保存一份 AES-256-GCM 加密密码,不写进 `~/.ssh/config`;默认不返回浏览器,在主机详情点“显示密码”时才按需解密到当前页面,30 秒后自动隐藏;
29
29
  - **Agent 工具**:`ssh_manager` 把同一套能力开放给模型。
30
30
 
31
31
  不用再在终端和 DSH 之间来回切换,也不用记每个主机在配置文件里的位置。
@@ -60,7 +60,7 @@
60
60
  ### 凭据与数据
61
61
 
62
62
  - 密码使用 AES-256-GCM 加密保存在插件自己的状态目录,插件启动时自动解锁,不需要每次输入主密码,也不使用系统 Keychain。
63
- - 密码只用于 SSH password authentication,同时兼容只提供 keyboard-interactive 的主机;不会写入 `~/.ssh/config`、命令参数、日志或返回给浏览器。
63
+ - 密码只用于 SSH password authentication,同时兼容只提供 keyboard-interactive 的主机;不会写入 `~/.ssh/config`、命令参数或日志。默认不返回浏览器;仅在你主动点击“显示密码”时按需解密到当前页面,30 秒后自动隐藏,且不会通过 Agent 工具返回。
64
64
  - 密码密文与本地密钥位于同一用户目录,主要防止文件被直接误读,不能抵御同一用户下的恶意进程。
65
65
  - 主机分组、标签、收藏、备注、巡检结果保存在 `hosts.json`;密码保存在 `vault.json`;删除/修改配置前写入备份目录。
66
66
 
@@ -98,16 +98,16 @@
98
98
 
99
99
  ### 从 npm 安装(推荐)
100
100
 
101
- npm 包:[`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage)(当前版本 `0.3.2`)。
101
+ npm 包:[`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage)(当前版本 `0.3.4`)。
102
102
 
103
103
  ```text
104
- plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.2
104
+ plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.4
105
105
  ```
106
106
 
107
107
  或使用 DSH CLI:
108
108
 
109
109
  ```bash
110
- dsh plugin add dsh-ssh-manage@0.3.2
110
+ dsh plugin add dsh-ssh-manage@0.3.4
111
111
  ```
112
112
 
113
113
  安装后刷新 Web 页面,左侧栏会出现「SSH 主机」入口,入口 id 为 `dsh-ssh-manage`。
package/client.js CHANGED
@@ -134,6 +134,9 @@ window.__ModuleLoader__.load({
134
134
  'editor.port': ['端口', 'Port'],
135
135
  'editor.group': ['分组', 'Group'],
136
136
  'editor.groupPlaceholder': ['例如 production', 'e.g. production'],
137
+ 'editor.groupChoose': ['选择已有分组', 'Choose an existing group'],
138
+ 'editor.groupNone': ['无分组', 'No group'],
139
+ 'editor.groupNoOptions': ['还没有已有分组,直接输入新名称即可。', 'No groups yet; type a new name.'],
137
140
  'editor.tags': ['标签', 'Tags'],
138
141
  'editor.tagsPlaceholder': ['web, critical(逗号分隔)', 'web, critical (comma separated)'],
139
142
  'editor.label': ['显示名称', 'Display name'],
@@ -206,7 +209,13 @@ window.__ModuleLoader__.load({
206
209
  'drawer.details.password': ['密码', 'Password'],
207
210
  'drawer.details.savePassword': ['保存密码', 'Save password'],
208
211
  'drawer.details.clearPassword': ['清除密码', 'Clear password'],
209
- 'drawer.details.passwordHint': ['密码使用 AES-256-GCM 加密保存在本机,不会回传到浏览器。', 'Passwords are AES-256-GCM encrypted on this machine and never returned to the browser.'],
212
+ 'drawer.details.passwordHint': ['密码使用 AES-256-GCM 加密保存在本机;点击“显示密码”可在当前页面临时明文查看,30 秒后自动隐藏。', 'Passwords are AES-256-GCM encrypted at rest; use Show password to reveal one temporarily on this page (auto-hides after 30 seconds).'],
213
+ 'drawer.details.reveal': ['显示密码', 'Show password'],
214
+ 'drawer.details.hide': ['隐藏密码', 'Hide password'],
215
+ 'drawer.details.copyPassword': ['复制密码', 'Copy password'],
216
+ 'drawer.details.passwordCopied': ['密码已复制', 'Password copied'],
217
+ 'drawer.details.revealWarning': ['密码已明文显示;注意旁人、录屏和截图,30 秒后自动隐藏。', 'The password is visible in plain text; watch for onlookers, screen recording and screenshots. It hides after 30 seconds.'],
218
+ 'drawer.details.revealFailed': ['无法显示密码:{message}', 'Could not reveal the password: {message}'],
210
219
  'drawer.details.system': ['主机信息', 'System'],
211
220
  'drawer.details.detect': ['检测信息', 'Detect'],
212
221
  'drawer.details.detecting': ['检测中…', 'Detecting…'],
@@ -780,6 +789,25 @@ window.__ModuleLoader__.load({
780
789
  )
781
790
  }
782
791
 
792
+ function IconEye({ size = 14 }) {
793
+ return h(
794
+ Svg,
795
+ { size },
796
+ h(P, { d: 'M2.5 12s3.6-6.2 9.5-6.2S21.5 12 21.5 12s-3.6 6.2-9.5 6.2S2.5 12 2.5 12Z' }),
797
+ h('circle', { cx: 12, cy: 12, r: 3, stroke: 'currentColor', strokeWidth: 1.5 }),
798
+ )
799
+ }
800
+
801
+ function IconEyeOff({ size = 14 }) {
802
+ return h(
803
+ Svg,
804
+ { size },
805
+ h(P, { d: 'M3 3l18 18' }),
806
+ h(P, { d: 'M10.6 6.1A10.7 10.7 0 0 1 12 6c5.9 0 9.5 6 9.5 6a17 17 0 0 1-3.1 3.6M6.4 6.8C3.9 8.7 2.5 12 2.5 12s3.6 6 9.5 6a10.9 10.9 0 0 0 4.5-1' }),
807
+ h(P, { d: 'M9.9 9.9a3 3 0 0 0 4.2 4.2' }),
808
+ )
809
+ }
810
+
783
811
  function IconEraser({ size = 15 }) {
784
812
  return h(
785
813
  Svg,
@@ -4429,20 +4457,71 @@ window.__ModuleLoader__.load({
4429
4457
  }
4430
4458
 
4431
4459
  function DetailsView({ model, host, onEdit, onDelete }) {
4432
- const { t, localeId, isPending, testHost, inspectHost, savePassword, removePassword } = model
4460
+ const { t, notify, localeId, isPending, testHost, inspectHost, savePassword, removePassword } = model
4433
4461
  const [password, setPassword] = React.useState('')
4434
4462
  const [saving, setSaving] = React.useState(false)
4463
+ const [revealedPassword, setRevealedPassword] = React.useState('')
4464
+ const [revealing, setRevealing] = React.useState(false)
4465
+ const revealTimer = React.useRef(null)
4435
4466
  const credential = credentialInfo(host, t)
4436
4467
  const testing = isPending(`test:${host.host}`)
4437
4468
  const inspecting = isPending(`inspect:${host.host}`)
4438
4469
  const hasSystem = Boolean(host.system || host.cpuCores || host.memoryGB || host.diskGB || host.latencyMs)
4439
4470
 
4471
+ const hideRevealed = () => {
4472
+ if (revealTimer.current) {
4473
+ clearTimeout(revealTimer.current)
4474
+ revealTimer.current = null
4475
+ }
4476
+ setRevealedPassword('')
4477
+ }
4478
+
4479
+ React.useEffect(() => {
4480
+ hideRevealed()
4481
+ return () => {
4482
+ if (revealTimer.current) clearTimeout(revealTimer.current)
4483
+ }
4484
+ }, [host.host])
4485
+
4486
+ async function toggleReveal() {
4487
+ if (revealedPassword) {
4488
+ hideRevealed()
4489
+ return
4490
+ }
4491
+ if (revealing || host.passwordConfigured !== true) return
4492
+ setRevealing(true)
4493
+ try {
4494
+ const value = await callAction('credential/reveal', { host: host.host })
4495
+ const revealed = String(value?.password ?? '')
4496
+ if (!revealed) return
4497
+ setRevealedPassword(revealed)
4498
+ if (revealTimer.current) clearTimeout(revealTimer.current)
4499
+ revealTimer.current = setTimeout(() => {
4500
+ revealTimer.current = null
4501
+ setRevealedPassword('')
4502
+ }, 30_000)
4503
+ } catch (failure) {
4504
+ notify(t('drawer.details.revealFailed', { message: errorMessage(failure) }), 'error')
4505
+ } finally {
4506
+ setRevealing(false)
4507
+ }
4508
+ }
4509
+
4510
+ async function copyRevealed() {
4511
+ if (!revealedPassword) return
4512
+ const copied = await copyTextToClipboard(revealedPassword)
4513
+ notify(copied ? t('drawer.details.passwordCopied') : t('drawer.details.revealFailed', { message: t('terminal.copyFailed') }), copied ? 'success' : 'error')
4514
+ }
4515
+
4440
4516
  async function submitPassword() {
4441
4517
  if (!password || saving) return
4442
4518
  setSaving(true)
4443
4519
  const ok = await savePassword(host, password)
4444
4520
  setSaving(false)
4445
- if (ok) setPassword('')
4521
+ if (ok) {
4522
+ setPassword('')
4523
+ hideRevealed()
4524
+ }
4446
4525
  }
4447
4526
 
4448
4527
  return h(
@@ -4484,6 +4563,30 @@ window.__ModuleLoader__.load({
4484
4563
  h('h3', { className: 'ssh-sectionTitle' }, t('drawer.details.credentials')),
4485
4564
  h(Badge, { tone: credential.tone }, credential.label),
4486
4565
  ),
4566
+ h(
4567
+ 'div',
4568
+ { className: 'ssh-credentialReveal' },
4569
+ h(
4570
+ Button,
4571
+ {
4572
+ size: 'sm',
4573
+ variant: 'ghost',
4574
+ onClick: () => void toggleReveal(),
4575
+ disabled: revealing || (!host.passwordConfigured && !revealedPassword),
4576
+ },
4577
+ revealing ? h(Spinner, { size: 12 }) : revealedPassword ? h(IconEyeOff, { size: 13 }) : h(IconEye, { size: 13 }),
4578
+ revealedPassword ? t('drawer.details.hide') : t('drawer.details.reveal'),
4579
+ ),
4580
+ revealedPassword
4581
+ ? h('code', { className: 'ssh-revealValue' }, revealedPassword)
4582
+ : null,
4583
+ revealedPassword
4584
+ ? h(IconButton, { title: t('drawer.details.copyPassword'), onClick: () => void copyRevealed() }, h(IconCopy, { size: 14 }))
4585
+ : null,
4586
+ ),
4587
+ revealedPassword
4588
+ ? h('div', { className: 'ssh-sectionHint ssh-revealWarning' }, h(IconEye, { size: 12 }), h('span', null, t('drawer.details.revealWarning')))
4589
+ : null,
4487
4590
  h(
4488
4591
  'div',
4489
4592
  { className: 'ssh-credentialRow' },
@@ -4505,7 +4608,7 @@ window.__ModuleLoader__.load({
4505
4608
  host.passwordConfigured
4506
4609
  ? h(
4507
4610
  Button,
4508
- { size: 'sm', variant: 'dangerGhost', onClick: () => void removePassword(host) },
4611
+ { size: 'sm', variant: 'dangerGhost', onClick: () => { hideRevealed(); void removePassword(host) } },
4509
4612
  t('drawer.details.clearPassword'),
4510
4613
  )
4511
4614
  : null,
@@ -4590,6 +4693,9 @@ window.__ModuleLoader__.load({
4590
4693
  const [password, setPassword] = React.useState('')
4591
4694
  const [passwordError, setPasswordError] = React.useState('')
4592
4695
  const [formError, setFormError] = React.useState('')
4696
+ const [groupMenuOpen, setGroupMenuOpen] = React.useState(false)
4697
+ const groupMenuRef = React.useRef(null)
4698
+ const groupOptions = (model.allGroups ?? []).filter((group) => group.key !== '').map((group) => group.key)
4593
4699
  const busy = isPending('editor')
4594
4700
  const passwordRequired = !original || original.passwordConfigured !== true
4595
4701
 
@@ -4619,6 +4725,27 @@ window.__ModuleLoader__.load({
4619
4725
  }
4620
4726
  })
4621
4727
 
4728
+ React.useEffect(() => {
4729
+ if (!groupMenuOpen) return undefined
4730
+ const close = (event) => {
4731
+ if (groupMenuRef.current && groupMenuRef.current.contains(event.target)) return
4732
+ setGroupMenuOpen(false)
4733
+ }
4734
+ const onKey = (event) => {
4735
+ if (event.key === 'Escape') {
4736
+ event.preventDefault()
4737
+ event.stopPropagation()
4738
+ setGroupMenuOpen(false)
4739
+ }
4740
+ }
4741
+ document.addEventListener('mousedown', close)
4742
+ document.addEventListener('keydown', onKey, true)
4743
+ return () => {
4744
+ document.removeEventListener('mousedown', close)
4745
+ document.removeEventListener('keydown', onKey, true)
4746
+ }
4747
+ }, [groupMenuOpen])
4748
+
4622
4749
  async function submit() {
4623
4750
  const alias = draft.host.trim()
4624
4751
  const hostname = draft.hostname.trim()
@@ -4745,11 +4872,59 @@ window.__ModuleLoader__.load({
4745
4872
  h(
4746
4873
  Field,
4747
4874
  { label: t('editor.group') },
4748
- h(TextInput, {
4749
- value: draft.group,
4750
- onChange: (value) => update('group', value),
4751
- placeholder: t('editor.groupPlaceholder'),
4752
- }),
4875
+ h(
4876
+ 'div',
4877
+ { className: 'ssh-groupCombo', ref: groupMenuRef },
4878
+ h(TextInput, {
4879
+ value: draft.group,
4880
+ onChange: (value) => update('group', value),
4881
+ placeholder: t('editor.groupPlaceholder'),
4882
+ }),
4883
+ h(
4884
+ 'button',
4885
+ {
4886
+ type: 'button',
4887
+ className: 'ssh-groupToggle',
4888
+ title: t('editor.groupChoose'),
4889
+ 'aria-label': t('editor.groupChoose'),
4890
+ 'aria-haspopup': 'listbox',
4891
+ 'aria-expanded': groupMenuOpen,
4892
+ onClick: () => setGroupMenuOpen((value) => !value),
4893
+ },
4894
+ h(IconChevronDown, { size: 13 }),
4895
+ ),
4896
+ groupMenuOpen
4897
+ ? h(
4898
+ 'div',
4899
+ { className: 'ssh-groupMenu', role: 'listbox' },
4900
+ h('button', {
4901
+ type: 'button',
4902
+ role: 'option',
4903
+ 'aria-selected': draft.group.trim() === '',
4904
+ className: cx('ssh-groupOption', draft.group.trim() === '' && 'active'),
4905
+ onClick: () => {
4906
+ update('group', '')
4907
+ setGroupMenuOpen(false)
4908
+ },
4909
+ }, t('editor.groupNone')),
4910
+ groupOptions.length === 0
4911
+ ? h('div', { className: 'ssh-groupEmpty' }, t('editor.groupNoOptions'))
4912
+ : groupOptions.map((option) =>
4913
+ h('button', {
4914
+ key: option,
4915
+ type: 'button',
4916
+ role: 'option',
4917
+ 'aria-selected': draft.group.trim() === option,
4918
+ className: cx('ssh-groupOption', draft.group.trim() === option && 'active'),
4919
+ onClick: () => {
4920
+ update('group', option)
4921
+ setGroupMenuOpen(false)
4922
+ },
4923
+ }, option),
4924
+ ),
4925
+ )
4926
+ : null,
4927
+ ),
4753
4928
  ),
4754
4929
  h(
4755
4930
  Field,
@@ -5298,7 +5473,7 @@ body.ssh-col-resizing,body.ssh-col-resizing *{cursor:col-resize!important;user-s
5298
5473
  .ssh-mainTop{flex:0 0 auto;display:flex;align-items:center;flex-wrap:wrap;gap:8px;row-gap:6px;padding:10px 14px;border-bottom:.5px solid var(--ssh-border);min-height:56px}
5299
5474
  .ssh-context{display:flex;align-items:baseline;gap:6px;min-width:0;max-width:180px;flex:0 0 auto}
5300
5475
  .ssh-contextName{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5301
- .ssh-contextCount{font-size:11px;color:var(--ssh-text-3);font-variant-numeric:tabular-nums}
5476
+ .ssh-contextCount{font-size:11px;color:var(--ssh-text-2);font-variant-numeric:tabular-nums}
5302
5477
  .ssh-search{position:relative;display:flex;align-items:center;flex:0 1 300px;min-width:120px}
5303
5478
  .ssh-search > svg{position:absolute;left:10px;color:var(--ssh-dim);pointer-events:none;z-index:1}
5304
5479
  .ssh-search .ssh-input{height:30px;padding-left:31px;border-radius:999px;background:var(--ssh-layer-1);font-size:12px}
@@ -5312,7 +5487,7 @@ body.ssh-col-resizing,body.ssh-col-resizing *{cursor:col-resize!important;user-s
5312
5487
  .ssh-table{flex:1;min-height:0;display:flex;flex-direction:column;min-width:0}
5313
5488
  .ssh-tableScroll{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column}
5314
5489
  .ssh-grid{display:grid;align-items:center;column-gap:8px;padding:0 12px;width:100%;min-width:0}
5315
- .ssh-head{flex:0 0 auto;height:38px;border-bottom:.5px solid var(--ssh-border);color:var(--ssh-text-3);font-size:11px;font-weight:500;background:var(--ssh-bg);position:sticky;top:0;z-index:3;min-width:640px}
5490
+ .ssh-head{flex:0 0 auto;height:38px;border-bottom:.5px solid var(--ssh-border);color:var(--ssh-text-2);font-size:11px;font-weight:500;background:var(--ssh-bg);position:sticky;top:0;z-index:3;min-width:640px}
5316
5491
  .ssh-cell{min-width:0;display:flex;align-items:center;gap:6px;overflow:hidden}
5317
5492
  .ssh-headBtn{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:0;border:0;background:transparent;color:inherit;font:inherit;font-size:11px;font-weight:500;cursor:pointer;white-space:nowrap;min-width:0}
5318
5493
  .ssh-headBtn:hover{color:var(--ssh-text)}
@@ -5330,32 +5505,32 @@ body.ssh-col-resizing,body.ssh-col-resizing *{cursor:col-resize!important;user-s
5330
5505
  .ssh-colLatency{min-width:0}
5331
5506
  .ssh-colSystem{min-width:0}
5332
5507
  .ssh-osBadge{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:8px;background:var(--ssh-layer-3);color:var(--ssh-brand)}
5333
- .ssh-osBadge.muted{color:var(--ssh-dim)}
5508
+ .ssh-osBadge.muted{color:var(--ssh-text-2)}
5334
5509
  .ssh-osName{font-size:11.5px;color:var(--ssh-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5335
- .ssh-osName.muted{color:var(--ssh-dim)}
5510
+ .ssh-osName.muted{color:var(--ssh-text-2)}
5336
5511
  .ssh-latency{appearance:none;display:inline-flex;align-items:center;gap:5px;height:24px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--ssh-text-2);font:inherit;font-size:11.5px;white-space:nowrap;cursor:pointer}
5337
- .ssh-latency.ok{color:var(--dsw-alias-state-success-label,var(--ssh-text-2))}
5338
- .ssh-latency.muted{color:var(--ssh-dim)}
5512
+ .ssh-latency.ok{color:var(--ssh-success)}
5513
+ .ssh-latency.muted{color:var(--ssh-text-2)}
5339
5514
  .ssh-latency:hover:not(:disabled){color:var(--ssh-text)}
5340
5515
  .ssh-latency:disabled{cursor:default}
5341
5516
  .ssh-latencyDot{width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.55}
5342
5517
  .ssh-latencyIcon{display:inline-flex;color:var(--dsw-alias-state-success-label,var(--ssh-success))}
5343
5518
  .ssh-colName{flex-direction:column;align-items:flex-start;gap:0}
5344
5519
  .ssh-nameMain{display:flex;align-items:center;gap:5px;max-width:100%;font-size:12.5px;font-weight:500;color:var(--ssh-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5345
- .ssh-nameSub{max-width:100%;font-size:10.5px;color:var(--ssh-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5520
+ .ssh-nameSub{max-width:100%;font-size:10.5px;color:var(--ssh-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5346
5521
  .ssh-favMark{color:var(--ssh-warn);display:inline-flex;flex:0 0 auto}
5347
5522
  .ssh-colAddress{gap:3px;min-width:0}
5348
- .ssh-address{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;color:var(--ssh-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
5523
+ .ssh-address{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;color:var(--ssh-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
5349
5524
  .ssh-inlineActions{display:inline-flex;gap:1px;flex:0 0 auto;opacity:.45;transition:opacity .12s ease}
5350
5525
  .ssh-row:hover .ssh-inlineActions{opacity:1}
5351
5526
  .ssh-miniBtn{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--ssh-text-3);cursor:pointer}
5352
5527
  .ssh-miniBtn:hover{background:var(--ssh-hover);color:var(--ssh-text)}
5353
5528
  .ssh-colInfo{gap:4px;row-gap:3px;flex-wrap:wrap;align-content:center;overflow:visible}
5354
5529
  .ssh-chip{display:inline-flex;flex:0 0 auto;align-items:center;height:21px;max-width:100%;padding:0 7px;border-radius:6px;border:.5px solid var(--ssh-border);background:var(--ssh-layer-3);font-size:10.5px;color:var(--ssh-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5355
- .ssh-mutedText{font-size:11.5px;color:var(--ssh-dim)}
5530
+ .ssh-mutedText{font-size:11.5px;color:var(--ssh-text-2)}
5356
5531
  .ssh-colNotes{min-width:0}
5357
5532
  .ssh-noteText{font-size:11.5px;color:var(--ssh-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5358
- .ssh-noteText.muted{color:var(--ssh-dim)}
5533
+ .ssh-noteText.muted{color:var(--ssh-text-2)}
5359
5534
  .ssh-colActions{gap:4px;justify-content:flex-start;overflow:visible}
5360
5535
  .ssh-colGear{justify-content:flex-end}
5361
5536
  .ssh-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:56px 24px;text-align:center;color:var(--ssh-text-3);font-size:12px;min-height:0}
@@ -5391,6 +5566,14 @@ body.ssh-col-resizing,body.ssh-col-resizing *{cursor:col-resize!important;user-s
5391
5566
  .ssh-input.mono,.ssh-textarea.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}
5392
5567
  .ssh-textarea{height:auto;padding:9px 11px;line-height:1.65;resize:vertical;min-height:74px}
5393
5568
  .ssh-field{display:flex;flex-direction:column;gap:5px;min-width:0}
5569
+ .ssh-groupCombo{position:relative;display:flex;align-items:center;gap:4px}
5570
+ .ssh-groupCombo .ssh-input{flex:1;min-width:0}
5571
+ .ssh-groupToggle{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:.5px solid var(--ssh-border-strong);border-radius:var(--ssh-radius-sm);background:var(--ssh-layer-2);color:var(--ssh-text-2);cursor:pointer;flex:0 0 auto}
5572
+ .ssh-groupToggle:hover{background:var(--ssh-hover);color:var(--ssh-text)}
5573
+ .ssh-groupMenu{position:absolute;z-index:12;top:calc(100% + 5px);left:0;right:0;max-height:190px;overflow:auto;padding:4px;border:.5px solid var(--ssh-border-strong);border-radius:var(--ssh-radius-md);background:var(--ssh-layer-1);box-shadow:var(--ssh-shadow)}
5574
+ .ssh-groupOption{appearance:none;display:block;width:100%;padding:6px 8px;border:0;border-radius:6px;background:transparent;color:var(--ssh-text);font:inherit;font-size:12px;text-align:left;cursor:pointer}
5575
+ .ssh-groupOption:hover,.ssh-groupOption.active{background:var(--ssh-hover)}
5576
+ .ssh-groupEmpty{padding:6px 8px;font-size:11.5px;color:var(--ssh-text-2)}
5394
5577
  .ssh-field.wide{grid-column:1/-1}
5395
5578
  .ssh-label{font-size:11.5px;font-weight:500;color:var(--ssh-text-2)}
5396
5579
  .ssh-hint{font-size:10.5px;color:var(--ssh-text-3);line-height:1.55}
@@ -5544,6 +5727,9 @@ body.ssh-col-resizing,body.ssh-col-resizing *{cursor:col-resize!important;user-s
5544
5727
  .ssh-kvValue{color:var(--ssh-text-2);min-width:0;word-break:break-word}
5545
5728
  .ssh-kvValue.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}
5546
5729
  .ssh-sectionHint{font-size:11.5px;color:var(--ssh-text-3);line-height:1.65}
5730
+ .ssh-credentialReveal{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:2px}
5731
+ .ssh-revealValue{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;padding:5px 8px;border:.5px solid var(--ssh-border-strong);border-radius:6px;background:var(--ssh-layer-3);color:var(--ssh-text);word-break:break-all;user-select:all}
5732
+ .ssh-revealWarning{display:flex;align-items:flex-start;gap:5px;color:var(--ssh-warn)}
5547
5733
  .ssh-tags{display:flex;gap:5px;flex-wrap:wrap}
5548
5734
  .ssh-detailsActions{display:flex;gap:8px;padding-top:2px}
5549
5735
  .ssh-credentialRow{display:flex;align-items:center;gap:6px}
package/index.js CHANGED
@@ -167,6 +167,12 @@ export async function runAction(paths, action, params = {}) {
167
167
  }
168
168
  case 'credential/delete':
169
169
  return await removePassword(paths, requireString(params, 'host'))
170
+ case 'credential/reveal': {
171
+ const host = await findHost(paths, params.host)
172
+ const password = await getPassword(paths, host.host)
173
+ if (!password) throw new Error('No password is stored for this host')
174
+ return { password }
175
+ }
170
176
  case 'host/metadata': {
171
177
  const alias = requireString(params, 'host')
172
178
  await findHost(paths, alias)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ssh-manage",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "A DSH sidebar plugin for managing SSH host profiles with a real interactive PTY terminal.",
5
5
  "license": "MIT",
6
6
  "private": false,