@hyzyn/dsh-tty 0.17.2 → 0.18.1
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 +171 -15
- package/README.md +170 -15
- package/client.js +115 -34
- package/lib/index.d.ts +2 -1
- package/lib/index.js +131 -25
- package/lib/index.js.map +1 -1
- package/lib/probe.js +1 -1
- package/lib/probe.js.map +1 -1
- package/lib/sftp.js +1 -1
- package/lib/sftp.js.map +1 -1
- package/lib/shell-integration.d.ts +20 -2
- package/lib/shell-integration.js +52 -2
- package/lib/shell-integration.js.map +1 -1
- package/lib/ssh.d.ts +39 -2
- package/lib/ssh.js +54 -12
- package/lib/ssh.js.map +1 -1
- package/lib/tunnels.d.ts +4 -0
- package/lib/tunnels.js +9 -5
- package/lib/tunnels.js.map +1 -1
- package/package.json +5 -4
package/lib/index.d.ts
CHANGED
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
* OSC 133/7 钩子(zsh ZDOTDIR 桩 / bash --rcfile 桩),输出流解析出命令
|
|
51
51
|
* 边界(tty_capture{last} / tty_expect 早停)与实时 cwd(tty_list)。
|
|
52
52
|
* 辅助路由:/api/dsh-tty/ssh-config(~/.ssh/config 导入候选)、
|
|
53
|
-
* /api/dsh-tty/
|
|
53
|
+
* /api/dsh-tty/credential-refs(凭据存储里已知的引用名,SSH 对话框选择器候选,只要名字)、
|
|
54
|
+
* /api/dsh-tty/env-vars(env 插件托管变量名;SSH 对话框已改从连接簿 + 凭据存储取候选,此路由保留兼容)、
|
|
54
55
|
* /api/dsh-tty/shells(设置卡片「Shell 路径」候选,仅路径)、
|
|
55
56
|
* /api/dsh-tty/sftp/*(SFTP 文件传输 0.7.0:list/mkdir/rename/remove/
|
|
56
57
|
* download/upload;0.8.0 起 mkdir 支持 parents 逐级补齐,spec 解析与
|
package/lib/index.js
CHANGED
|
@@ -14,9 +14,9 @@ import xtermHeadless from '@xterm/headless';
|
|
|
14
14
|
const HeadlessTerminal = xtermHeadless.Terminal;
|
|
15
15
|
import { definePlugin } from '@hyzyn/dsh-kit';
|
|
16
16
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
17
|
-
import { spawnSsh, sshTarget, expandHome } from './ssh.js';
|
|
17
|
+
import { spawnSsh, sshTarget, expandHome, setCredentialResolver } from './ssh.js';
|
|
18
18
|
import { probeSsh } from './probe.js';
|
|
19
|
-
import { buildCommandSpawn, buildShellSpawn } from './shell-integration.js';
|
|
19
|
+
import { buildCommandSpawn, buildShellSpawn, defaultShellPath } from './shell-integration.js';
|
|
20
20
|
import { parseSshConfig } from './ssh-config.js';
|
|
21
21
|
import { parseKnownHosts } from './known-hosts.js';
|
|
22
22
|
import { TunnelManager } from './tunnels.js';
|
|
@@ -1664,9 +1664,55 @@ function readManagedEnvKeys() {
|
|
|
1664
1664
|
}
|
|
1665
1665
|
}
|
|
1666
1666
|
/**
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1669
|
-
*
|
|
1667
|
+
* 凭据存储里**已知的引用名**(~/.dsh/.credentials.yaml 的 `refs:` 块键),只读给
|
|
1668
|
+
* SSH 对话框的引用选择器当候选。
|
|
1669
|
+
*
|
|
1670
|
+
* 为什么必须读文件:官方把「引用半边」设计成**不可枚举**——
|
|
1671
|
+
* `CredentialProvider.listRecords` 的注释原话是 "Unlike the reference half, which has no
|
|
1672
|
+
* enumeration because configuration surfaces learn which references exist from settings
|
|
1673
|
+
* schemas",而浏览器侧 `ctx.remote.credentials`(dsh-api-settings-controller)只开
|
|
1674
|
+
* `describe` / `set` / `unset`,连 `listRecords` 都没开。所以要让用户在下拉里看见
|
|
1675
|
+
* "这本存储里已经有什么名字",宿主侧读文件是唯一出路;**只要键名、不取值**(值只在本函数
|
|
1676
|
+
* 的局部 `lines` 里路过,不进任何返回值,也不写日志)。
|
|
1677
|
+
*
|
|
1678
|
+
* 解析刻意最小(与上面 readManagedEnvKeys 同款,不为它引 YAML 依赖):只认 `refs:` 顶层
|
|
1679
|
+
* 区块内「恰好两个空格 + POSIX 标识符 + 冒号」的行。本地 provider 写入时用 `yaml` 严格
|
|
1680
|
+
* 校验过(version: 1 / 值必须非空字符串 / 键必须是标识符),所以这个格式是稳的;真被手改
|
|
1681
|
+
* 坏了也只是候选少几个 —— 引用最终仍由连接时的凭据层校验存在性。
|
|
1682
|
+
*
|
|
1683
|
+
* 路径解析与本地 provider 的默认一致($DSH_HOME 优先,空串视为未设,再退 ~/.dsh)。边界:
|
|
1684
|
+
* 若有人给 provider 配了自定义 `path` / `dshHome`,这里看不到那些引用(字段仍可手输名字)。
|
|
1685
|
+
*/
|
|
1686
|
+
function readCredentialRefNames() {
|
|
1687
|
+
const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh');
|
|
1688
|
+
const file = join(dshHome, '.credentials.yaml');
|
|
1689
|
+
try {
|
|
1690
|
+
const names = [];
|
|
1691
|
+
let inRefs = false;
|
|
1692
|
+
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
1693
|
+
// 顶层键(0 缩进)切换区块;`refs:` 之后的条目才是引用名
|
|
1694
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*:/.test(line)) {
|
|
1695
|
+
inRefs = line.startsWith('refs:');
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
if (!inRefs)
|
|
1699
|
+
continue;
|
|
1700
|
+
const match = line.match(/^ {2}([A-Za-z_][A-Za-z0-9_]*):/);
|
|
1701
|
+
if (match !== null)
|
|
1702
|
+
names.push(match[1]);
|
|
1703
|
+
}
|
|
1704
|
+
return [...new Set(names)].sort().slice(0, 500);
|
|
1705
|
+
}
|
|
1706
|
+
catch {
|
|
1707
|
+
// 没有存储文件(还没存过任何东西)/ 没有读权限:候选为空,不报错
|
|
1708
|
+
return [];
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* 设置卡片「Shell 路径」候选(可选可输入的数据源):POSIX 走 /etc/shells + $SHELL +
|
|
1713
|
+
* 常见安装路径;**Windows 走 %COMSPEC% + Windows PowerShell + PowerShell 7**(原先这套
|
|
1714
|
+
* 候选在 Windows 上恒为空——/bin/zsh 那批路径一个都不存在)。去重后过滤「存在且可执行」,
|
|
1715
|
+
* 默认 shell 排最前。只回路径,不做任何执行。
|
|
1670
1716
|
*/
|
|
1671
1717
|
function listCandidateShells() {
|
|
1672
1718
|
const candidates = [];
|
|
@@ -1675,24 +1721,40 @@ function listCandidateShells() {
|
|
|
1675
1721
|
if (path !== '' && !candidates.includes(path))
|
|
1676
1722
|
candidates.push(path);
|
|
1677
1723
|
};
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1724
|
+
const fallback = defaultShellPath();
|
|
1725
|
+
if (process.platform === 'win32') {
|
|
1726
|
+
const systemRoot = process.env.SystemRoot?.trim() || 'C:\\Windows';
|
|
1727
|
+
const programFiles = process.env.ProgramFiles?.trim() || 'C:\\Program Files';
|
|
1728
|
+
const localAppData = process.env.LOCALAPPDATA?.trim() || '';
|
|
1729
|
+
// 全给**绝对路径**:候选的过滤口径是「存在且可执行」,裸命令名(pwsh.exe)在这里判不了,
|
|
1730
|
+
// 而这三处覆盖了 Windows 上实际存在的 shell(%COMSPEC% 必在,Windows PowerShell 必在,
|
|
1731
|
+
// PowerShell 7 装了才有)。
|
|
1732
|
+
push(process.env.COMSPEC);
|
|
1733
|
+
push(join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'));
|
|
1734
|
+
push(join(programFiles, 'PowerShell', '7', 'pwsh.exe'));
|
|
1735
|
+
if (localAppData !== '')
|
|
1736
|
+
push(join(localAppData, 'Microsoft', 'WindowsApps', 'pwsh.exe'));
|
|
1737
|
+
}
|
|
1738
|
+
else {
|
|
1739
|
+
try {
|
|
1740
|
+
for (const line of readFileSync('/etc/shells', 'utf8').split('\n')) {
|
|
1741
|
+
const path = line.trim();
|
|
1742
|
+
if (path !== '' && !path.startsWith('#'))
|
|
1743
|
+
push(path);
|
|
1744
|
+
}
|
|
1683
1745
|
}
|
|
1746
|
+
catch {
|
|
1747
|
+
/* 无 /etc/shells 时跳过 */
|
|
1748
|
+
}
|
|
1749
|
+
push(process.env.SHELL);
|
|
1750
|
+
for (const path of [
|
|
1751
|
+
'/bin/zsh', '/usr/bin/zsh', '/usr/local/bin/zsh', '/opt/homebrew/bin/zsh',
|
|
1752
|
+
'/bin/bash', '/usr/bin/bash', '/usr/local/bin/bash', '/opt/homebrew/bin/bash',
|
|
1753
|
+
'/bin/fish', '/usr/bin/fish', '/usr/local/bin/fish', '/opt/homebrew/bin/fish',
|
|
1754
|
+
'/bin/sh', '/bin/dash', '/bin/ksh', '/bin/tcsh', '/bin/csh',
|
|
1755
|
+
])
|
|
1756
|
+
push(path);
|
|
1684
1757
|
}
|
|
1685
|
-
catch {
|
|
1686
|
-
/* 无 /etc/shells(如 Windows)时跳过 */
|
|
1687
|
-
}
|
|
1688
|
-
push(process.env.SHELL);
|
|
1689
|
-
for (const path of [
|
|
1690
|
-
'/bin/zsh', '/usr/bin/zsh', '/usr/local/bin/zsh', '/opt/homebrew/bin/zsh',
|
|
1691
|
-
'/bin/bash', '/usr/bin/bash', '/usr/local/bin/bash', '/opt/homebrew/bin/bash',
|
|
1692
|
-
'/bin/fish', '/usr/bin/fish', '/usr/local/bin/fish', '/opt/homebrew/bin/fish',
|
|
1693
|
-
'/bin/sh', '/bin/dash', '/bin/ksh', '/bin/tcsh', '/bin/csh',
|
|
1694
|
-
])
|
|
1695
|
-
push(path);
|
|
1696
1758
|
const usable = candidates.filter((path) => {
|
|
1697
1759
|
try {
|
|
1698
1760
|
accessSync(path, fsConstants.X_OK);
|
|
@@ -1702,8 +1764,7 @@ function listCandidateShells() {
|
|
|
1702
1764
|
return false;
|
|
1703
1765
|
}
|
|
1704
1766
|
});
|
|
1705
|
-
|
|
1706
|
-
usable.sort((a, b) => (a === shell ? -1 : b === shell ? 1 : a.localeCompare(b)));
|
|
1767
|
+
usable.sort((a, b) => (a === fallback ? -1 : b === fallback ? 1 : a.localeCompare(b)));
|
|
1707
1768
|
return usable;
|
|
1708
1769
|
}
|
|
1709
1770
|
/**
|
|
@@ -1845,14 +1906,19 @@ const plugin = definePlugin({
|
|
|
1845
1906
|
if (config?.enabled === false)
|
|
1846
1907
|
return;
|
|
1847
1908
|
const live = new LiveConfig({
|
|
1848
|
-
|
|
1909
|
+
// 默认 shell 按平台取(Windows 没有 $SHELL,回落 /bin/zsh 会让本地终端一条都开不起来;
|
|
1910
|
+
// 见 defaultShellPath)。用户显式填了「Shell 路径」就照用。
|
|
1911
|
+
shell: config?.shell?.trim() || defaultShellPath(),
|
|
1849
1912
|
term: config?.term?.trim() || 'xterm-256color',
|
|
1850
1913
|
colorTerm: config?.colorTerm?.trim() || 'truecolor',
|
|
1851
1914
|
cwd: config?.cwd?.trim() || process.cwd(),
|
|
1852
1915
|
reconnectGraceSec: typeof config?.reconnectGraceSec === 'number' && Number.isInteger(config.reconnectGraceSec) && config.reconnectGraceSec >= 0 ? config.reconnectGraceSec : DEFAULT_RECONNECT_GRACE_SEC,
|
|
1853
1916
|
sshHosts: Array.isArray(config?.sshHosts) ? config.sshHosts : [],
|
|
1854
1917
|
hostKeys: Array.isArray(config?.hostKeys) ? config.hostKeys : [],
|
|
1855
|
-
|
|
1918
|
+
// shell 集成(OSC 133/7)靠 POSIX rc 注入 + `-c` 包装层:Windows 上的 cmd / PowerShell
|
|
1919
|
+
// 两者都不成立(实测 cmd 忽略 -c 空跑、PowerShell 报 export 不存在),所以恒关。
|
|
1920
|
+
// 配置项照旧留着(快照里会显示 false),界面上也说明原因。
|
|
1921
|
+
shellIntegration: process.platform !== 'win32' && config?.shellIntegration !== false,
|
|
1856
1922
|
tunnels: Array.isArray(config?.tunnels) ? config.tunnels : [],
|
|
1857
1923
|
persistence: config?.persistence === 'tmux' ? 'tmux' : 'off',
|
|
1858
1924
|
endOnPageClose: config?.endOnPageClose === true,
|
|
@@ -1922,6 +1988,11 @@ const plugin = definePlugin({
|
|
|
1922
1988
|
statsEnabled: live.statsEnabled,
|
|
1923
1989
|
sftpLimits: live.sftpLimits,
|
|
1924
1990
|
toolsRegistered: stateRef.toolsRegistered,
|
|
1991
|
+
/**
|
|
1992
|
+
* 宿主平台(`process.platform`):客户端据此把「Shell 路径 / shell 集成」的说明与候选
|
|
1993
|
+
* 按平台写(Windows 上不提 zsh/bash 那套,也不摆一个恒关闭的集成开关)。
|
|
1994
|
+
*/
|
|
1995
|
+
platform: process.platform,
|
|
1925
1996
|
});
|
|
1926
1997
|
/** 规范化并应用一份配置补丁(settings/updated 事件与 HTTP POST 共用;幂等)。 */
|
|
1927
1998
|
const applyPatch = (section) => {
|
|
@@ -2075,6 +2146,22 @@ const plugin = definePlugin({
|
|
|
2075
2146
|
}
|
|
2076
2147
|
return { patch };
|
|
2077
2148
|
};
|
|
2149
|
+
/*
|
|
2150
|
+
* credentials(**可选**依赖):连接簿里的 `env:NAME` 是**引用**,值归官方凭据 provider ——
|
|
2151
|
+
* 它自己叠 `file`(`$DSH_HOME/.credentials.yaml`)/ `env` / `project-env` / `user-env` 各层,
|
|
2152
|
+
* 并保证「每次操作重新解析」(改完下一个操作即生效,不必重启宿主)。
|
|
2153
|
+
*
|
|
2154
|
+
* 为什么必须走它:凭据存储里的值**永远不会 materialize 进环境**(provider README 原话),
|
|
2155
|
+
* 所以只读 `process.env` 等于"存进凭据存储的密码连接时读不到" —— 「保存时存入凭据存储」
|
|
2156
|
+
* 那条链此前就是断在这里(客户端那半切好了、宿主这半没切)。
|
|
2157
|
+
*
|
|
2158
|
+
* 服务缺失(老宿主 / 未装该 bundle)时不注册,`resolveSecret` 自己退回 `process.env`,行为与
|
|
2159
|
+
* 从前一致 —— 所以这是**可选**依赖,不抬高 engines 下限。
|
|
2160
|
+
*/
|
|
2161
|
+
ctx.inject(['credentials'], (credCtx) => {
|
|
2162
|
+
setCredentialResolver(credCtx.credentials ?? null);
|
|
2163
|
+
return () => { setCredentialResolver(null); };
|
|
2164
|
+
});
|
|
2078
2165
|
// webServer:WS upgrade 路由 + 配置读写路由(/api/dsh-tty/config)
|
|
2079
2166
|
ctx.inject(['webServer'], (webCtx) => {
|
|
2080
2167
|
webCtx.effect(() => {
|
|
@@ -2179,6 +2266,25 @@ const plugin = definePlugin({
|
|
|
2179
2266
|
writeJson(res, 200, { ok: true, names: readManagedEnvKeys() });
|
|
2180
2267
|
},
|
|
2181
2268
|
});
|
|
2269
|
+
// 凭据存储里已知的引用名(SSH 对话框选择器候选):只回**名字**,绝不回值。
|
|
2270
|
+
// 为什么不像其它设置界面那样只列"自己 schema 里的引用":官方那条路对这本存储
|
|
2271
|
+
// 来说列不全(引用半边不可枚举,见 readCredentialRefNames),而这个选择器的用途
|
|
2272
|
+
// 恰恰就是"我存过什么、能不能复用"。
|
|
2273
|
+
registerGated({
|
|
2274
|
+
kind: 'exact',
|
|
2275
|
+
path: '/api/dsh-tty/credential-refs',
|
|
2276
|
+
handler: async (req, res) => {
|
|
2277
|
+
if (!isLoopbackHttp(req)) {
|
|
2278
|
+
writeJson(res, 403, { error: 'forbidden: loopback-only' });
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
if (req.method !== 'GET') {
|
|
2282
|
+
writeJson(res, 405, { error: 'method not allowed: ' + String(req.method) });
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
writeJson(res, 200, { ok: true, names: readCredentialRefNames() });
|
|
2286
|
+
},
|
|
2287
|
+
});
|
|
2182
2288
|
// known_hosts 指纹导入候选(TOFU 预填充):hashed 条目用连接簿主机名还原
|
|
2183
2289
|
registerGated({
|
|
2184
2290
|
kind: 'exact',
|