dshb-exec-ssh 0.0.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/LICENSE +21 -0
- package/cordis.patch.yml +3 -0
- package/lib/connection-pool.d.ts +25 -0
- package/lib/connection-pool.js +54 -0
- package/lib/connection-pool.js.map +1 -0
- package/lib/connection-w4OewdmD.d.ts +46 -0
- package/lib/connection.d.ts +2 -0
- package/lib/connection.js +120 -0
- package/lib/connection.js.map +1 -0
- package/lib/env-scrub.d.ts +7 -0
- package/lib/env-scrub.js +40 -0
- package/lib/env-scrub.js.map +1 -0
- package/lib/executor.d.ts +46 -0
- package/lib/executor.js +212 -0
- package/lib/executor.js.map +1 -0
- package/lib/fs-provider.d.ts +50 -0
- package/lib/fs-provider.js +92 -0
- package/lib/fs-provider.js.map +1 -0
- package/lib/index-CDfaZYyK.d.ts +1125 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.js +30 -0
- package/lib/index.js.map +1 -0
- package/lib/process-provider-CUgC5gyu.d.ts +3328 -0
- package/lib/process-provider.d.ts +2 -0
- package/lib/process-provider.js +75 -0
- package/lib/process-provider.js.map +1 -0
- package/lib/runner.d.ts +13 -0
- package/lib/runner.js +62 -0
- package/lib/runner.js.map +1 -0
- package/lib/world.d.ts +79 -0
- package/lib/world.js +95 -0
- package/lib/world.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DSH-HoneyBee Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { n as Context } from "./index-CDfaZYyK.js";
|
|
2
|
+
import { n as SshConnection, o as SshSecrets, s as SshTarget } from "./connection-w4OewdmD.js";
|
|
3
|
+
//#region src/connection-pool.d.ts
|
|
4
|
+
interface PooledConnection {
|
|
5
|
+
target: SshTarget;
|
|
6
|
+
secrets: SshSecrets;
|
|
7
|
+
connection: SshConnection;
|
|
8
|
+
}
|
|
9
|
+
declare class SshConnectionPool {
|
|
10
|
+
private readonly ctx;
|
|
11
|
+
private readonly pool;
|
|
12
|
+
constructor(ctx: Context);
|
|
13
|
+
private key;
|
|
14
|
+
acquire(target: SshTarget, secrets: SshSecrets): SshConnection;
|
|
15
|
+
test(target: SshTarget, secrets: SshSecrets): Promise<{
|
|
16
|
+
ok: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
category?: string;
|
|
19
|
+
}>;
|
|
20
|
+
release(target: SshTarget): void;
|
|
21
|
+
closeAll(): void;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { PooledConnection, SshConnectionPool };
|
|
25
|
+
//# sourceMappingURL=connection-pool.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SshConnection, toConnectionError } from "./connection.js";
|
|
2
|
+
//#region src/connection-pool.ts
|
|
3
|
+
var SshConnectionPool = class {
|
|
4
|
+
ctx;
|
|
5
|
+
pool = /* @__PURE__ */ new Map();
|
|
6
|
+
constructor(ctx) {
|
|
7
|
+
this.ctx = ctx;
|
|
8
|
+
}
|
|
9
|
+
key(target) {
|
|
10
|
+
return `${target.host}:${target.port}:${target.username}`;
|
|
11
|
+
}
|
|
12
|
+
acquire(target, secrets) {
|
|
13
|
+
const k = this.key(target);
|
|
14
|
+
const existing = this.pool.get(k);
|
|
15
|
+
if (existing) return existing.connection;
|
|
16
|
+
const connection = new SshConnection(target, secrets);
|
|
17
|
+
this.pool.set(k, {
|
|
18
|
+
target,
|
|
19
|
+
secrets,
|
|
20
|
+
connection
|
|
21
|
+
});
|
|
22
|
+
return connection;
|
|
23
|
+
}
|
|
24
|
+
async test(target, secrets) {
|
|
25
|
+
try {
|
|
26
|
+
const conn = new SshConnection(target, secrets);
|
|
27
|
+
await conn.getClient();
|
|
28
|
+
conn.close();
|
|
29
|
+
return { ok: true };
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const e = toConnectionError(err);
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
error: e.message,
|
|
35
|
+
category: e.category
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
release(target) {
|
|
40
|
+
const k = this.key(target);
|
|
41
|
+
const entry = this.pool.get(k);
|
|
42
|
+
if (!entry) return;
|
|
43
|
+
entry.connection.close();
|
|
44
|
+
this.pool.delete(k);
|
|
45
|
+
}
|
|
46
|
+
closeAll() {
|
|
47
|
+
for (const entry of this.pool.values()) entry.connection.close();
|
|
48
|
+
this.pool.clear();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { SshConnectionPool };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=connection-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-pool.js","names":[],"sources":["../src/connection-pool.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis'\nimport { SshConnection, type SshSecrets, type SshTarget, toConnectionError } from './connection.js'\n\nexport interface PooledConnection {\n target: SshTarget\n secrets: SshSecrets\n connection: SshConnection\n}\n\nexport class SshConnectionPool {\n private readonly pool = new Map<string, PooledConnection>()\n\n constructor(private readonly ctx: Context) {}\n\n private key(target: SshTarget): string {\n return `${target.host}:${target.port}:${target.username}`\n }\n\n acquire(target: SshTarget, secrets: SshSecrets): SshConnection {\n const k = this.key(target)\n const existing = this.pool.get(k)\n if (existing) return existing.connection\n const connection = new SshConnection(target, secrets)\n this.pool.set(k, { target, secrets, connection })\n return connection\n }\n\n async test(target: SshTarget, secrets: SshSecrets): Promise<{ ok: boolean; error?: string; category?: string }> {\n try {\n const conn = new SshConnection(target, secrets)\n await conn.getClient()\n conn.close()\n return { ok: true }\n } catch (err) {\n const e = toConnectionError(err)\n return { ok: false, error: e.message, category: e.category }\n }\n }\n\n release(target: SshTarget): void {\n const k = this.key(target)\n const entry = this.pool.get(k)\n if (!entry) return\n entry.connection.close()\n this.pool.delete(k)\n }\n\n closeAll(): void {\n for (const entry of this.pool.values()) entry.connection.close()\n this.pool.clear()\n }\n}\n"],"mappings":";;AASA,IAAa,oBAAb,MAA+B;CAGA;CAF7B,uBAAwB,IAAI,IAA8B;CAE1D,YAAY,KAA+B;EAAd,KAAA,MAAA;CAAe;CAE5C,IAAY,QAA2B;EACrC,OAAO,GAAG,OAAO,KAAK,GAAG,OAAO,KAAK,GAAG,OAAO;CACjD;CAEA,QAAQ,QAAmB,SAAoC;EAC7D,MAAM,IAAI,KAAK,IAAI,MAAM;EACzB,MAAM,WAAW,KAAK,KAAK,IAAI,CAAC;EAChC,IAAI,UAAU,OAAO,SAAS;EAC9B,MAAM,aAAa,IAAI,cAAc,QAAQ,OAAO;EACpD,KAAK,KAAK,IAAI,GAAG;GAAE;GAAQ;GAAS;EAAW,CAAC;EAChD,OAAO;CACT;CAEA,MAAM,KAAK,QAAmB,SAAkF;EAC9G,IAAI;GACF,MAAM,OAAO,IAAI,cAAc,QAAQ,OAAO;GAC9C,MAAM,KAAK,UAAU;GACrB,KAAK,MAAM;GACX,OAAO,EAAE,IAAI,KAAK;EACpB,SAAS,KAAK;GACZ,MAAM,IAAI,kBAAkB,GAAG;GAC/B,OAAO;IAAE,IAAI;IAAO,OAAO,EAAE;IAAS,UAAU,EAAE;GAAS;EAC7D;CACF;CAEA,QAAQ,QAAyB;EAC/B,MAAM,IAAI,KAAK,IAAI,MAAM;EACzB,MAAM,QAAQ,KAAK,KAAK,IAAI,CAAC;EAC7B,IAAI,CAAC,OAAO;EACZ,MAAM,WAAW,MAAM;EACvB,KAAK,KAAK,OAAO,CAAC;CACpB;CAEA,WAAiB;EACf,KAAK,MAAM,SAAS,KAAK,KAAK,OAAO,GAAG,MAAM,WAAW,MAAM;EAC/D,KAAK,KAAK,MAAM;CAClB;AACF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Client } from "ssh2";
|
|
2
|
+
//#region src/connection.d.ts
|
|
3
|
+
interface SshAuthConfig {
|
|
4
|
+
kind: 'password' | 'key' | 'agent';
|
|
5
|
+
keyPath?: string;
|
|
6
|
+
}
|
|
7
|
+
interface SshJumpHop {
|
|
8
|
+
host: string;
|
|
9
|
+
port?: number;
|
|
10
|
+
username?: string;
|
|
11
|
+
keyPath?: string;
|
|
12
|
+
}
|
|
13
|
+
interface SshTarget {
|
|
14
|
+
host: string;
|
|
15
|
+
port: number;
|
|
16
|
+
username: string;
|
|
17
|
+
auth: SshAuthConfig;
|
|
18
|
+
jump?: SshJumpHop[];
|
|
19
|
+
}
|
|
20
|
+
interface SshSecrets {
|
|
21
|
+
password?: string;
|
|
22
|
+
privateKey?: string;
|
|
23
|
+
passphrase?: string;
|
|
24
|
+
}
|
|
25
|
+
type SshErrorCategory = 'auth' | 'network' | 'hostkey' | 'timeout' | 'unknown';
|
|
26
|
+
interface SshConnectionError extends Error {
|
|
27
|
+
category: SshErrorCategory;
|
|
28
|
+
cause?: unknown;
|
|
29
|
+
}
|
|
30
|
+
declare function classifySshError(err: unknown): SshErrorCategory;
|
|
31
|
+
declare function toConnectionError(err: unknown): SshConnectionError;
|
|
32
|
+
declare class SshConnection {
|
|
33
|
+
private readonly target;
|
|
34
|
+
private readonly secrets;
|
|
35
|
+
private client;
|
|
36
|
+
private connecting;
|
|
37
|
+
private closed;
|
|
38
|
+
constructor(target: SshTarget, secrets: SshSecrets);
|
|
39
|
+
getClient(): Promise<Client>;
|
|
40
|
+
private connectWithRetry;
|
|
41
|
+
private connectOnce;
|
|
42
|
+
close(): void;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { SshJumpHop as a, classifySshError as c, SshErrorCategory as i, toConnectionError as l, SshConnection as n, SshSecrets as o, SshConnectionError as r, SshTarget as s, SshAuthConfig as t };
|
|
46
|
+
//# sourceMappingURL=connection-w4OewdmD.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as SshJumpHop, c as classifySshError, i as SshErrorCategory, l as toConnectionError, n as SshConnection, o as SshSecrets, r as SshConnectionError, s as SshTarget, t as SshAuthConfig } from "./connection-w4OewdmD.js";
|
|
2
|
+
export { SshAuthConfig, SshConnection, SshConnectionError, SshErrorCategory, SshJumpHop, SshSecrets, SshTarget, classifySshError, toConnectionError };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Client } from "ssh2";
|
|
2
|
+
//#region src/connection.ts
|
|
3
|
+
const HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
4
|
+
const MAX_RETRIES = 5;
|
|
5
|
+
const BASE_DELAY_MS = 500;
|
|
6
|
+
function classifySshError(err) {
|
|
7
|
+
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
|
|
8
|
+
if (msg.includes("all configured authentication methods failed") || msg.includes("auth") || msg.includes("password") || msg.includes("publickey")) return "auth";
|
|
9
|
+
if (msg.includes("host key") || msg.includes("man-in-the-middle") || msg.includes("fingerprint")) return "hostkey";
|
|
10
|
+
if (msg.includes("timed out") || msg.includes("timeout") || msg.includes("ready timeout")) return "timeout";
|
|
11
|
+
if (msg.includes("econnrefused") || msg.includes("econnreset") || msg.includes("enotfound") || msg.includes("connect") || msg.includes("network")) return "network";
|
|
12
|
+
return "unknown";
|
|
13
|
+
}
|
|
14
|
+
function toConnectionError(err) {
|
|
15
|
+
const category = classifySshError(err);
|
|
16
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17
|
+
const e = new Error(message);
|
|
18
|
+
e.category = category;
|
|
19
|
+
e.cause = err;
|
|
20
|
+
return e;
|
|
21
|
+
}
|
|
22
|
+
function buildConnectConfig(target, secrets) {
|
|
23
|
+
const cfg = {
|
|
24
|
+
host: target.host,
|
|
25
|
+
port: target.port,
|
|
26
|
+
username: target.username,
|
|
27
|
+
readyTimeout: HANDSHAKE_TIMEOUT_MS,
|
|
28
|
+
keepaliveInterval: 0,
|
|
29
|
+
keepaliveCountMax: 3
|
|
30
|
+
};
|
|
31
|
+
switch (target.auth.kind) {
|
|
32
|
+
case "password":
|
|
33
|
+
if (secrets.password) cfg.password = secrets.password;
|
|
34
|
+
break;
|
|
35
|
+
case "key":
|
|
36
|
+
if (secrets.privateKey) {
|
|
37
|
+
cfg.privateKey = secrets.privateKey;
|
|
38
|
+
if (secrets.passphrase) cfg.passphrase = secrets.passphrase;
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
case "agent": cfg.agent = process.env.SSH_AUTH_SOCK ?? void 0;
|
|
42
|
+
}
|
|
43
|
+
return cfg;
|
|
44
|
+
}
|
|
45
|
+
var SshConnection = class {
|
|
46
|
+
target;
|
|
47
|
+
secrets;
|
|
48
|
+
client = null;
|
|
49
|
+
connecting = null;
|
|
50
|
+
closed = false;
|
|
51
|
+
constructor(target, secrets) {
|
|
52
|
+
this.target = target;
|
|
53
|
+
this.secrets = secrets;
|
|
54
|
+
}
|
|
55
|
+
async getClient() {
|
|
56
|
+
if (this.closed) throw toConnectionError(/* @__PURE__ */ new Error("connection closed"));
|
|
57
|
+
if (this.client) return this.client;
|
|
58
|
+
if (this.connecting) return this.connecting;
|
|
59
|
+
this.connecting = this.connectWithRetry();
|
|
60
|
+
try {
|
|
61
|
+
this.client = await this.connecting;
|
|
62
|
+
this.client.on("close", () => {
|
|
63
|
+
this.client = null;
|
|
64
|
+
});
|
|
65
|
+
this.client.on("error", () => {
|
|
66
|
+
this.client = null;
|
|
67
|
+
});
|
|
68
|
+
return this.client;
|
|
69
|
+
} finally {
|
|
70
|
+
this.connecting = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async connectWithRetry() {
|
|
74
|
+
let lastErr;
|
|
75
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
76
|
+
if (this.closed) throw toConnectionError(/* @__PURE__ */ new Error("connection closed"));
|
|
77
|
+
try {
|
|
78
|
+
return await this.connectOnce();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
lastErr = err;
|
|
81
|
+
const cat = classifySshError(err);
|
|
82
|
+
if (cat === "auth" || cat === "hostkey") throw toConnectionError(err);
|
|
83
|
+
if (attempt < MAX_RETRIES) {
|
|
84
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
85
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw toConnectionError(lastErr);
|
|
91
|
+
}
|
|
92
|
+
connectOnce() {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const client = new Client();
|
|
95
|
+
let settled = false;
|
|
96
|
+
const finish = (fn) => {
|
|
97
|
+
if (settled) return;
|
|
98
|
+
settled = true;
|
|
99
|
+
fn();
|
|
100
|
+
};
|
|
101
|
+
client.once("ready", () => finish(() => resolve(client)));
|
|
102
|
+
client.once("error", (err) => finish(() => reject(err)));
|
|
103
|
+
client.connect(buildConnectConfig(this.target, this.secrets));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
close() {
|
|
107
|
+
this.closed = true;
|
|
108
|
+
this.connecting = null;
|
|
109
|
+
if (this.client) {
|
|
110
|
+
try {
|
|
111
|
+
this.client.end();
|
|
112
|
+
} catch {}
|
|
113
|
+
this.client = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
//#endregion
|
|
118
|
+
export { SshConnection, classifySshError, toConnectionError };
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","names":[],"sources":["../src/connection.ts"],"sourcesContent":["import { Client, type ConnectConfig } from 'ssh2'\n\nexport interface SshAuthConfig {\n kind: 'password' | 'key' | 'agent'\n keyPath?: string\n}\n\nexport interface SshJumpHop {\n host: string\n port?: number\n username?: string\n keyPath?: string\n}\n\nexport interface SshTarget {\n host: string\n port: number\n username: string\n auth: SshAuthConfig\n jump?: SshJumpHop[]\n}\n\nexport interface SshSecrets {\n password?: string\n privateKey?: string\n passphrase?: string\n}\n\nexport type SshErrorCategory = 'auth' | 'network' | 'hostkey' | 'timeout' | 'unknown'\n\nexport interface SshConnectionError extends Error {\n category: SshErrorCategory\n cause?: unknown\n}\n\nconst HANDSHAKE_TIMEOUT_MS = 30_000\nconst MAX_RETRIES = 5\nconst BASE_DELAY_MS = 500\n\nexport function classifySshError(err: unknown): SshErrorCategory {\n const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase()\n if (msg.includes('all configured authentication methods failed') || msg.includes('auth') || msg.includes('password') || msg.includes('publickey')) return 'auth'\n if (msg.includes('host key') || msg.includes('man-in-the-middle') || msg.includes('fingerprint')) return 'hostkey'\n if (msg.includes('timed out') || msg.includes('timeout') || msg.includes('ready timeout')) return 'timeout'\n if (msg.includes('econnrefused') || msg.includes('econnreset') || msg.includes('enotfound') || msg.includes('connect') || msg.includes('network')) return 'network'\n return 'unknown'\n}\n\nexport function toConnectionError(err: unknown): SshConnectionError {\n const category = classifySshError(err)\n const message = err instanceof Error ? err.message : String(err)\n const e = new Error(message) as SshConnectionError\n e.category = category\n e.cause = err\n return e\n}\n\nfunction buildConnectConfig(target: SshTarget, secrets: SshSecrets): ConnectConfig {\n const cfg: ConnectConfig = {\n host: target.host,\n port: target.port,\n username: target.username,\n readyTimeout: HANDSHAKE_TIMEOUT_MS,\n keepaliveInterval: 0,\n keepaliveCountMax: 3,\n }\n switch (target.auth.kind) {\n case 'password':\n if (secrets.password) cfg.password = secrets.password\n break\n case 'key':\n if (secrets.privateKey) {\n cfg.privateKey = secrets.privateKey\n if (secrets.passphrase) cfg.passphrase = secrets.passphrase\n }\n break\n case 'agent':\n cfg.agent = process.env.SSH_AUTH_SOCK ?? undefined\n break\n }\n return cfg\n}\n\nexport class SshConnection {\n private client: Client | null = null\n private connecting: Promise<Client> | null = null\n private closed = false\n\n constructor(\n private readonly target: SshTarget,\n private readonly secrets: SshSecrets,\n ) {}\n\n async getClient(): Promise<Client> {\n if (this.closed) throw toConnectionError(new Error('connection closed'))\n if (this.client) return this.client\n if (this.connecting) return this.connecting\n this.connecting = this.connectWithRetry()\n try {\n this.client = await this.connecting\n this.client.on('close', () => {\n this.client = null\n })\n this.client.on('error', () => {\n this.client = null\n })\n return this.client\n } finally {\n this.connecting = null\n }\n }\n\n private async connectWithRetry(): Promise<Client> {\n let lastErr: unknown\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n if (this.closed) throw toConnectionError(new Error('connection closed'))\n try {\n return await this.connectOnce()\n } catch (err) {\n lastErr = err\n const cat = classifySshError(err)\n if (cat === 'auth' || cat === 'hostkey') throw toConnectionError(err)\n if (attempt < MAX_RETRIES) {\n const delay = BASE_DELAY_MS * Math.pow(2, attempt)\n await new Promise((r) => setTimeout(r, delay))\n continue\n }\n }\n }\n throw toConnectionError(lastErr)\n }\n\n private connectOnce(): Promise<Client> {\n return new Promise((resolve, reject) => {\n const client = new Client()\n let settled = false\n const finish = (fn: () => void) => {\n if (settled) return\n settled = true\n fn()\n }\n client.once('ready', () => finish(() => resolve(client)))\n client.once('error', (err: unknown) => finish(() => reject(err)))\n client.connect(buildConnectConfig(this.target, this.secrets))\n })\n }\n\n close(): void {\n this.closed = true\n this.connecting = null\n if (this.client) {\n try {\n this.client.end()\n } catch {}\n this.client = null\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/env-scrub.d.ts
|
|
2
|
+
declare function shouldScrubVar(name: string): boolean;
|
|
3
|
+
declare function scrubEnv(env: Record<string, string>): Record<string, string>;
|
|
4
|
+
declare const RUNNER_ENV_HINTS: string[];
|
|
5
|
+
//#endregion
|
|
6
|
+
export { RUNNER_ENV_HINTS, scrubEnv, shouldScrubVar };
|
|
7
|
+
//# sourceMappingURL=env-scrub.d.ts.map
|
package/lib/env-scrub.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/env-scrub.ts
|
|
2
|
+
const DANGEROUS_PREFIXES = [
|
|
3
|
+
"DSH",
|
|
4
|
+
"MCAI",
|
|
5
|
+
"OPENAI",
|
|
6
|
+
"ANTHROPIC",
|
|
7
|
+
"DEEPSEEK",
|
|
8
|
+
"LLM",
|
|
9
|
+
"API_KEY",
|
|
10
|
+
"TOKEN",
|
|
11
|
+
"SECRET",
|
|
12
|
+
"PASSWORD",
|
|
13
|
+
"CREDENTIAL"
|
|
14
|
+
];
|
|
15
|
+
function shouldScrubVar(name) {
|
|
16
|
+
const upper = name.toUpperCase();
|
|
17
|
+
return DANGEROUS_PREFIXES.some((p) => upper === p || upper.startsWith(`${p}_`));
|
|
18
|
+
}
|
|
19
|
+
function scrubEnv(env) {
|
|
20
|
+
const result = {};
|
|
21
|
+
for (const [key, value] of Object.entries(env)) if (!shouldScrubVar(key)) result[key] = value;
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
const RUNNER_ENV_HINTS = [
|
|
25
|
+
"DSH_*",
|
|
26
|
+
"MCAI_*",
|
|
27
|
+
"OPENAI*",
|
|
28
|
+
"ANTHROPIC*",
|
|
29
|
+
"DEEPSEEK*",
|
|
30
|
+
"LLM_*",
|
|
31
|
+
"API_KEY*",
|
|
32
|
+
"TOKEN*",
|
|
33
|
+
"SECRET*",
|
|
34
|
+
"PASSWORD*",
|
|
35
|
+
"CREDENTIAL*"
|
|
36
|
+
];
|
|
37
|
+
//#endregion
|
|
38
|
+
export { RUNNER_ENV_HINTS, scrubEnv, shouldScrubVar };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=env-scrub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-scrub.js","names":[],"sources":["../src/env-scrub.ts"],"sourcesContent":["const DANGEROUS_PREFIXES = ['DSH', 'MCAI', 'OPENAI', 'ANTHROPIC', 'DEEPSEEK', 'LLM', 'API_KEY', 'TOKEN', 'SECRET', 'PASSWORD', 'CREDENTIAL']\n\nexport function shouldScrubVar(name: string): boolean {\n const upper = name.toUpperCase()\n return DANGEROUS_PREFIXES.some((p) => upper === p || upper.startsWith(`${p}_`))\n}\n\nexport function scrubEnv(env: Record<string, string>): Record<string, string> {\n const result: Record<string, string> = {}\n for (const [key, value] of Object.entries(env)) {\n if (!shouldScrubVar(key)) result[key] = value\n }\n return result\n}\n\nexport const RUNNER_ENV_HINTS = [\n 'DSH_*',\n 'MCAI_*',\n 'OPENAI*',\n 'ANTHROPIC*',\n 'DEEPSEEK*',\n 'LLM_*',\n 'API_KEY*',\n 'TOKEN*',\n 'SECRET*',\n 'PASSWORD*',\n 'CREDENTIAL*',\n]\n"],"mappings":";AAAA,MAAM,qBAAqB;CAAC;CAAO;CAAQ;CAAU;CAAa;CAAY;CAAO;CAAW;CAAS;CAAU;CAAY;AAAY;AAE3I,SAAgB,eAAe,MAAuB;CACpD,MAAM,QAAQ,KAAK,YAAY;CAC/B,OAAO,mBAAmB,MAAM,MAAM,UAAU,KAAK,MAAM,WAAW,GAAG,EAAE,EAAE,CAAC;AAChF;AAEA,SAAgB,SAAS,KAAqD;CAC5E,MAAM,SAAiC,CAAC;CACxC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAC3C,IAAI,CAAC,eAAe,GAAG,GAAG,OAAO,OAAO;CAE1C,OAAO;AACT;AAEA,MAAa,mBAAmB;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { n as SshConnection } from "./connection-w4OewdmD.js";
|
|
2
|
+
//#region src/executor.d.ts
|
|
3
|
+
interface ExecResult {
|
|
4
|
+
code: number;
|
|
5
|
+
signal?: string;
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
}
|
|
9
|
+
declare class SshExecutor {
|
|
10
|
+
private readonly connection;
|
|
11
|
+
private readonly nodeId;
|
|
12
|
+
private readonly audit?;
|
|
13
|
+
private sftpCache;
|
|
14
|
+
constructor(connection: SshConnection, nodeId?: string, audit?: ((op: string, target: string, result: string) => void) | undefined);
|
|
15
|
+
exec(argv: string[], cwd: string, env?: Record<string, string>, stdinData?: Buffer): Promise<ExecResult>;
|
|
16
|
+
sftp(): Promise<import('ssh2').SFTPWrapper>;
|
|
17
|
+
readFile(path: string): Promise<Buffer>;
|
|
18
|
+
writeFile(path: string, content: Buffer | string): Promise<void>;
|
|
19
|
+
listDir(path: string): Promise<Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
isDir: boolean;
|
|
22
|
+
isFile: boolean;
|
|
23
|
+
isSymlink: boolean;
|
|
24
|
+
size: number;
|
|
25
|
+
mtime: number;
|
|
26
|
+
}>>;
|
|
27
|
+
stat(path: string): Promise<{
|
|
28
|
+
size: number;
|
|
29
|
+
mtime: number;
|
|
30
|
+
isDirectory: boolean;
|
|
31
|
+
isFile: boolean;
|
|
32
|
+
} | undefined>;
|
|
33
|
+
mkdir(path: string): Promise<void>;
|
|
34
|
+
remove(path: string): Promise<void>;
|
|
35
|
+
rename(src: string, dest: string): Promise<void>;
|
|
36
|
+
ensureRg(): Promise<void>;
|
|
37
|
+
pty(argv: string[], cwd: string, _env: Record<string, string>, cols: number, rows: number): Promise<{
|
|
38
|
+
channel: import('ssh2').ClientChannel;
|
|
39
|
+
onData: (cb: (chunk: Buffer) => void) => void;
|
|
40
|
+
resize: (cols: number, rows: number) => void;
|
|
41
|
+
kill: () => void;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { ExecResult, SshExecutor };
|
|
46
|
+
//# sourceMappingURL=executor.d.ts.map
|
package/lib/executor.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { REMOTE_RUNNER_COMMAND, encodeRunnerPayload } from "./runner.js";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
//#region src/executor.ts
|
|
6
|
+
var SshExecutor = class {
|
|
7
|
+
connection;
|
|
8
|
+
nodeId;
|
|
9
|
+
audit;
|
|
10
|
+
sftpCache = null;
|
|
11
|
+
constructor(connection, nodeId = "unknown", audit) {
|
|
12
|
+
this.connection = connection;
|
|
13
|
+
this.nodeId = nodeId;
|
|
14
|
+
this.audit = audit;
|
|
15
|
+
}
|
|
16
|
+
async exec(argv, cwd, env = {}, stdinData) {
|
|
17
|
+
const client = await this.connection.getClient();
|
|
18
|
+
const body = encodeRunnerPayload({
|
|
19
|
+
argv,
|
|
20
|
+
cwd,
|
|
21
|
+
env
|
|
22
|
+
});
|
|
23
|
+
const input = stdinData ? Buffer.concat([body, stdinData]) : body;
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
client.exec(REMOTE_RUNNER_COMMAND, (err, channel) => {
|
|
26
|
+
if (err) {
|
|
27
|
+
reject(err);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const stdout = [];
|
|
31
|
+
const stderr = [];
|
|
32
|
+
channel.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
33
|
+
channel.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
34
|
+
channel.on("close", (code, signal) => {
|
|
35
|
+
this.audit?.("exec", argv.join(" "), `code=${code ?? 0}`);
|
|
36
|
+
resolve({
|
|
37
|
+
code: code ?? 0,
|
|
38
|
+
signal,
|
|
39
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
40
|
+
stderr: Buffer.concat(stderr).toString("utf8")
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
channel.on("error", reject);
|
|
44
|
+
channel.end(input);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async sftp() {
|
|
49
|
+
if (this.sftpCache) return this.sftpCache;
|
|
50
|
+
const client = await this.connection.getClient();
|
|
51
|
+
this.sftpCache = new Promise((resolve, reject) => {
|
|
52
|
+
client.sftp((err, sftp) => {
|
|
53
|
+
if (err) reject(err);
|
|
54
|
+
else resolve(sftp);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
return this.sftpCache;
|
|
58
|
+
}
|
|
59
|
+
async readFile(path) {
|
|
60
|
+
const sftp = await this.sftp();
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
sftp.readFile(path, (err, data) => err ? reject(err) : resolve(data));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async writeFile(path, content) {
|
|
66
|
+
const sftp = await this.sftp();
|
|
67
|
+
const tmp = `${path}.dshb-tmp-${process.pid}-${Date.now()}`;
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
sftp.writeFile(tmp, content, (writeErr) => {
|
|
70
|
+
if (writeErr) {
|
|
71
|
+
reject(writeErr);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
sftp.ext_openssh_rename(tmp, path, (renameErr) => {
|
|
75
|
+
if (renameErr) reject(renameErr);
|
|
76
|
+
else {
|
|
77
|
+
this.audit?.("write", path, "ok");
|
|
78
|
+
resolve();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async listDir(path) {
|
|
85
|
+
const sftp = await this.sftp();
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
sftp.readdir(path, (err, list) => {
|
|
88
|
+
if (err) {
|
|
89
|
+
reject(err);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
resolve(list.map((e) => ({
|
|
93
|
+
name: e.filename,
|
|
94
|
+
isDir: e.attrs.isDirectory(),
|
|
95
|
+
isFile: e.attrs.isFile(),
|
|
96
|
+
isSymlink: e.attrs.isSymbolicLink(),
|
|
97
|
+
size: Number(e.attrs.size),
|
|
98
|
+
mtime: e.attrs.mtime * 1e3
|
|
99
|
+
})));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async stat(path) {
|
|
104
|
+
const sftp = await this.sftp();
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
sftp.stat(path, (err, stats) => {
|
|
107
|
+
if (err) {
|
|
108
|
+
resolve(void 0);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
resolve({
|
|
112
|
+
size: Number(stats.size),
|
|
113
|
+
mtime: stats.mtime * 1e3,
|
|
114
|
+
isDirectory: stats.isDirectory(),
|
|
115
|
+
isFile: stats.isFile()
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async mkdir(path) {
|
|
121
|
+
const sftp = await this.sftp();
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
sftp.mkdir(path, (err) => err ? reject(err) : resolve());
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async remove(path) {
|
|
127
|
+
const sftp = await this.sftp();
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
sftp.unlink(path, (err) => {
|
|
130
|
+
if (err) reject(err);
|
|
131
|
+
else {
|
|
132
|
+
this.audit?.("remove", path, "ok");
|
|
133
|
+
resolve();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
async rename(src, dest) {
|
|
139
|
+
const sftp = await this.sftp();
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
sftp.ext_openssh_rename(src, dest, (err) => {
|
|
142
|
+
if (err) reject(err);
|
|
143
|
+
else {
|
|
144
|
+
this.audit?.("move", `${src} -> ${dest}`, "ok");
|
|
145
|
+
resolve();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async ensureRg() {
|
|
151
|
+
if ((await this.exec([
|
|
152
|
+
"sh",
|
|
153
|
+
"-c",
|
|
154
|
+
"command -v rg || echo \"__DShB_RG_MISSING__\""
|
|
155
|
+
], "/")).stdout.includes("__DShB_RG_MISSING__")) {
|
|
156
|
+
const localRg = process.env.DSHB_LOCAL_RG ?? findLocalRg();
|
|
157
|
+
if (!localRg) return;
|
|
158
|
+
const remoteRgDir = "~/.dshb/bin";
|
|
159
|
+
await this.exec([
|
|
160
|
+
"mkdir",
|
|
161
|
+
"-p",
|
|
162
|
+
remoteRgDir
|
|
163
|
+
], "/");
|
|
164
|
+
const remoteRg = `${remoteRgDir}/rg`;
|
|
165
|
+
const content = readFileSync(localRg);
|
|
166
|
+
await this.writeFile(remoteRg, content);
|
|
167
|
+
await this.exec([
|
|
168
|
+
"chmod",
|
|
169
|
+
"+x",
|
|
170
|
+
remoteRg
|
|
171
|
+
], "/");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async pty(argv, cwd, _env, cols, rows) {
|
|
175
|
+
const client = await this.connection.getClient();
|
|
176
|
+
const q = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
177
|
+
const cmdParts = argv.map(q);
|
|
178
|
+
const command = `bash -c 'cd "$0" && exec env -i HOME="$HOME" PATH="\${PATH:-/usr/local/bin:/usr/bin:/bin}" "$@"' ${q(cwd)} ${cmdParts.join(" ")}`;
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
client.exec(command, { pty: {
|
|
181
|
+
cols,
|
|
182
|
+
rows,
|
|
183
|
+
term: "xterm-256color"
|
|
184
|
+
} }, (err, channel) => {
|
|
185
|
+
if (err) {
|
|
186
|
+
reject(err);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
resolve({
|
|
190
|
+
channel,
|
|
191
|
+
onData: (cb) => channel.on("data", cb),
|
|
192
|
+
resize: (c, r) => channel.setWindow(c, r, 0, 0),
|
|
193
|
+
kill: () => channel.close()
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
function findLocalRg() {
|
|
200
|
+
const candidates = [
|
|
201
|
+
join(homedir(), ".dsh", "node_modules", ".bin", "rg"),
|
|
202
|
+
join(homedir(), ".dsh", "node_modules", "@vscode", "ripgrep", "bin", "rg"),
|
|
203
|
+
join(homedir(), "go", "bin", "rg"),
|
|
204
|
+
"/usr/local/bin/rg",
|
|
205
|
+
"/usr/bin/rg"
|
|
206
|
+
];
|
|
207
|
+
for (const c of candidates) if (existsSync(c)) return c;
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
export { SshExecutor };
|
|
211
|
+
|
|
212
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","names":[],"sources":["../src/executor.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { encodeRunnerPayload, REMOTE_RUNNER_COMMAND, type RunnerPayload } from './runner.js'\nimport type { SshConnection } from './connection.js'\n\nexport interface ExecResult {\n code: number\n signal?: string\n stdout: string\n stderr: string\n}\n\nexport class SshExecutor {\n private sftpCache: Promise<import('ssh2').SFTPWrapper> | null = null\n\n constructor(\n private readonly connection: SshConnection,\n private readonly nodeId = 'unknown',\n private readonly audit?: (op: string, target: string, result: string) => void,\n ) {}\n\n async exec(argv: string[], cwd: string, env: Record<string, string> = {}, stdinData?: Buffer): Promise<ExecResult> {\n const client = await this.connection.getClient()\n const payload: RunnerPayload = { argv, cwd, env }\n const body = encodeRunnerPayload(payload)\n const input = stdinData ? Buffer.concat([body, stdinData]) : body\n return new Promise((resolve, reject) => {\n client.exec(REMOTE_RUNNER_COMMAND, (err, channel) => {\n if (err) {\n reject(err)\n return\n }\n const stdout: Buffer[] = []\n const stderr: Buffer[] = []\n channel.stdout.on('data', (chunk: Buffer) => stdout.push(chunk))\n channel.stderr.on('data', (chunk: Buffer) => stderr.push(chunk))\n channel.on('close', (code: number, signal: string) => {\n this.audit?.('exec', argv.join(' '), `code=${code ?? 0}`)\n resolve({\n code: code ?? 0,\n signal,\n stdout: Buffer.concat(stdout).toString('utf8'),\n stderr: Buffer.concat(stderr).toString('utf8'),\n })\n })\n channel.on('error', reject)\n channel.end(input)\n })\n })\n }\n\n async sftp(): Promise<import('ssh2').SFTPWrapper> {\n if (this.sftpCache) return this.sftpCache\n const client = await this.connection.getClient()\n this.sftpCache = new Promise((resolve, reject) => {\n client.sftp((err, sftp) => {\n if (err) reject(err)\n else resolve(sftp)\n })\n })\n return this.sftpCache\n }\n\n async readFile(path: string): Promise<Buffer> {\n const sftp = await this.sftp()\n return new Promise((resolve, reject) => {\n sftp.readFile(path, (err, data) => (err ? reject(err) : resolve(data)))\n })\n }\n\n async writeFile(path: string, content: Buffer | string): Promise<void> {\n const sftp = await this.sftp()\n const tmp = `${path}.dshb-tmp-${process.pid}-${Date.now()}`\n return new Promise((resolve, reject) => {\n sftp.writeFile(tmp, content, (writeErr) => {\n if (writeErr) {\n reject(writeErr)\n return\n }\n sftp.ext_openssh_rename(tmp, path, (renameErr) => {\n if (renameErr) reject(renameErr)\n else {\n this.audit?.('write', path, 'ok')\n resolve()\n }\n })\n })\n })\n }\n\n async listDir(path: string): Promise<Array<{ name: string; isDir: boolean; isFile: boolean; isSymlink: boolean; size: number; mtime: number }>> {\n const sftp = await this.sftp()\n return new Promise((resolve, reject) => {\n sftp.readdir(path, (err, list) => {\n if (err) {\n reject(err)\n return\n }\n resolve(\n list.map((e) => ({\n name: e.filename,\n isDir: e.attrs.isDirectory(),\n isFile: e.attrs.isFile(),\n isSymlink: e.attrs.isSymbolicLink(),\n size: Number(e.attrs.size),\n mtime: e.attrs.mtime * 1000,\n })),\n )\n })\n })\n }\n\n async stat(path: string): Promise<{ size: number; mtime: number; isDirectory: boolean; isFile: boolean } | undefined> {\n const sftp = await this.sftp()\n return new Promise((resolve) => {\n sftp.stat(path, (err, stats) => {\n if (err) {\n resolve(undefined)\n return\n }\n resolve({ size: Number(stats.size), mtime: stats.mtime * 1000, isDirectory: stats.isDirectory(), isFile: stats.isFile() })\n })\n })\n }\n\n async mkdir(path: string): Promise<void> {\n const sftp = await this.sftp()\n return new Promise((resolve, reject) => {\n sftp.mkdir(path, (err) => (err ? reject(err) : resolve()))\n })\n }\n\n async remove(path: string): Promise<void> {\n const sftp = await this.sftp()\n return new Promise((resolve, reject) => {\n sftp.unlink(path, (err) => {\n if (err) reject(err)\n else {\n this.audit?.('remove', path, 'ok')\n resolve()\n }\n })\n })\n }\n\n async rename(src: string, dest: string): Promise<void> {\n const sftp = await this.sftp()\n return new Promise((resolve, reject) => {\n sftp.ext_openssh_rename(src, dest, (err) => {\n if (err) reject(err)\n else {\n this.audit?.('move', `${src} -> ${dest}`, 'ok')\n resolve()\n }\n })\n })\n }\n\n async ensureRg(): Promise<void> {\n const result = await this.exec(['sh', '-c', 'command -v rg || echo \"__DShB_RG_MISSING__\"'], '/')\n if (result.stdout.includes('__DShB_RG_MISSING__')) {\n const localRg = process.env.DSHB_LOCAL_RG ?? findLocalRg()\n if (!localRg) return\n const remoteRgDir = '~/.dshb/bin'\n await this.exec(['mkdir', '-p', remoteRgDir], '/')\n const remoteRg = `${remoteRgDir}/rg`\n const content = readFileSync(localRg)\n await this.writeFile(remoteRg, content)\n await this.exec(['chmod', '+x', remoteRg], '/')\n }\n }\n\n async pty(argv: string[], cwd: string, _env: Record<string, string>, cols: number, rows: number): Promise<{ channel: import('ssh2').ClientChannel; onData: (cb: (chunk: Buffer) => void) => void; resize: (cols: number, rows: number) => void; kill: () => void }> {\n const client = await this.connection.getClient()\n const q = (s: string) => `'${s.replace(/'/g, \"'\\\\''\")}'`\n const cmdParts = argv.map(q)\n const command = `bash -c 'cd \"$0\" && exec env -i HOME=\"$HOME\" PATH=\"\\${PATH:-/usr/local/bin:/usr/bin:/bin}\" \"$@\"' ${q(cwd)} ${cmdParts.join(' ')}`\n return new Promise((resolve, reject) => {\n client.exec(\n command,\n { pty: { cols, rows, term: 'xterm-256color' } },\n (err, channel) => {\n if (err) {\n reject(err)\n return\n }\n resolve({\n channel,\n onData: (cb) => channel.on('data', cb),\n resize: (c, r) => channel.setWindow(c, r, 0, 0),\n kill: () => channel.close(),\n })\n },\n )\n })\n }\n}\n\nfunction findLocalRg(): string | undefined {\n const candidates = [\n join(homedir(), '.dsh', 'node_modules', '.bin', 'rg'),\n join(homedir(), '.dsh', 'node_modules', '@vscode', 'ripgrep', 'bin', 'rg'),\n join(homedir(), 'go', 'bin', 'rg'),\n '/usr/local/bin/rg',\n '/usr/bin/rg',\n ]\n for (const c of candidates) if (existsSync(c)) return c\n return undefined\n}\n"],"mappings":"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"}
|