@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumi-ai-lab/harness-data",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Installer and updater for Harness Data",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- console.warn("warning: runtime bundle has no sha256 asset; continuing without checksum");
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], { stdio: "inherit" });
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)) return auto;
87
- const value = await ask(`Path to ${name}:`, options);
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 run("git", ["-C", target, "pull", "--ff-only"], { stdio: "inherit" });
122
+ await runGitWithProtocol("https", ["-C", target, "pull", "--ff-only"], options);
114
123
  } else {
115
124
  fs.rmSync(target, { recursive: true, force: true });
116
- await run("git", ["clone", `https://x-access-token:${githubToken(options)}@github.com/${wikisRepo}.git`, target], { stdio: "inherit" });
125
+ await runGitWithProtocol("https", ["clone", gitUrls.https.wikis, target], options);
117
126
  }
118
- return { mode: "github", path: target };
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"], { stdio: "inherit" });
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], { stdio: "inherit" });
136
+ await run("gh", ["repo", "clone", wikisRepo, target]);
126
137
  }
127
- return { mode: "github", path: target };
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("Path to harness-data-wikis:", options));
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 username:", options);
146
- const password = await askSecret("CAS password:", options);
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, stdio: "inherit" });
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, stdio: "inherit" });
161
- await run(bin("qdm-cmr-cli"), ["config", "check-token"], { cwd: runtimeDir, env, stdio: "inherit" });
162
- await run(bin("qdm-indicators-cli"), ["config", "check-token"], { cwd: runtimeDir, env, stdio: "inherit" });
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
- const result = await run(cli, ["wikis", "build-index", "--skip-checks"], { cwd: runtimeDir, stdio: "inherit", allowFailure: true });
168
- if (result.code !== 0) console.warn("warning: wikis build-index --skip-checks failed; continue install and retry manually later");
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
- for (const check of doctor.checks) console.log(`${check.ok ? "PASS" : "FAIL"} ${check.name}`);
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(`Harness Data runtime installed: ${runtimeDir}`);
289
+ console.log(`安装完成:${runtimeDir}`);
290
+ console.log("");
291
+ console.log("下一步:");
292
+ console.log(`cd ${runtimeDir}`);
210
293
  }
@@ -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
- console.warn(`warning: ${tool.name} latest release has no ${assetName}; skipped`);
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
- console.log(`${tool.name}: up to date (${tag})`);
71
+ ok(`${tool.name} 已是最新 ${tag}`);
71
72
  return null;
72
73
  }
73
- console.log(`${tool.name} has update:`);
74
- console.log(` current: ${current.version || "unknown"}`);
75
- console.log(` remote: ${tag}`);
76
- if (!(await confirm(`Update ${tool.name}?`, { defaultNo: true }))) {
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
- return installed.installedTools?.[tool.name] || { version: tag, asset: assetName };
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
- console.log("wikis: local path mode; check manually");
93
+ skip("harness-data-wikis 为本地路径模式,请手动检查");
92
94
  return null;
93
95
  }
94
96
  if (!fs.existsSync(path.join(wikisDir, ".git"))) {
95
- console.log("wikis: not a git checkout; skipped");
97
+ skip("harness-data-wikis 不是 git checkout");
96
98
  return null;
97
99
  }
98
- await run("git", ["-C", wikisDir, "fetch", "origin"], { stdio: "inherit" });
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
- console.log(`wikis: up to date (${local.slice(0, 12)})`);
104
+ ok(`harness-data-wikis 已是最新 ${shortSha(local)}`);
103
105
  return null;
104
106
  }
105
- console.log("wikis has update:");
106
- console.log(` current: ${local}`);
107
- console.log(` remote: ${remote}`);
108
- if (!(await confirm("Update wikis?", { defaultNo: true }))) {
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"], { stdio: "inherit" });
113
- return { commit: (await run("git", ["-C", wikisDir, "rev-parse", "HEAD"])).stdout.trim() };
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
- console.log(`installer has update: ${packageVersion()} -> ${latestInstaller}`);
139
- console.log("Run the same npx command again to use the latest npm installer.");
150
+ warn(`installer 有新版本 ${packageVersion()} -> ${latestInstaller}`);
151
+ action("请重新执行:npx @lumi-ai-lab/harness-data@latest update");
140
152
  } else {
141
- console.log(`installer: up to date (${packageVersion()})`);
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
- console.log(`runtime bundle has update: ${state.runtimeTag} -> ${runtimeRelease.tag_name}`);
147
- if (await confirm("Update runtime bundle?", { defaultNo: true })) {
148
- const bundle = await installRuntimeBundle(runtimeDir, { ...options, force: true });
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
- console.log("Skipped runtime bundle");
167
+ skip("runtime bundle");
168
+ skipped.push(`runtime bundle ${runtimeRelease.tag_name}`);
153
169
  }
154
170
  } else {
155
- console.log(`runtime bundle: up to date (${state.runtimeTag || runtimeRelease.tag_name})`);
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
- console.log(`${tool.name}: local path mode; check manually`);
178
+ skip(`${tool.name} 为本地路径模式,请手动检查`);
161
179
  continue;
162
180
  }
163
- const installed = await maybeUpdateTool(runtimeDir, manifest, tool, options, state);
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
- const wikis = await updateWikis(runtimeDir, options, state);
171
- if (wikis) changed = true;
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, options);
175
- const doctor = await collectDoctor(runtimeDir, options);
176
- for (const check of doctor.checks) console.log(`${check.ok ? "PASS" : "FAIL"} ${check.name}${check.detail ? ` (${check.detail})` : ""}`);
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
- console.log("Harness Data runtime updated.");
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
- console.log("No selected updates were applied.");
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
+
@@ -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
- console.log(`Downloading ${tool.name} ${tool.version} (${key})`);
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) console.warn(`warning: ${tool.name} has no sha256; continuing without checksum`);
159
+ if (!sha) warn(`${tool.name} 未提供 sha256,已继续安装`);
160
160
  if (archive.endsWith(".zip")) {
161
- await run("unzip", ["-o", archive, "-d", binDir], { stdio: "inherit" });
161
+ await run("unzip", ["-o", archive, "-d", binDir]);
162
162
  } else {
163
- await run("tar", ["-xzf", archive, "-C", binDir], { stdio: "inherit" });
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("Choose Agent: claude, codex, pi, all [all] ")).trim().toLowerCase();
29
+ const answer = (await rl.question("选择 Agentclaude, 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;