@lengmoxxl/sdk 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 +53 -0
- package/dist/agent.d.ts +5 -0
- package/dist/channel.d.ts +56 -0
- package/dist/connection.d.ts +51 -0
- package/dist/index.cjs +520 -0
- package/dist/index.d.ts +7 -0
- package/dist/pty.d.ts +20 -0
- package/dist/types.d.ts +138 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lengmoXXL
|
|
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/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @lengmoxxl/sdk
|
|
2
|
+
The client SDK for [WangCai](https://github.com/lengmoXXL/WangCai), the terminal app. It reaches a machine, the one
|
|
3
|
+
the app runs on or one over SSH, and drives its terminals, files and subprocesses through a single connection.
|
|
4
|
+
|
|
5
|
+
A WangCai plugin compiles against this package, but at load time the app injects its own copy of this module, so a
|
|
6
|
+
plugin never carries its own connection pool.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
npm install @lengmoxxl/sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`ws` is the only runtime dependency. The bundled `dist/index.cjs` reads it at require time.
|
|
15
|
+
|
|
16
|
+
## Use
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { connect } from '@lengmoxxl/sdk';
|
|
20
|
+
|
|
21
|
+
const connection = await connect({ type: 'local', binary: '/path/to/wangcai' });
|
|
22
|
+
|
|
23
|
+
const sessions = await connection.pty.list();
|
|
24
|
+
const session = await connection.pty.create({ rows: 24, cols: 80 }, '/repo');
|
|
25
|
+
const terminal = await connection.pty.attach(session.id);
|
|
26
|
+
terminal.onData(({ data }) => process.stdout.write(data));
|
|
27
|
+
await terminal.write('git status\n');
|
|
28
|
+
|
|
29
|
+
const { stdout } = await connection.subprocess.exec('git', ['status'], { cwd: '/repo' });
|
|
30
|
+
connection.disconnect();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`connect` returns a `MachineConnection`. Connections are shared per target and reference counted, so two callers
|
|
34
|
+
asking for the same SSH host drive one tunnel; the last `disconnect()` tears it down. `connect({ type: 'ssh', host,
|
|
35
|
+
agent })` installs the carrier agent on the host first when `agent` is given.
|
|
36
|
+
|
|
37
|
+
- `connection.pty` - `list`, `cwd`, `create`, `attach`, `close`
|
|
38
|
+
- `connection.fs` - `stat`, `readDirectory`, `readFile`
|
|
39
|
+
- `connection.subprocess` - `exec`, which returns `{ stdout, stderr, code }` as `Uint8Array`
|
|
40
|
+
- `connection.onState` - connection status and the sessions it can see
|
|
41
|
+
|
|
42
|
+
The host-side context a plugin is activated with (`MainContext`, `UiContext`, `Bus`, and the workspace protocol
|
|
43
|
+
types) lives in the `@lengmoxxl/sdk/channel` entry. That entry is types only.
|
|
44
|
+
|
|
45
|
+
## Build
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
npm run build
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
From the repository root (the TypeScript and esbuild devDependencies live in the workspace root): esbuild bundles
|
|
52
|
+
`index.ts` into `dist/index.cjs` with `ws` left external, then `tsc` emits the declarations. `npm publish` and
|
|
53
|
+
`npm pack` run it for you.
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { AgentInfo } from './types';
|
|
2
|
+
export type Dispose = () => void | Promise<void>;
|
|
3
|
+
/** A sidebar tab the window opens for a plugin: what it is called, and what it draws. */
|
|
4
|
+
export interface TabOptions {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
tooltip?: string;
|
|
8
|
+
workspaceId?: string;
|
|
9
|
+
onClose?(): void;
|
|
10
|
+
mount(container: HTMLElement): TabContent;
|
|
11
|
+
}
|
|
12
|
+
export interface TabContent {
|
|
13
|
+
dispose: Dispose;
|
|
14
|
+
onSelect?(): void;
|
|
15
|
+
}
|
|
16
|
+
/** A tab the window remembers from the last run, so the plugin can open it again. */
|
|
17
|
+
export interface TabRecord {
|
|
18
|
+
plugin: string;
|
|
19
|
+
id: string;
|
|
20
|
+
workspaceId?: string;
|
|
21
|
+
}
|
|
22
|
+
/** The bus every plugin shares: what one publishes reaches the others, in either realm. */
|
|
23
|
+
export interface Bus {
|
|
24
|
+
publish(topic: string, data: unknown): Promise<void>;
|
|
25
|
+
subscribe<T>(topic: string, callback: (data: T) => void | Promise<void>): () => void;
|
|
26
|
+
}
|
|
27
|
+
/** What a main.cjs gets: the methods its own ui.js calls, and the machine it runs on. */
|
|
28
|
+
export interface MainContext {
|
|
29
|
+
global: Bus;
|
|
30
|
+
ui: {
|
|
31
|
+
handle<T = unknown>(method: string, handler: (params: T) => unknown): Dispose;
|
|
32
|
+
publish(topic: string, data: unknown): void;
|
|
33
|
+
};
|
|
34
|
+
host: {
|
|
35
|
+
dataDirectory: string;
|
|
36
|
+
resourcesDirectory: string;
|
|
37
|
+
agent: AgentInfo;
|
|
38
|
+
config: any;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** What a ui.js gets: its plugin's methods, and the window it is drawn in. */
|
|
42
|
+
export interface UiContext {
|
|
43
|
+
global: Bus;
|
|
44
|
+
ui: {
|
|
45
|
+
request<T = unknown>(method: string, params?: unknown): Promise<T>;
|
|
46
|
+
subscribe<T>(topic: string, callback: (data: T) => void | Promise<void>): () => void;
|
|
47
|
+
};
|
|
48
|
+
host: {
|
|
49
|
+
config: any;
|
|
50
|
+
preview: {
|
|
51
|
+
url: string;
|
|
52
|
+
message: string;
|
|
53
|
+
};
|
|
54
|
+
tabs(options: TabOptions): void;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ConnectionOptions, MachineState, Session, Size, DirectoryEntry, ExecOptions, ExecResult } from './types';
|
|
2
|
+
import { Pty } from './pty';
|
|
3
|
+
export declare class MachineConnection {
|
|
4
|
+
private options;
|
|
5
|
+
private listeners;
|
|
6
|
+
private terminals;
|
|
7
|
+
private instance?;
|
|
8
|
+
readonly ready: Promise<void>;
|
|
9
|
+
private resolveReady;
|
|
10
|
+
private rejectReady;
|
|
11
|
+
state: MachineState;
|
|
12
|
+
private ws?;
|
|
13
|
+
private tunnel?;
|
|
14
|
+
private retry?;
|
|
15
|
+
private heartbeat?;
|
|
16
|
+
private pending;
|
|
17
|
+
private stopped;
|
|
18
|
+
private attempt;
|
|
19
|
+
private epoch;
|
|
20
|
+
private serial;
|
|
21
|
+
private refs;
|
|
22
|
+
constructor(options: ConnectionOptions);
|
|
23
|
+
onState(callback: (state: MachineState) => void): () => void;
|
|
24
|
+
private publish;
|
|
25
|
+
retain(): void;
|
|
26
|
+
disconnect(): void;
|
|
27
|
+
private teardown;
|
|
28
|
+
pty: {
|
|
29
|
+
list: () => Promise<Session[]>;
|
|
30
|
+
cwd: (id: string) => Promise<string>;
|
|
31
|
+
create: (size?: Size, cwd?: string) => Promise<Session>;
|
|
32
|
+
attach: (id: string) => Promise<Pty>;
|
|
33
|
+
close: (id: string) => Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
fs: {
|
|
36
|
+
stat: (path: string) => Promise<{
|
|
37
|
+
isDirectory: boolean;
|
|
38
|
+
} | null>;
|
|
39
|
+
readDirectory: (path: string) => Promise<DirectoryEntry[]>;
|
|
40
|
+
readFile: (path: string) => Promise<Uint8Array>;
|
|
41
|
+
};
|
|
42
|
+
subprocess: {
|
|
43
|
+
exec: (program: string, args: string[], options: ExecOptions) => Promise<ExecResult>;
|
|
44
|
+
};
|
|
45
|
+
private cleanup;
|
|
46
|
+
private fail;
|
|
47
|
+
private open;
|
|
48
|
+
private forward;
|
|
49
|
+
private request;
|
|
50
|
+
private refresh;
|
|
51
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
connect: () => connect,
|
|
34
|
+
ensureAgent: () => ensureAgent
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// connection.ts
|
|
39
|
+
var import_node_child_process2 = require("node:child_process");
|
|
40
|
+
var import_node_net = require("node:net");
|
|
41
|
+
var import_node_util2 = require("node:util");
|
|
42
|
+
var import_ws = __toESM(require("ws"));
|
|
43
|
+
|
|
44
|
+
// pty.ts
|
|
45
|
+
var Pty = class {
|
|
46
|
+
constructor(id, request, remove) {
|
|
47
|
+
this.id = id;
|
|
48
|
+
this.request = request;
|
|
49
|
+
this.remove = remove;
|
|
50
|
+
}
|
|
51
|
+
sequence = -1;
|
|
52
|
+
error;
|
|
53
|
+
snapshot;
|
|
54
|
+
buffered = [];
|
|
55
|
+
snapshots = /* @__PURE__ */ new Set();
|
|
56
|
+
outputs = /* @__PURE__ */ new Set();
|
|
57
|
+
receive(event) {
|
|
58
|
+
if (this.error) return;
|
|
59
|
+
if (event.event === "snapshot") {
|
|
60
|
+
this.sequence = event.seq;
|
|
61
|
+
this.buffered = [];
|
|
62
|
+
this.snapshot = event;
|
|
63
|
+
for (const callback of this.snapshots) callback(event);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (event.seq <= this.sequence) return;
|
|
67
|
+
this.sequence = event.seq;
|
|
68
|
+
if (!this.outputs.size) this.buffered.push(event);
|
|
69
|
+
for (const callback of this.outputs) callback(event);
|
|
70
|
+
}
|
|
71
|
+
onSnapshot(callback) {
|
|
72
|
+
this.snapshots.add(callback);
|
|
73
|
+
if (this.snapshot) callback(this.snapshot);
|
|
74
|
+
return () => {
|
|
75
|
+
this.snapshots.delete(callback);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
onData(callback) {
|
|
79
|
+
this.outputs.add(callback);
|
|
80
|
+
for (const event of this.buffered) callback(event);
|
|
81
|
+
this.buffered = [];
|
|
82
|
+
return () => {
|
|
83
|
+
this.outputs.delete(callback);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async write(data) {
|
|
87
|
+
if (this.error) throw this.error;
|
|
88
|
+
const parts = Array.from(data);
|
|
89
|
+
const requests = [];
|
|
90
|
+
for (let offset = 0; offset < parts.length; offset += 8e3) {
|
|
91
|
+
requests.push(this.request("input", { session_id: this.id, data: parts.slice(offset, offset + 8e3).join("") }));
|
|
92
|
+
}
|
|
93
|
+
await Promise.all(requests);
|
|
94
|
+
}
|
|
95
|
+
async resize(size) {
|
|
96
|
+
if (this.error) throw this.error;
|
|
97
|
+
await this.request("resize", { session_id: this.id, ...size });
|
|
98
|
+
}
|
|
99
|
+
async detach() {
|
|
100
|
+
if (this.error) return;
|
|
101
|
+
this.remove();
|
|
102
|
+
this.release();
|
|
103
|
+
await this.request("detach", { session_id: this.id });
|
|
104
|
+
}
|
|
105
|
+
release(error = new Error("Terminal detached")) {
|
|
106
|
+
this.error = error;
|
|
107
|
+
this.snapshots.clear();
|
|
108
|
+
this.outputs.clear();
|
|
109
|
+
this.snapshot = void 0;
|
|
110
|
+
this.buffered = [];
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// agent.ts
|
|
115
|
+
var import_node_child_process = require("node:child_process");
|
|
116
|
+
var import_node_util = require("node:util");
|
|
117
|
+
var exec = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
118
|
+
function agentCommand(command) {
|
|
119
|
+
const home = process.env.WANGCAI_HOME;
|
|
120
|
+
const exportHome = home ? `export WANGCAI_HOME='${home.replace(/'/g, String.raw`'\''`)}'; ` : "";
|
|
121
|
+
return `${exportHome}export PATH="$HOME/.local/bin:$PATH"; ${command}`;
|
|
122
|
+
}
|
|
123
|
+
async function ensureAgent(host, { version, prefix, signal }) {
|
|
124
|
+
const targets = {
|
|
125
|
+
"Darwin x86_64": "x86_64-apple-darwin",
|
|
126
|
+
"Darwin arm64": "aarch64-apple-darwin",
|
|
127
|
+
"Linux x86_64": "x86_64-unknown-linux-musl"
|
|
128
|
+
};
|
|
129
|
+
const remote = (command) => ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=8", host, command];
|
|
130
|
+
const probe = await exec("ssh", remote(agentCommand("uname -s -m; wangcai --version 2>/dev/null; true")), { timeout: 3e4, signal }).catch(() => void 0);
|
|
131
|
+
const [platform, installed] = (probe?.stdout ?? "").split("\n").map((line) => line.trim());
|
|
132
|
+
if (!platform) return;
|
|
133
|
+
const target = targets[platform];
|
|
134
|
+
if (!target) throw new Error(`${platform} \u4E0A\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u5B89\u88C5 agent`);
|
|
135
|
+
if (installed === `wangcai ${version}`) return;
|
|
136
|
+
const response = await fetch(`${prefix}/v${version}/wangcai-${target}`, { signal });
|
|
137
|
+
if (!response.ok) throw new Error(`\u4E0B\u8F7D agent \u5931\u8D25\uFF1A${response.status} ${response.statusText}`);
|
|
138
|
+
const binary = Buffer.from(await response.arrayBuffer());
|
|
139
|
+
await new Promise((resolve, reject) => {
|
|
140
|
+
const child = (0, import_node_child_process.spawn)("ssh", remote("mkdir -p ~/.local/bin && cat > ~/.local/bin/.wangcai.tmp && chmod 755 ~/.local/bin/.wangcai.tmp && mv ~/.local/bin/.wangcai.tmp ~/.local/bin/wangcai"), { stdio: ["pipe", "ignore", "pipe"] });
|
|
141
|
+
let stderr = "";
|
|
142
|
+
child.stderr.on("data", (chunk) => {
|
|
143
|
+
stderr += chunk;
|
|
144
|
+
});
|
|
145
|
+
child.on("error", reject);
|
|
146
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr.trim() || `\u5199\u5165 agent \u5931\u8D25\uFF08ssh \u9000\u51FA\u7801 ${code}\uFF09`)));
|
|
147
|
+
child.stdin.end(binary);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// connection.ts
|
|
152
|
+
var exec2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
153
|
+
var MachineConnection = class {
|
|
154
|
+
constructor(options) {
|
|
155
|
+
this.options = options;
|
|
156
|
+
this.state = { status: "connecting", sessions: [], generation: 0 };
|
|
157
|
+
this.ready = new Promise((resolve, reject) => {
|
|
158
|
+
this.resolveReady = resolve;
|
|
159
|
+
this.rejectReady = reject;
|
|
160
|
+
});
|
|
161
|
+
void this.open();
|
|
162
|
+
}
|
|
163
|
+
listeners = /* @__PURE__ */ new Set();
|
|
164
|
+
terminals = /* @__PURE__ */ new Map();
|
|
165
|
+
instance;
|
|
166
|
+
ready;
|
|
167
|
+
resolveReady;
|
|
168
|
+
rejectReady;
|
|
169
|
+
state;
|
|
170
|
+
ws;
|
|
171
|
+
tunnel;
|
|
172
|
+
retry;
|
|
173
|
+
heartbeat;
|
|
174
|
+
pending = /* @__PURE__ */ new Map();
|
|
175
|
+
stopped = false;
|
|
176
|
+
attempt = 0;
|
|
177
|
+
epoch = 0;
|
|
178
|
+
serial = 0;
|
|
179
|
+
refs = 1;
|
|
180
|
+
onState(callback) {
|
|
181
|
+
this.listeners.add(callback);
|
|
182
|
+
callback({ ...this.state });
|
|
183
|
+
return () => {
|
|
184
|
+
this.listeners.delete(callback);
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
publish() {
|
|
188
|
+
for (const callback of this.listeners) callback({ ...this.state });
|
|
189
|
+
}
|
|
190
|
+
retain() {
|
|
191
|
+
this.refs++;
|
|
192
|
+
}
|
|
193
|
+
disconnect() {
|
|
194
|
+
if (this.refs === 0) return;
|
|
195
|
+
if (--this.refs > 0) return;
|
|
196
|
+
this.teardown();
|
|
197
|
+
}
|
|
198
|
+
teardown() {
|
|
199
|
+
this.refs = 0;
|
|
200
|
+
this.stopped = true;
|
|
201
|
+
this.cleanup();
|
|
202
|
+
this.rejectReady(new Error("Connection cancelled"));
|
|
203
|
+
for (const terminal of this.terminals.values()) terminal.release();
|
|
204
|
+
this.terminals.clear();
|
|
205
|
+
this.state.status = "disconnected";
|
|
206
|
+
this.state.error = void 0;
|
|
207
|
+
this.publish();
|
|
208
|
+
this.listeners.clear();
|
|
209
|
+
}
|
|
210
|
+
pty = {
|
|
211
|
+
list: () => this.request("list"),
|
|
212
|
+
cwd: (id) => this.request("cwd", { session_id: id }),
|
|
213
|
+
create: (size = { rows: 24, cols: 80 }, cwd) => this.request("create", { ...size, cwd }),
|
|
214
|
+
attach: async (id) => {
|
|
215
|
+
const existing = this.terminals.get(id);
|
|
216
|
+
if (existing) return existing;
|
|
217
|
+
const terminal = new Pty(id, (op, params) => this.request(op, params), () => {
|
|
218
|
+
this.terminals.delete(id);
|
|
219
|
+
});
|
|
220
|
+
this.terminals.set(id, terminal);
|
|
221
|
+
try {
|
|
222
|
+
await this.request("attach", { session_id: id });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this.terminals.delete(id);
|
|
225
|
+
terminal.release();
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
return terminal;
|
|
229
|
+
},
|
|
230
|
+
close: async (id) => {
|
|
231
|
+
await this.request("close", { session_id: id });
|
|
232
|
+
this.terminals.get(id)?.release();
|
|
233
|
+
this.terminals.delete(id);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
fs = {
|
|
237
|
+
stat: (path) => this.request("stat", { path }),
|
|
238
|
+
readDirectory: (path) => this.request("read_directory", { path }),
|
|
239
|
+
readFile: async (path) => {
|
|
240
|
+
const result = await this.request("read_file", { path });
|
|
241
|
+
return Buffer.from(result.data, "base64");
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
subprocess = {
|
|
245
|
+
exec: async (program, args, options) => {
|
|
246
|
+
const result = await this.request("exec", { program, args, ...options }, 2e4);
|
|
247
|
+
return { stdout: Buffer.from(result.stdout, "base64"), stderr: Buffer.from(result.stderr, "base64"), code: result.code };
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
cleanup() {
|
|
251
|
+
this.epoch++;
|
|
252
|
+
clearTimeout(this.retry);
|
|
253
|
+
clearInterval(this.heartbeat);
|
|
254
|
+
const socket = this.ws;
|
|
255
|
+
this.ws = void 0;
|
|
256
|
+
socket?.terminate();
|
|
257
|
+
this.tunnel?.kill();
|
|
258
|
+
this.tunnel = void 0;
|
|
259
|
+
for (const pending of this.pending.values()) {
|
|
260
|
+
clearTimeout(pending.timer);
|
|
261
|
+
pending.reject(new Error("Connection lost; input was not replayed."));
|
|
262
|
+
}
|
|
263
|
+
this.pending.clear();
|
|
264
|
+
}
|
|
265
|
+
fail(error) {
|
|
266
|
+
if (this.stopped) return;
|
|
267
|
+
if (this.state.generation === 0) {
|
|
268
|
+
this.rejectReady(error instanceof Error ? error : new Error(String(error)));
|
|
269
|
+
this.teardown();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.cleanup();
|
|
273
|
+
this.state.status = "connecting";
|
|
274
|
+
this.state.error = error instanceof Error ? error.message : String(error);
|
|
275
|
+
this.publish();
|
|
276
|
+
this.retry = setTimeout(() => void this.open(), Math.min(1e3 * 2 ** this.attempt++, 15e3));
|
|
277
|
+
}
|
|
278
|
+
async open() {
|
|
279
|
+
const epoch = ++this.epoch;
|
|
280
|
+
const current = () => !this.stopped && this.epoch === epoch;
|
|
281
|
+
this.state.status = "connecting";
|
|
282
|
+
this.publish();
|
|
283
|
+
try {
|
|
284
|
+
let info;
|
|
285
|
+
let port;
|
|
286
|
+
if (this.options.type === "local") {
|
|
287
|
+
const { stdout } = await exec2(this.options.binary ?? "wangcai", ["server", "start", "--json"], { timeout: 15e3 });
|
|
288
|
+
info = JSON.parse(stdout);
|
|
289
|
+
port = info.port;
|
|
290
|
+
} else {
|
|
291
|
+
if (this.options.agent) await ensureAgent(this.options.host, { ...this.options.agent, signal: this.options.signal });
|
|
292
|
+
const { stdout } = await exec2("ssh", [
|
|
293
|
+
"-T",
|
|
294
|
+
"-o",
|
|
295
|
+
"BatchMode=yes",
|
|
296
|
+
"-o",
|
|
297
|
+
"ConnectTimeout=8",
|
|
298
|
+
this.options.host,
|
|
299
|
+
agentCommand("wangcai server start --json")
|
|
300
|
+
], { timeout: 15e3, maxBuffer: 64 * 1024 });
|
|
301
|
+
info = JSON.parse(stdout);
|
|
302
|
+
if (!Number.isInteger(info.port) || info.port < 1 || info.port > 65535) throw new Error("Node returned an invalid port.");
|
|
303
|
+
if (!current()) return;
|
|
304
|
+
port = await this.forward(info.port, current);
|
|
305
|
+
}
|
|
306
|
+
if (!current()) return;
|
|
307
|
+
const socket = new import_ws.default(`ws://127.0.0.1:${port}`, { handshakeTimeout: 5e3, maxPayload: 64 * 1024 * 1024 });
|
|
308
|
+
this.ws = socket;
|
|
309
|
+
socket.on("message", (data, binary) => {
|
|
310
|
+
if (!current()) return;
|
|
311
|
+
try {
|
|
312
|
+
if (binary) {
|
|
313
|
+
const bytes = data;
|
|
314
|
+
const length = bytes.readUInt32BE(0);
|
|
315
|
+
if (length > bytes.length - 4) throw new Error("Invalid terminal packet");
|
|
316
|
+
const header = JSON.parse(bytes.subarray(4, 4 + length).toString());
|
|
317
|
+
this.terminals.get(header.session_id)?.receive({ ...header, data: bytes.subarray(4 + length) });
|
|
318
|
+
} else {
|
|
319
|
+
const message = JSON.parse(data.toString());
|
|
320
|
+
if (message.id) {
|
|
321
|
+
const pending = this.pending.get(message.id);
|
|
322
|
+
if (!pending) return;
|
|
323
|
+
clearTimeout(pending.timer);
|
|
324
|
+
this.pending.delete(message.id);
|
|
325
|
+
if (message.error) pending.reject(new Error(message.error));
|
|
326
|
+
else pending.resolve(message.result);
|
|
327
|
+
} else if (message.event === "sessions_changed") {
|
|
328
|
+
void this.refresh().catch(() => {
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
this.fail(error);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
socket.on("error", () => {
|
|
337
|
+
});
|
|
338
|
+
await new Promise((resolve, reject) => {
|
|
339
|
+
socket.once("open", resolve);
|
|
340
|
+
socket.once("error", reject);
|
|
341
|
+
});
|
|
342
|
+
if (!current()) {
|
|
343
|
+
socket.terminate();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
socket.on("close", () => {
|
|
347
|
+
if (current()) this.fail(new Error("Disconnected. Reconnecting\u2026"));
|
|
348
|
+
});
|
|
349
|
+
const actual = await this.request("info");
|
|
350
|
+
if (actual.instance_id !== info.instance_id || actual.protocol !== 1) throw new Error("Node instance or protocol mismatch.");
|
|
351
|
+
await this.refresh();
|
|
352
|
+
if (!current()) return;
|
|
353
|
+
if (this.instance && this.instance !== actual.instance_id) {
|
|
354
|
+
for (const terminal of this.terminals.values()) terminal.release(new Error("Node restarted; terminal no longer exists"));
|
|
355
|
+
this.terminals.clear();
|
|
356
|
+
}
|
|
357
|
+
this.instance = actual.instance_id;
|
|
358
|
+
for (const [id, terminal] of this.terminals) {
|
|
359
|
+
try {
|
|
360
|
+
await this.request("attach", { session_id: id });
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (!current()) return;
|
|
363
|
+
terminal.release(error);
|
|
364
|
+
this.terminals.delete(id);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (!current()) return;
|
|
368
|
+
this.state.status = "connected";
|
|
369
|
+
this.state.error = void 0;
|
|
370
|
+
this.state.generation++;
|
|
371
|
+
this.attempt = 0;
|
|
372
|
+
this.resolveReady();
|
|
373
|
+
this.publish();
|
|
374
|
+
let alive = true;
|
|
375
|
+
socket.on("pong", () => {
|
|
376
|
+
alive = true;
|
|
377
|
+
});
|
|
378
|
+
this.heartbeat = setInterval(() => {
|
|
379
|
+
if (!alive) {
|
|
380
|
+
this.fail(new Error("Node stopped responding. Reconnecting\u2026"));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
alive = false;
|
|
384
|
+
socket.ping();
|
|
385
|
+
}, 1e4);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (current()) this.fail(error);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async forward(remotePort, current) {
|
|
391
|
+
let lastError;
|
|
392
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
393
|
+
if (!current()) throw new Error("Connection cancelled");
|
|
394
|
+
const reservation = (0, import_node_net.createServer)();
|
|
395
|
+
await new Promise((resolve, reject) => {
|
|
396
|
+
reservation.once("error", reject);
|
|
397
|
+
reservation.listen(0, "127.0.0.1", resolve);
|
|
398
|
+
});
|
|
399
|
+
const port = reservation.address().port;
|
|
400
|
+
await new Promise((resolve) => reservation.close(() => resolve()));
|
|
401
|
+
if (!current()) throw new Error("Connection cancelled");
|
|
402
|
+
const tunnel = (0, import_node_child_process2.spawn)("ssh", [
|
|
403
|
+
"-N",
|
|
404
|
+
"-T",
|
|
405
|
+
"-o",
|
|
406
|
+
"BatchMode=yes",
|
|
407
|
+
"-o",
|
|
408
|
+
"ExitOnForwardFailure=yes",
|
|
409
|
+
"-o",
|
|
410
|
+
"ConnectTimeout=8",
|
|
411
|
+
"-o",
|
|
412
|
+
"ServerAliveInterval=10",
|
|
413
|
+
"-o",
|
|
414
|
+
"ServerAliveCountMax=2",
|
|
415
|
+
"-L",
|
|
416
|
+
`127.0.0.1:${port}:127.0.0.1:${remotePort}`,
|
|
417
|
+
this.options.host
|
|
418
|
+
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
419
|
+
this.tunnel = tunnel;
|
|
420
|
+
let stderr = "";
|
|
421
|
+
tunnel.stderr.on("data", (data) => {
|
|
422
|
+
stderr = (stderr + data.toString()).slice(-4096);
|
|
423
|
+
});
|
|
424
|
+
try {
|
|
425
|
+
await new Promise((resolve, reject) => {
|
|
426
|
+
let finished = false;
|
|
427
|
+
const finish = (error) => {
|
|
428
|
+
if (finished) return;
|
|
429
|
+
finished = true;
|
|
430
|
+
clearInterval(poll);
|
|
431
|
+
clearTimeout(timeout);
|
|
432
|
+
tunnel.off("exit", exited);
|
|
433
|
+
if (error) reject(error);
|
|
434
|
+
else resolve();
|
|
435
|
+
};
|
|
436
|
+
const exited = () => finish(new Error(stderr.trim() || "SSH tunnel exited."));
|
|
437
|
+
const poll = setInterval(() => {
|
|
438
|
+
if (!current()) {
|
|
439
|
+
finish(new Error("Connection cancelled"));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const probe = (0, import_node_net.createConnection)({ host: "127.0.0.1", port });
|
|
443
|
+
probe.once("connect", () => {
|
|
444
|
+
probe.destroy();
|
|
445
|
+
finish();
|
|
446
|
+
});
|
|
447
|
+
probe.once("error", () => probe.destroy());
|
|
448
|
+
probe.setTimeout(200, () => probe.destroy());
|
|
449
|
+
}, 100);
|
|
450
|
+
const timeout = setTimeout(() => finish(new Error(stderr.trim() || "SSH tunnel timed out.")), 1e4);
|
|
451
|
+
tunnel.once("exit", exited);
|
|
452
|
+
tunnel.once("error", finish);
|
|
453
|
+
});
|
|
454
|
+
tunnel.on("exit", () => {
|
|
455
|
+
if (current()) this.fail(new Error(stderr.trim() || "SSH tunnel closed."));
|
|
456
|
+
});
|
|
457
|
+
return port;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
tunnel.kill();
|
|
460
|
+
if (this.tunnel === tunnel) this.tunnel = void 0;
|
|
461
|
+
lastError = error;
|
|
462
|
+
if (!/address already in use|cannot listen to port|port forwarding failed/i.test(String(error))) throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
throw lastError;
|
|
466
|
+
}
|
|
467
|
+
async request(op, params = {}, timeout = 1e4) {
|
|
468
|
+
const socket = this.ws;
|
|
469
|
+
if (!socket || socket.readyState !== import_ws.default.OPEN) throw new Error("Machine is not connected.");
|
|
470
|
+
const id = String(++this.serial);
|
|
471
|
+
return new Promise((resolve, reject) => {
|
|
472
|
+
const timer = setTimeout(() => {
|
|
473
|
+
this.pending.delete(id);
|
|
474
|
+
reject(new Error(`${op} timed out; it was not retried.`));
|
|
475
|
+
}, timeout);
|
|
476
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
477
|
+
socket.send(JSON.stringify({ ...params, op, id }), (error) => {
|
|
478
|
+
if (!error) return;
|
|
479
|
+
clearTimeout(timer);
|
|
480
|
+
this.pending.delete(id);
|
|
481
|
+
reject(error);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
async refresh() {
|
|
486
|
+
this.state.sessions = await this.request("list");
|
|
487
|
+
this.publish();
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// index.ts
|
|
492
|
+
var connections = /* @__PURE__ */ new Map();
|
|
493
|
+
async function connect(options) {
|
|
494
|
+
options.signal?.throwIfAborted();
|
|
495
|
+
const target = options.type === "ssh" ? `ssh:${options.host}` : `local:${options.binary ?? "wangcai"}`;
|
|
496
|
+
const shared = connections.get(target);
|
|
497
|
+
const machine = shared ?? new MachineConnection(options);
|
|
498
|
+
if (shared) machine.retain();
|
|
499
|
+
else {
|
|
500
|
+
connections.set(target, machine);
|
|
501
|
+
machine.onState((state) => {
|
|
502
|
+
if (state.status === "disconnected" && connections.get(target) === machine) connections.delete(target);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const aborted = new Promise((_, reject) => {
|
|
506
|
+
options.signal?.addEventListener("abort", () => reject(new Error("Connection cancelled")), { once: true });
|
|
507
|
+
});
|
|
508
|
+
try {
|
|
509
|
+
await Promise.race([machine.ready, aborted]);
|
|
510
|
+
return machine;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
machine.disconnect();
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
517
|
+
0 && (module.exports = {
|
|
518
|
+
connect,
|
|
519
|
+
ensureAgent
|
|
520
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MachineConnection } from './connection';
|
|
2
|
+
import type { ConnectionOptions } from './types';
|
|
3
|
+
export type { AgentInfo, ConnectionOptions, FileClick, Machine, MachineState, Session, Size, Output, Snapshot, TerminalEvent, DirectoryEntry, ExecOptions, ExecResult, Profile, Theme, WorkspaceActive, WorkspaceRow, WorkspaceMenuItem } from './types';
|
|
4
|
+
export type { MachineConnection } from './connection';
|
|
5
|
+
export type { Pty } from './pty';
|
|
6
|
+
export { ensureAgent } from './agent';
|
|
7
|
+
export declare function connect(options: ConnectionOptions): Promise<MachineConnection>;
|
package/dist/pty.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Output, Snapshot, TerminalEvent, Size } from './types';
|
|
2
|
+
export declare class Pty {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
private request;
|
|
5
|
+
private remove;
|
|
6
|
+
private sequence;
|
|
7
|
+
private error?;
|
|
8
|
+
private snapshot?;
|
|
9
|
+
private buffered;
|
|
10
|
+
private snapshots;
|
|
11
|
+
private outputs;
|
|
12
|
+
constructor(id: string, request: (op: string, params: Record<string, unknown>) => Promise<unknown>, remove: () => void);
|
|
13
|
+
receive(event: TerminalEvent): void;
|
|
14
|
+
onSnapshot(callback: (event: Snapshot) => void): () => void;
|
|
15
|
+
onData(callback: (event: Output) => void): () => void;
|
|
16
|
+
write(data: string): Promise<void>;
|
|
17
|
+
resize(size: Size): Promise<void>;
|
|
18
|
+
detach(): Promise<void>;
|
|
19
|
+
release(error?: Error): void;
|
|
20
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export interface AgentInfo {
|
|
2
|
+
version: string;
|
|
3
|
+
prefix: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The workspace protocol. A plugin that init.ts lists under `workspaces` is a workspace provider, and
|
|
7
|
+
* the app drives it through these `ui` methods:
|
|
8
|
+
*
|
|
9
|
+
* workspaces() -> WorkspaceRow[] one flat row per workspace, in draw order
|
|
10
|
+
* workspace-menu() -> WorkspaceMenuItem[] what the menu offers to open; an entry that carries
|
|
11
|
+
* an error stays listed but is not clickable
|
|
12
|
+
* workspace-create({ key }) -> { id: string } the id of the workspace it made
|
|
13
|
+
* workspace-select({ id })
|
|
14
|
+
* workspace-close({ id })
|
|
15
|
+
* workspace-move({ id, before? })
|
|
16
|
+
*
|
|
17
|
+
* Every view that follows the workspace in front shares these channel topics, whatever the provider
|
|
18
|
+
* behind it is:
|
|
19
|
+
*
|
|
20
|
+
* 'workspaces' a provider published that its rows changed
|
|
21
|
+
* 'workspace:active' the workspace in front, or null; published by its provider
|
|
22
|
+
* 'workspace:query' a view asks for 'workspace:active' again
|
|
23
|
+
*/
|
|
24
|
+
export interface WorkspaceRow {
|
|
25
|
+
id: string;
|
|
26
|
+
label: string;
|
|
27
|
+
machine: string;
|
|
28
|
+
running: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface WorkspaceMenuItem {
|
|
31
|
+
key: string;
|
|
32
|
+
label: string;
|
|
33
|
+
hint?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface Machine {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
host?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface WorkspaceActive {
|
|
42
|
+
machine: Machine;
|
|
43
|
+
sessionId: string;
|
|
44
|
+
workspaceId: string;
|
|
45
|
+
}
|
|
46
|
+
/** Where a click on a file or a directory points, as the 'onclick' topic carries it. */
|
|
47
|
+
export interface FileClick {
|
|
48
|
+
type: 'file' | 'directory';
|
|
49
|
+
machine: Machine;
|
|
50
|
+
path: string;
|
|
51
|
+
line?: number;
|
|
52
|
+
column?: number;
|
|
53
|
+
}
|
|
54
|
+
export type ConnectionOptions = {
|
|
55
|
+
type: 'local';
|
|
56
|
+
binary?: string;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
} | {
|
|
59
|
+
type: 'ssh';
|
|
60
|
+
host: string;
|
|
61
|
+
agent?: AgentInfo;
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
};
|
|
64
|
+
export interface Size {
|
|
65
|
+
rows: number;
|
|
66
|
+
cols: number;
|
|
67
|
+
}
|
|
68
|
+
export interface Session extends Size {
|
|
69
|
+
id: string;
|
|
70
|
+
title: string;
|
|
71
|
+
pid: number;
|
|
72
|
+
exit_code: number | null;
|
|
73
|
+
}
|
|
74
|
+
export interface MachineState {
|
|
75
|
+
status: 'disconnected' | 'connecting' | 'connected';
|
|
76
|
+
error?: string;
|
|
77
|
+
sessions: Session[];
|
|
78
|
+
generation: number;
|
|
79
|
+
}
|
|
80
|
+
export interface Output {
|
|
81
|
+
data: Uint8Array;
|
|
82
|
+
seq: number;
|
|
83
|
+
}
|
|
84
|
+
export interface Snapshot extends Output, Size {
|
|
85
|
+
}
|
|
86
|
+
export type TerminalEvent = ({
|
|
87
|
+
event: 'snapshot';
|
|
88
|
+
} & Snapshot | {
|
|
89
|
+
event: 'output';
|
|
90
|
+
} & Output) & {
|
|
91
|
+
session_id: string;
|
|
92
|
+
};
|
|
93
|
+
export interface DirectoryEntry {
|
|
94
|
+
name: string;
|
|
95
|
+
isDirectory: boolean;
|
|
96
|
+
}
|
|
97
|
+
export interface ExecOptions {
|
|
98
|
+
cwd: string;
|
|
99
|
+
env?: Record<string, string>;
|
|
100
|
+
}
|
|
101
|
+
export interface ExecResult {
|
|
102
|
+
stdout: Uint8Array;
|
|
103
|
+
stderr: Uint8Array;
|
|
104
|
+
code: number;
|
|
105
|
+
}
|
|
106
|
+
export interface Theme {
|
|
107
|
+
background: string;
|
|
108
|
+
foreground: string;
|
|
109
|
+
cursor: string;
|
|
110
|
+
selection: string;
|
|
111
|
+
surface: string;
|
|
112
|
+
overlay: string;
|
|
113
|
+
border: string;
|
|
114
|
+
muted: string;
|
|
115
|
+
accent: string;
|
|
116
|
+
black: string;
|
|
117
|
+
red: string;
|
|
118
|
+
green: string;
|
|
119
|
+
yellow: string;
|
|
120
|
+
blue: string;
|
|
121
|
+
magenta: string;
|
|
122
|
+
cyan: string;
|
|
123
|
+
white: string;
|
|
124
|
+
brightBlack: string;
|
|
125
|
+
brightRed: string;
|
|
126
|
+
brightGreen: string;
|
|
127
|
+
brightYellow: string;
|
|
128
|
+
brightBlue: string;
|
|
129
|
+
brightMagenta: string;
|
|
130
|
+
brightCyan: string;
|
|
131
|
+
brightWhite: string;
|
|
132
|
+
}
|
|
133
|
+
export interface Profile {
|
|
134
|
+
theme: Theme;
|
|
135
|
+
agent: {
|
|
136
|
+
downloadPrefix: string;
|
|
137
|
+
};
|
|
138
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lengmoxxl/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Client SDK for WangCai: connect to a local or remote machine and drive its pty, files and git",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/lengmoXXL/WangCai.git",
|
|
9
|
+
"directory": "sdk"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"wangcai",
|
|
13
|
+
"pty",
|
|
14
|
+
"ssh",
|
|
15
|
+
"terminal",
|
|
16
|
+
"sdk"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22"
|
|
20
|
+
},
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"main": "dist/index.cjs",
|
|
23
|
+
"types": "dist/index.d.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.cjs"
|
|
31
|
+
},
|
|
32
|
+
"./channel": {
|
|
33
|
+
"types": "./dist/channel.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "esbuild index.ts --outfile=dist/index.cjs --bundle --platform=node --target=node22 --packages=external && tsc",
|
|
43
|
+
"prepack": "npm run build",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"ws": "^8.18.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^24.0.0",
|
|
51
|
+
"@types/ws": "^8.5.0",
|
|
52
|
+
"typescript": "^5.9.0",
|
|
53
|
+
"esbuild": "^0.25.0"
|
|
54
|
+
}
|
|
55
|
+
}
|