Kunkun
进阶

CLI + 扩展

将同一代码库同时作为 npx CLI 和 Kunkun 扩展——共享核心模式,附带两个真实案例。

最强大的 Kunkun 扩展——终端、磁盘扫描器——在启动器之外也很有用,可以作为独立的 npx 工具。Kunkun 的可移植核心架构意味着你无需二选一:同一个引擎可以通过 CLI 主机 Kunkun 扩展主机从同一代码库驱动。

本页介绍这种模式及两个真实案例(kkterminalspace-lens)——它们都作为 git 子模块存放在 extensions/ 中,其提交历史展示了构建的确切顺序。

是否应该双目标?

从简开始。大多数扩展不应这样做。

双目标会增加单体仓库、传输边界和第二个主机维护工作。仅当工具在独立运行时有真正价值有值得复用的非平凡引擎时才考虑。

扩展时…双目标时…
UI 就是产品引擎在没有 Kunkun UI 时仍有价值
UI 背后逻辑很少存在实质性、传输无关的核心
没有终端/CI/服务端使用需求用户也需要 npx yourtool 或浏览器版本
单人快速迭代能维护一个小型单体仓库

如果不确定,先构建一个普通扩展。之后随时可以提取核心——下面的案例正是这样发展的。

模式:核心 → 适配器 → 主机

三个层次,有一条硬性规定:核心不导入任何传输层和主机。

  • 核心——纯 TypeScript 契约(interface YourToolAPI)加领域逻辑。无 @kunkunsh/sdk、无 WebSocket、无 kkrpc。
  • 适配器——针对操作系统实现契约(SSH2、node-pty、原生绑定)。仍然与传输无关:无论从 CLI、浏览器还是 Kunkun 调用,都返回相同的 API。
  • 主机组合根——将适配器包装在传输层中的薄入口点:
    • CLI 通过 HTTP/WebSocket 提供服务(使浏览器 UI 可以连接)
    • Kunkun 扩展通过 exposeBackend 在 kkrpc 上暴露

由于 UI 与契约通信,同一个 SvelteKit 前端可以在两个主机下渲染。

代码中的连接点

关键在于两个主机都使用相同的引擎,仅暴露方式不同。

// apps/cli/src/bin.ts — CLI 组合根
import { createNodeEngine } from "@yourtool/node";
import { createServer } from "./server.ts"; // Hono + WebSocket

const engine = createNodeEngine({ dataDir });
serve({ fetch: createServer(engine).fetch, port });
// 浏览器连接到 http://127.0.0.1:PORT?token=…
// apps/kunkun-plugin/src/backend.ts — Kunkun 组合根
import { exposeBackend } from "@kunkunsh/sdk/backend";
import { createNodeEngine } from "@yourtool/node";

const engine = createNodeEngine({ dataDir });
exposeBackend<YourToolAPI>(engine.api); // 同一引擎,kkrpc 传输

那行 exposeBackend 是整个 Kunkun 专属的代码。之上的所有内容都是复用的。

案例研究 1 — kkterminal(SSH/SFTP 工作台)

一个 SSH/SFTP 客户端(类似 FinalShell/Termius),既可作为 CLI 服务的 Web 应用运行,也可作为 Kunkun 自定义视图运行。

布局

kkterminal/
├── packages/
│   ├── core/   # KkTerminalAPI 契约、SessionEvent 协议、验证
│   ├── node/   # SSH2 + node-pty + 会话管理器(实现契约)
│   └── pty/    # 原生 PTY 绑定
└── apps/
    ├── cli/            # `npx kkterminal` — Hono + WebSocket 主机
    ├── web/            # SvelteKit UI(共享)
    └── kunkun-plugin/  # Kunkun 自定义视图(同一 UI,kkrpc 后端)

契约位于 core,无传输依赖:

// packages/core/src/index.ts
export interface KkTerminalAPI {
  hosts: HostAPI; sessions: SessionAPI; sftp: SftpAPI; tunnels: TunnelAPI;
}
export type SessionEvent =
  | { type: "session:output"; sessionId: string; chunk: string }
  | { type: "session:auth-prompt"; /* … */ };

Kunkun 主机使用外观模式包装单一引擎实例:

// apps/kunkun-plugin/src/backend.ts
import { exposeBackend } from "@kunkunsh/sdk/backend";
import { createNodeKkTerminal } from "@kkterminal/node";

let engine: NodeKkTerminal;
const facade: KkTerminalAPI = {
  get sessions() { return engine.api.sessions; },
  get sftp() { return engine.api.sftp; },
  // …getter 委托给引擎(引擎在下面创建)
};
const frontend = exposeBackend<KkTerminalAPI, FrontendBridge>(facade);
engine = createNodeKkTerminal({ dataDir }).withFrontendDb(frontend.bridge.db);

注意 Kunkun 主机甚至通过桥接使用前端的记录存储来支持持久化——CLI 主机使用 JSON 文件。同一核心,不同适配器。

构建顺序(子模块历史):

顺序即教训:引擎和 CLI 先构建;Kunkun 主机最后添加,在契约稳定之后。

案例研究 2 — space-lens(可视化磁盘扫描器)

一个磁盘使用情况扫描器,核心用 Rust(napi-rs),被四个前端使用:TUI、CLI Web 服务器、浏览器 UI 和 Kunkun 自定义视图。

space-lens/
├── packages/
│   ├── space-lens/  # Rust napi 绑定(buildDirectoryTree, getLargestNodes)
│   └── node/        # Node 扫描器包装
└── apps/
    ├── cli/           # `spacelens`(OpenTUI)+ `spacelens-web`(WebSocket 服务器)
    ├── web/           # SvelteKit UI(共享)
    └── kunkun-plugin/ # Kunkun 自定义视图(Deno 后端 + 路径策略)

Kunkun 主机包装共享 API 以强制执行 Kunkun 的权限范围——这是主机适配器增加 CLI 不需要的安全性的绝佳示例:

// apps/kunkun-plugin/src/backend.ts
import { exposeBackend } from "@kunkunsh/sdk/backend";
import { createSpaceLensAPI } from "@space-lens/cli/web-service";
import { assertPathsUnderAllowedRoots } from "./path-policy.ts";

function createKunkunSpaceLensAPI(): SpaceLensAPI {
  const allowedRoots = normalizeAllowedRoots(readAllowedRootsFromEnv());
  const api = createSpaceLensAPI();
  return {
    ...api,
    async startScan(options) {
      assertPathsUnderAllowedRoots(options.paths, allowedRoots, "扫描路径");
      return api.startScan(options);
    },
    async executeCleanup() {
      throw new Error("已禁用本地清理;请改用主机代理的回收站。");
    },
  };
}
exposeBackend<SpaceLensAPI>(createKunkunSpaceLensAPI());

CLI 允许本地删除;Kunkun 主机禁用它并委托给主机的系统回收站权限。同一引擎,主机特定策略在组合根实现。

构建顺序(子模块历史): Rust 绑定 → 磁盘使用逻辑 → 紧凑扫描器 → 重大重写 → 重组为单体仓库(0.2.0)→ 添加 SvelteKit Web GUI → 最后添加 Kunkun 扩展传输。再次:核心和 CLI 优先,Kunkun 主机在契约稳定后添加。

逐渐演进

你不需要一开始就设计这个。先从普通扩展开始,然后:

  1. 仅发布扩展。 即使像 uuid-generator 这样单文件无视图的命令也是一个完整的扩展。
  2. 提取核心——当逻辑超出 UI 时:将领域逻辑提取到 core 包中,带有一个纯接口和零主机/传输导入。
  3. 添加 CLI 主机apps/cli)来服务核心——现在你有了 npx yourtool
  4. 添加 Kunkun 主机apps/kunkun-plugin)——通过一个 exposeBackend 组合根和共享 UI。

构建输出

每个主机独立构建:

  • CLIdist/bin.mjspackage.json 中的 bin 入口供 npx 使用)。
  • Kunkun 扩展dist/backend.js 加上复制的 Web UI,附带 backend 权限以允许该脚本(以及任何原生 .node 文件)。

成果:一个引擎、一个 UI、一次测试——同时作为 CLI、Web 应用和 Kunkun 扩展发布。Kunkun 专属代码在组合根只有几十行。

On this page