@lengmoxxl/sdk 0.1.4 → 0.1.6

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 CHANGED
@@ -1,54 +1,155 @@
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
- A WangCai plugin compiles against this package for the types it uses; the machines belong to the app, which keeps
6
- one connection per machine and hands it to every plugin that asks for one.
3
+ 本参考对应 SDK `0.1.5`:连接本机或 SSH 机器上的旺财后台进程,提供终端、文件与子进程
4
+ 操作。插件入口与生命周期见 [插件开发手册](../docs/plugins.md),进程关系见
5
+ [架构说明](../docs/architecture.md)。
7
6
 
8
- ## Install
7
+ ## 插件上下文
9
8
 
10
- ```
11
- npm install @lengmoxxl/sdk
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?, env? }`,可带 `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
- `ws` is the only runtime dependency. The bundled `dist/index.cjs` reads it at require time.
21
+ 请求与事件传可结构化克隆的数据,不传函数、DOM、Pty 或连接实例。
15
22
 
16
- ## Use
23
+ `ui.handle` 注册的方法名不能含 `:`;`ui.publish` 与 `ui.subscribe` 的主题由宿主加插件 id 前缀,
24
+ `global` 用原始主题,自定义主题要自己加命名空间(如 `directory-demo:opened`)。
17
25
 
18
- ```ts
19
- import { openMachine } from '@lengmoxxl/sdk';
26
+ `connect({})` 连接本机,`connect({ host })` 连接 SSH;`env` 是那台机器上节点启动时的环境。
20
27
 
21
- const connection = await openMachine({ type: 'local', binary: '/path/to/wangcai' });
28
+ 宿主为相同机器复用连接并计数,每次取得连接对应一次 `disconnect()`。
22
29
 
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');
30
+ `Dispose = () => void | Promise<void>`,卸载时清理资源,不要关闭用户仍要保留的 PTY。
28
31
 
29
- const { stdout } = await connection.subprocess.exec('git', ['status'], { cwd: '/repo' });
30
- connection.disconnect();
31
- ```
32
+ `Bus`:`publish(topic, data): Promise<void>`、`subscribe<T>(topic, callback): () => void`。
32
33
 
33
- `openMachine` returns a `MachineConnection`; every call opens its own. Sharing one belongs to the caller: a
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 })` calls `agent(host)` before it connects: putting
36
- the carrier agent on the host belongs to the app that embeds the SDK.
34
+ `host.dataDirectory` 默认 `~/.local/share/wangcai/data/<plugin-id>`。
37
35
 
38
- - `connection.pty` - `list`, `cwd`, `create`, `attach`, `close`
39
- - `connection.fs` - `stat`, `readDirectory`, `readFile`
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
- The host-side context a plugin is activated with (`MainContext`, `UiContext`, `Bus`, and the workspace protocol
44
- types) lives in the `@lengmoxxl/sdk/channel` entry. That entry is types only.
39
+ ## 标签页
45
40
 
46
- ## Build
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-rename({ id, name })`(名字为空表示交回 provider 自己跟随的名字)、
69
+ `workspace-move({ id, before? })`(`before` 是目标工作区 id,不是索引;省略为末尾,且只
70
+ 允许同一 provider 内拖动)。
71
+
72
+ provider 要保证工作区 id 在所有 provider 的行中唯一识别;`error` 由 provider 给出并在重试成功后清除,
73
+ 宿主不禁用带 `error` 的菜单项。
74
+
75
+ 约定的公共主题:`workspaces`(行变化,App 重新请求列表)、`workspace:active`
76
+ (`WorkspaceActive | null`,provider 发布)、`workspace:query`(视图请求 provider 再发布
77
+ 当前工作区)、`onclick`(`FileClick`)。
78
+
79
+ 字段类型见 SDK 导出的 `WorkspaceRow`、`WorkspaceMenuItem`、`Machine`、`WorkspaceActive`
80
+ 与 `FileClick`,定义在 `sdk/types.ts`。
81
+
82
+ ## 机器、终端与子进程
83
+
84
+ `MachineConnection`:`ready`(首次连接就绪)、`state`、`port`(本地 loopback 端口,SSH 时
85
+ 为转发端口)、`onState(callback)`、`retain()`(连接层使用)、`disconnect()`,以及 `pty`、`fs`、
86
+ `subprocess`。
87
+
88
+ `MachineState` 含 `status`、`error?`、`sessions`、`generation`。
89
+
90
+ 断线会自动重连并重新 attach 已挂载的终端与进程,失败的请求与输入不自动重放,后台进程重启时旧 Pty 与进程失效。
91
+
92
+ `connection.pty`:`list()`、`cwd(id)`、`create(size?, cwd?)`(默认 24 行 80 列;cwd 用绝对目录,
93
+ 省略时在后台进程的 HOME 下启动)、`attach(id)`(复用句柄)、`close(id)`(结束 shell 与后台子孙进程)。
94
+
95
+ `Size` 用整数:rows 2–300,cols 2–500。
96
+
97
+ `Session` 有 `id, title`、`pid, rows, cols`、`exit_code: number | null`。
98
+
99
+ `Pty`:`id`、`onSnapshot(callback)`、`onData(callback)`、`write(data)`、`resize(size)`、
100
+ `detach()`(停止挂载,不关闭 shell)。
101
+
102
+ `Output = { data: Uint8Array; seq: number }`,`Snapshot` 额外有 `rows, cols`。
103
+
104
+ 字节含终端控制序列,交给终端模拟器,不要把快照当键盘输入重发。
105
+
106
+ `receive` 与 `release(error?)` 由连接层使用。
107
+
108
+ `connection.fs`:`stat(path)` 返回 `{ isDirectory: boolean } | null`、`readDirectory(path)` 返回
109
+ `DirectoryEntry[]`(`{ name, isDirectory }`)、`readFile(path)` 返回原始字节。
110
+
111
+ 路径为绝对路径,`readFile` 上限 16 MiB;SDK 不提供写入、删除或上传接口。
112
+
113
+ `connection.subprocess.exec(program, args, options)`:`args` 是独立 argv(不经 shell 展开),
114
+ `options.cwd` 是绝对工作目录,`options.env` 覆盖继承的环境变量;返回
115
+ `{ stdout: Uint8Array; stderr: Uint8Array; code: number }`。非零退出码仍返回结果,无法启动、
116
+ 超时、输出超限或被信号终止时 reject;stdin 关闭,长时间或交互用 `subprocess.spawn` 或 PTY。
117
+
118
+ 执行上限 15 秒,SDK 等待响应上限 20 秒,stdout 与 stderr 各最多 8 MiB,断线取消未完成的 exec。
119
+
120
+ `connection.subprocess.spawn(program, args, options)`(`options` 同 `exec`)启动一个
121
+ 长期存活的进程,stdin、stdout、stderr 都是管道,返回 `ProcessInfo = { id, pid, exit_code }`;
122
+ `attach(id)` 复用句柄(未 attach 前不接收输出),`close(id)` 结束进程及其进程组;进程归后台进程所有,不 `close` 就一直留着。
123
+
124
+ `Process`:`id`、`onData(callback)`、`onExit(callback)`、`write(data)`。
125
+
126
+ `ProcessOutput = { data: Uint8Array; seq: number; stream: 'stdout' | 'stderr' }`,两路输出各自按序到达,
127
+ `seq` 单调递增,跨两路的先后以 `seq` 为准。
128
+
129
+ `onExit` 给出退出码(被信号结束时按 shell 的 128+signal),节点来不及投递时给出 `undefined`,此时
130
+ 输出流已不可信,进程可能仍在运行,请重新 `spawn`。
131
+
132
+ `write` 按 32 KiB 分片、按序应用,进程退出后 reject;先 `attach` 再写。
133
+
134
+ `receive` 与 `release(error?)` 由连接层使用。
135
+
136
+ ## 独立 Node 客户端
137
+
138
+ App 插件用 `context.connect`;插件之外才调用 `openMachine`:
139
+
140
+ ```ts
141
+ import { openMachine } from '@lengmoxxl/sdk';
142
+
143
+ const connection = await openMachine({ type: 'local', binary: '/path/to/wangcai' });
144
+ await connection.fs.readDirectory('/');
145
+ connection.disconnect();
48
146
  ```
49
- npm run build
50
- ```
51
147
 
52
- From the repository root (the TypeScript and esbuild devDependencies live in the workspace root): esbuild bundles
53
- `index.ts` into `dist/index.cjs` with `ws` left external, then `tsc` emits the declarations. `npm publish` and
54
- `npm pack` run it for you.
148
+ `openMachine` 返回 `Promise<MachineConnection>`。`type` 为 `'local'` 或 `'ssh'`;local 可给
149
+ `binary`,ssh 必须给 `host`(系统 ssh 目标或别名)与可选 `agent`(`InstallAgent`),两者
150
+ 都可给 `env`(节点启动时带上的环境,值里的 `$NAME` 用运行节点那台机器的环境展开)与 `signal`。
151
+ 不传 `agent` 时 SDK 不下载二进制;连接用 BatchMode SSH。
152
+
153
+ `agentCommand(command, env)` 返回拼接后的 shell 命令:在 PATH 前加入远端 `$HOME/.local/bin`,
154
+ 再带上当前进程的 WANGCAI_HOME 与配置要求的环境;值里的 `$NAME` 留给远端 shell 展开。命令本身
155
+ 不转义,只传可信内容。
package/dist/agent.d.ts CHANGED
@@ -1 +1,2 @@
1
- export declare function agentCommand(command: string): string;
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;
@@ -1,9 +1,11 @@
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;
7
9
  port?: number;
8
10
  private instance?;
9
11
  readonly ready: Promise<void>;
@@ -42,6 +44,9 @@ export declare class MachineConnection {
42
44
  };
43
45
  subprocess: {
44
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>;
45
50
  };
46
51
  private cleanup;
47
52
  private fail;
package/dist/index.cjs CHANGED
@@ -41,6 +41,68 @@ var import_node_net = require("node:net");
41
41
  var import_node_util = require("node:util");
42
42
  var import_ws = __toESM(require("ws"));
43
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
+
44
106
  // pty.ts
45
107
  var Pty = class {
46
108
  constructor(id, request, remove) {
@@ -112,14 +174,25 @@ var Pty = class {
112
174
  };
113
175
 
114
176
  // agent.ts
115
- function agentCommand(command) {
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 = {}) {
116
182
  const home = process.env.WANGCAI_HOME;
117
183
  const exportHome = home ? `export WANGCAI_HOME='${home.replace(/'/g, String.raw`'\''`)}'; ` : "";
118
- return `${exportHome}export PATH="$HOME/.local/bin:$PATH"; ${command}`;
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}`;
119
186
  }
120
187
 
121
188
  // connection.ts
122
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
+ }
123
196
  var MachineConnection = class {
124
197
  constructor(options) {
125
198
  this.options = options;
@@ -132,6 +205,7 @@ var MachineConnection = class {
132
205
  }
133
206
  listeners = /* @__PURE__ */ new Set();
134
207
  terminals = /* @__PURE__ */ new Map();
208
+ processes = /* @__PURE__ */ new Map();
135
209
  // Where the daemon listens: the window reaches it with this.
136
210
  port;
137
211
  instance;
@@ -174,6 +248,8 @@ var MachineConnection = class {
174
248
  this.rejectReady(new Error("Connection cancelled"));
175
249
  for (const terminal of this.terminals.values()) terminal.release();
176
250
  this.terminals.clear();
251
+ for (const process2 of this.processes.values()) process2.release();
252
+ this.processes.clear();
177
253
  this.state.status = "disconnected";
178
254
  this.state.error = void 0;
179
255
  this.publish();
@@ -217,6 +293,26 @@ var MachineConnection = class {
217
293
  exec: async (program, args, options) => {
218
294
  const result = await this.request("exec", { program, args, ...options }, 2e4);
219
295
  return { stdout: Buffer.from(result.stdout, "base64"), stderr: Buffer.from(result.stderr, "base64"), code: result.code };
296
+ },
297
+ spawn: (program, args, options) => this.request("spawn", { program, args, ...options }),
298
+ attach: async (id) => {
299
+ const existing = this.processes.get(id);
300
+ if (existing) return existing;
301
+ const process2 = new Process(id, (op, params) => this.request(op, params));
302
+ this.processes.set(id, process2);
303
+ try {
304
+ await this.request("process_attach", { session_id: id });
305
+ } catch (error) {
306
+ this.processes.delete(id);
307
+ process2.release();
308
+ throw error;
309
+ }
310
+ return process2;
311
+ },
312
+ close: async (id) => {
313
+ await this.request("process_close", { session_id: id });
314
+ this.processes.get(id)?.release();
315
+ this.processes.delete(id);
220
316
  }
221
317
  };
222
318
  cleanup() {
@@ -255,8 +351,12 @@ var MachineConnection = class {
255
351
  try {
256
352
  let info;
257
353
  let port;
354
+ const env = exportableEnvironment(this.options.env);
258
355
  if (this.options.type === "local") {
259
- const { stdout } = await exec(this.options.binary ?? "wangcai", ["server", "start", "--json"], { timeout: 15e3 });
356
+ const { stdout } = await exec(this.options.binary ?? "wangcai", ["server", "start", "--json"], {
357
+ timeout: 15e3,
358
+ env: { ...process.env, ...expandedEnvironment(env) }
359
+ });
260
360
  info = JSON.parse(stdout);
261
361
  port = info.port;
262
362
  } else {
@@ -268,7 +368,7 @@ var MachineConnection = class {
268
368
  "-o",
269
369
  "ConnectTimeout=8",
270
370
  this.options.host,
271
- agentCommand("wangcai server start --json")
371
+ agentCommand("wangcai server start --json", env)
272
372
  ], { timeout: 15e3, maxBuffer: 64 * 1024 });
273
373
  info = JSON.parse(stdout);
274
374
  if (!Number.isInteger(info.port) || info.port < 1 || info.port > 65535) throw new Error("Node returned an invalid port.");
@@ -285,9 +385,12 @@ var MachineConnection = class {
285
385
  if (binary) {
286
386
  const bytes = data;
287
387
  const length = bytes.readUInt32BE(0);
288
- if (length > bytes.length - 4) throw new Error("Invalid terminal packet");
388
+ if (length > bytes.length - 4) throw new Error("Invalid packet");
289
389
  const header = JSON.parse(bytes.subarray(4, 4 + length).toString());
290
- this.terminals.get(header.session_id)?.receive({ ...header, data: bytes.subarray(4 + length) });
390
+ const event = { ...header, data: bytes.subarray(4 + length) };
391
+ const terminal = this.terminals.get(header.session_id);
392
+ if (terminal) terminal.receive(event);
393
+ else this.processes.get(header.session_id)?.receive(event);
291
394
  } else {
292
395
  const message = JSON.parse(data.toString());
293
396
  if (message.id) {
@@ -326,6 +429,8 @@ var MachineConnection = class {
326
429
  if (this.instance && this.instance !== actual.instance_id) {
327
430
  for (const terminal of this.terminals.values()) terminal.release(new Error("Node restarted; terminal no longer exists"));
328
431
  this.terminals.clear();
432
+ for (const process2 of this.processes.values()) process2.release(new Error("Node restarted; process no longer exists"));
433
+ this.processes.clear();
329
434
  }
330
435
  this.instance = actual.instance_id;
331
436
  for (const [id, terminal] of this.terminals) {
@@ -337,6 +442,15 @@ var MachineConnection = class {
337
442
  this.terminals.delete(id);
338
443
  }
339
444
  }
445
+ for (const [id, process2] of this.processes) {
446
+ try {
447
+ await this.request("process_attach", { session_id: id });
448
+ } catch (error) {
449
+ if (!current()) return;
450
+ process2.release(error);
451
+ this.processes.delete(id);
452
+ }
453
+ }
340
454
  if (!current()) return;
341
455
  this.state.status = "connected";
342
456
  this.state.error = void 0;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
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, InstallAgent, Profile, Theme, WorkspaceActive, WorkspaceRow, WorkspaceMenuItem } from './types';
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';
5
5
  export type { MachineConnection } from './connection';
6
+ export type { Process } from './process';
6
7
  export type { Pty } from './pty';
7
8
  /** Opens one connection to a machine and waits for it to be ready. */
8
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
@@ -6,10 +6,11 @@ export type InstallAgent = (host: string, signal?: AbortSignal) => Promise<void>
6
6
  *
7
7
  * workspaces() -> WorkspaceRow[] one flat row per workspace, in draw order
8
8
  * workspace-menu() -> WorkspaceMenuItem[] what the menu offers to open; an entry that carries
9
- * an error stays listed but is not clickable
9
+ * an error stays listed with its reason, and a click retries it
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 {
@@ -88,6 +92,26 @@ export type TerminalEvent = ({
88
92
  } & Output) & {
89
93
  session_id: string;
90
94
  };
95
+ export interface ProcessInfo {
96
+ id: string;
97
+ pid: number;
98
+ exit_code: number | null;
99
+ }
100
+ /** `stream` names the pipe the bytes came from. */
101
+ export interface ProcessOutput extends Output {
102
+ stream: 'stdout' | 'stderr';
103
+ }
104
+ export type ProcessEvent = ({
105
+ event: 'stdout' | 'stderr';
106
+ } & Output | {
107
+ event: 'exit';
108
+ seq: number;
109
+ code: number;
110
+ } | {
111
+ event: 'lost';
112
+ }) & {
113
+ session_id: string;
114
+ };
91
115
  export interface DirectoryEntry {
92
116
  name: string;
93
117
  isDirectory: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lengmoxxl/sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Client SDK for WangCai: connect to a local or remote machine and drive its pty, files and git",
5
5
  "license": "MIT",
6
6
  "repository": {