@jeffusion/tianshu 0.1.0 → 0.1.1
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/{dist/plugin/.codex-plugin → .codex-plugin}/plugin.json +1 -1
- package/{dist/plugin/.mcp.json → .mcp.json} +3 -2
- package/README.md +17 -10
- package/{dist/plugin/THIRD_PARTY_NOTICES.txt → THIRD_PARTY_NOTICES.txt} +2 -30
- package/build-info.json +5 -0
- package/cli/main.mjs +130 -183
- package/dist/{plugin/dist/panel.html → panel.html} +2 -2
- package/dist/{plugin/dist/server.mjs → server.mjs} +26 -24
- package/docs/npm-distribution.md +30 -47
- package/{dist/plugin/ledger → ledger}/config.py +5 -5
- package/package.json +1 -47
- package/{dist/plugin/skills → skills}/coordinator/references/task-center.md +1 -1
- package/cli/probe.mjs +0 -37
- package/dist/build-info.json +0 -23
- package/dist/plugin/build-info.json +0 -5
- /package/{dist/plugin/ledger → ledger}/cli.py +0 -0
- /package/{dist/plugin/ledger → ledger}/coordinator.py +0 -0
- /package/{dist/plugin/ledger → ledger}/read_ledger.py +0 -0
- /package/{dist/plugin/skills → skills}/coordinator/SKILL.md +0 -0
- /package/{dist/plugin/skills → skills}/coordinator/agents/openai.yaml +0 -0
- /package/{dist/plugin/skills → skills}/coordinator/references/commands.md +0 -0
- /package/{dist/plugin/skills → skills}/coordinator/references/recovery.md +0 -0
- /package/{dist/plugin/skills → skills}/coordinator/scripts/coordinator.py +0 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
面向长期目标的智能协作工作台,以 Codex 本地插件交付。天枢将任务台账、执行聊天和验收证据连接起来,让跨聊天工作有持续的状态和明确的授权边界。
|
|
7
7
|
|
|
8
|
-
**当前阶段:0.1.
|
|
8
|
+
**当前阶段:0.1.1,早期开发。** 已实现任务中心、MCP 工具和协调 Skill;完整自主规划、后台常驻执行和多客户端协作尚未实现。
|
|
9
9
|
|
|
10
10
|
## 已实现
|
|
11
11
|
|
|
@@ -15,19 +15,25 @@
|
|
|
15
15
|
- 删除任务保留独立验收归档;聊天空闲、回合结束和业务验收分别记录。
|
|
16
16
|
- 明暗主题、只读浏览器预览、隔离测试与内容哈希构建标识。
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
发布物是 npm 包 `@jeffusion/tianshu`,由 Codex 原生插件机制下载和安装,无需全局安装天枢、克隆仓库或编译。
|
|
19
21
|
|
|
20
22
|
```sh
|
|
21
|
-
|
|
22
|
-
tianshu
|
|
23
|
-
tianshu doctor
|
|
23
|
+
codex plugin marketplace add jeffusion/tianshu
|
|
24
|
+
codex plugin add tianshu@jeffusion
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
需要本机 Node.js 24–26、npm、Python 3.12+ 和支持 npm 插件源的 Codex。首次使用显式创建业务台账:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx --yes @jeffusion/tianshu@0.1.1 init
|
|
31
|
+
npx --yes @jeffusion/tianshu@0.1.1 doctor
|
|
32
|
+
```
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
重开客户端、启用“天枢”,发送“@天枢 打开任务中心”。已有任务先按[配置说明](docs/configuration.md)接入既有台账;不要用另一份空库替代。市场标识 `jeffusion`,插件 `tianshu`,Skill `tianshu:coordinator`,MCP 服务名 `tianshu`。升级、卸载和版本发布见 [npm 分发说明](docs/npm-distribution.md)。
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
源码安装只用于开发测试。构建和测试不会更新当前 Codex 安装,也不会自动发布 npm。
|
|
31
37
|
|
|
32
38
|
## 工作方式与边界
|
|
33
39
|
|
|
@@ -55,8 +61,9 @@ npm run check
|
|
|
55
61
|
npm run build
|
|
56
62
|
```
|
|
57
63
|
|
|
58
|
-
`check` 包含 TypeScript、生产构建、Node 协议测试、Python 台账测试和真实 npm
|
|
64
|
+
`check` 包含 TypeScript、生产构建、Node 协议测试、Python 台账测试和真实 npm tarball 回归,无需 API key、模型服务或业务数据库。可发布包位于 `dist/package`;插件清单、MCP、Skill 和运行产物直接位于包根目录。`build-info.json` 记录版本和内容指纹。
|
|
59
65
|
|
|
66
|
+
本地测试不递增版本、不生成独立安装器。正式 npm 版本不可覆盖;本次 `0.1.1` 修正已发布 `0.1.0` 的安装结构。
|
|
60
67
|
## 工程结构
|
|
61
68
|
|
|
62
69
|
| 目录 | 内容 |
|
|
@@ -66,7 +73,7 @@ npm run build
|
|
|
66
73
|
| `src/contracts` | 产品元数据、任务和附件契约 |
|
|
67
74
|
| `ledger` | 配置解析、只读投影、事务台账 |
|
|
68
75
|
| `skills/coordinator` | 跨聊天协调规则与命令参考 |
|
|
69
|
-
| `scripts` |
|
|
76
|
+
| `scripts` | 演示、开发、构建、原生安装验收 |
|
|
70
77
|
| `tests` | 独立 fixture 和回归测试 |
|
|
71
78
|
|
|
72
79
|
参见[开发与验证](docs/development.md)、[贡献指南](CONTRIBUTING.md)、[安全说明](SECURITY.md)。私人配置、任务数据库、历史验收材料和安装产物不进入仓库。
|
|
@@ -1731,7 +1731,7 @@ SOFTWARE.
|
|
|
1731
1731
|
SOFTWARE
|
|
1732
1732
|
|
|
1733
1733
|
|
|
1734
|
-
===== @typescript/typescript-
|
|
1734
|
+
===== @typescript/typescript-linux-x64@7.0.2 (Apache-2.0) =====
|
|
1735
1735
|
|
|
1736
1736
|
|
|
1737
1737
|
--- LICENSE ---
|
|
@@ -3540,34 +3540,6 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
|
3540
3540
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
3541
3541
|
|
|
3542
3542
|
|
|
3543
|
-
===== fsevents@2.3.3 (MIT) =====
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
--- LICENSE ---
|
|
3547
|
-
MIT License
|
|
3548
|
-
-----------
|
|
3549
|
-
|
|
3550
|
-
Copyright (C) 2010-2020 by Philipp Dunkel, Ben Noordhuis, Elan Shankar, Paul Miller
|
|
3551
|
-
|
|
3552
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
3553
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
3554
|
-
in the Software without restriction, including without limitation the rights
|
|
3555
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
3556
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
3557
|
-
furnished to do so, subject to the following conditions:
|
|
3558
|
-
|
|
3559
|
-
The above copyright notice and this permission notice shall be included in
|
|
3560
|
-
all copies or substantial portions of the Software.
|
|
3561
|
-
|
|
3562
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
3563
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
3564
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
3565
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
3566
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
3567
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
3568
|
-
THE SOFTWARE.
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
3543
|
===== function-bind@1.1.2 (MIT) =====
|
|
3572
3544
|
|
|
3573
3545
|
|
|
@@ -4452,7 +4424,7 @@ This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
|
4452
4424
|
defined by the Mozilla Public License, v. 2.0.
|
|
4453
4425
|
|
|
4454
4426
|
|
|
4455
|
-
===== lightningcss-
|
|
4427
|
+
===== lightningcss-linux-x64-gnu@1.33.0 (MPL-2.0) =====
|
|
4456
4428
|
|
|
4457
4429
|
|
|
4458
4430
|
--- LICENSE ---
|
package/build-info.json
ADDED
package/cli/main.mjs
CHANGED
|
@@ -1,193 +1,140 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {fileURLToPath} from
|
|
5
|
-
import {execFileSync
|
|
6
|
-
import {createHash,randomUUID} from 'node:crypto';
|
|
7
|
-
import {probe} from './probe.mjs';
|
|
8
|
-
const packageRoot=fileURLToPath(new URL('../',import.meta.url));
|
|
9
|
-
const pkg=JSON.parse(readFileSync(join(packageRoot,'package.json'),'utf8'));
|
|
10
|
-
const read=p=>JSON.parse(readFileSync(p,'utf8'));
|
|
11
|
-
const print=value=>console.log(JSON.stringify(value,null,2));
|
|
12
|
-
const help=`天枢 Tianshu ${pkg.version}\n\n tianshu init 初始化独立台账(已有配置不覆盖)\n tianshu install 将已编译插件安装到 Codex\n tianshu doctor 检查依赖、配置和已安装 MCP\n tianshu upgrade 将当前 npm 包同步到插件,保留上一份\n tianshu rollback 恢复上一份插件/未完成安装,保留台账\n tianshu uninstall 移除 Codex 插件和市场,保留用户数据\n\n选项:--config <绝对路径>,--help,--version\n环境:TIANSHU_HOME、TIANSHU_PYTHON、TIANSHU_CODEX、CODEX_HOME\n安装 npm 包不会自动修改 Codex。需要 Node.js 24–26、Python 3.12+、Codex CLI。`;
|
|
1
|
+
// cli/main.mjs
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, lstatSync } from "node:fs";
|
|
3
|
+
import { join as join2, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
7
|
+
// cli/probe.mjs
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
function probe(server, python, config) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn(process.execPath, [server], { env: { ...process.env, TIANSHU_PYTHON: python, TIANSHU_CONFIG: config, PYTHONDONTWRITEBYTECODE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
|
|
12
|
+
let buffer = "", bytes = 0, finished = false;
|
|
13
|
+
const timer = setTimeout(() => finish(new Error("MCP health check timed out")), 12e3);
|
|
14
|
+
function finish(error, value) {
|
|
15
|
+
if (finished) return;
|
|
16
|
+
finished = true;
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
child.kill();
|
|
19
|
+
const force = setTimeout(() => child.kill("SIGKILL"), 1e3);
|
|
20
|
+
force.unref();
|
|
21
|
+
child.once("close", () => clearTimeout(force));
|
|
22
|
+
error ? reject(error) : resolve(value);
|
|
23
|
+
}
|
|
24
|
+
const send = (value) => child.stdin.write(JSON.stringify(value) + "\n");
|
|
25
|
+
child.on("error", (error) => finish(error));
|
|
26
|
+
child.on("exit", () => {
|
|
27
|
+
if (!finished) finish(new Error("MCP server exited before health check completed"));
|
|
28
|
+
});
|
|
29
|
+
child.stdin.on("error", (error) => finish(error));
|
|
30
|
+
child.stderr.resume();
|
|
31
|
+
child.stdout.on("data", (chunk) => {
|
|
32
|
+
bytes += chunk.length;
|
|
33
|
+
if (bytes > 4 * 1024 * 1024) return finish(new Error("MCP response exceeds health check limit"));
|
|
34
|
+
buffer += chunk.toString();
|
|
35
|
+
let newline;
|
|
36
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
37
|
+
const line = buffer.slice(0, newline);
|
|
38
|
+
buffer = buffer.slice(newline + 1);
|
|
39
|
+
if (!line.trim()) continue;
|
|
40
|
+
try {
|
|
41
|
+
const response = JSON.parse(line);
|
|
42
|
+
if (response.error) throw new Error("MCP health request rejected");
|
|
43
|
+
if (response.id === 1) {
|
|
44
|
+
if (response.result?.serverInfo?.name !== "tianshu") throw new Error("Unexpected MCP server identity");
|
|
45
|
+
send({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
46
|
+
send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
47
|
+
} else if (response.id === 2) {
|
|
48
|
+
const tools = (response.result?.tools || []).map((t) => t.name).sort();
|
|
49
|
+
if (JSON.stringify(tools) !== JSON.stringify(["delete_task", "get_task", "list_tasks", "open_workspace"])) throw new Error("Unexpected MCP tool inventory");
|
|
50
|
+
send({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "list_tasks", arguments: {} } });
|
|
51
|
+
} else if (response.id === 3) {
|
|
52
|
+
const data = response.result?.structuredContent;
|
|
53
|
+
if (response.result?.isError || !data?.readonly || !data.sources?.length || data.sources.some((s) => !s.ok)) throw new Error("Configured ledger is not healthy/readable");
|
|
54
|
+
finish(null, { build: data.build, task_count: data.tasks.length, tools: 4 });
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
finish(error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "tianshu-doctor", version: "1" } } });
|
|
62
|
+
});
|
|
32
63
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
|
|
65
|
+
// src/contracts/config-path.mjs
|
|
66
|
+
import { homedir } from "node:os";
|
|
67
|
+
import { isAbsolute, join } from "node:path";
|
|
68
|
+
function dataHome() {
|
|
69
|
+
return process.env.TIANSHU_HOME || (process.platform === "darwin" ? join(homedir(), "Library/Application Support/Tianshu") : join(process.env.XDG_DATA_HOME || join(homedir(), ".local/share"), "tianshu"));
|
|
70
|
+
}
|
|
71
|
+
function configPath(explicit) {
|
|
72
|
+
const path = explicit || process.env.TIANSHU_CONFIG || join(dataHome(), "config.json");
|
|
73
|
+
if (!isAbsolute(path)) throw new Error("Config path must be absolute");
|
|
37
74
|
return path;
|
|
38
75
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
76
|
+
|
|
77
|
+
// cli/main.mjs
|
|
78
|
+
var packageRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
79
|
+
var pkg = JSON.parse(readFileSync(join2(packageRoot, "package.json"), "utf8"));
|
|
80
|
+
var print = (value) => console.log(JSON.stringify(value, null, 2));
|
|
81
|
+
var help = `\u5929\u67A2 Tianshu ${pkg.version}
|
|
82
|
+
|
|
83
|
+
tianshu init \u521D\u59CB\u5316\u72EC\u7ACB\u53F0\u8D26\uFF1B\u5DF2\u6709\u914D\u7F6E\u53EA\u6821\u9A8C\uFF0C\u4E0D\u8986\u76D6
|
|
84
|
+
tianshu doctor \u68C0\u67E5\u672C\u5305\u4F9D\u8D56\u3001\u914D\u7F6E\u548C\u53EA\u8BFB MCP \u67E5\u8BE2
|
|
85
|
+
|
|
86
|
+
\u9009\u9879\uFF1A--config <\u7EDD\u5BF9\u8DEF\u5F84>\uFF0C--help\uFF0C--version
|
|
87
|
+
\u73AF\u5883\uFF1ATIANSHU_CONFIG\u3001TIANSHU_HOME\u3001TIANSHU_PYTHON
|
|
88
|
+
\u63D2\u4EF6\u5B89\u88C5\u548C\u5378\u8F7D\u8BF7\u4F7F\u7528 Codex \u539F\u751F\u547D\u4EE4\uFF1B\u672C\u547D\u4EE4\u4E0D\u7BA1\u7406\u63D2\u4EF6\u3002`;
|
|
89
|
+
function run(python, args) {
|
|
90
|
+
return execFileSync(python, args, { encoding: "utf8", timeout: 3e4, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, PYTHONDONTWRITEBYTECODE: "1" } });
|
|
46
91
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return
|
|
92
|
+
function pythonRuntime() {
|
|
93
|
+
const node = Number(process.versions.node.split(".")[0]);
|
|
94
|
+
if (node < 24 || node > 26) throw new Error("Node.js 24\u201326 is required");
|
|
95
|
+
const executable = process.env.TIANSHU_PYTHON || "python3";
|
|
96
|
+
const info = JSON.parse(run(executable, ["-c", 'import sys,json;print(json.dumps({"version":list(sys.version_info[:2]),"path":sys.executable}))']));
|
|
97
|
+
if (info.version[0] !== 3 || info.version[1] < 12) throw new Error("Python 3.12+ is required");
|
|
98
|
+
return info.path;
|
|
54
99
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
function walk(dir){for(const e of readdirSync(dir,{withFileTypes:true})){const p=join(dir,e.name);if(e.isSymbolicLink())throw new Error('Plugin payload contains symlinks');if(e.isDirectory())walk(p);else if(e.isFile())actual.push(relative(plugin,p).split('\\').join('/'));else throw new Error('Unsupported payload entry');}}
|
|
59
|
-
walk(plugin);const expected=Object.keys(manifest.files||{}).sort();
|
|
60
|
-
if(!expected.length||JSON.stringify(actual.sort())!==JSON.stringify(expected))throw new Error('Plugin payload inventory mismatch');
|
|
61
|
-
for(const path of expected){if(isAbsolute(path)||path.split(/[\\/]/).includes('..'))throw new Error('Invalid integrity path');const hash=createHash('sha256').update(readFileSync(join(plugin,path))).digest('hex');if(hash!==manifest.files[path])throw new Error('Plugin integrity check failed: '+path);}
|
|
62
|
-
const info=read(join(plugin,'build-info.json'));
|
|
63
|
-
if(info.name!=='tianshu'||!/^[0-9a-f]{20}$/.test(info.build_id)||!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(info.version))throw new Error('Invalid plugin identity');
|
|
64
|
-
return info;
|
|
100
|
+
function validate(python, config) {
|
|
101
|
+
const result = JSON.parse(run(python, [join2(packageRoot, "ledger/read_ledger.py"), "--config", config]));
|
|
102
|
+
if (!result.sources?.length || result.sources.some((s) => !s.ok)) throw new Error("Configured ledger is missing or unhealthy; no replacement database was created");
|
|
65
103
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
if(!
|
|
71
|
-
|
|
72
|
-
|
|
104
|
+
async function main(argv = process.argv.slice(2)) {
|
|
105
|
+
if (!argv.length || argv.includes("--help")) return console.log(help);
|
|
106
|
+
if (argv.length === 1 && argv[0] === "--version") return console.log(pkg.version);
|
|
107
|
+
const [command, ...args] = argv;
|
|
108
|
+
if (!["init", "doctor"].includes(command)) throw new Error("Unsupported command. Use Codex native plugin commands for installation and removal.");
|
|
109
|
+
let explicit;
|
|
110
|
+
for (let i = 0; i < args.length; i++) {
|
|
111
|
+
if (args[i] === "--config" && args[i + 1] && !explicit) explicit = args[++i];
|
|
112
|
+
else throw new Error("Unknown option: " + args[i]);
|
|
73
113
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if(!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?-[0-9a-f]{20}$/.test(id||''))throw new Error('No previous managed plugin is available');
|
|
80
|
-
const directory=owned(c,join(c.home,'releases',id)),info=verifyBundle(owned(c,join(directory,'plugin')),read(owned(c,join(directory,'integrity.json'))));
|
|
81
|
-
return {id,directory,info};
|
|
82
|
-
}
|
|
83
|
-
function validateConfig(c){
|
|
84
|
-
const result=JSON.parse(run(c.python,[join(packageRoot,'dist/plugin/ledger/read_ledger.py'),'--config',c.config]));
|
|
85
|
-
if(!result.sources?.length||result.sources.some(s=>!s.ok))throw new Error('Configured ledger is missing or unhealthy; no replacement database was created');
|
|
86
|
-
return result;
|
|
87
|
-
}
|
|
88
|
-
function initialize(c){
|
|
89
|
-
if(existsSync(c.config)){validateConfig(c);return false;}
|
|
90
|
-
if(c.explicitConfig)throw new Error('Explicit config does not exist; configure an existing ledger or run init without --config');
|
|
91
|
-
const data=owned(c,join(c.home,'data'));mkdirSync(data,{recursive:true,mode:0o700});
|
|
92
|
-
const db=owned(c,join(data,'coordinator.db'));
|
|
93
|
-
if(!existsSync(db))run(c.python,[join(packageRoot,'dist/plugin/ledger/coordinator.py'),'--db',db,'init']);
|
|
94
|
-
const temp=owned(c,join(c.home,'.config-'+randomUUID()+'.json'));
|
|
95
|
-
const config={sources:[{id:'daily',label:'日常任务',path:db}]};
|
|
96
|
-
try{atomic(temp,config);const candidate={...c,config:temp};validateConfig(candidate);writeFileSync(owned(c,c.config),JSON.stringify(config,null,2)+'\n',{flag:'wx',mode:0o600});}finally{rmSync(temp,{force:true});}
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
function marketplaceFile(c){
|
|
100
|
-
mkdirSync(owned(c,join(c.market,'.agents/plugins')),{recursive:true,mode:0o700});
|
|
101
|
-
atomic(join(c.market,'.agents/plugins/marketplace.json'),{name:'tianshu',interface:{displayName:'天枢'},plugins:[{name:'tianshu',source:{source:'local',path:'./plugins/tianshu'},policy:{installation:'AVAILABLE',authentication:'ON_INSTALL'},category:'Productivity'}]});
|
|
102
|
-
}
|
|
103
|
-
async function recover(c){
|
|
104
|
-
const journal=read(owned(c,c.journal));
|
|
105
|
-
if(journal.codex_home!==c.codexHome||!/^\.backup-[0-9a-f-]+$/.test(journal.backup))throw new Error('Invalid recovery record');
|
|
106
|
-
const backup=owned(c,join(c.home,journal.backup));
|
|
107
|
-
if(existsSync(backup)){rmSync(owned(c,c.active),{recursive:true,force:true});mkdirSync(dirname(c.active),{recursive:true});renameSync(backup,c.active);}
|
|
108
|
-
else if(!journal.had_active)rmSync(owned(c,c.active),{recursive:true,force:true});
|
|
109
|
-
if(journal.before_market){register(c,journal.before_market);}
|
|
110
|
-
else{codex(c,['plugin','remove','tianshu@tianshu','--json']);if(registeredMarket(c))codex(c,['plugin','marketplace','remove','tianshu','--json']);}
|
|
111
|
-
stateWrite(c,journal.before_state);rmSync(c.journal);return {recovered:true,data_preserved:true};
|
|
112
|
-
}
|
|
113
|
-
async function install(c,release){
|
|
114
|
-
const beforeMarket=registeredMarket(c);
|
|
115
|
-
if(beforeMarket&&canonical(beforeMarket.root)!==canonical(c.market)&&!c.explicitConfig&&!c.state)throw new Error('An existing Tianshu installation was found. Use --config with its existing configuration to preserve tasks.');
|
|
116
|
-
initialize(c);validateConfig(c);
|
|
117
|
-
const stage=owned(c,join(c.home,'.stage-'+randomUUID())),backup='.backup-'+randomUUID(),backupPath=owned(c,join(c.home,backup));
|
|
118
|
-
mkdirSync(stage,{mode:0o700});
|
|
119
|
-
try{
|
|
120
|
-
cpSync(join(release.directory,'plugin'),stage,{recursive:true});
|
|
121
|
-
atomic(join(stage,'runtime-config.json'),{config_path:c.config});
|
|
122
|
-
const cache=join(c.codexHome,'plugins/cache/tianshu/tianshu',release.info.version);
|
|
123
|
-
atomic(join(stage,'.mcp.json'),{mcpServers:{tianshu:{command:process.execPath,args:[join(cache,'dist/server.mjs')],env:{TIANSHU_PYTHON:c.python,TIANSHU_CONFIG:c.config,PYTHONDONTWRITEBYTECODE:'1'}}}});
|
|
124
|
-
await probe(join(stage,'dist/server.mjs'),c.python,c.config);
|
|
125
|
-
const journal={codex_home:c.codexHome,backup,had_active:existsSync(c.active),before_state:c.state,before_market:beforeMarket?.root||null};
|
|
126
|
-
atomic(owned(c,c.journal),journal);
|
|
127
|
-
try{
|
|
128
|
-
mkdirSync(owned(c,dirname(c.active)),{recursive:true,mode:0o700});
|
|
129
|
-
if(journal.had_active)renameSync(c.active,backupPath);renameSync(stage,c.active);marketplaceFile(c);
|
|
130
|
-
const installed=register(c,c.market);if(!installed||canonical(installed)!==canonical(cache))throw new Error('Unexpected Codex installed path: '+installed);
|
|
131
|
-
const health=await probe(join(cache,'dist/server.mjs'),c.python,c.config);
|
|
132
|
-
if(health.build?.build_id!==release.info.build_id)throw new Error('Installed build does not match requested build');
|
|
133
|
-
const previous=c.state?.active===release.id?c.state.previous:c.state?.active||null;
|
|
134
|
-
stateWrite(c,{schema:1,codex_home:c.codexHome,active:release.id,previous,config:c.config,installed:true,installed_at:new Date().toISOString(),adopted_marketplace:c.state?.adopted_marketplace||(beforeMarket?.root!==c.market?beforeMarket?.root:null)});
|
|
135
|
-
rmSync(c.journal);rmSync(backupPath,{recursive:true,force:true});
|
|
136
|
-
return {installed:true,plugin:'tianshu@tianshu',...release.info,health,data_preserved:true,reload_client:true};
|
|
137
|
-
}catch(error){
|
|
138
|
-
try{await recover(c);}catch{throw new Error('Install failed and recovery is incomplete. Run tianshu rollback after checking Codex availability; user data was preserved.');}
|
|
139
|
-
throw new Error('Install failed; previous plugin restored: '+error.message);
|
|
140
|
-
}
|
|
141
|
-
}finally{rmSync(stage,{recursive:true,force:true});}
|
|
142
|
-
}
|
|
143
|
-
async function lock(c){
|
|
144
|
-
mkdirSync(c.home,{recursive:true,mode:0o700});const path=owned(c,join(c.home,'lifecycle.lock'));
|
|
145
|
-
// Kernel flock is released on exit/crash. Never unlink the stable lock inode.
|
|
146
|
-
const code='import os,sys,fcntl; fd=os.open(sys.argv[1],os.O_CREAT|os.O_RDWR|os.O_NOFOLLOW,0o600); fcntl.flock(fd,fcntl.LOCK_EX|fcntl.LOCK_NB); print("LOCKED",flush=True); sys.stdin.buffer.read()';
|
|
147
|
-
const child=spawn(c.python,['-c',code,path],{stdio:['pipe','pipe','pipe']});child.stderr.resume();
|
|
148
|
-
await new Promise((resolve,reject)=>{
|
|
149
|
-
const timer=setTimeout(()=>{child.kill();reject(new Error('Lifecycle lock timed out'));},5000);
|
|
150
|
-
child.once('error',error=>{clearTimeout(timer);reject(error);});
|
|
151
|
-
child.stdout.once('data',data=>{clearTimeout(timer);data.toString().trim()==='LOCKED'?resolve():reject(new Error('Lifecycle lock failed'));});
|
|
152
|
-
child.once('exit',()=>{clearTimeout(timer);reject(new Error('Another lifecycle operation is running or lock acquisition failed'));});
|
|
153
|
-
});
|
|
154
|
-
return ()=>new Promise(resolve=>{const timer=setTimeout(()=>child.kill('SIGKILL'),1000);child.once('close',()=>{clearTimeout(timer);resolve();});child.stdin.end();});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function main(argv=process.argv.slice(2)){
|
|
158
|
-
if(!argv.length||argv.includes('--help'))return console.log(help);
|
|
159
|
-
if(argv.length===1&&argv[0]==='--version')return console.log(pkg.version);
|
|
160
|
-
const command=argv.shift(),options={};
|
|
161
|
-
for(let i=0;i<argv.length;i++){if(argv[i]==='--config'&&argv[i+1])options.config=argv[++i];else throw new Error('Unknown option: '+argv[i]);}
|
|
162
|
-
if(!['init','install','upgrade','rollback','uninstall','doctor'].includes(command))throw new Error('Unknown command: '+command);
|
|
163
|
-
const c=environment(options);preflight(c,command!=='init');
|
|
164
|
-
if(command==='doctor'){
|
|
165
|
-
if(existsSync(c.journal))throw new Error('An interrupted lifecycle operation requires tianshu rollback');
|
|
166
|
-
if(!c.state?.installed)throw new Error('Plugin is not installed by this CLI; run tianshu install');
|
|
167
|
-
validateConfig(c);const release=previousRelease(c,c.state.active),cache=join(c.codexHome,'plugins/cache/tianshu/tianshu',release.info.version);
|
|
168
|
-
const market=registeredMarket(c);if(!market||canonical(market.root)!==canonical(c.market))throw new Error('Codex marketplace does not match this installation');
|
|
169
|
-
const health=await probe(join(cache,'dist/server.mjs'),c.python,c.config);if(health.build?.build_id!==release.info.build_id)throw new Error('Installed build drift detected');
|
|
170
|
-
return print({ok:true,home:c.home,config:c.config,health,host_ui_verified:false});
|
|
114
|
+
const python = pythonRuntime(), config = configPath(explicit);
|
|
115
|
+
if (command === "doctor") {
|
|
116
|
+
validate(python, config);
|
|
117
|
+
const health = await probe(join2(packageRoot, "dist/server.mjs"), python, config);
|
|
118
|
+
return print({ ok: true, config, health, scope: "this_package", host_ui_verified: false });
|
|
171
119
|
}
|
|
172
|
-
if(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if(command==='upgrade'&&!c.state?.installed)throw new Error('No managed installation; run tianshu install first');
|
|
190
|
-
const release=command==='rollback'?previousRelease(c,c.state?.previous):archive(c);
|
|
191
|
-
return print(await install(c,release));
|
|
192
|
-
}finally{await unlock();}
|
|
120
|
+
if (existsSync(config)) {
|
|
121
|
+
validate(python, config);
|
|
122
|
+
return print({ initialized: false, config, data_preserved: true });
|
|
123
|
+
}
|
|
124
|
+
if (explicit || process.env.TIANSHU_CONFIG) throw new Error("Explicit config does not exist; init never creates a replacement for a specified configuration");
|
|
125
|
+
const directory = dirname(config);
|
|
126
|
+
if (existsSync(directory) && lstatSync(directory).isSymbolicLink()) throw new Error("Data home must not be a symbolic link");
|
|
127
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
128
|
+
const data = join2(directory, "data");
|
|
129
|
+
if (existsSync(data) && lstatSync(data).isSymbolicLink()) throw new Error("Data directory must not be a symbolic link");
|
|
130
|
+
mkdirSync(data, { recursive: true, mode: 448 });
|
|
131
|
+
const db = join2(data, "coordinator.db");
|
|
132
|
+
if (!existsSync(db)) run(python, [join2(packageRoot, "ledger/coordinator.py"), "--db", db, "init"]);
|
|
133
|
+
const value = { sources: [{ id: "daily", label: "\u65E5\u5E38\u4EFB\u52A1", path: db }] };
|
|
134
|
+
run(python, ["-c", 'import sys;sys.path.insert(0,sys.argv[1]);from read_ledger import read_source;read_source({"id":"daily","label":"\u65E5\u5E38\u4EFB\u52A1","path":sys.argv[2]})', join2(packageRoot, "ledger"), db]);
|
|
135
|
+
writeFileSync(config, JSON.stringify(value, null, 2) + "\n", { flag: "wx", mode: 384 });
|
|
136
|
+
return print({ initialized: true, config, data_preserved: true });
|
|
193
137
|
}
|
|
138
|
+
export {
|
|
139
|
+
main
|
|
140
|
+
};
|