@lumi-ai-lab/harness-data 0.0.5 → 0.0.6
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/package.json +1 -1
- package/src/commands/install.js +105 -22
- package/src/commands/update.js +89 -37
- package/src/lib/exec.js +1 -1
- package/src/lib/log.js +39 -0
- package/src/lib/manifest.js +5 -5
- package/src/lib/prompt.js +1 -1
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -9,6 +9,8 @@ import { binaryName, platformKey } from "../lib/platform.js";
|
|
|
9
9
|
import { collectDoctor } from "./doctor.js";
|
|
10
10
|
import { packageVersion } from "../lib/package.js";
|
|
11
11
|
import { downloadReleaseAsset, findReleaseAsset, githubToken, hasGithubAuth, latestRelease } from "../lib/github.js";
|
|
12
|
+
import { action, blank, fail, header, ok, shortSha, skip, step, warn } from "../lib/log.js";
|
|
13
|
+
import { gitUrls, runGitWithProtocol } from "../lib/git-auth.js";
|
|
12
14
|
|
|
13
15
|
const runtimeRepo = "lumi-ai-lab/harness-data";
|
|
14
16
|
const wikisRepo = "lumi-ai-lab/harness-data-wikis";
|
|
@@ -17,6 +19,7 @@ async function requireCommands(commands) {
|
|
|
17
19
|
for (const command of commands) {
|
|
18
20
|
if (!(await commandExists(command))) throw new Error(`missing required command: ${command}`);
|
|
19
21
|
}
|
|
22
|
+
ok(commands.join(", "));
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
async function prepareRuntimeDir(options) {
|
|
@@ -29,6 +32,7 @@ export async function installRuntimeBundle(runtimeDir, options = {}) {
|
|
|
29
32
|
if (!options.force && fs.existsSync(path.join(runtimeDir, "agents")) &&
|
|
30
33
|
fs.existsSync(path.join(runtimeDir, "config")) &&
|
|
31
34
|
fs.existsSync(path.join(runtimeDir, "bootstrap", "cli-manifest.json"))) {
|
|
35
|
+
skip("runtime bundle 已存在");
|
|
32
36
|
return { tag: "", skipped: true };
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -42,6 +46,7 @@ export async function installRuntimeBundle(runtimeDir, options = {}) {
|
|
|
42
46
|
const cacheDir = path.join(runtimeDir, ".bootstrap-cache");
|
|
43
47
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
44
48
|
const archive = path.join(cacheDir, assetName);
|
|
49
|
+
action(`下载 harness-data-runtime ${tag}`);
|
|
45
50
|
await downloadReleaseAsset(asset, archive, options);
|
|
46
51
|
if (shaAsset) {
|
|
47
52
|
const shaFile = `${archive}.sha256`;
|
|
@@ -51,11 +56,11 @@ export async function installRuntimeBundle(runtimeDir, options = {}) {
|
|
|
51
56
|
const actual = crypto.createHash("sha256").update(fs.readFileSync(archive)).digest("hex");
|
|
52
57
|
if (expected && actual !== expected) throw new Error("runtime bundle sha256 mismatch");
|
|
53
58
|
} else {
|
|
54
|
-
|
|
59
|
+
warn("runtime bundle 未提供 sha256,已继续安装");
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
const extractDir = fs.mkdtempSync(path.join(cacheDir, "runtime-"));
|
|
58
|
-
await run("tar", ["-xzf", archive, "-C", extractDir]
|
|
63
|
+
await run("tar", ["-xzf", archive, "-C", extractDir]);
|
|
59
64
|
for (const dir of ["agents", "bootstrap"]) {
|
|
60
65
|
const source = path.join(extractDir, dir);
|
|
61
66
|
if (!fs.existsSync(source)) throw new Error(`runtime bundle missing ${dir}/`);
|
|
@@ -69,6 +74,7 @@ export async function installRuntimeBundle(runtimeDir, options = {}) {
|
|
|
69
74
|
fs.copyFileSync(path.join(configSource, file), path.join(runtimeDir, "config", file));
|
|
70
75
|
}
|
|
71
76
|
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
77
|
+
ok(`runtime bundle ${tag}`);
|
|
72
78
|
return { tag, skipped: false };
|
|
73
79
|
}
|
|
74
80
|
|
|
@@ -83,8 +89,11 @@ function executable(file) {
|
|
|
83
89
|
|
|
84
90
|
async function promptExecutable(runtimeDir, name, options = {}) {
|
|
85
91
|
const auto = path.join(runtimeDir, "bin", binaryName(name));
|
|
86
|
-
if (executable(auto))
|
|
87
|
-
|
|
92
|
+
if (executable(auto)) {
|
|
93
|
+
ok(`自动识别 ${name}: ${auto}`);
|
|
94
|
+
return auto;
|
|
95
|
+
}
|
|
96
|
+
const value = await ask(`请输入 ${name} 的绝对路径:`, options);
|
|
88
97
|
const file = path.resolve(value);
|
|
89
98
|
if (!executable(file)) throw new Error(`${name} path is missing or not executable: ${file}`);
|
|
90
99
|
return file;
|
|
@@ -110,30 +119,35 @@ async function installWikis(runtimeDir, options = {}) {
|
|
|
110
119
|
const target = path.join(runtimeDir, "wikis");
|
|
111
120
|
if (githubToken(options)) {
|
|
112
121
|
if (fs.existsSync(path.join(target, ".git"))) {
|
|
113
|
-
await
|
|
122
|
+
await runGitWithProtocol("https", ["-C", target, "pull", "--ff-only"], options);
|
|
114
123
|
} else {
|
|
115
124
|
fs.rmSync(target, { recursive: true, force: true });
|
|
116
|
-
await
|
|
125
|
+
await runGitWithProtocol("https", ["clone", gitUrls.https.wikis, target], options);
|
|
117
126
|
}
|
|
118
|
-
|
|
127
|
+
const commit = (await run("git", ["-C", target, "rev-parse", "HEAD"])).stdout.trim();
|
|
128
|
+
ok(`harness-data-wikis ${shortSha(commit)}`);
|
|
129
|
+
return { mode: "github", path: target, commit };
|
|
119
130
|
}
|
|
120
131
|
if (await hasGithubAuth(options)) {
|
|
121
132
|
if (fs.existsSync(path.join(target, ".git"))) {
|
|
122
|
-
await run("git", ["-C", target, "pull", "--ff-only"]
|
|
133
|
+
await run("git", ["-C", target, "pull", "--ff-only"]);
|
|
123
134
|
} else {
|
|
124
135
|
fs.rmSync(target, { recursive: true, force: true });
|
|
125
|
-
await run("gh", ["repo", "clone", wikisRepo, target]
|
|
136
|
+
await run("gh", ["repo", "clone", wikisRepo, target]);
|
|
126
137
|
}
|
|
127
|
-
|
|
138
|
+
const commit = (await run("git", ["-C", target, "rev-parse", "HEAD"])).stdout.trim();
|
|
139
|
+
ok(`harness-data-wikis ${shortSha(commit)}`);
|
|
140
|
+
return { mode: "github", path: target, commit };
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
const auto = path.join(runtimeDir, "harness-data-wikis");
|
|
131
|
-
const source = fs.existsSync(auto) ? auto : path.resolve(await ask("
|
|
144
|
+
const source = fs.existsSync(auto) ? auto : path.resolve(await ask("请输入 harness-data-wikis 的绝对路径:", options));
|
|
132
145
|
for (const dir of ["spec", "playbooks", "templates"]) {
|
|
133
146
|
if (!fs.existsSync(path.join(source, dir))) throw new Error(`harness-data-wikis missing ${dir}/: ${source}`);
|
|
134
147
|
}
|
|
135
148
|
fs.rmSync(target, { recursive: true, force: true });
|
|
136
149
|
fs.cpSync(source, target, { recursive: true });
|
|
150
|
+
ok(`harness-data-wikis 本地路径 ${source}`);
|
|
137
151
|
return { mode: "local-path", source, path: target };
|
|
138
152
|
}
|
|
139
153
|
|
|
@@ -142,12 +156,13 @@ function casConfigDir(runtimeDir) {
|
|
|
142
156
|
}
|
|
143
157
|
|
|
144
158
|
async function writeCasCredentials(runtimeDir, options = {}) {
|
|
145
|
-
const username = await ask("CAS
|
|
146
|
-
const password = await askSecret("CAS
|
|
159
|
+
const username = await ask("CAS 用户名:", options);
|
|
160
|
+
const password = await askSecret("CAS 密码:", options);
|
|
147
161
|
if (!username || !password) throw new Error("CAS username and password are required");
|
|
148
162
|
const dir = casConfigDir(runtimeDir);
|
|
149
163
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
150
164
|
fs.writeFileSync(path.join(dir, "config.json"), `${JSON.stringify({ cas: { username, password } }, null, 2)}\n`, { mode: 0o600 });
|
|
165
|
+
ok("CAS 凭证已保存到 .qdm-auth/cas/config.json");
|
|
151
166
|
return dir;
|
|
152
167
|
}
|
|
153
168
|
|
|
@@ -155,24 +170,66 @@ async function configureTokens(runtimeDir, casDir) {
|
|
|
155
170
|
const env = { QDM_CAS_CONFIG_DIR: casDir };
|
|
156
171
|
const bin = (name) => path.join(runtimeDir, "bin", binaryName(name));
|
|
157
172
|
const cmrToken = (await run(bin("cas-cli"), ["token", "--app", "cmr"], { cwd: runtimeDir, env })).stdout.trim();
|
|
158
|
-
await run(bin("qdm-cmr-cli"), ["config", "set-token", cmrToken], { cwd: runtimeDir, env
|
|
173
|
+
await run(bin("qdm-cmr-cli"), ["config", "set-token", cmrToken], { cwd: runtimeDir, env });
|
|
174
|
+
ok("CMR Token 已配置");
|
|
159
175
|
const indicatorsToken = (await run(bin("cas-cli"), ["token", "--app", "indicators"], { cwd: runtimeDir, env })).stdout.trim();
|
|
160
|
-
await run(bin("qdm-indicators-cli"), ["config", "set-token", indicatorsToken], { cwd: runtimeDir, env
|
|
161
|
-
|
|
162
|
-
await run(bin("qdm-
|
|
176
|
+
await run(bin("qdm-indicators-cli"), ["config", "set-token", indicatorsToken], { cwd: runtimeDir, env });
|
|
177
|
+
ok("Indicators Token 已配置");
|
|
178
|
+
await run(bin("qdm-cmr-cli"), ["config", "check-token"], { cwd: runtimeDir, env });
|
|
179
|
+
await run(bin("qdm-indicators-cli"), ["config", "check-token"], { cwd: runtimeDir, env });
|
|
163
180
|
}
|
|
164
181
|
|
|
165
182
|
export async function buildAndCheck(runtimeDir, options = {}) {
|
|
166
183
|
const cli = path.join(runtimeDir, "bin", binaryName("data-harness-cli"));
|
|
167
|
-
|
|
168
|
-
|
|
184
|
+
action("执行:data-harness-cli wikis build-index --skip-checks");
|
|
185
|
+
const result = await run(cli, ["wikis", "build-index", "--skip-checks"], { cwd: runtimeDir, allowFailure: true });
|
|
186
|
+
if (result.code !== 0) {
|
|
187
|
+
warn("wikis 索引构建失败,安装会继续;后续可手动执行 data-harness-cli wikis build-index --skip-checks");
|
|
188
|
+
return { ok: false };
|
|
189
|
+
}
|
|
190
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
191
|
+
const docs = output.match(/\bdocs=(\d+)/)?.[1];
|
|
192
|
+
const recall = output.match(/\brecall=(\d+)/)?.[1];
|
|
193
|
+
const runtimeDocs = output.match(/\bruntimeDocs=(\d+)/)?.[1];
|
|
194
|
+
ok([docs ? `docs=${docs}` : "", recall ? `recall=${recall}` : "", runtimeDocs ? `runtimeDocs=${runtimeDocs}` : ""].filter(Boolean).join(", ") || "Wikis 索引已构建");
|
|
195
|
+
return { ok: true, docs, recall, runtimeDocs };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function printDoctorSummary(doctor) {
|
|
199
|
+
const failed = doctor.checks.filter((check) => !check.ok);
|
|
200
|
+
if (!failed.length) {
|
|
201
|
+
ok("runtime");
|
|
202
|
+
ok("wikis/spec");
|
|
203
|
+
ok("wikis/playbooks");
|
|
204
|
+
ok("wikis/templates");
|
|
205
|
+
ok("4 个 CLI");
|
|
206
|
+
ok("本地配置");
|
|
207
|
+
ok("CAS 凭证");
|
|
208
|
+
ok("CMR Token");
|
|
209
|
+
ok("Indicators Token");
|
|
210
|
+
ok("Agent Hook");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const check of failed) fail(`${check.name}${check.detail ? ` (${check.detail})` : ""}`);
|
|
169
214
|
}
|
|
170
215
|
|
|
171
216
|
export async function installCommand(options = {}) {
|
|
172
|
-
platformKey();
|
|
217
|
+
const key = platformKey();
|
|
218
|
+
header("Harness Data 安装器", packageVersion(), [
|
|
219
|
+
`安装目录:${resolveWorkspaceDir(options.dir || process.cwd())}`,
|
|
220
|
+
`平台:${key}`
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
step(1, 8, "检查本机依赖");
|
|
173
224
|
await requireCommands(["git", "tar", "unzip"]);
|
|
225
|
+
blank();
|
|
226
|
+
|
|
227
|
+
step(2, 8, "安装 runtime bundle");
|
|
174
228
|
const runtimeDir = await prepareRuntimeDir(options);
|
|
175
229
|
const bundle = await installRuntimeBundle(runtimeDir, options);
|
|
230
|
+
blank();
|
|
231
|
+
|
|
232
|
+
step(3, 8, "安装 CLI 工具");
|
|
176
233
|
const manifestPath = path.resolve(options.manifest || path.join(runtimeDir, "bootstrap", "cli-manifest.json"));
|
|
177
234
|
const tokenMode = await hasGithubAuth(options);
|
|
178
235
|
|
|
@@ -185,17 +242,40 @@ export async function installCommand(options = {}) {
|
|
|
185
242
|
await installToolsFromManifest(runtimeDir, manifestPath, { ...options, tools: ["data-harness-cli"] });
|
|
186
243
|
localTools = await installLocalTools(runtimeDir, options);
|
|
187
244
|
}
|
|
245
|
+
ok(`${Object.keys(manifest.installedTools || {}).length + Object.keys(localTools).length} 个 CLI 已安装到 bin/`);
|
|
246
|
+
blank();
|
|
188
247
|
|
|
248
|
+
step(4, 8, "同步 Wikis 知识库");
|
|
189
249
|
await installWikis(runtimeDir, options);
|
|
250
|
+
blank();
|
|
251
|
+
|
|
252
|
+
step(5, 8, "生成本地配置");
|
|
190
253
|
writeLocalConfig(runtimeDir, { overwrite: true });
|
|
254
|
+
ok("config/harness-config.yaml");
|
|
255
|
+
ok("config/qdm-cli-paths.env");
|
|
256
|
+
blank();
|
|
257
|
+
|
|
258
|
+
step(6, 8, "配置 CAS 认证");
|
|
191
259
|
const casDir = await writeCasCredentials(runtimeDir, options);
|
|
192
260
|
await configureTokens(runtimeDir, casDir);
|
|
261
|
+
blank();
|
|
262
|
+
|
|
263
|
+
step(7, 8, "构建 Wikis 索引");
|
|
193
264
|
await buildAndCheck(runtimeDir, options);
|
|
265
|
+
blank();
|
|
266
|
+
|
|
267
|
+
step(8, 8, "配置 Agent Hook");
|
|
194
268
|
linkAgents(runtimeDir, await chooseAgent(options));
|
|
269
|
+
for (const [target, source] of [[".claude", "agents/claude"], [".codex", "agents/codex"], [".pi", "agents/pi"]]) {
|
|
270
|
+
if (fs.existsSync(path.join(runtimeDir, target))) ok(`${target} -> ${source}`);
|
|
271
|
+
}
|
|
272
|
+
blank();
|
|
195
273
|
|
|
274
|
+
console.log("安装校验");
|
|
196
275
|
const doctor = await collectDoctor(runtimeDir, { ...options, casConfigDir: casDir });
|
|
197
|
-
|
|
276
|
+
printDoctorSummary(doctor);
|
|
198
277
|
if (doctor.checks.some((check) => !check.ok)) throw new Error("doctor failed; install is incomplete");
|
|
278
|
+
blank();
|
|
199
279
|
|
|
200
280
|
writeState(runtimeDir, {
|
|
201
281
|
installMode: tokenMode ? "github-token" : "local-path",
|
|
@@ -206,5 +286,8 @@ export async function installCommand(options = {}) {
|
|
|
206
286
|
packageVersion: packageVersion(),
|
|
207
287
|
lastCheckAt: new Date().toISOString()
|
|
208
288
|
});
|
|
209
|
-
console.log(
|
|
289
|
+
console.log(`安装完成:${runtimeDir}`);
|
|
290
|
+
console.log("");
|
|
291
|
+
console.log("下一步:");
|
|
292
|
+
console.log(`cd ${runtimeDir}`);
|
|
210
293
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -7,8 +7,9 @@ import { installToolsFromManifest, manifestDigest, readManifest } from "../lib/m
|
|
|
7
7
|
import { packageVersion } from "../lib/package.js";
|
|
8
8
|
import { platformKey } from "../lib/platform.js";
|
|
9
9
|
import { githubToken, latestRelease } from "../lib/github.js";
|
|
10
|
-
import { buildAndCheck, installRuntimeBundle } from "./install.js";
|
|
10
|
+
import { buildAndCheck, installRuntimeBundle, printDoctorSummary } from "./install.js";
|
|
11
11
|
import { collectDoctor } from "./doctor.js";
|
|
12
|
+
import { action, blank, header, ok, shortSha, skip, step, warn } from "../lib/log.js";
|
|
12
13
|
|
|
13
14
|
async function npmLatest() {
|
|
14
15
|
try {
|
|
@@ -60,21 +61,20 @@ async function maybeUpdateTool(runtimeDir, manifest, tool, options, state) {
|
|
|
60
61
|
const assetName = toolAssetName(tool, tag, key);
|
|
61
62
|
const asset = releaseAsset(release, assetName);
|
|
62
63
|
if (!asset) {
|
|
63
|
-
|
|
64
|
+
warn(`${tool.name} 最新 release 缺少 ${assetName},已跳过`);
|
|
64
65
|
return null;
|
|
65
66
|
}
|
|
66
67
|
const current = state.tools?.[tool.name] || {};
|
|
67
68
|
const tagChanged = current.version && current.version !== tag;
|
|
68
69
|
const firstInstall = !current.version;
|
|
69
70
|
if (!tagChanged && !firstInstall) {
|
|
70
|
-
|
|
71
|
+
ok(`${tool.name} 已是最新 ${tag}`);
|
|
71
72
|
return null;
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
console.log(`Skipped ${tool.name}`);
|
|
74
|
+
action(`发现更新:${tool.name} ${current.version || "unknown"} -> ${tag}`);
|
|
75
|
+
if (!(await confirm(`是否更新 ${tool.name}?`, { defaultNo: true }))) {
|
|
76
|
+
skip(tool.name);
|
|
77
|
+
options.skippedUpdates?.push(`${tool.name} ${tag}`);
|
|
78
78
|
return null;
|
|
79
79
|
}
|
|
80
80
|
const updatedManifest = oneToolManifest(manifest, tool, tag, asset, key);
|
|
@@ -82,35 +82,38 @@ async function maybeUpdateTool(runtimeDir, manifest, tool, options, state) {
|
|
|
82
82
|
...options,
|
|
83
83
|
manifestOverride: updatedManifest
|
|
84
84
|
});
|
|
85
|
-
|
|
85
|
+
const result = installed.installedTools?.[tool.name] || { version: tag, asset: assetName };
|
|
86
|
+
ok(`${tool.name} 已更新到 ${tag}`);
|
|
87
|
+
return result;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
async function updateWikis(runtimeDir, options, state) {
|
|
89
91
|
const wikisDir = path.join(runtimeDir, "wikis");
|
|
90
92
|
if (!githubToken(options) && state.installMode !== "github-token") {
|
|
91
|
-
|
|
93
|
+
skip("harness-data-wikis 为本地路径模式,请手动检查");
|
|
92
94
|
return null;
|
|
93
95
|
}
|
|
94
96
|
if (!fs.existsSync(path.join(wikisDir, ".git"))) {
|
|
95
|
-
|
|
97
|
+
skip("harness-data-wikis 不是 git checkout");
|
|
96
98
|
return null;
|
|
97
99
|
}
|
|
98
|
-
await run("git", ["-C", wikisDir, "fetch", "origin"]
|
|
100
|
+
await run("git", ["-C", wikisDir, "fetch", "origin"]);
|
|
99
101
|
const local = (await run("git", ["-C", wikisDir, "rev-parse", "HEAD"])).stdout.trim();
|
|
100
102
|
const remote = (await run("git", ["-C", wikisDir, "rev-parse", "origin/HEAD"], { allowFailure: true })).stdout.trim();
|
|
101
103
|
if (!remote || local === remote) {
|
|
102
|
-
|
|
104
|
+
ok(`harness-data-wikis 已是最新 ${shortSha(local)}`);
|
|
103
105
|
return null;
|
|
104
106
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
console.log("Skipped wikis");
|
|
107
|
+
action(`发现更新:harness-data-wikis ${shortSha(local)} -> ${shortSha(remote)}`);
|
|
108
|
+
if (!(await confirm("是否更新 harness-data-wikis?", { defaultNo: true }))) {
|
|
109
|
+
skip("harness-data-wikis");
|
|
110
|
+
options.skippedUpdates?.push(`harness-data-wikis ${shortSha(remote)}`);
|
|
110
111
|
return null;
|
|
111
112
|
}
|
|
112
|
-
await run("git", ["-C", wikisDir, "pull", "--ff-only"]
|
|
113
|
-
|
|
113
|
+
await run("git", ["-C", wikisDir, "pull", "--ff-only"]);
|
|
114
|
+
const commit = (await run("git", ["-C", wikisDir, "rev-parse", "HEAD"])).stdout.trim();
|
|
115
|
+
ok(`harness-data-wikis 已更新到 ${shortSha(commit)}`);
|
|
116
|
+
return { commit };
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
export async function checkUpdates(workspace, options = {}) {
|
|
@@ -126,54 +129,85 @@ export async function checkUpdates(workspace, options = {}) {
|
|
|
126
129
|
export async function updateCommand(options = {}) {
|
|
127
130
|
const runtimeDir = findWorkspaceDir(options.dir);
|
|
128
131
|
if (!fs.existsSync(runtimeDir)) throw new Error(`runtime directory does not exist: ${runtimeDir}`);
|
|
132
|
+
const key = platformKey();
|
|
133
|
+
header("Harness Data 更新器", packageVersion(), [
|
|
134
|
+
`运行目录:${runtimeDir}`,
|
|
135
|
+
`平台:${key}`
|
|
136
|
+
]);
|
|
129
137
|
const state = readUserState();
|
|
130
138
|
const manifestPath = path.join(runtimeDir, "bootstrap", "cli-manifest.json");
|
|
131
139
|
const manifest = readManifest(manifestPath);
|
|
132
140
|
let changed = false;
|
|
133
141
|
let runtimeTag = state.runtimeTag || "";
|
|
134
142
|
const nextTools = { ...(state.tools || {}) };
|
|
143
|
+
const applied = [];
|
|
144
|
+
const skipped = [];
|
|
145
|
+
const trackingOptions = { ...options, skippedUpdates: skipped };
|
|
135
146
|
|
|
147
|
+
step(1, 6, "检查 installer");
|
|
136
148
|
const latestInstaller = await npmLatest();
|
|
137
149
|
if (latestInstaller && latestInstaller !== packageVersion()) {
|
|
138
|
-
|
|
139
|
-
|
|
150
|
+
warn(`installer 有新版本 ${packageVersion()} -> ${latestInstaller}`);
|
|
151
|
+
action("请重新执行:npx @lumi-ai-lab/harness-data@latest update");
|
|
140
152
|
} else {
|
|
141
|
-
|
|
153
|
+
ok(`installer 已是最新 ${packageVersion()}`);
|
|
142
154
|
}
|
|
155
|
+
blank();
|
|
143
156
|
|
|
157
|
+
step(2, 6, "检查 runtime bundle");
|
|
144
158
|
const runtimeRelease = await latestRelease("lumi-ai-lab/harness-data", options);
|
|
145
159
|
if (state.runtimeTag && state.runtimeTag !== runtimeRelease.tag_name) {
|
|
146
|
-
|
|
147
|
-
if (await confirm("
|
|
148
|
-
const bundle = await installRuntimeBundle(runtimeDir, { ...
|
|
160
|
+
action(`发现更新:runtime bundle ${state.runtimeTag} -> ${runtimeRelease.tag_name}`);
|
|
161
|
+
if (await confirm("是否更新 runtime bundle?", { defaultNo: true })) {
|
|
162
|
+
const bundle = await installRuntimeBundle(runtimeDir, { ...trackingOptions, force: true });
|
|
149
163
|
runtimeTag = bundle.tag || runtimeRelease.tag_name;
|
|
150
164
|
changed = true;
|
|
165
|
+
applied.push(`runtime bundle ${runtimeTag}`);
|
|
151
166
|
} else {
|
|
152
|
-
|
|
167
|
+
skip("runtime bundle");
|
|
168
|
+
skipped.push(`runtime bundle ${runtimeRelease.tag_name}`);
|
|
153
169
|
}
|
|
154
170
|
} else {
|
|
155
|
-
|
|
171
|
+
ok(`runtime bundle 已是最新 ${state.runtimeTag || runtimeRelease.tag_name}`);
|
|
156
172
|
}
|
|
173
|
+
blank();
|
|
157
174
|
|
|
175
|
+
step(3, 6, "检查 CLI 工具");
|
|
158
176
|
for (const tool of manifest.tools || []) {
|
|
159
177
|
if (state.installMode === "local-path" && tool.name !== "data-harness-cli") {
|
|
160
|
-
|
|
178
|
+
skip(`${tool.name} 为本地路径模式,请手动检查`);
|
|
161
179
|
continue;
|
|
162
180
|
}
|
|
163
|
-
const installed = await maybeUpdateTool(runtimeDir, manifest, tool,
|
|
181
|
+
const installed = await maybeUpdateTool(runtimeDir, manifest, tool, trackingOptions, state);
|
|
164
182
|
if (installed) {
|
|
165
183
|
nextTools[tool.name] = installed;
|
|
166
184
|
changed = true;
|
|
185
|
+
applied.push(`${tool.name} ${installed.version || ""}`.trim());
|
|
167
186
|
}
|
|
168
187
|
}
|
|
188
|
+
blank();
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
|
|
190
|
+
step(4, 6, "检查 Wikis 知识库");
|
|
191
|
+
const wikis = await updateWikis(runtimeDir, trackingOptions, state);
|
|
192
|
+
if (wikis) {
|
|
193
|
+
changed = true;
|
|
194
|
+
applied.push(`harness-data-wikis ${shortSha(wikis.commit)}`);
|
|
195
|
+
}
|
|
196
|
+
blank();
|
|
172
197
|
|
|
198
|
+
step(5, 6, "构建 Wikis 索引");
|
|
173
199
|
if (changed) {
|
|
174
|
-
await buildAndCheck(runtimeDir,
|
|
175
|
-
|
|
176
|
-
|
|
200
|
+
await buildAndCheck(runtimeDir, trackingOptions);
|
|
201
|
+
} else {
|
|
202
|
+
skip("没有组件更新");
|
|
203
|
+
}
|
|
204
|
+
blank();
|
|
205
|
+
|
|
206
|
+
step(6, 6, "安装校验");
|
|
207
|
+
if (changed) {
|
|
208
|
+
const doctor = await collectDoctor(runtimeDir, trackingOptions);
|
|
209
|
+
printDoctorSummary(doctor);
|
|
210
|
+
if (doctor.checks.some((check) => !check.ok)) throw new Error("doctor failed; update is incomplete");
|
|
177
211
|
writeState(runtimeDir, {
|
|
178
212
|
...state,
|
|
179
213
|
runtimeTag,
|
|
@@ -181,9 +215,27 @@ export async function updateCommand(options = {}) {
|
|
|
181
215
|
manifestSha256: manifestDigest(manifest),
|
|
182
216
|
lastCheckAt: new Date().toISOString()
|
|
183
217
|
});
|
|
184
|
-
|
|
218
|
+
blank();
|
|
219
|
+
console.log(`更新完成:${runtimeDir}`);
|
|
220
|
+
if (applied.length) {
|
|
221
|
+
console.log("");
|
|
222
|
+
console.log("已更新:");
|
|
223
|
+
for (const item of applied) console.log(`- ${item}`);
|
|
224
|
+
}
|
|
225
|
+
if (skipped.length) {
|
|
226
|
+
console.log("");
|
|
227
|
+
console.log("已跳过:");
|
|
228
|
+
for (const item of skipped) console.log(`- ${item}`);
|
|
229
|
+
}
|
|
185
230
|
} else {
|
|
231
|
+
skip("没有组件更新");
|
|
186
232
|
writeState(runtimeDir, { ...state, lastCheckAt: new Date().toISOString() });
|
|
187
|
-
|
|
233
|
+
blank();
|
|
234
|
+
console.log(skipped.length ? "没有应用任何更新。" : "没有发现需要更新的内容。");
|
|
235
|
+
if (skipped.length) {
|
|
236
|
+
console.log("");
|
|
237
|
+
console.log("已跳过:");
|
|
238
|
+
for (const item of skipped) console.log(`- ${item}`);
|
|
239
|
+
}
|
|
188
240
|
}
|
|
189
241
|
}
|
package/src/lib/exec.js
CHANGED
|
@@ -4,7 +4,7 @@ export function run(command, args = [], options = {}) {
|
|
|
4
4
|
return new Promise((resolve, reject) => {
|
|
5
5
|
const child = spawn(command, args, {
|
|
6
6
|
cwd: options.cwd,
|
|
7
|
-
env: { ...process.env, ...(options.env || {}) },
|
|
7
|
+
env: { ...process.env, GH_NO_UPDATE_NOTIFIER: "1", ...(options.env || {}) },
|
|
8
8
|
shell: options.shell || false,
|
|
9
9
|
stdio: options.stdio || "pipe"
|
|
10
10
|
});
|
package/src/lib/log.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function header(title, version, rows = []) {
|
|
2
|
+
console.log(`${title} ${version}`);
|
|
3
|
+
console.log("");
|
|
4
|
+
for (const row of rows) console.log(row);
|
|
5
|
+
if (rows.length) console.log("");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function step(index, total, title) {
|
|
9
|
+
console.log(`[${index}/${total}] ${title}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ok(message) {
|
|
13
|
+
console.log(`通过:${message}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function warn(message) {
|
|
17
|
+
console.warn(`提示:${message}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function skip(message) {
|
|
21
|
+
console.log(`跳过:${message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function fail(message) {
|
|
25
|
+
console.log(`失败:${message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function action(message) {
|
|
29
|
+
console.log(message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function blank() {
|
|
33
|
+
console.log("");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function shortSha(value) {
|
|
37
|
+
return value ? value.slice(0, 7) : "";
|
|
38
|
+
}
|
|
39
|
+
|
package/src/lib/manifest.js
CHANGED
|
@@ -4,6 +4,7 @@ import https from "node:https";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { commandExists, run } from "./exec.js";
|
|
6
6
|
import { platformKey } from "./platform.js";
|
|
7
|
+
import { action, warn } from "./log.js";
|
|
7
8
|
|
|
8
9
|
export function readManifest(file) {
|
|
9
10
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
@@ -115,7 +116,6 @@ async function downloadAsset(tool, asset, file, options = {}) {
|
|
|
115
116
|
await download(asset.url, file);
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
|
-
console.log(`Downloading private ${tool.name} asset from ${tool.repo}`);
|
|
119
119
|
if (await downloadPrivateWithGh(asset, file)) return;
|
|
120
120
|
if (await downloadPrivateWithToken(asset, file, options)) return;
|
|
121
121
|
throw new Error(`private GitHub Release asset requires gh auth login, GITHUB_TOKEN, or --github-token: ${assetName(asset)}`);
|
|
@@ -151,16 +151,16 @@ export async function installToolsFromManifest(workspace, manifestPath, options
|
|
|
151
151
|
const asset = tool.platforms?.[key];
|
|
152
152
|
if (!asset?.url) throw new Error(`manifest missing ${tool.name} asset for ${key}`);
|
|
153
153
|
const archive = path.join(cacheDir, assetName(asset));
|
|
154
|
-
|
|
154
|
+
if (options.log !== false) action(`下载 ${tool.name} ${tool.version} (${key})`);
|
|
155
155
|
await downloadAsset(tool, asset, archive, options);
|
|
156
156
|
const sha = await expectedSha256(tool, asset, options);
|
|
157
157
|
const actualSha = fileSha256(archive);
|
|
158
158
|
if (sha && actualSha !== sha) throw new Error(`${tool.name} sha256 mismatch`);
|
|
159
|
-
if (!sha)
|
|
159
|
+
if (!sha) warn(`${tool.name} 未提供 sha256,已继续安装`);
|
|
160
160
|
if (archive.endsWith(".zip")) {
|
|
161
|
-
await run("unzip", ["-o", archive, "-d", binDir]
|
|
161
|
+
await run("unzip", ["-o", archive, "-d", binDir]);
|
|
162
162
|
} else {
|
|
163
|
-
await run("tar", ["-xzf", archive, "-C", binDir]
|
|
163
|
+
await run("tar", ["-xzf", archive, "-C", binDir]);
|
|
164
164
|
}
|
|
165
165
|
const binary = path.join(binDir, tool.binary);
|
|
166
166
|
if (!fs.existsSync(binary)) throw new Error(`${tool.binary} was not extracted to bin/`);
|
package/src/lib/prompt.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function chooseAgent(options = {}) {
|
|
|
26
26
|
if (options.yes) return "all";
|
|
27
27
|
const rl = readline.createInterface({ input, output });
|
|
28
28
|
try {
|
|
29
|
-
const answer = (await rl.question("
|
|
29
|
+
const answer = (await rl.question("选择 Agent:claude, codex, pi, all [all] ")).trim().toLowerCase();
|
|
30
30
|
const value = answer || "all";
|
|
31
31
|
if (!agentChoices.includes(value)) throw new Error("agent must be claude, codex, pi, or all");
|
|
32
32
|
return value;
|