@lengmoxxl/sdk 0.1.5 → 0.1.7
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 +19 -10
- package/dist/agent.d.ts +2 -1
- package/dist/channel.d.ts +2 -3
- package/dist/connection.d.ts +3 -1
- package/dist/index.cjs +47 -4
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# @lengmoxxl/sdk:接口参考
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[架构说明](../docs/architecture.md)。
|
|
3
|
+
SDK 连接本机或 SSH 机器上的旺财后台进程,提供终端、文件与子进程操作。插件入口与生命周期见
|
|
4
|
+
[插件开发手册](../docs/plugins.md),进程关系见 [架构说明](../docs/architecture.md)。
|
|
6
5
|
|
|
7
6
|
## 插件上下文
|
|
8
7
|
|
|
@@ -13,7 +12,7 @@
|
|
|
13
12
|
| `ui.publish(topic, data)` | 后端向本插件 UI 发事件,对应前端 `ui.subscribe` |
|
|
14
13
|
| `ui.subscribe<T>(topic, callback)` | 前端订阅本插件后端事件,返回取消订阅的函数 |
|
|
15
14
|
| `ui.request<T>(method, params?)` | 前端调用本插件后端方法,返回 `Promise<T>` |
|
|
16
|
-
| `connect(machine, signal?)` | 后端借用机器连接:`{ host
|
|
15
|
+
| `connect(machine, signal?)` | 后端借用机器连接:`{ host?, env? }`,可带 `AbortSignal` |
|
|
17
16
|
| `host.dataDirectory` / `host.config` | 插件私有数据目录 / 本插件设置 |
|
|
18
17
|
| `host.tabs(options)` / `host.open(url)` | 前端开侧栏标签页 / 用系统浏览器打开 http、https |
|
|
19
18
|
| `host.preview` | `{ url, message }`,HTML 预览 frame 的地址与握手消息 |
|
|
@@ -23,7 +22,7 @@
|
|
|
23
22
|
`ui.handle` 注册的方法名不能含 `:`;`ui.publish` 与 `ui.subscribe` 的主题由宿主加插件 id 前缀,
|
|
24
23
|
`global` 用原始主题,自定义主题要自己加命名空间(如 `directory-demo:opened`)。
|
|
25
24
|
|
|
26
|
-
`connect({})` 连接本机,`connect({ host })` 连接 SSH
|
|
25
|
+
`connect({})` 连接本机,`connect({ host })` 连接 SSH;`env` 是那台机器上节点启动时的环境。
|
|
27
26
|
|
|
28
27
|
宿主为相同机器复用连接并计数,每次取得连接对应一次 `disconnect()`。
|
|
29
28
|
|
|
@@ -65,6 +64,7 @@
|
|
|
65
64
|
放入 init.ts `workspaces` 的插件用 `ui.handle` 提供:`workspaces()`(`WorkspaceRow[]`,
|
|
66
65
|
按显示顺序)、`workspace-menu()`(`WorkspaceMenuItem[]`)、`workspace-create({ key })`
|
|
67
66
|
(返回 `{ id }`)、`workspace-select({ id })`、`workspace-close({ id })`、
|
|
67
|
+
`workspace-rename({ id, name })`(名字为空表示交回 provider 自己跟随的名字)、
|
|
68
68
|
`workspace-move({ id, before? })`(`before` 是目标工作区 id,不是索引;省略为末尾,且只
|
|
69
69
|
允许同一 provider 内拖动)。
|
|
70
70
|
|
|
@@ -86,7 +86,8 @@ provider 要保证工作区 id 在所有 provider 的行中唯一识别;`error
|
|
|
86
86
|
|
|
87
87
|
`MachineState` 含 `status`、`error?`、`sessions`、`generation`。
|
|
88
88
|
|
|
89
|
-
断线会自动重连并重新 attach
|
|
89
|
+
断线会自动重连并重新 attach 已挂载的终端、进程与文件订阅,失败的请求与输入不自动重放,后台进程重启时
|
|
90
|
+
旧 Pty 与进程失效。
|
|
90
91
|
|
|
91
92
|
`connection.pty`:`list()`、`cwd(id)`、`create(size?, cwd?)`(默认 24 行 80 列;cwd 用绝对目录,
|
|
92
93
|
省略时在后台进程的 HOME 下启动)、`attach(id)`(复用句柄)、`close(id)`(结束 shell 与后台子孙进程)。
|
|
@@ -109,6 +110,12 @@ provider 要保证工作区 id 在所有 provider 的行中唯一识别;`error
|
|
|
109
110
|
|
|
110
111
|
路径为绝对路径,`readFile` 上限 16 MiB;SDK 不提供写入、删除或上传接口。
|
|
111
112
|
|
|
113
|
+
`connection.fs.watch(path, options, callback)` 订阅该路径的变化,`options.recursive` 决定是否含子目录,
|
|
114
|
+
返回的 `() => void` 关闭订阅。回调收到的是 `FileChanges = { created, modified, removed }`,一个安静窗口
|
|
115
|
+
(约 200ms 没新事件,最长 2 秒)一批,路径按订阅时给的根路径书写;连接彻底关闭(最后一个持有者
|
|
116
|
+
`disconnect()`)时一并结束。macOS 走 FSEvents,创建与删除多数时候报成 `modified`;Linux 走 inotify,
|
|
117
|
+
三类齐全。
|
|
118
|
+
|
|
112
119
|
`connection.subprocess.exec(program, args, options)`:`args` 是独立 argv(不经 shell 展开),
|
|
113
120
|
`options.cwd` 是绝对工作目录,`options.env` 覆盖继承的环境变量;返回
|
|
114
121
|
`{ stdout: Uint8Array; stderr: Uint8Array; code: number }`。非零退出码仍返回结果,无法启动、
|
|
@@ -118,7 +125,7 @@ provider 要保证工作区 id 在所有 provider 的行中唯一识别;`error
|
|
|
118
125
|
|
|
119
126
|
`connection.subprocess.spawn(program, args, options)`(`options` 同 `exec`)启动一个
|
|
120
127
|
长期存活的进程,stdin、stdout、stderr 都是管道,返回 `ProcessInfo = { id, pid, exit_code }`;
|
|
121
|
-
`attach(id)` 复用句柄(未 attach 前不接收输出),`close(id)`
|
|
128
|
+
`attach(id)` 复用句柄(未 attach 前不接收输出),`close(id)` 结束进程及其进程组;进程归后台进程所有,除 `close` 外,只有起它或最后 `attach` 它的那条连接断开后 60 秒内没有连接再 `attach`,节点才会自己收回。
|
|
122
129
|
|
|
123
130
|
`Process`:`id`、`onData(callback)`、`onExit(callback)`、`write(data)`。
|
|
124
131
|
|
|
@@ -146,7 +153,9 @@ connection.disconnect();
|
|
|
146
153
|
|
|
147
154
|
`openMachine` 返回 `Promise<MachineConnection>`。`type` 为 `'local'` 或 `'ssh'`;local 可给
|
|
148
155
|
`binary`,ssh 必须给 `host`(系统 ssh 目标或别名)与可选 `agent`(`InstallAgent`),两者
|
|
149
|
-
都可给 `
|
|
156
|
+
都可给 `env`(节点启动时带上的环境,值里的 `$NAME` 用运行节点那台机器的环境展开)与 `signal`。
|
|
157
|
+
不传 `agent` 时 SDK 不下载二进制;连接用 BatchMode SSH。
|
|
150
158
|
|
|
151
|
-
`agentCommand(command)` 返回拼接后的 shell 命令:在 PATH 前加入远端 `$HOME/.local/bin`,
|
|
152
|
-
|
|
159
|
+
`agentCommand(command, env)` 返回拼接后的 shell 命令:在 PATH 前加入远端 `$HOME/.local/bin`,
|
|
160
|
+
再带上当前进程的 WANGCAI_HOME 与配置要求的环境;值里的 `$NAME` 留给远端 shell 展开。命令本身
|
|
161
|
+
不转义,只传可信内容。
|
package/dist/agent.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export declare function
|
|
1
|
+
export declare function exportableEnvironment(env?: Record<string, string>): Record<string, string>;
|
|
2
|
+
export declare function agentCommand(command: string, env?: Record<string, string>): string;
|
package/dist/channel.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MachineConnection } from './connection';
|
|
2
|
+
import type { Machine } from './types';
|
|
2
3
|
export type Dispose = () => void | Promise<void>;
|
|
3
4
|
/** A sidebar tab the window opens for a plugin: what it is called, and what it draws. */
|
|
4
5
|
export interface TabOptions {
|
|
@@ -31,9 +32,7 @@ export interface MainContext {
|
|
|
31
32
|
handle<T = unknown>(method: string, handler: (params: T) => unknown): Dispose;
|
|
32
33
|
publish(topic: string, data: unknown): void;
|
|
33
34
|
};
|
|
34
|
-
connect(machine:
|
|
35
|
-
host?: string;
|
|
36
|
-
}, signal?: AbortSignal): Promise<MachineConnection>;
|
|
35
|
+
connect(machine: Pick<Machine, 'host' | 'env'>, signal?: AbortSignal): Promise<MachineConnection>;
|
|
37
36
|
host: {
|
|
38
37
|
dataDirectory: string;
|
|
39
38
|
config: any;
|
package/dist/connection.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConnectionOptions, MachineState, Session, Size, DirectoryEntry, ExecOptions, ExecResult, ProcessInfo } from './types';
|
|
1
|
+
import type { ConnectionOptions, MachineState, Session, Size, DirectoryEntry, ExecOptions, ExecResult, ProcessInfo, FileChanges, WatchOptions } from './types';
|
|
2
2
|
import { Process } from './process';
|
|
3
3
|
import { Pty } from './pty';
|
|
4
4
|
export declare class MachineConnection {
|
|
@@ -6,6 +6,7 @@ export declare class MachineConnection {
|
|
|
6
6
|
private listeners;
|
|
7
7
|
private terminals;
|
|
8
8
|
private processes;
|
|
9
|
+
private watchers;
|
|
9
10
|
port?: number;
|
|
10
11
|
private instance?;
|
|
11
12
|
readonly ready: Promise<void>;
|
|
@@ -41,6 +42,7 @@ export declare class MachineConnection {
|
|
|
41
42
|
} | null>;
|
|
42
43
|
readDirectory: (path: string) => Promise<DirectoryEntry[]>;
|
|
43
44
|
readFile: (path: string) => Promise<Uint8Array>;
|
|
45
|
+
watch: (path: string, options: WatchOptions, callback: (changes: FileChanges) => void) => Promise<() => void>;
|
|
44
46
|
};
|
|
45
47
|
subprocess: {
|
|
46
48
|
exec: (program: string, args: string[], options: ExecOptions) => Promise<ExecResult>;
|
package/dist/index.cjs
CHANGED
|
@@ -174,14 +174,25 @@ var Pty = class {
|
|
|
174
174
|
};
|
|
175
175
|
|
|
176
176
|
// agent.ts
|
|
177
|
-
|
|
177
|
+
var quoted = (value) => value.replace(/[\\`"]/g, "\\$&");
|
|
178
|
+
function exportableEnvironment(env = {}) {
|
|
179
|
+
return Object.fromEntries(Object.entries(env).filter(([name]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)));
|
|
180
|
+
}
|
|
181
|
+
function agentCommand(command, env = {}) {
|
|
178
182
|
const home = process.env.WANGCAI_HOME;
|
|
179
183
|
const exportHome = home ? `export WANGCAI_HOME='${home.replace(/'/g, String.raw`'\''`)}'; ` : "";
|
|
180
|
-
|
|
184
|
+
const exports2 = Object.entries(exportableEnvironment(env)).map(([name, value]) => `export ${name}="${quoted(value)}"; `).join("");
|
|
185
|
+
return `${exportHome}export PATH="$HOME/.local/bin:$PATH"; ${exports2}${command}`;
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
// connection.ts
|
|
184
189
|
var exec = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
190
|
+
function expandedEnvironment(env) {
|
|
191
|
+
return Object.fromEntries(Object.entries(env).map(([name, value]) => [
|
|
192
|
+
name,
|
|
193
|
+
value.replace(/\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g, (_, variable) => process.env[variable] ?? "")
|
|
194
|
+
]));
|
|
195
|
+
}
|
|
185
196
|
var MachineConnection = class {
|
|
186
197
|
constructor(options) {
|
|
187
198
|
this.options = options;
|
|
@@ -195,6 +206,7 @@ var MachineConnection = class {
|
|
|
195
206
|
listeners = /* @__PURE__ */ new Set();
|
|
196
207
|
terminals = /* @__PURE__ */ new Map();
|
|
197
208
|
processes = /* @__PURE__ */ new Map();
|
|
209
|
+
watchers = /* @__PURE__ */ new Map();
|
|
198
210
|
// Where the daemon listens: the window reaches it with this.
|
|
199
211
|
port;
|
|
200
212
|
instance;
|
|
@@ -239,6 +251,7 @@ var MachineConnection = class {
|
|
|
239
251
|
this.terminals.clear();
|
|
240
252
|
for (const process2 of this.processes.values()) process2.release();
|
|
241
253
|
this.processes.clear();
|
|
254
|
+
this.watchers.clear();
|
|
242
255
|
this.state.status = "disconnected";
|
|
243
256
|
this.state.error = void 0;
|
|
244
257
|
this.publish();
|
|
@@ -276,6 +289,22 @@ var MachineConnection = class {
|
|
|
276
289
|
readFile: async (path) => {
|
|
277
290
|
const result = await this.request("read_file", { path });
|
|
278
291
|
return Buffer.from(result.data, "base64");
|
|
292
|
+
},
|
|
293
|
+
watch: async (path, options, callback) => {
|
|
294
|
+
const watchId = `watch-${++this.serial}`;
|
|
295
|
+
const recursive = options.recursive ?? false;
|
|
296
|
+
this.watchers.set(watchId, { recursive, path, callback });
|
|
297
|
+
try {
|
|
298
|
+
await this.request("watch", { watch_id: watchId, path, recursive });
|
|
299
|
+
} catch (error) {
|
|
300
|
+
this.watchers.delete(watchId);
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
return () => {
|
|
304
|
+
this.watchers.delete(watchId);
|
|
305
|
+
void this.request("unwatch", { watch_id: watchId }).catch(() => {
|
|
306
|
+
});
|
|
307
|
+
};
|
|
279
308
|
}
|
|
280
309
|
};
|
|
281
310
|
subprocess = {
|
|
@@ -340,8 +369,12 @@ var MachineConnection = class {
|
|
|
340
369
|
try {
|
|
341
370
|
let info;
|
|
342
371
|
let port;
|
|
372
|
+
const env = exportableEnvironment(this.options.env);
|
|
343
373
|
if (this.options.type === "local") {
|
|
344
|
-
const { stdout } = await exec(this.options.binary ?? "wangcai", ["server", "start", "--json"], {
|
|
374
|
+
const { stdout } = await exec(this.options.binary ?? "wangcai", ["server", "start", "--json"], {
|
|
375
|
+
timeout: 15e3,
|
|
376
|
+
env: { ...process.env, ...expandedEnvironment(env) }
|
|
377
|
+
});
|
|
345
378
|
info = JSON.parse(stdout);
|
|
346
379
|
port = info.port;
|
|
347
380
|
} else {
|
|
@@ -353,7 +386,7 @@ var MachineConnection = class {
|
|
|
353
386
|
"-o",
|
|
354
387
|
"ConnectTimeout=8",
|
|
355
388
|
this.options.host,
|
|
356
|
-
agentCommand("wangcai server start --json")
|
|
389
|
+
agentCommand("wangcai server start --json", env)
|
|
357
390
|
], { timeout: 15e3, maxBuffer: 64 * 1024 });
|
|
358
391
|
info = JSON.parse(stdout);
|
|
359
392
|
if (!Number.isInteger(info.port) || info.port < 1 || info.port > 65535) throw new Error("Node returned an invalid port.");
|
|
@@ -388,6 +421,9 @@ var MachineConnection = class {
|
|
|
388
421
|
} else if (message.event === "sessions_changed") {
|
|
389
422
|
void this.refresh().catch(() => {
|
|
390
423
|
});
|
|
424
|
+
} else if (message.event === "fs_changed") {
|
|
425
|
+
const watcher = this.watchers.get(message.watch_id);
|
|
426
|
+
if (watcher) watcher.callback({ created: message.created, modified: message.modified, removed: message.removed });
|
|
391
427
|
}
|
|
392
428
|
}
|
|
393
429
|
} catch (error) {
|
|
@@ -436,6 +472,13 @@ var MachineConnection = class {
|
|
|
436
472
|
this.processes.delete(id);
|
|
437
473
|
}
|
|
438
474
|
}
|
|
475
|
+
for (const [watchId, watcher] of this.watchers) {
|
|
476
|
+
try {
|
|
477
|
+
await this.request("watch", { watch_id: watchId, path: watcher.path, recursive: watcher.recursive });
|
|
478
|
+
} catch {
|
|
479
|
+
if (!current()) return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
439
482
|
if (!current()) return;
|
|
440
483
|
this.state.status = "connected";
|
|
441
484
|
this.state.error = void 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MachineConnection } from './connection';
|
|
2
2
|
import type { ConnectionOptions } from './types';
|
|
3
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
|
+
export type { ConnectionOptions, FileClick, Machine, MachineState, Session, Size, Output, Snapshot, TerminalEvent, DirectoryEntry, FileChanges, WatchOptions, ExecOptions, ExecResult, ProcessEvent, ProcessInfo, ProcessOutput, InstallAgent, Profile, Theme, WorkspaceActive, WorkspaceRow, WorkspaceMenuItem } from './types';
|
|
5
5
|
export type { MachineConnection } from './connection';
|
|
6
6
|
export type { Process } from './process';
|
|
7
7
|
export type { Pty } from './pty';
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type InstallAgent = (host: string, signal?: AbortSignal) => Promise<void>
|
|
|
10
10
|
* workspace-create({ key }) -> { id: string } the id of the workspace it made
|
|
11
11
|
* workspace-select({ id })
|
|
12
12
|
* workspace-close({ id })
|
|
13
|
+
* workspace-rename({ id, name }) an empty name gives the workspace back to the name it followed
|
|
13
14
|
* workspace-move({ id, before? })
|
|
14
15
|
*
|
|
15
16
|
* Every view that follows the workspace in front shares these channel topics, whatever the provider
|
|
@@ -35,6 +36,7 @@ export interface Machine {
|
|
|
35
36
|
id: string;
|
|
36
37
|
name: string;
|
|
37
38
|
host?: string;
|
|
39
|
+
env?: Record<string, string>;
|
|
38
40
|
}
|
|
39
41
|
export interface WorkspaceActive {
|
|
40
42
|
machine: Machine;
|
|
@@ -52,11 +54,13 @@ export interface FileClick {
|
|
|
52
54
|
export type ConnectionOptions = {
|
|
53
55
|
type: 'local';
|
|
54
56
|
binary?: string;
|
|
57
|
+
env?: Record<string, string>;
|
|
55
58
|
signal?: AbortSignal;
|
|
56
59
|
} | {
|
|
57
60
|
type: 'ssh';
|
|
58
61
|
host: string;
|
|
59
62
|
agent?: InstallAgent;
|
|
63
|
+
env?: Record<string, string>;
|
|
60
64
|
signal?: AbortSignal;
|
|
61
65
|
};
|
|
62
66
|
export interface Size {
|
|
@@ -112,6 +116,14 @@ export interface DirectoryEntry {
|
|
|
112
116
|
name: string;
|
|
113
117
|
isDirectory: boolean;
|
|
114
118
|
}
|
|
119
|
+
export interface FileChanges {
|
|
120
|
+
created: string[];
|
|
121
|
+
modified: string[];
|
|
122
|
+
removed: string[];
|
|
123
|
+
}
|
|
124
|
+
export interface WatchOptions {
|
|
125
|
+
recursive?: boolean;
|
|
126
|
+
}
|
|
115
127
|
export interface ExecOptions {
|
|
116
128
|
cwd: string;
|
|
117
129
|
env?: Record<string, string>;
|