@lumi-ai-lab/harness-data 0.0.4 → 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/README.md CHANGED
@@ -1,19 +1,43 @@
1
1
  # Harness Data npm installer
2
2
 
3
+ Install a Harness Data runtime in the current directory:
4
+
3
5
  ```bash
4
6
  npx @lumi-ai-lab/harness-data install
5
- npx @lumi-ai-lab/harness-data install --git-protocol ssh
6
- npx @lumi-ai-lab/harness-data install --git-protocol https
7
- GITHUB_TOKEN=... npx @lumi-ai-lab/harness-data install --yes --agent codex --git-protocol https --github-token-env GITHUB_TOKEN --cas-config-dir /secure/path/to/cas
8
- npx @lumi-ai-lab/harness-data install --yes --agent pi --cas-config-dir /secure/path/to/cas
9
- npx @lumi-ai-lab/harness-data doctor --dir ~/harness-data
10
- npx @lumi-ai-lab/harness-data update --dir ~/harness-data
11
7
  ```
12
8
 
13
- This package is only the installer and updater. The full Harness Data workspace is cloned and maintained outside the npm package.
9
+ Install into an explicit runtime directory:
10
+
11
+ ```bash
12
+ npx @lumi-ai-lab/harness-data install --dir /path/to/runtime
13
+ ```
14
+
15
+ Use a GitHub token for private Release assets:
16
+
17
+ ```bash
18
+ npx @lumi-ai-lab/harness-data install --github-token ...
19
+ ```
20
+
21
+ or:
22
+
23
+ ```bash
24
+ GITHUB_TOKEN=... npx @lumi-ai-lab/harness-data install
25
+ ```
26
+
27
+ Without a GitHub token, the installer interactively asks for local absolute paths to `cas-cli`, `qdm-indicators-cli`, `qdm-cmr-cli`, and `harness-data-wikis`. CAS username and password are always collected interactively.
28
+
29
+ Update an existing runtime interactively:
30
+
31
+ ```bash
32
+ npx @lumi-ai-lab/harness-data update
33
+ ```
34
+
35
+ Diagnose a runtime:
14
36
 
15
- Private GitHub repository access defaults to `--git-protocol auto`: SSH is tried first, then HTTPS. HTTPS uses Git Credential Manager, `gh auth login`, or a token supplied through `--github-token-env`; GitHub account passwords are not supported.
37
+ ```bash
38
+ npx @lumi-ai-lab/harness-data doctor
39
+ ```
16
40
 
17
- The `qdm-cmr-cli`, `qdm-indicators-cli`, and `cas-cli` binaries are downloaded from private GitHub Releases in `pengmide/qdm-cmr-cli`, `pengmide/qdm-indicators-cli`, and `pengmide/qdm-cas-cli`. The installer uses `gh auth login` first and falls back to `--github-token-env`.
41
+ The runtime is assembled from the `harness-data` runtime bundle, platform-specific CLI Release assets, `harness-data-wikis`, generated local config, CAS credentials, and selected Agent symlinks.
18
42
 
19
- `--agent` supports `claude`, `codex`, `pi`, `both` (Claude + Codex), and `all` (Claude + Codex + Pi).
43
+ `--agent` supports `claude`, `codex`, `pi`, and `all`; the default is `all`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumi-ai-lab/harness-data",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Installer and updater for Harness Data",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -36,13 +36,13 @@ export async function main(argv) {
36
36
  console.log(`Usage: harness-data <install|update|doctor|version> [options]
37
37
 
38
38
  Commands:
39
- install Clone/reuse a Harness Data workspace and complete setup
40
- update Check and apply installer, repository, CLI, and wikis updates
39
+ install Install a Harness Data runtime in the current directory
40
+ update Interactively check and apply runtime, CLI, and wikis updates
41
41
  doctor Diagnose workspace CLI, config, auth, index, and Agent hooks
42
42
  version Print installer, repository, wikis, and manifest versions
43
43
 
44
44
  Install options:
45
- --git-protocol auto|ssh|https Git protocol for private repositories (default: auto)
46
- --github-token-env NAME Token env var for HTTPS fallback or CI`);
45
+ --dir PATH Runtime directory (default: current directory)
46
+ --github-token TOKEN GitHub token for private Release assets`);
47
47
  if (unknown) process.exitCode = 1;
48
48
  }
@@ -2,8 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { run } from "../lib/exec.js";
4
4
  import { findWorkspaceDir } from "../lib/paths.js";
5
- import { currentCommit, submoduleCommit } from "../lib/git.js";
6
- import { validateCasConfigDir } from "../lib/config.js";
5
+ import { binaryName } from "../lib/platform.js";
7
6
 
8
7
  function existsExecutable(file) {
9
8
  try {
@@ -16,7 +15,7 @@ function existsExecutable(file) {
16
15
 
17
16
  function agentOk(workspace, name) {
18
17
  const target = path.join(workspace, `.${name}`);
19
- const source = path.join(workspace, ".agents", name);
18
+ const source = path.join(workspace, "agents", name);
20
19
  if (!fs.existsSync(target) || !fs.existsSync(source)) return false;
21
20
  try {
22
21
  return fs.realpathSync(target) === fs.realpathSync(source);
@@ -26,7 +25,7 @@ function agentOk(workspace, name) {
26
25
  }
27
26
 
28
27
  async function tokenCheck(workspace, binary, env) {
29
- const file = path.join(workspace, "bin", binary);
28
+ const file = path.join(workspace, "bin", binaryName(binary));
30
29
  if (!existsExecutable(file)) return false;
31
30
  const result = await run(file, ["config", "check-token"], { cwd: workspace, env, allowFailure: true });
32
31
  return result.code === 0;
@@ -40,9 +39,9 @@ function configPathsValid(workspace) {
40
39
  return matches.length >= 3 && matches.every((entry) => fs.existsSync(entry));
41
40
  }
42
41
 
43
- function casCredentialsValid(dir) {
42
+ function jsonFileValid(file) {
44
43
  try {
45
- validateCasConfigDir(dir);
44
+ JSON.parse(fs.readFileSync(file, "utf8"));
46
45
  return true;
47
46
  } catch {
48
47
  return false;
@@ -55,24 +54,27 @@ export async function collectDoctor(workspace, options = {}) {
55
54
  const checks = [];
56
55
  const add = (name, ok, detail = "") => checks.push({ name, ok, detail });
57
56
 
58
- add("workspace", fs.existsSync(path.join(workspace, "bootstrap", "cli-manifest.json")), workspace);
59
- add("wikis submodule", fs.existsSync(path.join(workspace, "wikis", ".git")) || fs.existsSync(path.join(workspace, ".git", "modules", "wikis")));
57
+ add("runtime", fs.existsSync(path.join(workspace, "bootstrap", "cli-manifest.json")) && fs.existsSync(path.join(workspace, "agents")), workspace);
58
+ add("wikis/spec", fs.existsSync(path.join(workspace, "wikis", "spec")));
59
+ add("wikis/playbooks", fs.existsSync(path.join(workspace, "wikis", "playbooks")));
60
+ add("wikis/templates", fs.existsSync(path.join(workspace, "wikis", "templates")));
60
61
  for (const binary of ["data-harness-cli", "qdm-cmr-cli", "qdm-indicators-cli", "cas-cli"]) {
61
- add(`bin/${binary}`, existsExecutable(path.join(workspace, "bin", binary)));
62
+ add(`bin/${binary}`, existsExecutable(path.join(workspace, "bin", binaryName(binary))));
62
63
  }
63
64
  add("config/harness-config.yaml", fs.existsSync(path.join(workspace, "config", "harness-config.yaml")));
64
65
  add("config/qdm-cli-paths.env", fs.existsSync(path.join(workspace, "config", "qdm-cli-paths.env")));
65
66
  add("config CLI paths", configPathsValid(workspace));
66
- add("CAS credentials", casCredentialsValid(casConfigDir), casConfigDir);
67
+ add("CAS credentials file", jsonFileValid(path.join(casConfigDir, "config.json")), casConfigDir);
67
68
  add("CMR token", await tokenCheck(workspace, "qdm-cmr-cli", env));
68
69
  add("Indicators token", await tokenCheck(workspace, "qdm-indicators-cli", env));
69
- add("wikis index", fs.existsSync(path.join(workspace, ".harness", "index", "wikis-index.json")) || fs.existsSync(path.join(workspace, ".harness", "index", "wikis-runtime-index.json")));
70
70
  add("Agent hook", agentOk(workspace, "claude") || agentOk(workspace, "codex") || agentOk(workspace, "pi"));
71
+ if (!fs.existsSync(path.join(workspace, ".harness", "index", "wikis-index.json")) &&
72
+ !fs.existsSync(path.join(workspace, ".harness", "index", "wikis-runtime-index.json"))) {
73
+ checks.push({ name: "wikis index", ok: true, detail: "missing; run data-harness-cli wikis build-index --skip-checks" });
74
+ }
71
75
 
72
76
  return {
73
77
  workspace,
74
- mainCommit: await currentCommit(workspace),
75
- wikisCommit: await submoduleCommit(workspace),
76
78
  checks
77
79
  };
78
80
  }
@@ -83,7 +85,7 @@ export async function doctorCommand(options = {}) {
83
85
  if (options.json) {
84
86
  console.log(JSON.stringify(report, null, 2));
85
87
  } else {
86
- console.log(`Harness Data doctor: ${report.workspace}`);
88
+ console.log(`Harness Data runtime doctor: ${report.workspace}`);
87
89
  for (const check of report.checks) {
88
90
  console.log(`${check.ok ? "PASS" : "FAIL"} ${check.name}${check.detail ? ` (${check.detail})` : ""}`);
89
91
  }
@@ -1,124 +1,293 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { commandExists, run } from "../lib/exec.js";
4
- import { writeLocalConfig, validateCasConfigDir, linkAgents } from "../lib/config.js";
5
- import { confirm, chooseAgent } from "../lib/prompt.js";
6
- import { defaultWorkspaceDir, looksLikeWorkspace, resolveWorkspaceDir, writeState } from "../lib/paths.js";
7
- import { isGitRepo, currentCommit, submoduleCommit } from "../lib/git.js";
4
+ import { writeLocalConfig, linkAgents } from "../lib/config.js";
5
+ import { ask, askSecret, chooseAgent } from "../lib/prompt.js";
6
+ import { resolveWorkspaceDir, writeState } from "../lib/paths.js";
8
7
  import { installToolsFromManifest, manifestDigest, readManifest } from "../lib/manifest.js";
9
- import { platformKey } from "../lib/platform.js";
8
+ import { binaryName, platformKey } from "../lib/platform.js";
10
9
  import { collectDoctor } from "./doctor.js";
11
- import { checkUpdates } from "./update.js";
12
10
  import { packageVersion } from "../lib/package.js";
13
- import { gitUrls, repoProtocol, repoUrl, resolveInstallProtocol, runGitWithProtocol, syncWikisSubmodule } from "../lib/git-auth.js";
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";
14
+
15
+ const runtimeRepo = "lumi-ai-lab/harness-data";
16
+ const wikisRepo = "lumi-ai-lab/harness-data-wikis";
14
17
 
15
18
  async function requireCommands(commands) {
16
19
  for (const command of commands) {
17
20
  if (!(await commandExists(command))) throw new Error(`missing required command: ${command}`);
18
21
  }
22
+ ok(commands.join(", "));
23
+ }
24
+
25
+ async function prepareRuntimeDir(options) {
26
+ const runtimeDir = resolveWorkspaceDir(options.dir || process.cwd());
27
+ fs.mkdirSync(runtimeDir, { recursive: true });
28
+ return runtimeDir;
19
29
  }
20
30
 
21
- async function prepareWorkspace(options) {
22
- const target = resolveWorkspaceDir(options.dir || (looksLikeWorkspace(process.cwd()) ? process.cwd() : defaultWorkspaceDir()));
23
- const branch = options.branch || "master";
24
- if (!fs.existsSync(target)) {
25
- const protocol = await resolveInstallProtocol(options);
26
- const repo = options.repo || gitUrls[protocol].repo;
27
- if (!(await confirm(`Clone ${repo} to ${target}?`, { yes: options.yes }))) throw new Error("install cancelled");
28
- fs.mkdirSync(path.dirname(target), { recursive: true });
29
- await runGitWithProtocol(protocol, ["clone", "--branch", branch, repo, target], { ...options, stdio: "inherit" });
30
- await syncWikisSubmodule(target, protocol);
31
- await runGitWithProtocol(protocol, ["submodule", "update", "--init", "--recursive", "wikis"], { ...options, cwd: target, stdio: "inherit" });
32
- return { workspace: target, gitProtocol: protocol, repoUrl: repo };
31
+ export async function installRuntimeBundle(runtimeDir, options = {}) {
32
+ if (!options.force && fs.existsSync(path.join(runtimeDir, "agents")) &&
33
+ fs.existsSync(path.join(runtimeDir, "config")) &&
34
+ fs.existsSync(path.join(runtimeDir, "bootstrap", "cli-manifest.json"))) {
35
+ skip("runtime bundle 已存在");
36
+ return { tag: "", skipped: true };
37
+ }
38
+
39
+ const release = await latestRelease(runtimeRepo, options);
40
+ const tag = release.tag_name;
41
+ const assetName = `harness-data-runtime-${tag}.tar.gz`;
42
+ const asset = findReleaseAsset(release, assetName);
43
+ const shaAsset = findReleaseAsset(release, `${assetName}.sha256`);
44
+ if (!asset) throw new Error(`runtime bundle asset missing in ${runtimeRepo} ${tag}: ${assetName}`);
45
+
46
+ const cacheDir = path.join(runtimeDir, ".bootstrap-cache");
47
+ fs.mkdirSync(cacheDir, { recursive: true });
48
+ const archive = path.join(cacheDir, assetName);
49
+ action(`下载 harness-data-runtime ${tag}`);
50
+ await downloadReleaseAsset(asset, archive, options);
51
+ if (shaAsset) {
52
+ const shaFile = `${archive}.sha256`;
53
+ await downloadReleaseAsset(shaAsset, shaFile, options);
54
+ const expected = fs.readFileSync(shaFile, "utf8").trim().split(/\s+/)[0];
55
+ const crypto = await import("node:crypto");
56
+ const actual = crypto.createHash("sha256").update(fs.readFileSync(archive)).digest("hex");
57
+ if (expected && actual !== expected) throw new Error("runtime bundle sha256 mismatch");
58
+ } else {
59
+ warn("runtime bundle 未提供 sha256,已继续安装");
60
+ }
61
+
62
+ const extractDir = fs.mkdtempSync(path.join(cacheDir, "runtime-"));
63
+ await run("tar", ["-xzf", archive, "-C", extractDir]);
64
+ for (const dir of ["agents", "bootstrap"]) {
65
+ const source = path.join(extractDir, dir);
66
+ if (!fs.existsSync(source)) throw new Error(`runtime bundle missing ${dir}/`);
67
+ fs.rmSync(path.join(runtimeDir, dir), { recursive: true, force: true });
68
+ fs.cpSync(source, path.join(runtimeDir, dir), { recursive: true });
33
69
  }
34
- if (!(await isGitRepo(target)) || !looksLikeWorkspace(target)) {
35
- throw new Error(`${target} exists but is not a valid harness-data workspace; pass --dir or move the directory`);
70
+ const configSource = path.join(extractDir, "config");
71
+ if (!fs.existsSync(configSource)) throw new Error("runtime bundle missing config/");
72
+ fs.mkdirSync(path.join(runtimeDir, "config"), { recursive: true });
73
+ for (const file of fs.readdirSync(configSource)) {
74
+ fs.copyFileSync(path.join(configSource, file), path.join(runtimeDir, "config", file));
36
75
  }
37
- if (!(await confirm(`Reuse existing workspace ${target}?`, { yes: options.yes }))) throw new Error("install cancelled");
38
- return { workspace: target, gitProtocol: await repoProtocol(target), repoUrl: await repoUrl(target) };
76
+ fs.rmSync(extractDir, { recursive: true, force: true });
77
+ ok(`runtime bundle ${tag}`);
78
+ return { tag, skipped: false };
39
79
  }
40
80
 
41
- async function configureAuth(workspace, options) {
42
- const casConfigDir = path.resolve(options.casConfigDir || path.join(workspace, ".qdm-auth", "cas"));
43
- fs.mkdirSync(casConfigDir, { recursive: true, mode: 0o700 });
44
- const env = { QDM_CAS_CONFIG_DIR: casConfigDir };
45
- if (options.casConfigDir) {
46
- validateCasConfigDir(casConfigDir);
47
- } else if (options.yes) {
48
- throw new Error("non-interactive install requires --cas-config-dir with existing CAS credentials");
49
- } else {
50
- await run(path.join(workspace, "bin", "cas-cli"), ["config", "set-credentials"], { cwd: workspace, env, stdio: "inherit" });
51
- validateCasConfigDir(casConfigDir);
81
+ function executable(file) {
82
+ try {
83
+ fs.accessSync(file, fs.constants.X_OK);
84
+ return true;
85
+ } catch {
86
+ return false;
52
87
  }
53
- await run("sh", ["-c", `"${path.join(workspace, "bin", "qdm-cmr-cli")}" config set-token "$("${path.join(workspace, "bin", "cas-cli")}" token --app cmr)"`], {
54
- cwd: workspace,
55
- env,
56
- stdio: "inherit"
57
- });
58
- await run("sh", ["-c", `"${path.join(workspace, "bin", "qdm-indicators-cli")}" config set-token "$("${path.join(workspace, "bin", "cas-cli")}" token --app indicators)"`], {
59
- cwd: workspace,
60
- env,
61
- stdio: "inherit"
62
- });
63
- await run(path.join(workspace, "bin", "qdm-cmr-cli"), ["config", "check-token"], { cwd: workspace, env, stdio: "inherit" });
64
- await run(path.join(workspace, "bin", "qdm-indicators-cli"), ["config", "check-token"], { cwd: workspace, env, stdio: "inherit" });
65
- return casConfigDir;
66
88
  }
67
89
 
68
- export async function buildAndCheck(workspace, options) {
69
- const cli = path.join(workspace, "bin", "data-harness-cli");
70
- const buildIndexArgs = ["wikis", "build-index"];
71
- if (options.skipWikisCheck) buildIndexArgs.push("--skip-checks");
72
- await run(cli, buildIndexArgs, { cwd: workspace, stdio: "inherit" });
73
- await run(cli, ["context", "--question", "会员复购为什么下降?", "--json"], { cwd: workspace, stdio: "inherit" });
74
- let runFullCheck = !options.skipWikisCheck;
75
- if (!options.yes && !options.skipWikisCheck) {
76
- runFullCheck = await confirm("Run wikis check-all?", { defaultNo: false });
90
+ async function promptExecutable(runtimeDir, name, options = {}) {
91
+ const auto = path.join(runtimeDir, "bin", binaryName(name));
92
+ if (executable(auto)) {
93
+ ok(`自动识别 ${name}: ${auto}`);
94
+ return auto;
77
95
  }
78
- if (runFullCheck) {
79
- const result = await run(cli, ["wikis", "check-all"], { cwd: workspace, stdio: "inherit", allowFailure: true });
80
- if (result.code !== 0) console.warn("warning: wikis check-all reported issues");
81
- } else {
82
- console.warn("warning: wikis check-all skipped");
96
+ const value = await ask(`请输入 ${name} 的绝对路径:`, options);
97
+ const file = path.resolve(value);
98
+ if (!executable(file)) throw new Error(`${name} path is missing or not executable: ${file}`);
99
+ return file;
100
+ }
101
+
102
+ async function installLocalTools(runtimeDir, options = {}) {
103
+ const binDir = path.join(runtimeDir, "bin");
104
+ fs.mkdirSync(binDir, { recursive: true });
105
+ const installed = {};
106
+ for (const name of ["cas-cli", "qdm-indicators-cli", "qdm-cmr-cli"]) {
107
+ const source = await promptExecutable(runtimeDir, name, options);
108
+ const target = path.join(binDir, binaryName(name));
109
+ if (path.resolve(source) !== path.resolve(target)) {
110
+ fs.copyFileSync(source, target);
111
+ fs.chmodSync(target, 0o755);
112
+ }
113
+ installed[name] = { mode: "local-path", source };
114
+ }
115
+ return installed;
116
+ }
117
+
118
+ async function installWikis(runtimeDir, options = {}) {
119
+ const target = path.join(runtimeDir, "wikis");
120
+ if (githubToken(options)) {
121
+ if (fs.existsSync(path.join(target, ".git"))) {
122
+ await runGitWithProtocol("https", ["-C", target, "pull", "--ff-only"], options);
123
+ } else {
124
+ fs.rmSync(target, { recursive: true, force: true });
125
+ await runGitWithProtocol("https", ["clone", gitUrls.https.wikis, target], options);
126
+ }
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 };
130
+ }
131
+ if (await hasGithubAuth(options)) {
132
+ if (fs.existsSync(path.join(target, ".git"))) {
133
+ await run("git", ["-C", target, "pull", "--ff-only"]);
134
+ } else {
135
+ fs.rmSync(target, { recursive: true, force: true });
136
+ await run("gh", ["repo", "clone", wikisRepo, target]);
137
+ }
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 };
141
+ }
142
+
143
+ const auto = path.join(runtimeDir, "harness-data-wikis");
144
+ const source = fs.existsSync(auto) ? auto : path.resolve(await ask("请输入 harness-data-wikis 的绝对路径:", options));
145
+ for (const dir of ["spec", "playbooks", "templates"]) {
146
+ if (!fs.existsSync(path.join(source, dir))) throw new Error(`harness-data-wikis missing ${dir}/: ${source}`);
147
+ }
148
+ fs.rmSync(target, { recursive: true, force: true });
149
+ fs.cpSync(source, target, { recursive: true });
150
+ ok(`harness-data-wikis 本地路径 ${source}`);
151
+ return { mode: "local-path", source, path: target };
152
+ }
153
+
154
+ function casConfigDir(runtimeDir) {
155
+ return path.join(runtimeDir, ".qdm-auth", "cas");
156
+ }
157
+
158
+ async function writeCasCredentials(runtimeDir, options = {}) {
159
+ const username = await ask("CAS 用户名:", options);
160
+ const password = await askSecret("CAS 密码:", options);
161
+ if (!username || !password) throw new Error("CAS username and password are required");
162
+ const dir = casConfigDir(runtimeDir);
163
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
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");
166
+ return dir;
167
+ }
168
+
169
+ async function configureTokens(runtimeDir, casDir) {
170
+ const env = { QDM_CAS_CONFIG_DIR: casDir };
171
+ const bin = (name) => path.join(runtimeDir, "bin", binaryName(name));
172
+ const cmrToken = (await run(bin("cas-cli"), ["token", "--app", "cmr"], { cwd: runtimeDir, env })).stdout.trim();
173
+ await run(bin("qdm-cmr-cli"), ["config", "set-token", cmrToken], { cwd: runtimeDir, env });
174
+ ok("CMR Token 已配置");
175
+ const indicatorsToken = (await run(bin("cas-cli"), ["token", "--app", "indicators"], { cwd: runtimeDir, env })).stdout.trim();
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 });
180
+ }
181
+
182
+ export async function buildAndCheck(runtimeDir, options = {}) {
183
+ const cli = path.join(runtimeDir, "bin", binaryName("data-harness-cli"));
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;
83
212
  }
213
+ for (const check of failed) fail(`${check.name}${check.detail ? ` (${check.detail})` : ""}`);
84
214
  }
85
215
 
86
216
  export async function installCommand(options = {}) {
87
- platformKey();
88
- await requireCommands(["git", "curl", "tar", "unzip"]);
89
- const prepared = await prepareWorkspace(options);
90
- const workspace = prepared.workspace;
91
- const manifestPath = path.resolve(options.manifest || path.join(workspace, "bootstrap", "cli-manifest.json"));
92
- if (!(await confirm("Download or refresh CLI binaries?", { yes: options.yes }))) throw new Error("install cancelled");
93
- const manifest = await installToolsFromManifest(workspace, manifestPath, options);
94
- const configExists = fs.existsSync(path.join(workspace, "config", "harness-config.yaml")) ||
95
- fs.existsSync(path.join(workspace, "config", "qdm-cli-paths.env"));
96
- const overwrite = configExists && !options.yes ? await confirm("Overwrite existing local config files?", { defaultNo: true }) : !configExists;
97
- if (configExists && !overwrite) {
98
- console.log("Reusing existing local config files");
217
+ const key = platformKey();
218
+ header("Harness Data 安装器", packageVersion(), [
219
+ `安装目录:${resolveWorkspaceDir(options.dir || process.cwd())}`,
220
+ `平台:${key}`
221
+ ]);
222
+
223
+ step(1, 8, "检查本机依赖");
224
+ await requireCommands(["git", "tar", "unzip"]);
225
+ blank();
226
+
227
+ step(2, 8, "安装 runtime bundle");
228
+ const runtimeDir = await prepareRuntimeDir(options);
229
+ const bundle = await installRuntimeBundle(runtimeDir, options);
230
+ blank();
231
+
232
+ step(3, 8, "安装 CLI 工具");
233
+ const manifestPath = path.resolve(options.manifest || path.join(runtimeDir, "bootstrap", "cli-manifest.json"));
234
+ const tokenMode = await hasGithubAuth(options);
235
+
236
+ let manifest;
237
+ let localTools = {};
238
+ if (tokenMode) {
239
+ manifest = await installToolsFromManifest(runtimeDir, manifestPath, options);
99
240
  } else {
100
- writeLocalConfig(workspace, { overwrite: true });
241
+ manifest = readManifest(manifestPath);
242
+ await installToolsFromManifest(runtimeDir, manifestPath, { ...options, tools: ["data-harness-cli"] });
243
+ localTools = await installLocalTools(runtimeDir, options);
101
244
  }
102
- const casConfigDir = await configureAuth(workspace, options);
103
- await buildAndCheck(workspace, options);
104
- const agent = await chooseAgent(options);
105
- linkAgents(workspace, agent);
106
- const doctor = await collectDoctor(workspace, { ...options, casConfigDir });
107
- for (const check of doctor.checks) console.log(`${check.ok ? "PASS" : "FAIL"} ${check.name}`);
108
- if (doctor.checks.some((check) => !check.ok)) throw new Error("doctor failed; install is incomplete");
109
- const gitProtocol = prepared.gitProtocol || await repoProtocol(workspace) || "ssh";
110
- const updates = await checkUpdates(workspace, { ...options, gitProtocol });
111
- if (updates.hasUpdates) {
112
- console.log(`Updates available. Next step: npx @lumi-ai-lab/harness-data update --dir "${workspace}"`);
245
+ ok(`${Object.keys(manifest.installedTools || {}).length + Object.keys(localTools).length} 个 CLI 已安装到 bin/`);
246
+ blank();
247
+
248
+ step(4, 8, "同步 Wikis 知识库");
249
+ await installWikis(runtimeDir, options);
250
+ blank();
251
+
252
+ step(5, 8, "生成本地配置");
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 认证");
259
+ const casDir = await writeCasCredentials(runtimeDir, options);
260
+ await configureTokens(runtimeDir, casDir);
261
+ blank();
262
+
263
+ step(7, 8, "构建 Wikis 索引");
264
+ await buildAndCheck(runtimeDir, options);
265
+ blank();
266
+
267
+ step(8, 8, "配置 Agent Hook");
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}`);
113
271
  }
114
- writeState(workspace, {
115
- mainCommit: await currentCommit(workspace),
116
- wikisCommit: await submoduleCommit(workspace),
272
+ blank();
273
+
274
+ console.log("安装校验");
275
+ const doctor = await collectDoctor(runtimeDir, { ...options, casConfigDir: casDir });
276
+ printDoctorSummary(doctor);
277
+ if (doctor.checks.some((check) => !check.ok)) throw new Error("doctor failed; install is incomplete");
278
+ blank();
279
+
280
+ writeState(runtimeDir, {
281
+ installMode: tokenMode ? "github-token" : "local-path",
282
+ runtimeTag: bundle.tag,
283
+ localTools,
284
+ tools: manifest.installedTools || {},
117
285
  manifestSha256: manifestDigest(manifest),
118
- lastCheckAt: new Date().toISOString(),
119
286
  packageVersion: packageVersion(),
120
- gitProtocol,
121
- repoUrl: prepared.repoUrl
287
+ lastCheckAt: new Date().toISOString()
122
288
  });
123
- console.log(`Harness Data installed: ${workspace}`);
289
+ console.log(`安装完成:${runtimeDir}`);
290
+ console.log("");
291
+ console.log("下一步:");
292
+ console.log(`cd ${runtimeDir}`);
124
293
  }