@lengmoxxl/sdk 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -38
- package/dist/agent.d.ts +0 -4
- package/dist/connection.d.ts +7 -1
- package/dist/index.cjs +113 -56
- package/dist/index.d.ts +3 -1
- package/dist/process.d.ts +19 -0
- package/dist/types.d.ts +24 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,54 +1,152 @@
|
|
|
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.
|
|
1
|
+
# @lengmoxxl/sdk:接口参考
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
本参考对应 SDK `0.1.5`:连接本机或 SSH 机器上的旺财后台进程,提供终端、文件与子进程
|
|
4
|
+
操作。插件入口与生命周期见 [插件开发手册](../docs/plugins.md),进程关系见
|
|
5
|
+
[架构说明](../docs/architecture.md)。
|
|
7
6
|
|
|
8
|
-
##
|
|
7
|
+
## 插件上下文
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
| 成员 | 说明 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `global` | 公共 `Bus`,跨插件与前后端通信 |
|
|
12
|
+
| `ui.handle<T>(method, handler)` | 后端注册方法,返回取消注册的 `Dispose` |
|
|
13
|
+
| `ui.publish(topic, data)` | 后端向本插件 UI 发事件,对应前端 `ui.subscribe` |
|
|
14
|
+
| `ui.subscribe<T>(topic, callback)` | 前端订阅本插件后端事件,返回取消订阅的函数 |
|
|
15
|
+
| `ui.request<T>(method, params?)` | 前端调用本插件后端方法,返回 `Promise<T>` |
|
|
16
|
+
| `connect(machine, signal?)` | 后端借用机器连接:`{ host?: string }`,可带 `AbortSignal` |
|
|
17
|
+
| `host.dataDirectory` / `host.config` | 插件私有数据目录 / 本插件设置 |
|
|
18
|
+
| `host.tabs(options)` / `host.open(url)` | 前端开侧栏标签页 / 用系统浏览器打开 http、https |
|
|
19
|
+
| `host.preview` | `{ url, message }`,HTML 预览 frame 的地址与握手消息 |
|
|
13
20
|
|
|
14
|
-
|
|
21
|
+
请求与事件传可结构化克隆的数据,不传函数、DOM、Pty 或连接实例。
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
`ui.handle` 注册的方法名不能含 `:`;`ui.publish` 与 `ui.subscribe` 的主题由宿主加插件 id 前缀,
|
|
24
|
+
`global` 用原始主题,自定义主题要自己加命名空间(如 `directory-demo:opened`)。
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
import { openMachine } from '@lengmoxxl/sdk';
|
|
26
|
+
`connect({})` 连接本机,`connect({ host })` 连接 SSH。
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
宿主为相同机器复用连接并计数,每次取得连接对应一次 `disconnect()`。
|
|
22
29
|
|
|
23
|
-
|
|
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');
|
|
30
|
+
`Dispose = () => void | Promise<void>`,卸载时清理资源,不要关闭用户仍要保留的 PTY。
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
connection.disconnect();
|
|
31
|
-
```
|
|
32
|
+
`Bus`:`publish(topic, data): Promise<void>`、`subscribe<T>(topic, callback): () => void`。
|
|
32
33
|
|
|
33
|
-
`
|
|
34
|
-
connection is reference counted, so callers that hand the same one around keep it alive until the last
|
|
35
|
-
`disconnect()`. `openMachine({ type: 'ssh', host, agent })` installs the carrier agent on the host first when
|
|
36
|
-
`agent` is given.
|
|
34
|
+
`host.dataDirectory` 默认 `~/.local/share/wangcai/data/<plugin-id>`。
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- `connection.subprocess` - `exec`, which returns `{ stdout, stderr, code }` as `Uint8Array`
|
|
41
|
-
- `connection.onState` - connection status and the sessions it can see
|
|
36
|
+
`host.config` 是浅合并,避免把自定义字段命名为 `theme`,配色键见
|
|
37
|
+
[README 主题参数](../README.md#主题参数)。
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
types) lives in the `@lengmoxxl/sdk/channel` entry. That entry is types only.
|
|
39
|
+
## 标签页
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
`host.tabs({ id, title, tooltip?, workspaceId?, onClose?, mount })`:`id`、`title` 必填,
|
|
42
|
+
`tooltip?` 是标签上的悬停提示(缺省用 `title`),`workspaceId` 缺省用当前工作区;
|
|
43
|
+
`mount(container)` 返回的 `TabContent.dispose` 必填,`onSelect?` 在选中、工作区刷新或侧栏重新显示时
|
|
44
|
+
都可能调用,`onClose?` 在移除标签页时先于 `dispose` 调用。
|
|
47
45
|
|
|
46
|
+
相同插件 id、工作区 id、标签 id 再次调用 `host.tabs` 会复用并选中;尚无工作区时打开的标签页
|
|
47
|
+
会在第一个工作区激活时绑定到它。
|
|
48
|
+
|
|
49
|
+
`TabRecord = { plugin, id, workspaceId? }` 只保存身份,插件的 `open(context, record)` 据此恢复
|
|
50
|
+
内容,资源必须仍在。
|
|
51
|
+
|
|
52
|
+
## 预览
|
|
53
|
+
|
|
54
|
+
用 `host.preview.url` 建 iframe,设 `sandbox="allow-scripts"`,不要加
|
|
55
|
+
`allow-same-origin`;收到来源为该 frame、`event.data === host.preview.message` 的消息后
|
|
56
|
+
`postMessage(html, '*')`。预览会执行其中脚本。
|
|
57
|
+
|
|
58
|
+
## 插件配置
|
|
59
|
+
|
|
60
|
+
插件 `config` 是嵌套对象,叶子声明 `type` 与可选 `default`;宿主只保留 schema 声明的键,
|
|
61
|
+
缺失或类型不符时取 default,`array` 内部条目由插件自己验证。
|
|
62
|
+
|
|
63
|
+
## 工作区协议
|
|
64
|
+
|
|
65
|
+
放入 init.ts `workspaces` 的插件用 `ui.handle` 提供:`workspaces()`(`WorkspaceRow[]`,
|
|
66
|
+
按显示顺序)、`workspace-menu()`(`WorkspaceMenuItem[]`)、`workspace-create({ key })`
|
|
67
|
+
(返回 `{ id }`)、`workspace-select({ id })`、`workspace-close({ id })`、
|
|
68
|
+
`workspace-move({ id, before? })`(`before` 是目标工作区 id,不是索引;省略为末尾,且只
|
|
69
|
+
允许同一 provider 内拖动)。
|
|
70
|
+
|
|
71
|
+
provider 要保证工作区 id 在所有 provider 的行中唯一识别;`error` 由 provider 给出并在重试成功后清除,
|
|
72
|
+
宿主不禁用带 `error` 的菜单项。
|
|
73
|
+
|
|
74
|
+
约定的公共主题:`workspaces`(行变化,App 重新请求列表)、`workspace:active`
|
|
75
|
+
(`WorkspaceActive | null`,provider 发布)、`workspace:query`(视图请求 provider 再发布
|
|
76
|
+
当前工作区)、`onclick`(`FileClick`)。
|
|
77
|
+
|
|
78
|
+
字段类型见 SDK 导出的 `WorkspaceRow`、`WorkspaceMenuItem`、`Machine`、`WorkspaceActive`
|
|
79
|
+
与 `FileClick`,定义在 `sdk/types.ts`。
|
|
80
|
+
|
|
81
|
+
## 机器、终端与子进程
|
|
82
|
+
|
|
83
|
+
`MachineConnection`:`ready`(首次连接就绪)、`state`、`port`(本地 loopback 端口,SSH 时
|
|
84
|
+
为转发端口)、`onState(callback)`、`retain()`(连接层使用)、`disconnect()`,以及 `pty`、`fs`、
|
|
85
|
+
`subprocess`。
|
|
86
|
+
|
|
87
|
+
`MachineState` 含 `status`、`error?`、`sessions`、`generation`。
|
|
88
|
+
|
|
89
|
+
断线会自动重连并重新 attach 已挂载的终端与进程,失败的请求与输入不自动重放,后台进程重启时旧 Pty 与进程失效。
|
|
90
|
+
|
|
91
|
+
`connection.pty`:`list()`、`cwd(id)`、`create(size?, cwd?)`(默认 24 行 80 列;cwd 用绝对目录,
|
|
92
|
+
省略时在后台进程的 HOME 下启动)、`attach(id)`(复用句柄)、`close(id)`(结束 shell 与后台子孙进程)。
|
|
93
|
+
|
|
94
|
+
`Size` 用整数:rows 2–300,cols 2–500。
|
|
95
|
+
|
|
96
|
+
`Session` 有 `id, title`、`pid, rows, cols`、`exit_code: number | null`。
|
|
97
|
+
|
|
98
|
+
`Pty`:`id`、`onSnapshot(callback)`、`onData(callback)`、`write(data)`、`resize(size)`、
|
|
99
|
+
`detach()`(停止挂载,不关闭 shell)。
|
|
100
|
+
|
|
101
|
+
`Output = { data: Uint8Array; seq: number }`,`Snapshot` 额外有 `rows, cols`。
|
|
102
|
+
|
|
103
|
+
字节含终端控制序列,交给终端模拟器,不要把快照当键盘输入重发。
|
|
104
|
+
|
|
105
|
+
`receive` 与 `release(error?)` 由连接层使用。
|
|
106
|
+
|
|
107
|
+
`connection.fs`:`stat(path)` 返回 `{ isDirectory: boolean } | null`、`readDirectory(path)` 返回
|
|
108
|
+
`DirectoryEntry[]`(`{ name, isDirectory }`)、`readFile(path)` 返回原始字节。
|
|
109
|
+
|
|
110
|
+
路径为绝对路径,`readFile` 上限 16 MiB;SDK 不提供写入、删除或上传接口。
|
|
111
|
+
|
|
112
|
+
`connection.subprocess.exec(program, args, options)`:`args` 是独立 argv(不经 shell 展开),
|
|
113
|
+
`options.cwd` 是绝对工作目录,`options.env` 覆盖继承的环境变量;返回
|
|
114
|
+
`{ stdout: Uint8Array; stderr: Uint8Array; code: number }`。非零退出码仍返回结果,无法启动、
|
|
115
|
+
超时、输出超限或被信号终止时 reject;stdin 关闭,长时间或交互用 `subprocess.spawn` 或 PTY。
|
|
116
|
+
|
|
117
|
+
执行上限 15 秒,SDK 等待响应上限 20 秒,stdout 与 stderr 各最多 8 MiB,断线取消未完成的 exec。
|
|
118
|
+
|
|
119
|
+
`connection.subprocess.spawn(program, args, options)`(`options` 同 `exec`)启动一个
|
|
120
|
+
长期存活的进程,stdin、stdout、stderr 都是管道,返回 `ProcessInfo = { id, pid, exit_code }`;
|
|
121
|
+
`attach(id)` 复用句柄(未 attach 前不接收输出),`close(id)` 结束进程及其进程组;进程归后台进程所有,不 `close` 就一直留着。
|
|
122
|
+
|
|
123
|
+
`Process`:`id`、`onData(callback)`、`onExit(callback)`、`write(data)`。
|
|
124
|
+
|
|
125
|
+
`ProcessOutput = { data: Uint8Array; seq: number; stream: 'stdout' | 'stderr' }`,两路输出各自按序到达,
|
|
126
|
+
`seq` 单调递增,跨两路的先后以 `seq` 为准。
|
|
127
|
+
|
|
128
|
+
`onExit` 给出退出码(被信号结束时按 shell 的 128+signal),节点来不及投递时给出 `undefined`,此时
|
|
129
|
+
输出流已不可信,进程可能仍在运行,请重新 `spawn`。
|
|
130
|
+
|
|
131
|
+
`write` 按 32 KiB 分片、按序应用,进程退出后 reject;先 `attach` 再写。
|
|
132
|
+
|
|
133
|
+
`receive` 与 `release(error?)` 由连接层使用。
|
|
134
|
+
|
|
135
|
+
## 独立 Node 客户端
|
|
136
|
+
|
|
137
|
+
App 插件用 `context.connect`;插件之外才调用 `openMachine`:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { openMachine } from '@lengmoxxl/sdk';
|
|
141
|
+
|
|
142
|
+
const connection = await openMachine({ type: 'local', binary: '/path/to/wangcai' });
|
|
143
|
+
await connection.fs.readDirectory('/');
|
|
144
|
+
connection.disconnect();
|
|
48
145
|
```
|
|
49
|
-
npm run build
|
|
50
|
-
```
|
|
51
146
|
|
|
52
|
-
|
|
53
|
-
`
|
|
54
|
-
`
|
|
147
|
+
`openMachine` 返回 `Promise<MachineConnection>`。`type` 为 `'local'` 或 `'ssh'`;local 可给
|
|
148
|
+
`binary`,ssh 必须给 `host`(系统 ssh 目标或别名)与可选 `agent`(`InstallAgent`),两者
|
|
149
|
+
都可给 `signal`。不传 `agent` 时 SDK 不下载二进制;连接用 BatchMode SSH。
|
|
150
|
+
|
|
151
|
+
`agentCommand(command)` 返回拼接后的 shell 命令:在 PATH 前加入远端 `$HOME/.local/bin`,
|
|
152
|
+
并携带当前进程的 WANGCAI_HOME(会转义;command 本身不转义,只传可信命令)。
|
package/dist/agent.d.ts
CHANGED
package/dist/connection.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { ConnectionOptions, MachineState, Session, Size, DirectoryEntry, ExecOptions, ExecResult } from './types';
|
|
1
|
+
import type { ConnectionOptions, MachineState, Session, Size, DirectoryEntry, ExecOptions, ExecResult, ProcessInfo } from './types';
|
|
2
|
+
import { Process } from './process';
|
|
2
3
|
import { Pty } from './pty';
|
|
3
4
|
export declare class MachineConnection {
|
|
4
5
|
private options;
|
|
5
6
|
private listeners;
|
|
6
7
|
private terminals;
|
|
8
|
+
private processes;
|
|
9
|
+
port?: number;
|
|
7
10
|
private instance?;
|
|
8
11
|
readonly ready: Promise<void>;
|
|
9
12
|
private resolveReady;
|
|
@@ -41,6 +44,9 @@ export declare class MachineConnection {
|
|
|
41
44
|
};
|
|
42
45
|
subprocess: {
|
|
43
46
|
exec: (program: string, args: string[], options: ExecOptions) => Promise<ExecResult>;
|
|
47
|
+
spawn: (program: string, args: string[], options: ExecOptions) => Promise<ProcessInfo>;
|
|
48
|
+
attach: (id: string) => Promise<Process>;
|
|
49
|
+
close: (id: string) => Promise<void>;
|
|
44
50
|
};
|
|
45
51
|
private cleanup;
|
|
46
52
|
private fail;
|
package/dist/index.cjs
CHANGED
|
@@ -30,16 +30,79 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
agentCommand: () => agentCommand,
|
|
33
34
|
openMachine: () => openMachine
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(index_exports);
|
|
36
37
|
|
|
37
38
|
// connection.ts
|
|
38
|
-
var
|
|
39
|
+
var import_node_child_process = require("node:child_process");
|
|
39
40
|
var import_node_net = require("node:net");
|
|
40
|
-
var
|
|
41
|
+
var import_node_util = require("node:util");
|
|
41
42
|
var import_ws = __toESM(require("ws"));
|
|
42
43
|
|
|
44
|
+
// process.ts
|
|
45
|
+
var CHUNK = 32 * 1024;
|
|
46
|
+
var Process = class {
|
|
47
|
+
constructor(id, request) {
|
|
48
|
+
this.id = id;
|
|
49
|
+
this.request = request;
|
|
50
|
+
}
|
|
51
|
+
sequence = -1;
|
|
52
|
+
error;
|
|
53
|
+
ended = false;
|
|
54
|
+
code;
|
|
55
|
+
buffered = [];
|
|
56
|
+
outputs = /* @__PURE__ */ new Set();
|
|
57
|
+
exits = /* @__PURE__ */ new Set();
|
|
58
|
+
receive(event) {
|
|
59
|
+
if (this.error) return;
|
|
60
|
+
if (event.event === "exit" || event.event === "lost") {
|
|
61
|
+
this.ended = true;
|
|
62
|
+
this.code = event.event === "exit" ? event.code : void 0;
|
|
63
|
+
for (const callback of this.exits) callback(this.code);
|
|
64
|
+
this.exits.clear();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (event.seq <= this.sequence) return;
|
|
68
|
+
this.sequence = event.seq;
|
|
69
|
+
const output = { data: event.data, seq: event.seq, stream: event.event };
|
|
70
|
+
if (!this.outputs.size) this.buffered.push(output);
|
|
71
|
+
for (const callback of this.outputs) callback(output);
|
|
72
|
+
}
|
|
73
|
+
onData(callback) {
|
|
74
|
+
this.outputs.add(callback);
|
|
75
|
+
for (const event of this.buffered) callback(event);
|
|
76
|
+
this.buffered = [];
|
|
77
|
+
return () => {
|
|
78
|
+
this.outputs.delete(callback);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Its exit code, or nothing when the node lost the stream and the process may still be running. */
|
|
82
|
+
onExit(callback) {
|
|
83
|
+
this.exits.add(callback);
|
|
84
|
+
if (this.ended) callback(this.code);
|
|
85
|
+
return () => {
|
|
86
|
+
this.exits.delete(callback);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async write(data) {
|
|
90
|
+
if (this.error) throw this.error;
|
|
91
|
+
const requests = [];
|
|
92
|
+
for (let offset = 0; offset < data.length; offset += CHUNK) {
|
|
93
|
+
const chunk = Buffer.from(data.subarray(offset, offset + CHUNK)).toString("base64");
|
|
94
|
+
requests.push(this.request("process_write", { session_id: this.id, data: chunk }));
|
|
95
|
+
}
|
|
96
|
+
await Promise.all(requests);
|
|
97
|
+
}
|
|
98
|
+
release(error = new Error("Process detached")) {
|
|
99
|
+
this.error = error;
|
|
100
|
+
this.outputs.clear();
|
|
101
|
+
this.exits.clear();
|
|
102
|
+
this.buffered = [];
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
43
106
|
// pty.ts
|
|
44
107
|
var Pty = class {
|
|
45
108
|
constructor(id, request, remove) {
|
|
@@ -111,61 +174,14 @@ var Pty = class {
|
|
|
111
174
|
};
|
|
112
175
|
|
|
113
176
|
// agent.ts
|
|
114
|
-
var import_node_child_process = require("node:child_process");
|
|
115
|
-
var import_node_util = require("node:util");
|
|
116
|
-
var exec = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
117
|
-
var DOWNLOAD_TIMEOUT = 3e4;
|
|
118
177
|
function agentCommand(command) {
|
|
119
178
|
const home = process.env.WANGCAI_HOME;
|
|
120
179
|
const exportHome = home ? `export WANGCAI_HOME='${home.replace(/'/g, String.raw`'\''`)}'; ` : "";
|
|
121
180
|
return `${exportHome}export PATH="$HOME/.local/bin:$PATH"; ${command}`;
|
|
122
181
|
}
|
|
123
|
-
async function ensureAgent(host, { version, prefixes, 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 failures = [];
|
|
137
|
-
for (const prefix of prefixes) {
|
|
138
|
-
signal?.throwIfAborted();
|
|
139
|
-
const url = `${prefix}/v${version}/wangcai-${target}`;
|
|
140
|
-
const deadline = AbortSignal.timeout(DOWNLOAD_TIMEOUT);
|
|
141
|
-
let binary;
|
|
142
|
-
try {
|
|
143
|
-
const response = await fetch(url, { signal: signal ? AbortSignal.any([signal, deadline]) : deadline });
|
|
144
|
-
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
145
|
-
binary = Buffer.from(await response.arrayBuffer());
|
|
146
|
-
} catch (error) {
|
|
147
|
-
failures.push(`${url}
|
|
148
|
-
${error instanceof Error ? error.message : String(error)}`);
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
await new Promise((resolve, reject) => {
|
|
152
|
-
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"] });
|
|
153
|
-
let stderr = "";
|
|
154
|
-
child.stderr.on("data", (chunk) => {
|
|
155
|
-
stderr += chunk;
|
|
156
|
-
});
|
|
157
|
-
child.on("error", reject);
|
|
158
|
-
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr.trim() || `\u5199\u5165 agent \u5931\u8D25\uFF08ssh \u9000\u51FA\u7801 ${code}\uFF09`)));
|
|
159
|
-
child.stdin.end(binary);
|
|
160
|
-
});
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
throw new Error(`\u4E0B\u8F7D agent \u5931\u8D25\uFF1A
|
|
164
|
-
${failures.join("\n\n")}`);
|
|
165
|
-
}
|
|
166
182
|
|
|
167
183
|
// connection.ts
|
|
168
|
-
var
|
|
184
|
+
var exec = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
169
185
|
var MachineConnection = class {
|
|
170
186
|
constructor(options) {
|
|
171
187
|
this.options = options;
|
|
@@ -178,6 +194,9 @@ var MachineConnection = class {
|
|
|
178
194
|
}
|
|
179
195
|
listeners = /* @__PURE__ */ new Set();
|
|
180
196
|
terminals = /* @__PURE__ */ new Map();
|
|
197
|
+
processes = /* @__PURE__ */ new Map();
|
|
198
|
+
// Where the daemon listens: the window reaches it with this.
|
|
199
|
+
port;
|
|
181
200
|
instance;
|
|
182
201
|
ready;
|
|
183
202
|
resolveReady;
|
|
@@ -218,6 +237,8 @@ var MachineConnection = class {
|
|
|
218
237
|
this.rejectReady(new Error("Connection cancelled"));
|
|
219
238
|
for (const terminal of this.terminals.values()) terminal.release();
|
|
220
239
|
this.terminals.clear();
|
|
240
|
+
for (const process2 of this.processes.values()) process2.release();
|
|
241
|
+
this.processes.clear();
|
|
221
242
|
this.state.status = "disconnected";
|
|
222
243
|
this.state.error = void 0;
|
|
223
244
|
this.publish();
|
|
@@ -261,6 +282,26 @@ var MachineConnection = class {
|
|
|
261
282
|
exec: async (program, args, options) => {
|
|
262
283
|
const result = await this.request("exec", { program, args, ...options }, 2e4);
|
|
263
284
|
return { stdout: Buffer.from(result.stdout, "base64"), stderr: Buffer.from(result.stderr, "base64"), code: result.code };
|
|
285
|
+
},
|
|
286
|
+
spawn: (program, args, options) => this.request("spawn", { program, args, ...options }),
|
|
287
|
+
attach: async (id) => {
|
|
288
|
+
const existing = this.processes.get(id);
|
|
289
|
+
if (existing) return existing;
|
|
290
|
+
const process2 = new Process(id, (op, params) => this.request(op, params));
|
|
291
|
+
this.processes.set(id, process2);
|
|
292
|
+
try {
|
|
293
|
+
await this.request("process_attach", { session_id: id });
|
|
294
|
+
} catch (error) {
|
|
295
|
+
this.processes.delete(id);
|
|
296
|
+
process2.release();
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
return process2;
|
|
300
|
+
},
|
|
301
|
+
close: async (id) => {
|
|
302
|
+
await this.request("process_close", { session_id: id });
|
|
303
|
+
this.processes.get(id)?.release();
|
|
304
|
+
this.processes.delete(id);
|
|
264
305
|
}
|
|
265
306
|
};
|
|
266
307
|
cleanup() {
|
|
@@ -300,12 +341,12 @@ var MachineConnection = class {
|
|
|
300
341
|
let info;
|
|
301
342
|
let port;
|
|
302
343
|
if (this.options.type === "local") {
|
|
303
|
-
const { stdout } = await
|
|
344
|
+
const { stdout } = await exec(this.options.binary ?? "wangcai", ["server", "start", "--json"], { timeout: 15e3 });
|
|
304
345
|
info = JSON.parse(stdout);
|
|
305
346
|
port = info.port;
|
|
306
347
|
} else {
|
|
307
|
-
if (this.options.agent) await
|
|
308
|
-
const { stdout } = await
|
|
348
|
+
if (this.options.agent) await this.options.agent(this.options.host, this.options.signal);
|
|
349
|
+
const { stdout } = await exec("ssh", [
|
|
309
350
|
"-T",
|
|
310
351
|
"-o",
|
|
311
352
|
"BatchMode=yes",
|
|
@@ -319,6 +360,7 @@ var MachineConnection = class {
|
|
|
319
360
|
if (!current()) return;
|
|
320
361
|
port = await this.forward(info.port, current);
|
|
321
362
|
}
|
|
363
|
+
this.port = port;
|
|
322
364
|
if (!current()) return;
|
|
323
365
|
const socket = new import_ws.default(`ws://127.0.0.1:${port}`, { handshakeTimeout: 5e3, maxPayload: 64 * 1024 * 1024 });
|
|
324
366
|
this.ws = socket;
|
|
@@ -328,9 +370,12 @@ var MachineConnection = class {
|
|
|
328
370
|
if (binary) {
|
|
329
371
|
const bytes = data;
|
|
330
372
|
const length = bytes.readUInt32BE(0);
|
|
331
|
-
if (length > bytes.length - 4) throw new Error("Invalid
|
|
373
|
+
if (length > bytes.length - 4) throw new Error("Invalid packet");
|
|
332
374
|
const header = JSON.parse(bytes.subarray(4, 4 + length).toString());
|
|
333
|
-
|
|
375
|
+
const event = { ...header, data: bytes.subarray(4 + length) };
|
|
376
|
+
const terminal = this.terminals.get(header.session_id);
|
|
377
|
+
if (terminal) terminal.receive(event);
|
|
378
|
+
else this.processes.get(header.session_id)?.receive(event);
|
|
334
379
|
} else {
|
|
335
380
|
const message = JSON.parse(data.toString());
|
|
336
381
|
if (message.id) {
|
|
@@ -369,6 +414,8 @@ var MachineConnection = class {
|
|
|
369
414
|
if (this.instance && this.instance !== actual.instance_id) {
|
|
370
415
|
for (const terminal of this.terminals.values()) terminal.release(new Error("Node restarted; terminal no longer exists"));
|
|
371
416
|
this.terminals.clear();
|
|
417
|
+
for (const process2 of this.processes.values()) process2.release(new Error("Node restarted; process no longer exists"));
|
|
418
|
+
this.processes.clear();
|
|
372
419
|
}
|
|
373
420
|
this.instance = actual.instance_id;
|
|
374
421
|
for (const [id, terminal] of this.terminals) {
|
|
@@ -380,6 +427,15 @@ var MachineConnection = class {
|
|
|
380
427
|
this.terminals.delete(id);
|
|
381
428
|
}
|
|
382
429
|
}
|
|
430
|
+
for (const [id, process2] of this.processes) {
|
|
431
|
+
try {
|
|
432
|
+
await this.request("process_attach", { session_id: id });
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (!current()) return;
|
|
435
|
+
process2.release(error);
|
|
436
|
+
this.processes.delete(id);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
383
439
|
if (!current()) return;
|
|
384
440
|
this.state.status = "connected";
|
|
385
441
|
this.state.error = void 0;
|
|
@@ -415,7 +471,7 @@ var MachineConnection = class {
|
|
|
415
471
|
const port = reservation.address().port;
|
|
416
472
|
await new Promise((resolve) => reservation.close(() => resolve()));
|
|
417
473
|
if (!current()) throw new Error("Connection cancelled");
|
|
418
|
-
const tunnel = (0,
|
|
474
|
+
const tunnel = (0, import_node_child_process.spawn)("ssh", [
|
|
419
475
|
"-N",
|
|
420
476
|
"-T",
|
|
421
477
|
"-o",
|
|
@@ -521,5 +577,6 @@ async function openMachine(options) {
|
|
|
521
577
|
}
|
|
522
578
|
// Annotate the CommonJS export names for ESM import in node:
|
|
523
579
|
0 && (module.exports = {
|
|
580
|
+
agentCommand,
|
|
524
581
|
openMachine
|
|
525
582
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { MachineConnection } from './connection';
|
|
2
2
|
import type { ConnectionOptions } from './types';
|
|
3
|
-
export
|
|
3
|
+
export { agentCommand } from './agent';
|
|
4
|
+
export type { ConnectionOptions, FileClick, Machine, MachineState, Session, Size, Output, Snapshot, TerminalEvent, DirectoryEntry, ExecOptions, ExecResult, ProcessEvent, ProcessInfo, ProcessOutput, InstallAgent, Profile, Theme, WorkspaceActive, WorkspaceRow, WorkspaceMenuItem } from './types';
|
|
4
5
|
export type { MachineConnection } from './connection';
|
|
6
|
+
export type { Process } from './process';
|
|
5
7
|
export type { Pty } from './pty';
|
|
6
8
|
/** Opens one connection to a machine and waits for it to be ready. */
|
|
7
9
|
export declare function openMachine(options: ConnectionOptions): Promise<MachineConnection>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ProcessEvent, ProcessOutput } from './types';
|
|
2
|
+
export declare class Process {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
private request;
|
|
5
|
+
private sequence;
|
|
6
|
+
private error?;
|
|
7
|
+
private ended;
|
|
8
|
+
private code?;
|
|
9
|
+
private buffered;
|
|
10
|
+
private outputs;
|
|
11
|
+
private exits;
|
|
12
|
+
constructor(id: string, request: (op: string, params: Record<string, unknown>) => Promise<unknown>);
|
|
13
|
+
receive(event: ProcessEvent): void;
|
|
14
|
+
onData(callback: (event: ProcessOutput) => void): () => void;
|
|
15
|
+
/** Its exit code, or nothing when the node lost the stream and the process may still be running. */
|
|
16
|
+
onExit(callback: (code?: number) => void): () => void;
|
|
17
|
+
write(data: Uint8Array): Promise<void>;
|
|
18
|
+
release(error?: Error): void;
|
|
19
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
prefixes: string[];
|
|
4
|
-
}
|
|
1
|
+
/** What puts the agent on an ssh machine, which the SDK calls before it connects to it. */
|
|
2
|
+
export type InstallAgent = (host: string, signal?: AbortSignal) => Promise<void>;
|
|
5
3
|
/**
|
|
6
4
|
* The workspace protocol. A plugin that init.ts lists under `workspaces` is a workspace provider, and
|
|
7
5
|
* the app drives it through these `ui` methods:
|
|
8
6
|
*
|
|
9
7
|
* workspaces() -> WorkspaceRow[] one flat row per workspace, in draw order
|
|
10
8
|
* workspace-menu() -> WorkspaceMenuItem[] what the menu offers to open; an entry that carries
|
|
11
|
-
* an error stays listed
|
|
9
|
+
* an error stays listed with its reason, and a click retries it
|
|
12
10
|
* workspace-create({ key }) -> { id: string } the id of the workspace it made
|
|
13
11
|
* workspace-select({ id })
|
|
14
12
|
* workspace-close({ id })
|
|
@@ -58,7 +56,7 @@ export type ConnectionOptions = {
|
|
|
58
56
|
} | {
|
|
59
57
|
type: 'ssh';
|
|
60
58
|
host: string;
|
|
61
|
-
agent?:
|
|
59
|
+
agent?: InstallAgent;
|
|
62
60
|
signal?: AbortSignal;
|
|
63
61
|
};
|
|
64
62
|
export interface Size {
|
|
@@ -90,6 +88,26 @@ export type TerminalEvent = ({
|
|
|
90
88
|
} & Output) & {
|
|
91
89
|
session_id: string;
|
|
92
90
|
};
|
|
91
|
+
export interface ProcessInfo {
|
|
92
|
+
id: string;
|
|
93
|
+
pid: number;
|
|
94
|
+
exit_code: number | null;
|
|
95
|
+
}
|
|
96
|
+
/** `stream` names the pipe the bytes came from. */
|
|
97
|
+
export interface ProcessOutput extends Output {
|
|
98
|
+
stream: 'stdout' | 'stderr';
|
|
99
|
+
}
|
|
100
|
+
export type ProcessEvent = ({
|
|
101
|
+
event: 'stdout' | 'stderr';
|
|
102
|
+
} & Output | {
|
|
103
|
+
event: 'exit';
|
|
104
|
+
seq: number;
|
|
105
|
+
code: number;
|
|
106
|
+
} | {
|
|
107
|
+
event: 'lost';
|
|
108
|
+
}) & {
|
|
109
|
+
session_id: string;
|
|
110
|
+
};
|
|
93
111
|
export interface DirectoryEntry {
|
|
94
112
|
name: string;
|
|
95
113
|
isDirectory: boolean;
|