@goodandready/dsh-remote-workspace 0.1.0
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/LICENSE +21 -0
- package/README.md +238 -0
- package/README.ru.md +238 -0
- package/README.zh.md +238 -0
- package/cordis.patch.yml +3 -0
- package/docs/design/DESIGN.md +62 -0
- package/docs/plans/findings.md +7 -0
- package/docs/plans/progress.md +14 -0
- package/docs/plans/task_plan.md +26 -0
- package/lib/client.js +966 -0
- package/lib/index.js +116 -0
- package/lib/mirror-sync-service.js +180 -0
- package/lib/remote-fs-service.js +117 -0
- package/lib/routes.js +164 -0
- package/lib/ssh-service.js +176 -0
- package/lib/tools.js +138 -0
- package/lib/tunnel-service.js +88 -0
- package/lib/types.js +29 -0
- package/package.json +75 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
|
|
6
|
+
export class SshService {
|
|
7
|
+
constructor(ctx) {
|
|
8
|
+
this.ctx = ctx;
|
|
9
|
+
/** @type {Map<string, Client>} */
|
|
10
|
+
this.connections = new Map();
|
|
11
|
+
/** @type {Map<string, any>} */
|
|
12
|
+
this.sftpSessions = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve private key content either from direct string or file path
|
|
17
|
+
*/
|
|
18
|
+
resolvePrivateKey(profile) {
|
|
19
|
+
if (profile.privateKey && profile.privateKey.trim()) {
|
|
20
|
+
return profile.privateKey;
|
|
21
|
+
}
|
|
22
|
+
if (profile.privateKeyPath) {
|
|
23
|
+
let resolvedPath = profile.privateKeyPath;
|
|
24
|
+
if (resolvedPath.startsWith('~')) {
|
|
25
|
+
resolvedPath = path.join(os.homedir(), resolvedPath.slice(1));
|
|
26
|
+
}
|
|
27
|
+
if (fs.existsSync(resolvedPath)) {
|
|
28
|
+
return fs.readFileSync(resolvedPath, 'utf8');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Connect to remote machine with pooling and keep-alive
|
|
36
|
+
*/
|
|
37
|
+
async getConnection(profile) {
|
|
38
|
+
const connId = profile.id;
|
|
39
|
+
const existing = this.connections.get(connId);
|
|
40
|
+
if (existing && existing._sock && !existing._sock.destroyed) {
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const conn = new Client();
|
|
46
|
+
const connConfig = {
|
|
47
|
+
host: profile.host,
|
|
48
|
+
port: profile.port || 22,
|
|
49
|
+
username: profile.username || 'root',
|
|
50
|
+
keepaliveInterval: 15000,
|
|
51
|
+
keepaliveCountMax: 3,
|
|
52
|
+
readyTimeout: 20000
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (profile.authType === 'password') {
|
|
56
|
+
connConfig.password = profile.password;
|
|
57
|
+
} else if (profile.authType === 'agent') {
|
|
58
|
+
connConfig.agent = process.env.SSH_AUTH_SOCK;
|
|
59
|
+
} else {
|
|
60
|
+
const privateKey = this.resolvePrivateKey(profile);
|
|
61
|
+
if (privateKey) {
|
|
62
|
+
connConfig.privateKey = privateKey;
|
|
63
|
+
if (profile.passphrase) {
|
|
64
|
+
connConfig.passphrase = profile.passphrase;
|
|
65
|
+
}
|
|
66
|
+
} else if (profile.password) {
|
|
67
|
+
connConfig.password = profile.password;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
conn.on('ready', () => {
|
|
72
|
+
this.connections.set(connId, conn);
|
|
73
|
+
resolve(conn);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
conn.on('error', (err) => {
|
|
77
|
+
this.connections.delete(connId);
|
|
78
|
+
this.sftpSessions.delete(connId);
|
|
79
|
+
reject(err);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
conn.on('close', () => {
|
|
83
|
+
this.connections.delete(connId);
|
|
84
|
+
this.sftpSessions.delete(connId);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
conn.connect(connConfig);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Test connection and measure ping / detect OS
|
|
93
|
+
*/
|
|
94
|
+
async testConnection(profile) {
|
|
95
|
+
const start = Date.now();
|
|
96
|
+
const conn = await this.getConnection(profile);
|
|
97
|
+
const latency = Date.now() - start;
|
|
98
|
+
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
conn.exec('uname -s -r -m || ver', (err, stream) => {
|
|
101
|
+
if (err) {
|
|
102
|
+
return resolve({ success: true, latency, os: 'Unknown' });
|
|
103
|
+
}
|
|
104
|
+
let output = '';
|
|
105
|
+
stream.on('data', (d) => { output += d.toString(); });
|
|
106
|
+
stream.on('close', () => {
|
|
107
|
+
resolve({ success: true, latency, os: output.trim() });
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Obtain active SFTP wrapper
|
|
115
|
+
*/
|
|
116
|
+
async getSftp(profile) {
|
|
117
|
+
const connId = profile.id;
|
|
118
|
+
if (this.sftpSessions.has(connId)) {
|
|
119
|
+
return this.sftpSessions.get(connId);
|
|
120
|
+
}
|
|
121
|
+
const conn = await this.getConnection(profile);
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
conn.sftp((err, sftp) => {
|
|
124
|
+
if (err) return reject(err);
|
|
125
|
+
this.sftpSessions.set(connId, sftp);
|
|
126
|
+
resolve(sftp);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Execute command on remote host
|
|
133
|
+
*/
|
|
134
|
+
async exec(profile, command, cwd, onData) {
|
|
135
|
+
const conn = await this.getConnection(profile);
|
|
136
|
+
const shellCmd = cwd ? `cd ${JSON.stringify(cwd)} && ${command}` : command;
|
|
137
|
+
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
conn.exec(shellCmd, (err, stream) => {
|
|
140
|
+
if (err) return reject(err);
|
|
141
|
+
let stdout = '';
|
|
142
|
+
let stderr = '';
|
|
143
|
+
stream.on('data', (data) => {
|
|
144
|
+
const str = data.toString();
|
|
145
|
+
stdout += str;
|
|
146
|
+
if (onData) onData({ type: 'stdout', data: str });
|
|
147
|
+
});
|
|
148
|
+
stream.stderr.on('data', (data) => {
|
|
149
|
+
const str = data.toString();
|
|
150
|
+
stderr += str;
|
|
151
|
+
if (onData) onData({ type: 'stderr', data: str });
|
|
152
|
+
});
|
|
153
|
+
stream.on('close', (code, signal) => {
|
|
154
|
+
resolve({ code: code ?? 0, signal, stdout, stderr });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
disconnect(profileId) {
|
|
161
|
+
const conn = this.connections.get(profileId);
|
|
162
|
+
if (conn) {
|
|
163
|
+
conn.end();
|
|
164
|
+
this.connections.delete(profileId);
|
|
165
|
+
this.sftpSessions.delete(profileId);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
disconnectAll() {
|
|
170
|
+
for (const [id, conn] of this.connections.entries()) {
|
|
171
|
+
try { conn.end(); } catch (_) {}
|
|
172
|
+
}
|
|
173
|
+
this.connections.clear();
|
|
174
|
+
this.sftpSessions.clear();
|
|
175
|
+
}
|
|
176
|
+
}
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunnelService, getActiveProfile) {
|
|
2
|
+
if (!ctx.tools || typeof ctx.tools.register !== 'function') {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const genericOutput = {
|
|
7
|
+
schema: { type: 'object' },
|
|
8
|
+
render: (_args, value) => ({ type: 'text', content: typeof value === 'string' ? value : JSON.stringify(value, null, 2) })
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// 1. remote_exec
|
|
12
|
+
ctx.tools.register({
|
|
13
|
+
name: 'remote_exec',
|
|
14
|
+
description: 'Execute bash/shell command on the active remote workspace host',
|
|
15
|
+
parameters: {
|
|
16
|
+
command: { type: 'string', required: true, description: 'Command line to execute on remote server' },
|
|
17
|
+
cwd: { type: 'string', description: 'Working directory on remote server (defaults to remote workspace)' }
|
|
18
|
+
},
|
|
19
|
+
output: genericOutput,
|
|
20
|
+
async execute(args) {
|
|
21
|
+
const profile = getActiveProfile();
|
|
22
|
+
if (!profile) throw new Error('No active remote workspace profile selected');
|
|
23
|
+
const dir = args.cwd || profile.remoteWorkspace;
|
|
24
|
+
const res = await sshService.exec(profile, args.command, dir);
|
|
25
|
+
return {
|
|
26
|
+
exitCode: res.code,
|
|
27
|
+
stdout: res.stdout,
|
|
28
|
+
stderr: res.stderr
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 2. remote_fs
|
|
34
|
+
ctx.tools.register({
|
|
35
|
+
name: 'remote_fs',
|
|
36
|
+
description: 'Perform file operations (read, write, stat, list, mkdir, remove) on remote workspace',
|
|
37
|
+
parameters: {
|
|
38
|
+
action: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
required: true,
|
|
41
|
+
description: 'Filesystem operation: read, write, stat, list, mkdir, remove'
|
|
42
|
+
},
|
|
43
|
+
path: { type: 'string', required: true, description: 'Absolute or workspace-relative remote path' },
|
|
44
|
+
content: { type: 'string', description: 'Content for write action' },
|
|
45
|
+
recursive: { type: 'boolean', description: 'Recursive flag for remove action' }
|
|
46
|
+
},
|
|
47
|
+
output: genericOutput,
|
|
48
|
+
async execute(args) {
|
|
49
|
+
const profile = getActiveProfile();
|
|
50
|
+
if (!profile) throw new Error('No active remote workspace profile selected');
|
|
51
|
+
const { action, path: targetPath, content, recursive } = args;
|
|
52
|
+
|
|
53
|
+
switch (action) {
|
|
54
|
+
case 'read': {
|
|
55
|
+
const data = await remoteFs.readFile(profile, targetPath, 'utf8');
|
|
56
|
+
return { content: data };
|
|
57
|
+
}
|
|
58
|
+
case 'write': {
|
|
59
|
+
if (content === undefined) throw new Error('Content is required for write action');
|
|
60
|
+
const res = await remoteFs.writeFile(profile, targetPath, content, 'utf8');
|
|
61
|
+
return res;
|
|
62
|
+
}
|
|
63
|
+
case 'stat': {
|
|
64
|
+
return await remoteFs.stat(profile, targetPath);
|
|
65
|
+
}
|
|
66
|
+
case 'list': {
|
|
67
|
+
const entries = await remoteFs.listDir(profile, targetPath);
|
|
68
|
+
return { entries };
|
|
69
|
+
}
|
|
70
|
+
case 'mkdir': {
|
|
71
|
+
return await remoteFs.mkdir(profile, targetPath);
|
|
72
|
+
}
|
|
73
|
+
case 'remove': {
|
|
74
|
+
return await remoteFs.remove(profile, targetPath, recursive);
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
throw new Error(`Unknown action: ${action}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// 3. remote_sync
|
|
83
|
+
ctx.tools.register({
|
|
84
|
+
name: 'remote_sync',
|
|
85
|
+
description: 'Perform 3-way conflict-aware synchronization between local mirror and remote server',
|
|
86
|
+
parameters: {
|
|
87
|
+
direction: { type: 'string', required: true, description: 'pull (remote -> local) or push (local -> remote)' },
|
|
88
|
+
force: { type: 'boolean', description: 'Overwrite conflicts if true (use with caution)' },
|
|
89
|
+
dryRun: { type: 'boolean', description: 'Simulate changes without writing files' }
|
|
90
|
+
},
|
|
91
|
+
output: genericOutput,
|
|
92
|
+
async execute(args) {
|
|
93
|
+
const profile = getActiveProfile();
|
|
94
|
+
if (!profile) throw new Error('No active remote workspace profile selected');
|
|
95
|
+
if (!profile.remoteWorkspace || !profile.localMirrorPath) {
|
|
96
|
+
throw new Error('Both remoteWorkspace and localMirrorPath must be configured');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (args.direction === 'pull') {
|
|
100
|
+
return await mirrorSync.pull(profile, profile.remoteWorkspace, profile.localMirrorPath, args.dryRun);
|
|
101
|
+
} else {
|
|
102
|
+
return await mirrorSync.push(profile, profile.localMirrorPath, profile.remoteWorkspace, args.force, args.dryRun);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// 4. remote_tunnel
|
|
108
|
+
ctx.tools.register({
|
|
109
|
+
name: 'remote_tunnel',
|
|
110
|
+
description: 'Manage SSH port forwarding tunnels (start, stop, list)',
|
|
111
|
+
parameters: {
|
|
112
|
+
action: { type: 'string', required: true, description: 'start, stop, or list' },
|
|
113
|
+
localPort: { type: 'number', description: 'Local listening port' },
|
|
114
|
+
remotePort: { type: 'number', description: 'Remote destination port' },
|
|
115
|
+
tunnelId: { type: 'string', description: 'Tunnel ID to stop' }
|
|
116
|
+
},
|
|
117
|
+
output: genericOutput,
|
|
118
|
+
async execute(args) {
|
|
119
|
+
const profile = getActiveProfile();
|
|
120
|
+
if (!profile && args.action !== 'list' && args.action !== 'stop') {
|
|
121
|
+
throw new Error('No active remote workspace profile selected');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (args.action === 'list') {
|
|
125
|
+
return { tunnels: tunnelService.listActiveTunnels() };
|
|
126
|
+
}
|
|
127
|
+
if (args.action === 'start') {
|
|
128
|
+
if (!args.localPort || !args.remotePort) throw new Error('localPort and remotePort required to start tunnel');
|
|
129
|
+
return await tunnelService.startLocalTunnel(profile, args.localPort, '127.0.0.1', args.remotePort);
|
|
130
|
+
}
|
|
131
|
+
if (args.action === 'stop') {
|
|
132
|
+
if (!args.tunnelId) throw new Error('tunnelId required to stop tunnel');
|
|
133
|
+
const ok = tunnelService.stopTunnel(args.tunnelId);
|
|
134
|
+
return { success: ok };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
|
|
3
|
+
export class TunnelService {
|
|
4
|
+
/**
|
|
5
|
+
* @param {import('./ssh-service.js').SshService} sshService
|
|
6
|
+
*/
|
|
7
|
+
constructor(sshService) {
|
|
8
|
+
this.sshService = sshService;
|
|
9
|
+
/** @type {Map<string, { server?: net.Server, profileId: string, localPort: number, remotePort: number }>} */
|
|
10
|
+
this.activeTunnels = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Start Local Port Forwarding: 127.0.0.1:localPort -> Remote:targetHost:targetPort
|
|
15
|
+
*/
|
|
16
|
+
async startLocalTunnel(profile, localPort, targetHost = '127.0.0.1', targetPort) {
|
|
17
|
+
const tunnelId = `${profile.id}:${localPort}->${targetPort}`;
|
|
18
|
+
if (this.activeTunnels.has(tunnelId)) {
|
|
19
|
+
return { success: true, message: 'Tunnel already active', tunnelId };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const conn = await this.sshService.getConnection(profile);
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const server = net.createServer((socket) => {
|
|
26
|
+
conn.forwardOut(
|
|
27
|
+
'127.0.0.1',
|
|
28
|
+
socket.remotePort || 0,
|
|
29
|
+
targetHost,
|
|
30
|
+
targetPort,
|
|
31
|
+
(err, stream) => {
|
|
32
|
+
if (err) {
|
|
33
|
+
socket.destroy();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
socket.pipe(stream).pipe(socket);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
server.listen(localPort, '127.0.0.1', () => {
|
|
42
|
+
this.activeTunnels.set(tunnelId, {
|
|
43
|
+
server,
|
|
44
|
+
profileId: profile.id,
|
|
45
|
+
localPort,
|
|
46
|
+
remotePort: targetPort
|
|
47
|
+
});
|
|
48
|
+
resolve({ success: true, tunnelId, localPort, targetPort });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
server.on('error', (err) => {
|
|
52
|
+
this.activeTunnels.delete(tunnelId);
|
|
53
|
+
reject(err);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Stop active tunnel
|
|
60
|
+
*/
|
|
61
|
+
stopTunnel(tunnelId) {
|
|
62
|
+
const tunnel = this.activeTunnels.get(tunnelId);
|
|
63
|
+
if (tunnel && tunnel.server) {
|
|
64
|
+
tunnel.server.close();
|
|
65
|
+
this.activeTunnels.delete(tunnelId);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
listActiveTunnels() {
|
|
72
|
+
return Array.from(this.activeTunnels.entries()).map(([id, t]) => ({
|
|
73
|
+
id,
|
|
74
|
+
profileId: t.profileId,
|
|
75
|
+
localPort: t.localPort,
|
|
76
|
+
remotePort: t.remotePort
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
stopAll() {
|
|
81
|
+
for (const [id, t] of this.activeTunnels.entries()) {
|
|
82
|
+
if (t.server) {
|
|
83
|
+
try { t.server.close(); } catch (_) {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
this.activeTunnels.clear();
|
|
87
|
+
}
|
|
88
|
+
}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} MachineProfile
|
|
3
|
+
* @property {string} id
|
|
4
|
+
* @property {string} name
|
|
5
|
+
* @property {string} host
|
|
6
|
+
* @property {number} port
|
|
7
|
+
* @property {string} username
|
|
8
|
+
* @property {'key'|'password'|'agent'} authType
|
|
9
|
+
* @property {string} [privateKeyPath]
|
|
10
|
+
* @property {string} [privateKey]
|
|
11
|
+
* @property {string} [passphrase]
|
|
12
|
+
* @property {string} [password]
|
|
13
|
+
* @property {string} [jumpHost]
|
|
14
|
+
* @property {string} [remoteWorkspace]
|
|
15
|
+
* @property {string} [localMirrorPath]
|
|
16
|
+
* @property {number} [lastPingMs]
|
|
17
|
+
* @property {string} [remoteOs]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} TunnelConfig
|
|
22
|
+
* @property {string} id
|
|
23
|
+
* @property {string} machineId
|
|
24
|
+
* @property {'local'|'reverse'} direction
|
|
25
|
+
* @property {number} localPort
|
|
26
|
+
* @property {string} remoteHost
|
|
27
|
+
* @property {number} remotePort
|
|
28
|
+
* @property {boolean} active
|
|
29
|
+
*/
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodandready/dsh-remote-workspace",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Enterprise Remote Workspace Plugin for DeepSeek Harness: SSH2 connection pool, SFTP operations, 3-way conflict-aware mirror sync, port forwarding, and native DSH UI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"lib/",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"README.md",
|
|
17
|
+
"README.ru.md",
|
|
18
|
+
"README.zh.md",
|
|
19
|
+
"docs/",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"test": "node --test test/*.test.mjs"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"deepseek-harness",
|
|
28
|
+
"dsh",
|
|
29
|
+
"dsh-plugin",
|
|
30
|
+
"remote",
|
|
31
|
+
"ssh",
|
|
32
|
+
"sftp",
|
|
33
|
+
"workspace",
|
|
34
|
+
"tunnel",
|
|
35
|
+
"sync"
|
|
36
|
+
],
|
|
37
|
+
"homepage": "https://github.com/GooDAnDReaDY/dsh-remote-workspace#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-remote-workspace/issues"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/GooDAnDReaDY/dsh-remote-workspace.git"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"dsh": {
|
|
47
|
+
"bundle": {
|
|
48
|
+
"patch": "./cordis.patch.yml"
|
|
49
|
+
},
|
|
50
|
+
"client": {
|
|
51
|
+
"platform": "web",
|
|
52
|
+
"inject": []
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"ssh2": "^1.17.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
60
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
61
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
62
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
63
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/node": "^22.0.0",
|
|
67
|
+
"@types/ssh2": "^1.15.4",
|
|
68
|
+
"tsup": "^8.0.0",
|
|
69
|
+
"typescript": "^5.5.0"
|
|
70
|
+
},
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public",
|
|
73
|
+
"registry": "https://registry.npmjs.org/"
|
|
74
|
+
}
|
|
75
|
+
}
|