@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 CHANGED
@@ -1,8 +1,7 @@
1
1
  # @lengmoxxl/sdk:接口参考
2
2
 
3
- 本参考对应 SDK `0.1.5`:连接本机或 SSH 机器上的旺财后台进程,提供终端、文件与子进程
4
- 操作。插件入口与生命周期见 [插件开发手册](../docs/plugins.md),进程关系见
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?: string }`,可带 `AbortSignal` |
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 已挂载的终端与进程,失败的请求与输入不自动重放,后台进程重启时旧 Pty 与进程失效。
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)` 结束进程及其进程组;进程归后台进程所有,不 `close` 就一直留着。
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
- 都可给 `signal`。不传 `agent` 时 SDK 不下载二进制;连接用 BatchMode SSH。
156
+ 都可给 `env`(节点启动时带上的环境,值里的 `$NAME` 用运行节点那台机器的环境展开)与 `signal`。
157
+ 不传 `agent` 时 SDK 不下载二进制;连接用 BatchMode SSH。
150
158
 
151
- `agentCommand(command)` 返回拼接后的 shell 命令:在 PATH 前加入远端 `$HOME/.local/bin`,
152
- 并携带当前进程的 WANGCAI_HOME(会转义;command 本身不转义,只传可信命令)。
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 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,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
- 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 = {}) {
178
182
  const home = process.env.WANGCAI_HOME;
179
183
  const exportHome = home ? `export WANGCAI_HOME='${home.replace(/'/g, String.raw`'\''`)}'; ` : "";
180
- 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}`;
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"], { timeout: 15e3 });
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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lengmoxxl/sdk",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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": {