Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/xlings-usage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ xlings subos new agent-ws-2 --from subos:dev-env@latest

```bash
xlings interface
# NDJSON protocol v1.0 over stdio
# → {"protocol":"1.0","capabilities":[...]}
# NDJSON protocol v1.1 over stdio
# → {"protocol":"1.1","capabilities":[...]}
```

### One-shot command execution
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Core capabilities:
src/
├── main.cpp # Entry point
├── cli.cppm # CLI dispatch (positional + flag parsing)
├── interface.cppm # NDJSON programmatic interface (protocol v1.0)
├── interface.cppm # NDJSON programmatic interface (protocol v1.1)
├── core/
│ ├── config.cppm # 3-layer config (global → subos → project)
│ ├── subos.cppm # SubOS lifecycle (create/use/fork/remove/stop)
Expand Down
2 changes: 1 addition & 1 deletion docs/design/interface-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ xlings interface --version
返回:

```json
{"protocol_version":"1.0"}
{"protocol_version":"1.1"}
```

### 能力发现
Expand Down
93 changes: 84 additions & 9 deletions docs/spec/interface-ndjson-v1.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
> 编写日期: 2026-05-17 | 版本: 0.4.36
> 编写日期: 2026-05-17 | 版本: 2026.9.27.1

# NDJSON 接口协议规范 v1.0
# NDJSON 接口协议规范 v1.1

## 1. 概述

`xlings interface` 提供面向程序的结构化 API,使外部客户端(IDE 插件、CI 脚本、AI agent 等)可通过标准 IO 与 xlings 交互。协议版本为 **1.0**,基于 NDJSON(Newline-Delimited JSON)。
`xlings interface` 提供面向程序的结构化 API,使外部客户端(IDE 插件、CI 脚本、AI agent 等)可通过标准 IO 与 xlings 交互。协议版本为 **1.1**,基于 NDJSON(Newline-Delimited JSON)。

| 协议版本 | xlings | 变化 |
|----------|--------|------|
| 1.0 | 0.4.36 起 | 初始版本 |
| 1.1 | 2026.9.27.1 起 | 增补:`install_targets` 事件;`install_plan` 条目的第三个元素 `revision`(§6.3.1) |

次版本号的变化只做增补,1.0 客户端无需修改即可读取 1.1 的输出。客户端应通过**探测能力**
判断服务端是否提供某项功能(例如 `install_targets` 事件是否出现),而不是比较版本号。

## 2. 传输层

Expand Down Expand Up @@ -34,7 +42,7 @@ xlings interface --version
服务端输出一行后退出:

```json
{"protocol_version":"1.0"}
{"protocol_version":"1.1"}
```

### 3.2 查询可用能力
Expand All @@ -46,7 +54,7 @@ xlings interface --list
服务端输出能力清单后退出:

```json
{"protocol_version":"1.0","capabilities":[{"name":"install_packages","description":"...","destructive":true,"inputSchema":{...},"outputSchema":{...}}, ...]}
{"protocol_version":"1.1","capabilities":[{"name":"install_packages","description":"...","destructive":true,"inputSchema":{...},"outputSchema":{...}}, ...]}
```

### 3.3 执行能力
Expand Down Expand Up @@ -158,6 +166,70 @@ xlings interface install_packages --args '{"targets":["gcc@14"],"yes":true}'
| dataKind | string | 数据类别标识 |
| payload | object/string | 结构化数据;若原始 JSON 解析失败则为字符串 |

#### 6.3.1 安装相关的 data 事件

`install_packages`(以及 CLI 的 `xlings install`)按以下顺序输出三类 data 事件。

**`install_plan`**:待安装的节点(依赖已展开,已安装且为当前载荷的节点不列出)。全部目标均已
安装时不输出。`plan_install`(dry-run)输出此事件后结束。

```json
{"kind":"data","dataKind":"install_plan","payload":{"packages":[["xim:glibc@2.44.3","reinstall: recipe revision 1, installed revision 0",1],["xim:node@22.4.0","",0]]}}
```

`packages` 的每一项为数组 `[坐标, 说明, 修订号]`:

| 位置 | 类型 | 说明 |
|------|------|------|
| 0 | string | `<namespace>:<name>@<version>` |
| 1 | string | 说明;磁盘上已有该版本但修订号不同时为重装原因,否则为空串 |
| 2 | integer | 配方对该版本声明的打包修订号(1.1 起;见 xpkg 规范 5.1.1) |

**`install_summary`**:本次运行的计数。全部目标均已安装时不输出。

```json
{"kind":"data","dataKind":"install_summary","payload":{"success":1,"failed":0}}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| success | integer | 本次运行实际安装的节点数;载荷已存在且为当前载荷的节点不计入 |
| failed | integer | 失败的节点数 |

**`install_targets`**(1.1 起):每个请求目标解析到的版本与载荷位置。由顶层安装调用在
**每一条路径**上输出一次,包括全部目标均已安装、解析失败和安装失败;配方在安装过程中
通过 `pkgmanager.install()` 发起的嵌套安装不输出此事件。`update_packages` 升级到新版本时
内部执行的安装同样输出此事件;`plan_install`(dry-run)不输出。

```json
{"kind":"data","dataKind":"install_targets","payload":{"targets":[{"request":"glibc@2.44","namespace":"xim","name":"glibc","version":"2.44.3","revision":1,"status":"installed","payload_dir":"/home/u/.xlings/data/xpkgs/xim-x-glibc/2.44.3"}]}}
```

`targets` 与请求中的 `targets` 一一对应、顺序相同。`status` 描述本次运行是否满足了该请求:
运行在生成安装计划之前终止时(例如另一个目标无法解析),每一项均为 `failed`,已解析的项仍
填写 `namespace`、`name`、`version`。每一项包含:

| 字段 | 类型 | 说明 |
|------|------|------|
| request | string | 调用方请求的原始字符串 |
| namespace | string | 解析到的包命名空间;未能解析时为空串 |
| name | string | 解析到的包名;未能解析时为空串 |
| version | string | 实际解析到的版本;未能解析时为空串 |
| revision | integer | 配方对该版本声明的打包修订号 |
| status | string | `installed`(本次运行安装)/ `already_present`(载荷已存在且为当前载荷)/ `failed` |
| payload_dir | string | 载荷目录的绝对路径;`status` 为 `failed` 时为空串 |

三种情形的示例:

```jsonc
// 全新安装
{"kind":"data","dataKind":"install_targets","payload":{"targets":[{"request":"xim:node@22","namespace":"xim","name":"node","version":"22.4.0","revision":0,"status":"installed","payload_dir":"/home/u/.xlings/data/xpkgs/xim-x-node/22.4.0"}]}}
// 全部已安装(此时不输出 install_plan 与 install_summary)
{"kind":"data","dataKind":"install_targets","payload":{"targets":[{"request":"xim:node@22","namespace":"xim","name":"node","version":"22.4.0","revision":0,"status":"already_present","payload_dir":"/home/u/.xlings/data/xpkgs/xim-x-node/22.4.0"}]}}
// 失败(此处为目标不存在;解析成功而安装失败时 namespace/name/version 仍会填写)
{"kind":"data","dataKind":"install_targets","payload":{"targets":[{"request":"xim:nosuchpkg","namespace":"","name":"","version":"","revision":0,"status":"failed","payload_dir":""}]}}
```

### 6.4 prompt

服务端向客户端请求用户输入。
Expand Down Expand Up @@ -207,13 +279,15 @@ xlings interface install_packages --args '{"targets":["gcc@14"],"yes":true}'
终止行,标志会话结束。每次执行恰好输出一行。

```json
{"kind":"result","exitCode":0,"data":{"installed":["gcc@14.2.0"]}}
{"kind":"result","exitCode":0}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| exitCode | integer | 0 成功,非 0 失败,130 表示取消 |
| data | object? | 能力返回的结构化结果(可选) |
| data | object? | 能力返回值中 `exitCode` 以外的字段(可选;返回值只有 `exitCode` 时省略) |

安装结果不在 `result` 行中,而在 `install_targets` 事件中(§6.3.1)。

### 6.7 heartbeat

Expand Down Expand Up @@ -332,8 +406,9 @@ stdout 输出(每行一个 JSON):
{"kind":"progress","phase":"downloading","percent":80,"message":"node-v22.4.0-linux-x64.tar.xz"}
{"kind":"log","level":"info","message":"extracting node-v22.4.0-linux-x64.tar.xz"}
{"kind":"progress","phase":"installing","percent":90,"message":"linking shims"}
{"kind":"data","dataKind":"install_summary","payload":{"installed":["node@22.4.0"]}}
{"kind":"result","exitCode":0,"data":{"installed":["node@22.4.0"]}}
{"kind":"data","dataKind":"install_summary","payload":{"success":1,"failed":0}}
{"kind":"data","dataKind":"install_targets","payload":{"targets":[{"request":"node@22","namespace":"xim","name":"node","version":"22.4.0","revision":0,"status":"installed","payload_dir":"/home/u/.xlings/data/xpkgs/xim-x-node/22.4.0"}]}}
{"kind":"result","exitCode":0}
```

客户端收到 `kind: result` 后即可关闭 stdin 并退出。
28 changes: 28 additions & 0 deletions docs/spec/xpkg-manifest-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ xpkg 包描述文件是一个 Lua 脚本(`.lua`),用于声明软件包的
| `url` | string / table | 下载地址;table 时为镜像表 `{GLOBAL=..., CN=...}` |
| `sha256` | string / nil | 校验和;nil 表示暂未计算 |
| `ref` | string | 引用另一版本(别名),如 `latest` → 实际版本 |
| `revision` | integer | 打包修订号,非负整数,缺省为 0;见 5.1.1 |

示例:

Expand All @@ -70,6 +71,33 @@ xpkg 包描述文件是一个 Lua 脚本(`.lua`),用于声明软件包的
},
```

#### 5.1.1 打包修订号 `revision`(xlings 2026.9.27.1 / libxpkg 0.0.58)

上游版本不变而安装内容发生变化时(重新构建的归档、修正后的安装钩子),版本条目以
`revision` 递增表达这一变化。未声明或取值不是非负整数时按 0 读取;`ref` 别名不携带
修订号,其值取自别名最终指向的条目。

xlings 安装载荷时把修订号写入载荷目录的 `.xpkg-install.json`(`"revision": N`)。
判定规则只有一条:载荷记录的修订号等于配方对该版本声明的修订号时,载荷为当前载荷;
否则视为未安装,下一次 `xlings install` 重新安装该版本并输出原因,例如
`reinstalling glibc@2.44.3: recipe revision 1, installed revision 0`。

- 不含 `revision` 字段的记录(2026.9.27.1 之前的客户端写入)按修订号 0 读取,因此
配方声明 `revision = 1` 即覆盖所有旧载荷,未声明修订号的配方不触发重装。
- 载荷目录中没有 `.xpkg-install.json` 时不作判定,行为与此前相同。
- 重装过程中载荷始终存在:先下载并校验归档,再把旧载荷移至 `<data>/stale/`,在原路径
安装新载荷(配方会把 `install_dir()` 写入所安装的文件,因此不能在临时目录安装后改名);
成功后删除旧载荷,失败时删除不完整的新载荷并移回旧载荷。其他工具写入旧载荷目录的文件
(例如 mcpp 的 `.mcpp_ok`、`.mcpp-fixup.json`)随旧载荷一并删除。

```lua
["2.44.3"] = {
url = "https://example.com/glibc-2.44.3-linux-x86_64.tar.gz",
sha256 = "abcdef...",
revision = 1,
},
```

### 5.2 平台级字段

除版本条目外,平台 table 还可包含以下键(非版本条目,加载器会跳过):
Expand Down
2 changes: 1 addition & 1 deletion examples/clients/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Three minimal clients exercising the v1 NDJSON protocol from different runtimes:

Each client does the same flow:

1. Probe `xlings interface --version` → assert `protocol_version == "1.0"`
1. Probe `xlings interface --version` → assert the `protocol_version` major is `1` (a minor bump is additive)
2. Probe `--list` → print capability count
3. Call `env` capability → print active sub-OS + paths
4. Call `list_subos` capability → print sub-OSs
Expand Down
3 changes: 2 additions & 1 deletion examples/clients/node_client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ console.log(`→ using ${xlings}`);
const { events } = callXlings(xlings, "interface", "--version");
const v = events[0]?.protocol_version;
console.log(`protocol_version: ${v}`);
if (v !== "1.0") {
// Major version only: a minor bump is additive.
if (typeof v !== "string" || v.split(".")[0] !== "1") {
console.error(`ERROR: unexpected version ${v}`);
process.exit(1);
}
Expand Down
4 changes: 3 additions & 1 deletion examples/clients/python_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ def main() -> int:
# 1. protocol_version (one-line JSON, not NDJSON)
[version_obj] = list(call(xlings, "interface", "--version"))
print(f"protocol_version: {version_obj['protocol_version']}")
assert version_obj["protocol_version"] == "1.0"
# Major version only: a minor bump is additive, and a capability is
# detected by its presence on the wire, not by this number.
assert version_obj["protocol_version"].split(".")[0] == "1"

# 2. capability list (one big JSON object)
[list_obj] = list(call(xlings, "interface", "--list"))
Expand Down
4 changes: 2 additions & 2 deletions mcpp.lock
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ hash = "fnv1a:7dcd2b0fcf679705"

[package."mcpplibs.xpkg"]
namespace = "mcpplibs"
version = "0.0.57"
source = "index+mcpplibs@0.0.57"
version = "0.0.58"
source = "index+mcpplibs@0.0.58"
hash = "fnv1a:91399fb9d7d59ebc"

[package."mcpplibs.capi.lua"]
Expand Down
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ members = [

[package]
name = "xlings"
version = "2026.9.26.3"
version = "2026.9.27.1"
description = "Universal package management infrastructure tool with SubOS isolation"
license = "Apache-2.0"
repo = "https://github.com/openxlings/xlings"
Expand Down Expand Up @@ -126,7 +126,7 @@ libarchive = "3.8.7"

[dependencies.mcpplibs]
cmdline = "0.0.2"
xpkg = "0.0.57"
xpkg = "0.0.58"
capi.lua = "0.0.3"

[dev-dependencies.compat]
Expand Down
5 changes: 3 additions & 2 deletions src/agent/text_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,9 @@ void render_data_event(const DataEvent& e) {
}
}
}
// download_progress, system_info, env, repo_list, subos_shims:
// these are either noisy (progress) or already covered by info_panel.
// download_progress, system_info, env, repo_list, subos_shims,
// install_targets: these are either noisy (progress), already covered by
// info_panel, or a structured duplicate of the plan and the summary.
// Skip silently — the log messages carry the essential info.
}

Expand Down
3 changes: 3 additions & 0 deletions src/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ bool kind_is_interface_only_(std::string_view kind) {
};
static constexpr std::string_view kWireDuplicate[] = {
"remove_blocked", "update_plan", "update_summary",
// What each request of an install resolved to (protocol 1.1); the
// terminal already shows the plan, the progress and the summary.
"install_targets",
};
return std::ranges::contains(kCapabilityOnly, kind)
|| std::ranges::contains(kWireDuplicate, kind);
Expand Down
2 changes: 1 addition & 1 deletion src/core/config.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import xlings.core.xvm.db;
namespace xlings {

export struct Info {
static constexpr std::string_view VERSION = "2026.9.26.3";
static constexpr std::string_view VERSION = "2026.9.27.1";
static constexpr std::string_view REPO = "https://github.com/openxlings/xlings";
};

Expand Down
Loading