@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 +34 -10
- package/package.json +1 -1
- package/src/cli.js +4 -4
- package/src/commands/doctor.js +16 -14
- package/src/commands/install.js +261 -92
- package/src/commands/update.js +200 -117
- package/src/commands/version.js +8 -8
- package/src/lib/config.js +5 -4
- package/src/lib/exec.js +1 -1
- package/src/lib/git-auth.js +6 -10
- package/src/lib/github.js +52 -0
- package/src/lib/log.js +39 -0
- package/src/lib/manifest.js +20 -13
- package/src/lib/paths.js +2 -5
- package/src/lib/platform.js +5 -1
- package/src/lib/prompt.js +37 -6
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
|
-
|
|
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
|
-
|
|
37
|
+
```bash
|
|
38
|
+
npx @lumi-ai-lab/harness-data doctor
|
|
39
|
+
```
|
|
16
40
|
|
|
17
|
-
The
|
|
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`,
|
|
43
|
+
`--agent` supports `claude`, `codex`, `pi`, and `all`; the default is `all`.
|
package/package.json
CHANGED
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
|
|
40
|
-
update
|
|
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
|
-
--
|
|
46
|
-
--github-token
|
|
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
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -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 {
|
|
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, "
|
|
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
|
|
42
|
+
function jsonFileValid(file) {
|
|
44
43
|
try {
|
|
45
|
-
|
|
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("
|
|
59
|
-
add("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",
|
|
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
|
}
|
package/src/commands/install.js
CHANGED
|
@@ -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,
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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 {
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
76
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
77
|
+
ok(`runtime bundle ${tag}`);
|
|
78
|
+
return { tag, skipped: false };
|
|
39
79
|
}
|
|
40
80
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
241
|
+
manifest = readManifest(manifestPath);
|
|
242
|
+
await installToolsFromManifest(runtimeDir, manifestPath, { ...options, tools: ["data-harness-cli"] });
|
|
243
|
+
localTools = await installLocalTools(runtimeDir, options);
|
|
101
244
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
repoUrl: prepared.repoUrl
|
|
287
|
+
lastCheckAt: new Date().toISOString()
|
|
122
288
|
});
|
|
123
|
-
console.log(
|
|
289
|
+
console.log(`安装完成:${runtimeDir}`);
|
|
290
|
+
console.log("");
|
|
291
|
+
console.log("下一步:");
|
|
292
|
+
console.log(`cd ${runtimeDir}`);
|
|
124
293
|
}
|