@tiphareth/dsh-hardssh 0.1.2
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.md +50 -0
- package/cordis.patch.yml +26 -0
- package/lib/client.js +17780 -0
- package/lib/environment-BL1jddfB.js +449 -0
- package/lib/fs.js +478 -0
- package/lib/index.js +6758 -0
- package/lib/subprocess.js +600 -0
- package/lib/switch-fs-CAJpFY9C.js +193 -0
- package/lib/switch-fs-RrZtG2gv.js +210 -0
- package/lib/types/backend.d.ts +108 -0
- package/lib/types/base/capability.d.ts +107 -0
- package/lib/types/base/index.d.ts +18 -0
- package/lib/types/base/ledger-router.d.ts +48 -0
- package/lib/types/base/ledger.d.ts +82 -0
- package/lib/types/base/model.d.ts +108 -0
- package/lib/types/base/namespace.d.ts +57 -0
- package/lib/types/base/plugin.d.ts +102 -0
- package/lib/types/base/registry.d.ts +60 -0
- package/lib/types/base/router.d.ts +40 -0
- package/lib/types/client/api.d.ts +85 -0
- package/lib/types/client/directory-flow.d.ts +82 -0
- package/lib/types/client/icons.d.ts +19 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/locales.d.ts +104 -0
- package/lib/types/client/manager-button.d.ts +32 -0
- package/lib/types/client/migrate.d.ts +20 -0
- package/lib/types/client/ssh/api.d.ts +89 -0
- package/lib/types/client/ssh/apply.d.ts +23 -0
- package/lib/types/client/ssh/locales.d.ts +156 -0
- package/lib/types/client/ssh/mount.d.ts +13 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/HostFingerprintDialog.d.ts +16 -0
- package/lib/types/client/ssh/panel/HostFormDialog.d.ts +13 -0
- package/lib/types/client/ssh/panel/HostsTab.d.ts +10 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +16 -0
- package/lib/types/client/ssh/panel/SshPanel.d.ts +14 -0
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +12 -0
- package/lib/types/client/ssh/panel/TransferTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/controller.d.ts +23 -0
- package/lib/types/client/ssh/panel/helpers.d.ts +15 -0
- package/lib/types/client/ssh/panel/xterm.css.d.ts +3 -0
- package/lib/types/client/ssh/sidebar-entry.d.ts +25 -0
- package/lib/types/client/state.d.ts +32 -0
- package/lib/types/client/text.d.ts +11 -0
- package/lib/types/client/workspace-badges.d.ts +38 -0
- package/lib/types/client/workspace-gate.d.ts +15 -0
- package/lib/types/client-http.d.ts +15 -0
- package/lib/types/core.d.ts +43 -0
- package/lib/types/fs.d.ts +36 -0
- package/lib/types/host-http.d.ts +25 -0
- package/lib/types/index.d.ts +53 -0
- package/lib/types/ledger.d.ts +132 -0
- package/lib/types/protocol.d.ts +101 -0
- package/lib/types/providers/index.d.ts +19 -0
- package/lib/types/providers/local/provider.d.ts +58 -0
- package/lib/types/providers/ssh/provider.d.ts +112 -0
- package/lib/types/remote/environment.d.ts +29 -0
- package/lib/types/remote/output.d.ts +38 -0
- package/lib/types/remote/remote-fs.d.ts +69 -0
- package/lib/types/remote/remote-process.d.ts +41 -0
- package/lib/types/remote/remote-subprocess.d.ts +31 -0
- package/lib/types/remote/remote-terminal.d.ts +41 -0
- package/lib/types/remote-runner.d.ts +83 -0
- package/lib/types/remote-search.d.ts +50 -0
- package/lib/types/routes.d.ts +24 -0
- package/lib/types/runtime/workspace-core.d.ts +59 -0
- package/lib/types/seam-state.d.ts +69 -0
- package/lib/types/shell.d.ts +8 -0
- package/lib/types/ssh/connection/lease.d.ts +16 -0
- package/lib/types/ssh/connection/pool.d.ts +57 -0
- package/lib/types/ssh/engine.d.ts +434 -0
- package/lib/types/ssh/exec/output.d.ts +31 -0
- package/lib/types/ssh/known-hosts.d.ts +89 -0
- package/lib/types/ssh/plugin.d.ts +57 -0
- package/lib/types/ssh/protocol.d.ts +236 -0
- package/lib/types/ssh/routes.d.ts +44 -0
- package/lib/types/ssh/store.d.ts +107 -0
- package/lib/types/ssh/tools.d.ts +35 -0
- package/lib/types/ssh/transfer/progress.d.ts +14 -0
- package/lib/types/ssh/vault.d.ts +110 -0
- package/lib/types/subprocess.d.ts +32 -0
- package/lib/types/switch/switch-fs.d.ts +88 -0
- package/lib/types/switch/switch-subprocess.d.ts +34 -0
- package/lib/types/tools.d.ts +12 -0
- package/package.json +134 -0
- package/src/backend.ts +624 -0
- package/src/base/capability.ts +86 -0
- package/src/base/index.ts +18 -0
- package/src/base/ledger-router.ts +128 -0
- package/src/base/ledger.ts +299 -0
- package/src/base/model.ts +118 -0
- package/src/base/namespace.ts +102 -0
- package/src/base/plugin.ts +170 -0
- package/src/base/registry.ts +109 -0
- package/src/base/router.ts +43 -0
- package/src/client/api.ts +196 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/directory-flow.tsx +482 -0
- package/src/client/icons.tsx +50 -0
- package/src/client/index.ts +210 -0
- package/src/client/locales.ts +105 -0
- package/src/client/manager-button.tsx +269 -0
- package/src/client/migrate.ts +109 -0
- package/src/client/ssh/api.ts +411 -0
- package/src/client/ssh/apply.ts +50 -0
- package/src/client/ssh/locales.ts +322 -0
- package/src/client/ssh/mount.tsx +83 -0
- package/src/client/ssh/panel/ClusterTab.tsx +123 -0
- package/src/client/ssh/panel/HostFingerprintDialog.tsx +82 -0
- package/src/client/ssh/panel/HostFormDialog.tsx +228 -0
- package/src/client/ssh/panel/HostsTab.tsx +236 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +80 -0
- package/src/client/ssh/panel/SshPanel.tsx +77 -0
- package/src/client/ssh/panel/TerminalTab.tsx +176 -0
- package/src/client/ssh/panel/TransferTab.tsx +232 -0
- package/src/client/ssh/panel/TunnelsTab.tsx +177 -0
- package/src/client/ssh/panel/controller.ts +48 -0
- package/src/client/ssh/panel/helpers.ts +26 -0
- package/src/client/ssh/panel/panel.module.css +1006 -0
- package/src/client/ssh/panel/xterm.css.ts +2 -0
- package/src/client/ssh/sidebar-entry.ts +123 -0
- package/src/client/state.ts +99 -0
- package/src/client/text.ts +22 -0
- package/src/client/workspace-badges.ts +96 -0
- package/src/client/workspace-gate.ts +225 -0
- package/src/client/workspace.module.css +283 -0
- package/src/client-http.ts +45 -0
- package/src/core.ts +47 -0
- package/src/fs.ts +85 -0
- package/src/host-http.ts +74 -0
- package/src/index.ts +244 -0
- package/src/ledger.ts +416 -0
- package/src/protocol.ts +114 -0
- package/src/providers/index.ts +34 -0
- package/src/providers/local/provider.ts +179 -0
- package/src/providers/ssh/provider.ts +274 -0
- package/src/remote/environment.ts +123 -0
- package/src/remote/output.ts +142 -0
- package/src/remote/remote-fs.ts +532 -0
- package/src/remote/remote-process.ts +203 -0
- package/src/remote/remote-subprocess.ts +159 -0
- package/src/remote/remote-terminal.ts +141 -0
- package/src/remote-runner.ts +201 -0
- package/src/remote-search.ts +163 -0
- package/src/routes.ts +395 -0
- package/src/runtime/workspace-core.ts +154 -0
- package/src/seam-state.ts +185 -0
- package/src/shell.ts +10 -0
- package/src/ssh/connection/lease.ts +17 -0
- package/src/ssh/connection/pool.ts +275 -0
- package/src/ssh/engine.ts +1761 -0
- package/src/ssh/exec/output.ts +70 -0
- package/src/ssh/known-hosts.ts +214 -0
- package/src/ssh/plugin.ts +184 -0
- package/src/ssh/protocol.ts +227 -0
- package/src/ssh/routes.ts +892 -0
- package/src/ssh/store.ts +544 -0
- package/src/ssh/tools.ts +402 -0
- package/src/ssh/transfer/progress.ts +75 -0
- package/src/ssh/vault.ts +477 -0
- package/src/subprocess.ts +71 -0
- package/src/switch/switch-fs.ts +253 -0
- package/src/switch/switch-subprocess.ts +59 -0
- package/src/tools.ts +221 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/* SSH workspace flow styles — follow the dsh --dsw-* theme tokens so the
|
|
2
|
+
dialogs match the active GUI theme exactly like the rest of dsh: native
|
|
3
|
+
dsh cards (12px radius, hairline border, flat, no shadow), 8px controls,
|
|
4
|
+
near-black primary buttons, brand blue only for focus/accents. */
|
|
5
|
+
|
|
6
|
+
.dialogGrid {
|
|
7
|
+
display: grid;
|
|
8
|
+
grid-template-columns: 1fr;
|
|
9
|
+
gap: 10px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.field {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
gap: 5px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.field > span {
|
|
19
|
+
font-size: 13px;
|
|
20
|
+
font-weight: 500;
|
|
21
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.field input,
|
|
25
|
+
.field select {
|
|
26
|
+
padding: 7px 10px;
|
|
27
|
+
min-height: 36px;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
font-size: 13px;
|
|
30
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
31
|
+
background: var(--dsw-alias-bg-layer-1, #fff);
|
|
32
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.14));
|
|
33
|
+
border-radius: 8px;
|
|
34
|
+
outline: none;
|
|
35
|
+
font-family: inherit;
|
|
36
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.field select option {
|
|
40
|
+
background: var(--dsw-alias-bg-base, #fff);
|
|
41
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.field input::placeholder,
|
|
45
|
+
.field select::placeholder {
|
|
46
|
+
color: var(--dsw-alias-label-tertiary, #81858c);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.field input:focus,
|
|
50
|
+
.field select:focus {
|
|
51
|
+
border-color: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6);
|
|
52
|
+
box-shadow: 0 0 0 2px rgba(65, 118, 230, 0.15);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.dialogFail {
|
|
56
|
+
margin-top: 10px;
|
|
57
|
+
padding: 8px 12px;
|
|
58
|
+
border-radius: 6px;
|
|
59
|
+
background: var(--dsw-alias-state-error-bg, rgba(245, 63, 63, 0.1));
|
|
60
|
+
border: 1px solid var(--dsw-alias-state-error-primary, rgba(245, 63, 63, 0.4));
|
|
61
|
+
color: var(--dsw-alias-state-error-primary, #d03050);
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.dialogInfo {
|
|
66
|
+
margin-top: 10px;
|
|
67
|
+
padding: 8px 12px;
|
|
68
|
+
border-radius: 6px;
|
|
69
|
+
background: var(--dsw-alias-bg-base-2, rgba(0, 0, 0, 0.04));
|
|
70
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
71
|
+
color: #000000;
|
|
72
|
+
font-size: 12px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.dialogActions {
|
|
76
|
+
display: flex;
|
|
77
|
+
justify-content: flex-end;
|
|
78
|
+
gap: 8px;
|
|
79
|
+
margin-top: 14px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.button {
|
|
83
|
+
padding: 7px 15px;
|
|
84
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.15));
|
|
85
|
+
border-radius: 8px;
|
|
86
|
+
background: transparent;
|
|
87
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
88
|
+
font-size: 13px;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
transition: background 0.15s ease;
|
|
91
|
+
font-family: inherit;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.button:hover:not(:disabled) {
|
|
95
|
+
background: var(--dsw-alias-bg-overlay, rgba(0, 0, 0, 0.05));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.button:disabled {
|
|
99
|
+
opacity: 0.55;
|
|
100
|
+
cursor: default;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.buttonPrimary {
|
|
104
|
+
background: var(--dsw-alias-button-primary-fill, #0f1115);
|
|
105
|
+
border-color: transparent;
|
|
106
|
+
color: var(--dsw-alias-label-primary-foreground, #fff);
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.buttonPrimary:hover:not(:disabled) {
|
|
111
|
+
background: var(--dsw-alias-button-primary-hover, #2b2f36);
|
|
112
|
+
color: var(--dsw-alias-label-primary-foreground, #fff);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.dialogHint {
|
|
116
|
+
margin-top: 10px;
|
|
117
|
+
font-size: 11.5px;
|
|
118
|
+
line-height: 1.45;
|
|
119
|
+
color: var(--dsw-alias-label-tertiary, #81858c);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.hostRow {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 8px;
|
|
126
|
+
padding: 7px 10px;
|
|
127
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
128
|
+
border-radius: 8px;
|
|
129
|
+
background: var(--dsw-alias-bg-base, rgba(0, 0, 0, 0.02));
|
|
130
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
131
|
+
font-size: 13px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.hostRow:hover {
|
|
135
|
+
background: var(--dsw-alias-bg-overlay, rgba(0, 0, 0, 0.05));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.hostAlias {
|
|
139
|
+
font-weight: 500;
|
|
140
|
+
font-size: 13px;
|
|
141
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
142
|
+
white-space: nowrap;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.hostMeta {
|
|
146
|
+
flex: 1;
|
|
147
|
+
min-width: 0;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
text-overflow: ellipsis;
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
font-size: 12px;
|
|
152
|
+
color: var(--dsw-alias-label-tertiary, #81858c);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.danger {
|
|
156
|
+
background: var(--dsw-alias-state-error-primary, #d03050);
|
|
157
|
+
border-color: transparent;
|
|
158
|
+
color: #fff;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* SSH workspace manager header button */
|
|
162
|
+
.managerButton {
|
|
163
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.2));
|
|
164
|
+
height: 32px;
|
|
165
|
+
min-width: 111px;
|
|
166
|
+
color: #000000;
|
|
167
|
+
font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
|
168
|
+
Roboto, system-ui, 'PingFang SC', 'Microsoft YaHei', sans-serif);
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
background: transparent;
|
|
171
|
+
border-radius: 12px;
|
|
172
|
+
justify-content: center;
|
|
173
|
+
align-items: center;
|
|
174
|
+
gap: 4px;
|
|
175
|
+
padding: 6px 12px;
|
|
176
|
+
font-size: 13px;
|
|
177
|
+
font-weight: 400;
|
|
178
|
+
line-height: 20px;
|
|
179
|
+
display: inline-flex;
|
|
180
|
+
white-space: nowrap;
|
|
181
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
182
|
+
}
|
|
183
|
+
.managerButton:hover {
|
|
184
|
+
background: var(--dsw-alias-bg-overlay, rgba(0, 0, 0, 0.05));
|
|
185
|
+
}
|
|
186
|
+
.managerButton svg {
|
|
187
|
+
flex: none;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.managerMenuHeader {
|
|
191
|
+
margin: 0 0 2px;
|
|
192
|
+
font-size: 13px;
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
195
|
+
}
|
|
196
|
+
.managerMenuClose {
|
|
197
|
+
float: right;
|
|
198
|
+
border: none;
|
|
199
|
+
background: transparent;
|
|
200
|
+
color: var(--dsw-alias-label-secondary, #61666b);
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
font-size: 14px;
|
|
203
|
+
line-height: 18px;
|
|
204
|
+
padding: 0 2px;
|
|
205
|
+
}
|
|
206
|
+
.managerMenuClose:hover { color: var(--dsw-alias-label-primary, #0f1115); }
|
|
207
|
+
|
|
208
|
+
/* Dropdown panel: native dsh flat card — 12px radius, hairline border,
|
|
209
|
+
no shadow. */
|
|
210
|
+
.menuDropdown {
|
|
211
|
+
position: fixed;
|
|
212
|
+
z-index: 99999;
|
|
213
|
+
min-width: 230px;
|
|
214
|
+
max-width: calc(100vw - 16px);
|
|
215
|
+
box-sizing: border-box;
|
|
216
|
+
overflow-x: hidden;
|
|
217
|
+
overflow-y: auto;
|
|
218
|
+
max-height: min(600px, calc(100vh - 64px));
|
|
219
|
+
padding: 8px;
|
|
220
|
+
border-radius: 12px;
|
|
221
|
+
background: var(--dsw-alias-bg-base, #fff);
|
|
222
|
+
border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.04));
|
|
223
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
224
|
+
font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
|
225
|
+
Roboto, system-ui, 'PingFang SC', 'Microsoft YaHei', sans-serif);
|
|
226
|
+
font-size: 13px;
|
|
227
|
+
line-height: 20px;
|
|
228
|
+
scrollbar-width: thin;
|
|
229
|
+
scrollbar-color: var(--dsw-alias-label-caption, rgba(128, 128, 128, 0.5)) transparent;
|
|
230
|
+
}
|
|
231
|
+
.menuItem {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: 9px;
|
|
235
|
+
width: 100%;
|
|
236
|
+
border: none;
|
|
237
|
+
background: transparent;
|
|
238
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
239
|
+
font: inherit;
|
|
240
|
+
text-align: left;
|
|
241
|
+
padding: 8px 11px;
|
|
242
|
+
border-radius: 8px;
|
|
243
|
+
cursor: pointer;
|
|
244
|
+
white-space: nowrap;
|
|
245
|
+
transition: background 0.12s ease, color 0.12s ease;
|
|
246
|
+
}
|
|
247
|
+
.menuItem:hover:not(:disabled) { background: var(--dsw-alias-bg-overlay, rgba(0, 0, 0, 0.05)); }
|
|
248
|
+
.menuItem:disabled { opacity: 0.55; cursor: default; }
|
|
249
|
+
.menuItemIcon { flex: none; font-size: 14px; line-height: 1; }
|
|
250
|
+
.menuDivider { height: 1px; margin: 5px 9px; background: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1)); }
|
|
251
|
+
|
|
252
|
+
/* dsh-hardssh */
|
|
253
|
+
.managerGroupHeader {
|
|
254
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
255
|
+
margin-bottom: 5px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Icon-only round buttons on server rows (edit/delete) */
|
|
259
|
+
.menuItemIconButton {
|
|
260
|
+
flex: none;
|
|
261
|
+
width: 22px;
|
|
262
|
+
height: 22px;
|
|
263
|
+
display: inline-flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
justify-content: center;
|
|
266
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.15));
|
|
267
|
+
border-radius: 6px;
|
|
268
|
+
background: transparent;
|
|
269
|
+
color: var(--dsw-alias-label-secondary, #4a5060);
|
|
270
|
+
font-size: 13px;
|
|
271
|
+
line-height: 1;
|
|
272
|
+
cursor: pointer;
|
|
273
|
+
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
|
|
274
|
+
}
|
|
275
|
+
.menuItemIconButton:hover:not(:disabled) {
|
|
276
|
+
background: var(--dsw-alias-bg-overlay, rgba(0, 0, 0, 0.06));
|
|
277
|
+
border-color: var(--dsw-alias-border-l3, rgba(0, 0, 0, 0.12));
|
|
278
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
279
|
+
}
|
|
280
|
+
.menuItemIconButton:disabled {
|
|
281
|
+
opacity: 0.4;
|
|
282
|
+
cursor: default;
|
|
283
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side HTTP helpers for the dsh-hardssh route clients.
|
|
3
|
+
* Plain fetch / same-origin — bundled inline into the client bundle.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Error carrying the route's JSON error message and stable code/status. */
|
|
7
|
+
export class HttpApiError extends Error {
|
|
8
|
+
constructor(
|
|
9
|
+
message: string,
|
|
10
|
+
public readonly code?: string,
|
|
11
|
+
public readonly status?: number,
|
|
12
|
+
) {
|
|
13
|
+
super(message)
|
|
14
|
+
this.name = 'HttpApiError'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Query-string helper (skips undefined and empty values). */
|
|
19
|
+
export function buildQuery(params: Record<string, string | number | undefined>): string {
|
|
20
|
+
const search = new URLSearchParams()
|
|
21
|
+
for (const [key, value] of Object.entries(params)) {
|
|
22
|
+
if (value !== undefined && value !== '') search.set(key, String(value))
|
|
23
|
+
}
|
|
24
|
+
const text = search.toString()
|
|
25
|
+
return text === '' ? '' : '?' + text
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Parse a JSON response or throw an HttpApiError (code/status preserved). */
|
|
29
|
+
export async function readJson<T>(response: Response): Promise<T> {
|
|
30
|
+
let body: unknown
|
|
31
|
+
try {
|
|
32
|
+
body = await response.json()
|
|
33
|
+
} catch {
|
|
34
|
+
throw new HttpApiError(`HTTP ${response.status}: invalid JSON response`, undefined, response.status)
|
|
35
|
+
}
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const record = typeof body === 'object' && body !== null
|
|
38
|
+
? body as { error?: unknown; code?: unknown }
|
|
39
|
+
: undefined
|
|
40
|
+
const message = typeof record?.error === 'string' ? record.error : `HTTP ${response.status}`
|
|
41
|
+
const code = typeof record?.code === 'string' ? record.code : undefined
|
|
42
|
+
throw new HttpApiError(message, code, response.status)
|
|
43
|
+
}
|
|
44
|
+
return body as T
|
|
45
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared workspace core: the host store, the SSH engine, and the
|
|
3
|
+
* workspace ledger, provided as `ctx.hardsshCore` by the main plugin row so
|
|
4
|
+
* the two switch rows (fs / subprocess) resolve one instance each.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SshEngine } from './ssh/engine.ts'
|
|
8
|
+
import type { SshHostEntry, SshHostSummary } from './ssh/protocol.ts'
|
|
9
|
+
import type { SshWorkspaceLedger } from './ledger.ts'
|
|
10
|
+
import type { RemoteWorkspaceRunner } from './remote-runner.ts'
|
|
11
|
+
import type { WorkspaceSeamState } from './seam-state.ts'
|
|
12
|
+
|
|
13
|
+
/** Read-only host-store surface exposed on the core (the write paths live in
|
|
14
|
+
* the SSH routes, which get the full store). Avoids coupling the core type
|
|
15
|
+
* to either the plaintext HostStore or the vault-backed SecureHostStore. */
|
|
16
|
+
export interface HostStoreView {
|
|
17
|
+
readonly path: string
|
|
18
|
+
list(): SshHostEntry[]
|
|
19
|
+
find(alias: string): SshHostEntry | undefined
|
|
20
|
+
summarize(entry: SshHostEntry): SshHostSummary
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One process-wide core shared by every dsh-hardssh row. */
|
|
24
|
+
export interface HardsshCore {
|
|
25
|
+
hosts: HostStoreView
|
|
26
|
+
engine: SshEngine
|
|
27
|
+
/** The SSH-bound workspace ledger (anchor dir -> remote dir). */
|
|
28
|
+
ledger: SshWorkspaceLedger
|
|
29
|
+
/** The shared seam state: ledger-derived routing snapshot + per-record
|
|
30
|
+
* fs/subprocess instances, consumed by the fs/subprocess switch rows. */
|
|
31
|
+
seams: WorkspaceSeamState
|
|
32
|
+
/** Remote-workspace runner: resolve local anchor paths to remote channels
|
|
33
|
+
* (git / files / commands) so other plugins (dsh-workbench-tiphareth) can
|
|
34
|
+
* operate SSH-bound workspaces transparently. Optional in older builds. */
|
|
35
|
+
resolveRemote?: RemoteWorkspaceRunner['resolveRemote']
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @deprecated Use HardsshCore. Kept as a source-compatible migration alias.
|
|
40
|
+
*/
|
|
41
|
+
export type EasysshCore = HardsshCore
|
|
42
|
+
|
|
43
|
+
declare module '@deepseek-ai/cordis' {
|
|
44
|
+
interface Context {
|
|
45
|
+
hardsshCore: HardsshCore
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/fs.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `ctx.fs` switch row: provides the workspace-routing filesystem facade in
|
|
3
|
+
* the host scope. The local backend (the deployment's sandboxed filesystem) is
|
|
4
|
+
* mounted in an isolated child scope so its own `ctx.fs` provide never
|
|
5
|
+
* collides. Routing and per-record remote backends are owned by the SHARED
|
|
6
|
+
* seam state (hardsshCore.seams): the row only binds its per-record backend
|
|
7
|
+
* factory and reads the state on every call. Every SSH-bound workspace record
|
|
8
|
+
* gets its OWN remote backend instance bound to that record's alias + remote
|
|
9
|
+
* root; the facade routes each call by the session cwd — an anchor path of an
|
|
10
|
+
* SSH workspace routes remote, everything else stays local. The facade
|
|
11
|
+
* auto-provides `fs` here because the `fs-sandbox` row is disabled by the
|
|
12
|
+
* profile patch.
|
|
13
|
+
*
|
|
14
|
+
* The seam state re-applies the ledger snapshot synchronously on every
|
|
15
|
+
* commit, so creating or removing an SSH workspace takes effect without a
|
|
16
|
+
* plugin restart and the fs/subprocess seams can never disagree. Before the
|
|
17
|
+
* initial ledger load finishes, routing degrades to local with a one-time
|
|
18
|
+
* warning for anchor-root paths.
|
|
19
|
+
*
|
|
20
|
+
* @module dsh-hardssh/fs
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
24
|
+
import { SandboxedFileSystem } from '@deepseek-ai/dsh-fs-sandbox'
|
|
25
|
+
import type { HardsshCore } from './core.ts'
|
|
26
|
+
import { isPathUnderAnchor } from './ledger.ts'
|
|
27
|
+
import type { WorkspaceState } from './protocol.ts'
|
|
28
|
+
import { SshFileSystem } from './remote/remote-fs.ts'
|
|
29
|
+
import { WFS_NAMESPACE_MARKER, SwitchFileSystem } from './switch/switch-fs.ts'
|
|
30
|
+
|
|
31
|
+
declare module '@deepseek-ai/cordis' {
|
|
32
|
+
interface Context {
|
|
33
|
+
hardsshCore: HardsshCore
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Stable cordis plugin name. */
|
|
38
|
+
export const name = 'hardssh-fs'
|
|
39
|
+
|
|
40
|
+
/** Services required: the shared workspace core (mode store + engine) and the
|
|
41
|
+
* sandbox policy the local backend (`SandboxedFileSystem`) consumes. */
|
|
42
|
+
export const inject = ['hardsshCore', 'sandboxPolicy']
|
|
43
|
+
|
|
44
|
+
/** A remote backend fixed to one SSH workspace's alias + remote root. */
|
|
45
|
+
function fixedRemoteState(alias: string, remoteRoot: string): () => WorkspaceState {
|
|
46
|
+
return () => ({ mode: 'remote' as const, alias, remoteRoot })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Mount the switching filesystem facade. */
|
|
50
|
+
export function apply(ctx: Context): void {
|
|
51
|
+
const core = ctx.hardsshCore
|
|
52
|
+
|
|
53
|
+
// The local backend lives in an isolated scope: its `fs` provide shadows
|
|
54
|
+
// only below this scope, so consumers keep resolving our facade.
|
|
55
|
+
const localCtx = ctx.isolate('fs')
|
|
56
|
+
const localFs = new SandboxedFileSystem(localCtx, {
|
|
57
|
+
cwd: process.env.DSH_CWD ?? process.cwd(),
|
|
58
|
+
diffBasisMaxBytes: 10 * 1024 * 1024,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Bind the per-record remote backend builder into the shared seam state;
|
|
62
|
+
// instances are built lazily on first route and reused across refreshes.
|
|
63
|
+
core.seams.bindFs((record) => ({
|
|
64
|
+
backend: new SshFileSystem(ctx.isolate('fs'), core.engine, fixedRemoteState(record.alias, record.remoteRoot)),
|
|
65
|
+
namespace: `${WFS_NAMESPACE_MARKER}${record.id.toLowerCase()}/`,
|
|
66
|
+
anchorPath: record.anchorPath,
|
|
67
|
+
remoteRoot: record.remoteRoot,
|
|
68
|
+
}))
|
|
69
|
+
|
|
70
|
+
const anchorRoot = core.ledger.anchorsRoot()
|
|
71
|
+
let warnedUnready = false
|
|
72
|
+
|
|
73
|
+
// The SwitchFileSystem constructor registers the `fs` provide on ctx.
|
|
74
|
+
new SwitchFileSystem(ctx, {
|
|
75
|
+
local: localFs,
|
|
76
|
+
worldFor: (cwd) => {
|
|
77
|
+
if (!core.seams.isReady() && !warnedUnready && cwd !== undefined && isPathUnderAnchor(anchorRoot, cwd)) {
|
|
78
|
+
warnedUnready = true
|
|
79
|
+
console.warn('[dsh-hardssh] fs routing is not ready yet (workspace ledger still loading) — operating on the local anchor until the snapshot is applied')
|
|
80
|
+
}
|
|
81
|
+
return core.seams.worldForFs(cwd) ?? { backend: localFs, namespace: '' }
|
|
82
|
+
},
|
|
83
|
+
worldForNamespace: (namespace) => core.seams.worldForFsNamespace(namespace),
|
|
84
|
+
})
|
|
85
|
+
}
|
package/src/host-http.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side HTTP route helpers for the dsh-hardssh route family.
|
|
3
|
+
* HOST-ONLY: these import node:http types and must never end up in the
|
|
4
|
+
* client bundle (the client half never imports this module).
|
|
5
|
+
*/
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
7
|
+
|
|
8
|
+
/** One JSON response. */
|
|
9
|
+
export function writeJson(res: ServerResponse, status: number, body: unknown): void {
|
|
10
|
+
const payload = JSON.stringify(body)
|
|
11
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'referrer-policy': 'no-referrer' })
|
|
12
|
+
res.end(payload)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** URL query helper (first value, decoded). */
|
|
16
|
+
export function queryParam(url: URL, name: string): string | undefined {
|
|
17
|
+
const value = url.searchParams.get(name)
|
|
18
|
+
return value === null ? undefined : value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Loopback literal check plus browser same-origin markers (mirrors dsh-ssh). */
|
|
22
|
+
export function isLoopbackRequest(request: IncomingMessage): boolean {
|
|
23
|
+
const address = request.socket.remoteAddress
|
|
24
|
+
if (address !== '127.0.0.1' && address !== '::1' && address !== '::ffff:127.0.0.1') return false
|
|
25
|
+
const host = request.headers.host
|
|
26
|
+
if (typeof host !== 'string') return false
|
|
27
|
+
let hostUrl: URL
|
|
28
|
+
try {
|
|
29
|
+
hostUrl = new URL(`http://${host}`)
|
|
30
|
+
} catch {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
if (hostUrl.hostname !== '127.0.0.1' && hostUrl.hostname !== 'localhost' && hostUrl.hostname !== '[::1]') return false
|
|
34
|
+
if (request.headers['sec-fetch-site'] === 'cross-site') return false
|
|
35
|
+
const origin = request.headers.origin
|
|
36
|
+
if (origin === undefined) return true
|
|
37
|
+
try {
|
|
38
|
+
return new URL(origin).host === hostUrl.host
|
|
39
|
+
} catch {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type ReadJsonBodyResult =
|
|
45
|
+
| { ok: true; body: Record<string, unknown> }
|
|
46
|
+
| { ok: false; reason: 'too-large' | 'malformed' | 'not-object' }
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read a JSON request body with a hard byte cap. Discriminative result so
|
|
50
|
+
* the caller keeps its own error mapping (silent undefined vs 400/413).
|
|
51
|
+
*/
|
|
52
|
+
export async function readJsonBody(
|
|
53
|
+
req: IncomingMessage,
|
|
54
|
+
maxBytes: number,
|
|
55
|
+
): Promise<ReadJsonBodyResult> {
|
|
56
|
+
const chunks: Buffer[] = []
|
|
57
|
+
let size = 0
|
|
58
|
+
for await (const chunk of req) {
|
|
59
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
60
|
+
size += buffer.length
|
|
61
|
+
if (size > maxBytes) return { ok: false, reason: 'too-large' }
|
|
62
|
+
chunks.push(buffer)
|
|
63
|
+
}
|
|
64
|
+
let parsed: unknown
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
67
|
+
} catch {
|
|
68
|
+
return { ok: false, reason: 'malformed' }
|
|
69
|
+
}
|
|
70
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
71
|
+
return { ok: false, reason: 'not-object' }
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, body: parsed as Record<string, unknown> }
|
|
74
|
+
}
|