Skip to content
Merged
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
24 changes: 24 additions & 0 deletions .agents/docs/2026-09-27-release-2026.9.27.1-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 2026.9.27.1 发布说明

> 相关:xlings#620(配方打包修订号)、#621(glibc 的 locale 与 gconv);libxpkg 0.0.58;xim-pkgindex#892。
> 本文记录本版的变化与客户端可见的差异;发布后的验证结果追加在 §3。

## 1. 变化

| 项 | 内容 |
|---|---|
| 打包修订号 | 版本条目可写 `revision`(libxpkg 0.0.58 解析)。载荷的安装记录 `.xpkg-install.json` 写入修订号;记录的修订号低于配方时视为未安装,`install` 就地重装,并输出 `recipe revision <n>, installed revision <m>` |
| 就地重装 | 旧载荷先移入 `<data>/stale/`,新载荷装到原路径;成功后删除旧载荷,失败时移回。配方会把 `install_dir()` 写进所装文件,因此不能先装到临时目录再改名 |
| 中断恢复 | 移出旧载荷之前,在其旁写入标记 `<parked>.origin`(原路径与所属进程,原子写入)。每条 `install` 命令开始时处理 `<data>/stale/`:所属进程已不存在的条目,原路径缺失或为空占位时移回,否则连同标记删除;进程仍存活的条目不动。进程内的析构回滚保持不变 |
| `install_targets` 事件 | 接口协议 1.1:顶层安装在所有路径上报告每个请求解析到的命名空间、名称、版本、修订号、状态与载荷目录。mcpp 2026.9.27.1 优先读取这一记录 |
| 版本选择一致性向量 | `tests/data/semver-vectors.tsv` 由单元测试驱动;mcpp 以同一份向量测试其版本选择 |
| 重命名失败的提示 | Windows 上目录中的文件被占用而无法移动时,提示点名被占用的文件 |

## 2. 客户端可见的差异

- `install_summary.success` 只计本次实际安装的数量;已是当前修订的载荷报告为 `already_present`,不计入。
- 旧版 xlings 忽略 `revision`:持有旧载荷的机器要升级到本版后才会重装。

## 3. 发布后验证

(发布后追加。)
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 并退出。
30 changes: 30 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,35 @@ 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`)随旧载荷一并删除。
- 移出旧载荷之前,在 `<data>/stale/` 中写入记录原路径与所属进程的标记。进程被强制终止时,
后续的 `install` 在该进程已不存在后处理遗留的旧载荷:原路径缺失或为空时移回,否则删除。

```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
30 changes: 15 additions & 15 deletions mcpp.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,77 +9,77 @@ version = 2
namespace = "compat"
version = "1.0.8"
source = "index+compat@1.0.8"
hash = "fnv1a:3672c2797c5f2faf"
hash = "fnv1a:e0e808c73d8f562d"

[package."compat.ftxui"]
namespace = "compat"
version = "6.1.9"
source = "index+compat@6.1.9"
hash = "fnv1a:f4f0f58dc9107814"
hash = "fnv1a:2798f2ba9c6400b1"

[package."compat.libarchive"]
namespace = "compat"
version = "3.8.7"
source = "index+compat@3.8.7"
hash = "fnv1a:e412742c0533bef7"
hash = "fnv1a:c4f73335678a009e"

[package."compat.lua"]
namespace = "compat"
version = "5.4.7"
source = "index+compat@5.4.7"
hash = "fnv1a:6bda787e2c850eac"
hash = "fnv1a:62ff6fb178577847"

[package."compat.lz4"]
namespace = "compat"
version = "1.10.0"
source = "index+compat@1.10.0"
hash = "fnv1a:77a204c2fbefcdc5"
hash = "fnv1a:c1f0fef08251ee13"

[package."compat.mbedtls"]
namespace = "compat"
version = "3.6.1"
source = "index+compat@3.6.1"
hash = "fnv1a:83dea67ac1379be3"
hash = "fnv1a:bb700e69e973ccd0"

[package."compat.xz"]
namespace = "compat"
version = "5.8.3"
source = "index+compat@5.8.3"
hash = "fnv1a:3c6c899487663f43"
hash = "fnv1a:8556dee38703a791"

[package."compat.zlib"]
namespace = "compat"
version = "1.3.2"
source = "index+compat@1.3.2"
hash = "fnv1a:bb0a6ebd39272a80"
hash = "fnv1a:9f13dbeaae3ef4c0"

[package."compat.zstd"]
namespace = "compat"
version = "1.5.7"
source = "index+compat@1.5.7"
hash = "fnv1a:bfb039f8b4a4b3f3"
hash = "fnv1a:cc855914d41f9bfb"

[package."mcpplibs.cmdline"]
namespace = "mcpplibs"
version = "0.0.2"
source = "index+mcpplibs@0.0.2"
hash = "fnv1a:8ee8a8e51ac69885"
hash = "fnv1a:aad61299abc2c4b7"

[package."tinyhttps"]
namespace = "mcpplibs"
version = "0.2.9"
source = "index+mcpplibs@0.2.9"
hash = "fnv1a:7dcd2b0fcf679705"
hash = "fnv1a:a01132893163f775"

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

[package."mcpplibs.capi.lua"]
namespace = "mcpplibs.capi"
version = "0.0.3"
source = "index+mcpplibs.capi@0.0.3"
hash = "fnv1a:c8e7ef9d60a1a3a6"
hash = "fnv1a:f3031630d0b22d94"

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
Loading
Loading