@lumi-ai-lab/harness-data 0.0.4 → 0.0.5
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 +176 -90
- package/src/commands/update.js +150 -119
- package/src/commands/version.js +8 -8
- package/src/lib/config.js +5 -4
- package/src/lib/git-auth.js +6 -10
- package/src/lib/github.js +52 -0
- package/src/lib/manifest.js +15 -8
- 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,16 +1,17 @@
|
|
|
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
|
+
|
|
13
|
+
const runtimeRepo = "lumi-ai-lab/harness-data";
|
|
14
|
+
const wikisRepo = "lumi-ai-lab/harness-data-wikis";
|
|
14
15
|
|
|
15
16
|
async function requireCommands(commands) {
|
|
16
17
|
for (const command of commands) {
|
|
@@ -18,107 +19,192 @@ async function requireCommands(commands) {
|
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
async function
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
async function prepareRuntimeDir(options) {
|
|
23
|
+
const runtimeDir = resolveWorkspaceDir(options.dir || process.cwd());
|
|
24
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
25
|
+
return runtimeDir;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function installRuntimeBundle(runtimeDir, options = {}) {
|
|
29
|
+
if (!options.force && fs.existsSync(path.join(runtimeDir, "agents")) &&
|
|
30
|
+
fs.existsSync(path.join(runtimeDir, "config")) &&
|
|
31
|
+
fs.existsSync(path.join(runtimeDir, "bootstrap", "cli-manifest.json"))) {
|
|
32
|
+
return { tag: "", skipped: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const release = await latestRelease(runtimeRepo, options);
|
|
36
|
+
const tag = release.tag_name;
|
|
37
|
+
const assetName = `harness-data-runtime-${tag}.tar.gz`;
|
|
38
|
+
const asset = findReleaseAsset(release, assetName);
|
|
39
|
+
const shaAsset = findReleaseAsset(release, `${assetName}.sha256`);
|
|
40
|
+
if (!asset) throw new Error(`runtime bundle asset missing in ${runtimeRepo} ${tag}: ${assetName}`);
|
|
41
|
+
|
|
42
|
+
const cacheDir = path.join(runtimeDir, ".bootstrap-cache");
|
|
43
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
44
|
+
const archive = path.join(cacheDir, assetName);
|
|
45
|
+
await downloadReleaseAsset(asset, archive, options);
|
|
46
|
+
if (shaAsset) {
|
|
47
|
+
const shaFile = `${archive}.sha256`;
|
|
48
|
+
await downloadReleaseAsset(shaAsset, shaFile, options);
|
|
49
|
+
const expected = fs.readFileSync(shaFile, "utf8").trim().split(/\s+/)[0];
|
|
50
|
+
const crypto = await import("node:crypto");
|
|
51
|
+
const actual = crypto.createHash("sha256").update(fs.readFileSync(archive)).digest("hex");
|
|
52
|
+
if (expected && actual !== expected) throw new Error("runtime bundle sha256 mismatch");
|
|
53
|
+
} else {
|
|
54
|
+
console.warn("warning: runtime bundle has no sha256 asset; continuing without checksum");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const extractDir = fs.mkdtempSync(path.join(cacheDir, "runtime-"));
|
|
58
|
+
await run("tar", ["-xzf", archive, "-C", extractDir], { stdio: "inherit" });
|
|
59
|
+
for (const dir of ["agents", "bootstrap"]) {
|
|
60
|
+
const source = path.join(extractDir, dir);
|
|
61
|
+
if (!fs.existsSync(source)) throw new Error(`runtime bundle missing ${dir}/`);
|
|
62
|
+
fs.rmSync(path.join(runtimeDir, dir), { recursive: true, force: true });
|
|
63
|
+
fs.cpSync(source, path.join(runtimeDir, dir), { recursive: true });
|
|
33
64
|
}
|
|
34
|
-
|
|
35
|
-
|
|
65
|
+
const configSource = path.join(extractDir, "config");
|
|
66
|
+
if (!fs.existsSync(configSource)) throw new Error("runtime bundle missing config/");
|
|
67
|
+
fs.mkdirSync(path.join(runtimeDir, "config"), { recursive: true });
|
|
68
|
+
for (const file of fs.readdirSync(configSource)) {
|
|
69
|
+
fs.copyFileSync(path.join(configSource, file), path.join(runtimeDir, "config", file));
|
|
36
70
|
}
|
|
37
|
-
|
|
38
|
-
return {
|
|
71
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
72
|
+
return { tag, skipped: false };
|
|
39
73
|
}
|
|
40
74
|
|
|
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);
|
|
75
|
+
function executable(file) {
|
|
76
|
+
try {
|
|
77
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
52
81
|
}
|
|
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
82
|
}
|
|
67
83
|
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
async function promptExecutable(runtimeDir, name, options = {}) {
|
|
85
|
+
const auto = path.join(runtimeDir, "bin", binaryName(name));
|
|
86
|
+
if (executable(auto)) return auto;
|
|
87
|
+
const value = await ask(`Path to ${name}:`, options);
|
|
88
|
+
const file = path.resolve(value);
|
|
89
|
+
if (!executable(file)) throw new Error(`${name} path is missing or not executable: ${file}`);
|
|
90
|
+
return file;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function installLocalTools(runtimeDir, options = {}) {
|
|
94
|
+
const binDir = path.join(runtimeDir, "bin");
|
|
95
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
96
|
+
const installed = {};
|
|
97
|
+
for (const name of ["cas-cli", "qdm-indicators-cli", "qdm-cmr-cli"]) {
|
|
98
|
+
const source = await promptExecutable(runtimeDir, name, options);
|
|
99
|
+
const target = path.join(binDir, binaryName(name));
|
|
100
|
+
if (path.resolve(source) !== path.resolve(target)) {
|
|
101
|
+
fs.copyFileSync(source, target);
|
|
102
|
+
fs.chmodSync(target, 0o755);
|
|
103
|
+
}
|
|
104
|
+
installed[name] = { mode: "local-path", source };
|
|
77
105
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
106
|
+
return installed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function installWikis(runtimeDir, options = {}) {
|
|
110
|
+
const target = path.join(runtimeDir, "wikis");
|
|
111
|
+
if (githubToken(options)) {
|
|
112
|
+
if (fs.existsSync(path.join(target, ".git"))) {
|
|
113
|
+
await run("git", ["-C", target, "pull", "--ff-only"], { stdio: "inherit" });
|
|
114
|
+
} else {
|
|
115
|
+
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" });
|
|
117
|
+
}
|
|
118
|
+
return { mode: "github", path: target };
|
|
83
119
|
}
|
|
120
|
+
if (await hasGithubAuth(options)) {
|
|
121
|
+
if (fs.existsSync(path.join(target, ".git"))) {
|
|
122
|
+
await run("git", ["-C", target, "pull", "--ff-only"], { stdio: "inherit" });
|
|
123
|
+
} else {
|
|
124
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
125
|
+
await run("gh", ["repo", "clone", wikisRepo, target], { stdio: "inherit" });
|
|
126
|
+
}
|
|
127
|
+
return { mode: "github", path: target };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
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));
|
|
132
|
+
for (const dir of ["spec", "playbooks", "templates"]) {
|
|
133
|
+
if (!fs.existsSync(path.join(source, dir))) throw new Error(`harness-data-wikis missing ${dir}/: ${source}`);
|
|
134
|
+
}
|
|
135
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
136
|
+
fs.cpSync(source, target, { recursive: true });
|
|
137
|
+
return { mode: "local-path", source, path: target };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function casConfigDir(runtimeDir) {
|
|
141
|
+
return path.join(runtimeDir, ".qdm-auth", "cas");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function writeCasCredentials(runtimeDir, options = {}) {
|
|
145
|
+
const username = await ask("CAS username:", options);
|
|
146
|
+
const password = await askSecret("CAS password:", options);
|
|
147
|
+
if (!username || !password) throw new Error("CAS username and password are required");
|
|
148
|
+
const dir = casConfigDir(runtimeDir);
|
|
149
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
150
|
+
fs.writeFileSync(path.join(dir, "config.json"), `${JSON.stringify({ cas: { username, password } }, null, 2)}\n`, { mode: 0o600 });
|
|
151
|
+
return dir;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function configureTokens(runtimeDir, casDir) {
|
|
155
|
+
const env = { QDM_CAS_CONFIG_DIR: casDir };
|
|
156
|
+
const bin = (name) => path.join(runtimeDir, "bin", binaryName(name));
|
|
157
|
+
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" });
|
|
159
|
+
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" });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function buildAndCheck(runtimeDir, options = {}) {
|
|
166
|
+
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");
|
|
84
169
|
}
|
|
85
170
|
|
|
86
171
|
export async function installCommand(options = {}) {
|
|
87
172
|
platformKey();
|
|
88
|
-
await requireCommands(["git", "
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const manifestPath = path.resolve(options.manifest || path.join(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
console.log("Reusing existing local config files");
|
|
173
|
+
await requireCommands(["git", "tar", "unzip"]);
|
|
174
|
+
const runtimeDir = await prepareRuntimeDir(options);
|
|
175
|
+
const bundle = await installRuntimeBundle(runtimeDir, options);
|
|
176
|
+
const manifestPath = path.resolve(options.manifest || path.join(runtimeDir, "bootstrap", "cli-manifest.json"));
|
|
177
|
+
const tokenMode = await hasGithubAuth(options);
|
|
178
|
+
|
|
179
|
+
let manifest;
|
|
180
|
+
let localTools = {};
|
|
181
|
+
if (tokenMode) {
|
|
182
|
+
manifest = await installToolsFromManifest(runtimeDir, manifestPath, options);
|
|
99
183
|
} else {
|
|
100
|
-
|
|
184
|
+
manifest = readManifest(manifestPath);
|
|
185
|
+
await installToolsFromManifest(runtimeDir, manifestPath, { ...options, tools: ["data-harness-cli"] });
|
|
186
|
+
localTools = await installLocalTools(runtimeDir, options);
|
|
101
187
|
}
|
|
102
|
-
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
188
|
+
|
|
189
|
+
await installWikis(runtimeDir, options);
|
|
190
|
+
writeLocalConfig(runtimeDir, { overwrite: true });
|
|
191
|
+
const casDir = await writeCasCredentials(runtimeDir, options);
|
|
192
|
+
await configureTokens(runtimeDir, casDir);
|
|
193
|
+
await buildAndCheck(runtimeDir, options);
|
|
194
|
+
linkAgents(runtimeDir, await chooseAgent(options));
|
|
195
|
+
|
|
196
|
+
const doctor = await collectDoctor(runtimeDir, { ...options, casConfigDir: casDir });
|
|
107
197
|
for (const check of doctor.checks) console.log(`${check.ok ? "PASS" : "FAIL"} ${check.name}`);
|
|
108
198
|
if (doctor.checks.some((check) => !check.ok)) throw new Error("doctor failed; install is incomplete");
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
mainCommit: await currentCommit(workspace),
|
|
116
|
-
wikisCommit: await submoduleCommit(workspace),
|
|
199
|
+
|
|
200
|
+
writeState(runtimeDir, {
|
|
201
|
+
installMode: tokenMode ? "github-token" : "local-path",
|
|
202
|
+
runtimeTag: bundle.tag,
|
|
203
|
+
localTools,
|
|
204
|
+
tools: manifest.installedTools || {},
|
|
117
205
|
manifestSha256: manifestDigest(manifest),
|
|
118
|
-
lastCheckAt: new Date().toISOString(),
|
|
119
206
|
packageVersion: packageVersion(),
|
|
120
|
-
|
|
121
|
-
repoUrl: prepared.repoUrl
|
|
207
|
+
lastCheckAt: new Date().toISOString()
|
|
122
208
|
});
|
|
123
|
-
console.log(`Harness Data installed: ${
|
|
209
|
+
console.log(`Harness Data runtime installed: ${runtimeDir}`);
|
|
124
210
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -4,10 +4,11 @@ import { confirm } from "../lib/prompt.js";
|
|
|
4
4
|
import { findWorkspaceDir, readUserState, writeState } from "../lib/paths.js";
|
|
5
5
|
import { run } from "../lib/exec.js";
|
|
6
6
|
import { installToolsFromManifest, manifestDigest, readManifest } from "../lib/manifest.js";
|
|
7
|
-
import { behindCount, currentCommit, dirtyPaths, submoduleCommit, submodulePointerChanged, submoduleRemoteBehind } from "../lib/git.js";
|
|
8
7
|
import { packageVersion } from "../lib/package.js";
|
|
8
|
+
import { platformKey } from "../lib/platform.js";
|
|
9
|
+
import { githubToken, latestRelease } from "../lib/github.js";
|
|
10
|
+
import { buildAndCheck, installRuntimeBundle } from "./install.js";
|
|
9
11
|
import { collectDoctor } from "./doctor.js";
|
|
10
|
-
import { normalizeGitProtocol, repoProtocol, repoUrl, runGitWithProtocol, syncWikisSubmodule } from "../lib/git-auth.js";
|
|
11
12
|
|
|
12
13
|
async function npmLatest() {
|
|
13
14
|
try {
|
|
@@ -20,139 +21,169 @@ async function npmLatest() {
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
if (result.code !== 0 || !result.stdout.trim()) return null;
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(result.stdout);
|
|
30
|
-
} catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
24
|
+
function archiveSuffix(url) {
|
|
25
|
+
if (url.endsWith(".zip")) return "zip";
|
|
26
|
+
if (url.endsWith(".tar.gz")) return "tar.gz";
|
|
27
|
+
return "";
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
function
|
|
36
|
-
|
|
30
|
+
function toolAssetName(tool, tag, key) {
|
|
31
|
+
const current = tool.platforms?.[key]?.url || "";
|
|
32
|
+
const suffix = archiveSuffix(current) || (key.startsWith("windows-") ? "zip" : "tar.gz");
|
|
33
|
+
return `${tool.binary}-${tag}-${key}.${suffix}`;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
remoteDigest,
|
|
57
|
-
previousDigest: state.manifestSha256 || "",
|
|
58
|
-
update: Boolean((state.manifestSha256 && state.manifestSha256 !== digest) || (remoteDigest && remoteDigest !== digest)),
|
|
59
|
-
tools: manifest.tools || [],
|
|
60
|
-
remoteTools: remote?.tools || []
|
|
61
|
-
}
|
|
36
|
+
function releaseAsset(release, name) {
|
|
37
|
+
return (release.assets || []).find((asset) => asset.name === name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function oneToolManifest(manifest, tool, tag, asset, key) {
|
|
41
|
+
return {
|
|
42
|
+
...manifest,
|
|
43
|
+
tools: [{
|
|
44
|
+
...tool,
|
|
45
|
+
version: tag,
|
|
46
|
+
platforms: {
|
|
47
|
+
[key]: {
|
|
48
|
+
url: asset.browser_download_url || `https://github.com/${tool.repo}/releases/download/${tag}/${asset.name}`,
|
|
49
|
+
sha256: ""
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}]
|
|
62
53
|
};
|
|
63
|
-
updates.wikis.update = updates.wikis.update || updates.wikis.pointerChanged;
|
|
64
|
-
updates.hasUpdates = updates.installer.update || updates.mainRepo.update || updates.wikis.update || updates.cli.update;
|
|
65
|
-
return updates;
|
|
66
54
|
}
|
|
67
55
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
56
|
+
async function maybeUpdateTool(runtimeDir, manifest, tool, options, state) {
|
|
57
|
+
const key = platformKey();
|
|
58
|
+
const release = await latestRelease(tool.repo, options);
|
|
59
|
+
const tag = release.tag_name;
|
|
60
|
+
const assetName = toolAssetName(tool, tag, key);
|
|
61
|
+
const asset = releaseAsset(release, assetName);
|
|
62
|
+
if (!asset) {
|
|
63
|
+
console.warn(`warning: ${tool.name} latest release has no ${assetName}; skipped`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const current = state.tools?.[tool.name] || {};
|
|
67
|
+
const tagChanged = current.version && current.version !== tag;
|
|
68
|
+
const firstInstall = !current.version;
|
|
69
|
+
if (!tagChanged && !firstInstall) {
|
|
70
|
+
console.log(`${tool.name}: up to date (${tag})`);
|
|
71
|
+
return null;
|
|
79
72
|
}
|
|
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}`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const updatedManifest = oneToolManifest(manifest, tool, tag, asset, key);
|
|
81
|
+
const installed = await installToolsFromManifest(runtimeDir, path.join(runtimeDir, ".bootstrap-cache", `${tool.name}-manifest.json`), {
|
|
82
|
+
...options,
|
|
83
|
+
manifestOverride: updatedManifest
|
|
84
|
+
});
|
|
85
|
+
return installed.installedTools?.[tool.name] || { version: tag, asset: assetName };
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
async function
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
88
|
+
async function updateWikis(runtimeDir, options, state) {
|
|
89
|
+
const wikisDir = path.join(runtimeDir, "wikis");
|
|
90
|
+
if (!githubToken(options) && state.installMode !== "github-token") {
|
|
91
|
+
console.log("wikis: local path mode; check manually");
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (!fs.existsSync(path.join(wikisDir, ".git"))) {
|
|
95
|
+
console.log("wikis: not a git checkout; skipped");
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
await run("git", ["-C", wikisDir, "fetch", "origin"], { stdio: "inherit" });
|
|
99
|
+
const local = (await run("git", ["-C", wikisDir, "rev-parse", "HEAD"])).stdout.trim();
|
|
100
|
+
const remote = (await run("git", ["-C", wikisDir, "rev-parse", "origin/HEAD"], { allowFailure: true })).stdout.trim();
|
|
101
|
+
if (!remote || local === remote) {
|
|
102
|
+
console.log(`wikis: up to date (${local.slice(0, 12)})`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
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");
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
await run("git", ["-C", wikisDir, "pull", "--ff-only"], { stdio: "inherit" });
|
|
113
|
+
return { commit: (await run("git", ["-C", wikisDir, "rev-parse", "HEAD"])).stdout.trim() };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function checkUpdates(workspace, options = {}) {
|
|
85
117
|
const state = readUserState();
|
|
86
|
-
|
|
118
|
+
const latestInstaller = await npmLatest();
|
|
119
|
+
return {
|
|
120
|
+
installer: { current: packageVersion(), latest: latestInstaller, update: Boolean(latestInstaller && latestInstaller !== packageVersion()) },
|
|
121
|
+
state,
|
|
122
|
+
hasUpdates: Boolean(latestInstaller && latestInstaller !== packageVersion())
|
|
123
|
+
};
|
|
87
124
|
}
|
|
88
125
|
|
|
89
126
|
export async function updateCommand(options = {}) {
|
|
90
|
-
const
|
|
91
|
-
if (!fs.existsSync(
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
printUpdateSummary(updates);
|
|
106
|
-
if (options.check) {
|
|
107
|
-
writeState(workspace, {
|
|
108
|
-
mainCommit: await currentCommit(workspace),
|
|
109
|
-
wikisCommit: await submoduleCommit(workspace),
|
|
110
|
-
manifestSha256: updates.cli.digest,
|
|
111
|
-
packageVersion: packageVersion(),
|
|
112
|
-
lastCheckAt: new Date().toISOString(),
|
|
113
|
-
gitProtocol,
|
|
114
|
-
repoUrl: originUrl
|
|
115
|
-
});
|
|
116
|
-
return updates;
|
|
117
|
-
}
|
|
118
|
-
if (!updates.hasUpdates) {
|
|
119
|
-
console.log("No repository or wikis updates detected.");
|
|
120
|
-
writeState(workspace, {
|
|
121
|
-
mainCommit: await currentCommit(workspace),
|
|
122
|
-
wikisCommit: await submoduleCommit(workspace),
|
|
123
|
-
manifestSha256: updates.cli.digest,
|
|
124
|
-
packageVersion: packageVersion(),
|
|
125
|
-
lastCheckAt: new Date().toISOString(),
|
|
126
|
-
gitProtocol,
|
|
127
|
-
repoUrl: originUrl
|
|
128
|
-
});
|
|
129
|
-
return updates;
|
|
130
|
-
}
|
|
131
|
-
if (!(await confirm(`Apply updates to ${workspace}?`, { yes: options.yes, defaultNo: true }))) throw new Error("update cancelled");
|
|
132
|
-
if (updates.mainRepo.update) {
|
|
133
|
-
await runGitWithProtocol(gitProtocol, ["pull", "--ff-only"], { ...options, cwd: workspace, stdio: "inherit" });
|
|
127
|
+
const runtimeDir = findWorkspaceDir(options.dir);
|
|
128
|
+
if (!fs.existsSync(runtimeDir)) throw new Error(`runtime directory does not exist: ${runtimeDir}`);
|
|
129
|
+
const state = readUserState();
|
|
130
|
+
const manifestPath = path.join(runtimeDir, "bootstrap", "cli-manifest.json");
|
|
131
|
+
const manifest = readManifest(manifestPath);
|
|
132
|
+
let changed = false;
|
|
133
|
+
let runtimeTag = state.runtimeTag || "";
|
|
134
|
+
const nextTools = { ...(state.tools || {}) };
|
|
135
|
+
|
|
136
|
+
const latestInstaller = await npmLatest();
|
|
137
|
+
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.");
|
|
140
|
+
} else {
|
|
141
|
+
console.log(`installer: up to date (${packageVersion()})`);
|
|
134
142
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
|
|
144
|
+
const runtimeRelease = await latestRelease("lumi-ai-lab/harness-data", options);
|
|
145
|
+
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 });
|
|
149
|
+
runtimeTag = bundle.tag || runtimeRelease.tag_name;
|
|
150
|
+
changed = true;
|
|
138
151
|
} else {
|
|
139
|
-
|
|
140
|
-
await runGitWithProtocol(gitProtocol, ["submodule", "update", "--init", "--recursive", "--remote", "wikis"], { ...options, cwd: workspace, stdio: "inherit" });
|
|
152
|
+
console.log("Skipped runtime bundle");
|
|
141
153
|
}
|
|
154
|
+
} else {
|
|
155
|
+
console.log(`runtime bundle: up to date (${state.runtimeTag || runtimeRelease.tag_name})`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const tool of manifest.tools || []) {
|
|
159
|
+
if (state.installMode === "local-path" && tool.name !== "data-harness-cli") {
|
|
160
|
+
console.log(`${tool.name}: local path mode; check manually`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const installed = await maybeUpdateTool(runtimeDir, manifest, tool, options, state);
|
|
164
|
+
if (installed) {
|
|
165
|
+
nextTools[tool.name] = installed;
|
|
166
|
+
changed = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const wikis = await updateWikis(runtimeDir, options, state);
|
|
171
|
+
if (wikis) changed = true;
|
|
172
|
+
|
|
173
|
+
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})` : ""}`);
|
|
177
|
+
writeState(runtimeDir, {
|
|
178
|
+
...state,
|
|
179
|
+
runtimeTag,
|
|
180
|
+
tools: nextTools,
|
|
181
|
+
manifestSha256: manifestDigest(manifest),
|
|
182
|
+
lastCheckAt: new Date().toISOString()
|
|
183
|
+
});
|
|
184
|
+
console.log("Harness Data runtime updated.");
|
|
185
|
+
} else {
|
|
186
|
+
writeState(runtimeDir, { ...state, lastCheckAt: new Date().toISOString() });
|
|
187
|
+
console.log("No selected updates were applied.");
|
|
142
188
|
}
|
|
143
|
-
await installToolsFromManifest(workspace, path.join(workspace, "bootstrap", "cli-manifest.json"), options);
|
|
144
|
-
await run(path.join(workspace, "bin", "data-harness-cli"), ["wikis", "build-index"], { cwd: workspace, stdio: "inherit" });
|
|
145
|
-
const doctor = await collectDoctor(workspace, options);
|
|
146
|
-
for (const check of doctor.checks) console.log(`${check.ok ? "PASS" : "FAIL"} ${check.name}`);
|
|
147
|
-
if (doctor.checks.some((check) => !check.ok)) throw new Error("doctor failed after update");
|
|
148
|
-
writeState(workspace, {
|
|
149
|
-
mainCommit: await currentCommit(workspace),
|
|
150
|
-
wikisCommit: await submoduleCommit(workspace),
|
|
151
|
-
manifestSha256: updates.cli.digest,
|
|
152
|
-
packageVersion: packageVersion(),
|
|
153
|
-
lastCheckAt: new Date().toISOString(),
|
|
154
|
-
gitProtocol,
|
|
155
|
-
repoUrl: originUrl
|
|
156
|
-
});
|
|
157
|
-
console.log(`Harness Data updated: ${workspace}`);
|
|
158
189
|
}
|
package/src/commands/version.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { findWorkspaceDir } from "../lib/paths.js";
|
|
4
|
-
import { currentCommit, submoduleCommit } from "../lib/git.js";
|
|
3
|
+
import { findWorkspaceDir, readUserState } from "../lib/paths.js";
|
|
5
4
|
import { readManifest } from "../lib/manifest.js";
|
|
6
5
|
import { packageVersion } from "../lib/package.js";
|
|
7
6
|
|
|
8
7
|
export async function versionCommand(options = {}) {
|
|
9
8
|
const workspace = findWorkspaceDir(options.dir);
|
|
10
9
|
const manifestPath = path.join(workspace, "bootstrap", "cli-manifest.json");
|
|
10
|
+
const state = readUserState();
|
|
11
11
|
const result = {
|
|
12
12
|
installer: packageVersion(),
|
|
13
|
-
workspace,
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
runtime: workspace,
|
|
14
|
+
runtimeTag: state.runtimeTag || "",
|
|
15
|
+
installMode: state.installMode || "",
|
|
16
16
|
tools: []
|
|
17
17
|
};
|
|
18
18
|
if (fs.existsSync(manifestPath)) {
|
|
@@ -26,9 +26,9 @@ export async function versionCommand(options = {}) {
|
|
|
26
26
|
console.log(JSON.stringify(result, null, 2));
|
|
27
27
|
} else {
|
|
28
28
|
console.log(`installer ${result.installer}`);
|
|
29
|
-
console.log(`
|
|
30
|
-
if (result.
|
|
31
|
-
if (result.
|
|
29
|
+
console.log(`runtime ${result.runtime}`);
|
|
30
|
+
if (result.runtimeTag) console.log(`runtime bundle ${result.runtimeTag}`);
|
|
31
|
+
if (result.installMode) console.log(`install mode ${result.installMode}`);
|
|
32
32
|
for (const tool of result.tools) console.log(`${tool.name} ${tool.version}`);
|
|
33
33
|
}
|
|
34
34
|
return result;
|
package/src/lib/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { binaryName } from "./platform.js";
|
|
3
4
|
|
|
4
5
|
export function writeLocalConfig(workspace, options = {}) {
|
|
5
6
|
const configDir = path.join(workspace, "config");
|
|
@@ -9,7 +10,7 @@ export function writeLocalConfig(workspace, options = {}) {
|
|
|
9
10
|
if ((fs.existsSync(harness) || fs.existsSync(env)) && !options.overwrite) {
|
|
10
11
|
throw new Error("local config already exists; rerun interactively and confirm overwrite or remove the files");
|
|
11
12
|
}
|
|
12
|
-
const bin = (name) => path.join(workspace, "bin", name).replaceAll("\\", "/");
|
|
13
|
+
const bin = (name) => path.join(workspace, "bin", binaryName(name)).replaceAll("\\", "/");
|
|
13
14
|
fs.writeFileSync(harness, `paths:\n spec: wikis/spec\n playbooks: wikis/playbooks\n templates: wikis/templates\n\ncli:\n qdm_cmr_cli: ${bin("qdm-cmr-cli")}\n qdm_indicators_cli: ${bin("qdm-indicators-cli")}\n qdm_cas_cli: ${bin("cas-cli")}\n`);
|
|
14
15
|
fs.writeFileSync(env, `export QDM_CMR_CLI="${bin("qdm-cmr-cli")}"\nexport QDM_INDICATORS_CLI="${bin("qdm-indicators-cli")}"\nexport QDM_CAS_CLI="${bin("cas-cli")}"\n`);
|
|
15
16
|
}
|
|
@@ -29,9 +30,9 @@ export function validateCasConfigDir(dir) {
|
|
|
29
30
|
|
|
30
31
|
export function linkAgents(workspace, agent) {
|
|
31
32
|
const pairs = [];
|
|
32
|
-
if (agent === "claude" || agent === "
|
|
33
|
-
if (agent === "codex" || agent === "
|
|
34
|
-
if (agent === "pi" || agent === "all") pairs.push(["
|
|
33
|
+
if (agent === "claude" || agent === "all") pairs.push(["agents/claude", ".claude"]);
|
|
34
|
+
if (agent === "codex" || agent === "all") pairs.push(["agents/codex", ".codex"]);
|
|
35
|
+
if (agent === "pi" || agent === "all") pairs.push(["agents/pi", ".pi"]);
|
|
35
36
|
for (const [sourceRel, targetRel] of pairs) {
|
|
36
37
|
const source = path.join(workspace, sourceRel);
|
|
37
38
|
const target = path.join(workspace, targetRel);
|
package/src/lib/git-auth.js
CHANGED
|
@@ -40,21 +40,17 @@ function httpsFailureMessage() {
|
|
|
40
40
|
return [
|
|
41
41
|
"HTTPS access to the private GitHub repositories failed.",
|
|
42
42
|
"Fix by configuring a GitHub SSH key, running gh auth login, enabling Git Credential Manager,",
|
|
43
|
-
"or passing --github-token
|
|
43
|
+
"or passing --github-token TOKEN."
|
|
44
44
|
].join(" ");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function tokenEnvName(options = {}) {
|
|
48
|
-
|
|
49
|
-
if (!name) return "";
|
|
50
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error("--github-token-env must be a valid environment variable name");
|
|
51
|
-
if (!process.env[name]) throw new Error(`environment variable ${name} is empty or unset`);
|
|
52
|
-
return name;
|
|
48
|
+
return options.githubToken || process.env.GITHUB_TOKEN || "";
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
async function withHttpsAuthEnv(options, callback) {
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
52
|
+
const token = tokenEnvName(options);
|
|
53
|
+
if (!token) return callback({ GIT_TERMINAL_PROMPT: "0" });
|
|
58
54
|
|
|
59
55
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "harness-data-git-"));
|
|
60
56
|
const askpass = path.join(dir, "askpass.sh");
|
|
@@ -62,7 +58,7 @@ async function withHttpsAuthEnv(options, callback) {
|
|
|
62
58
|
"#!/bin/sh",
|
|
63
59
|
"case \"$1\" in",
|
|
64
60
|
"*Username*) printf '%s\\n' x-access-token ;;",
|
|
65
|
-
"*Password*)
|
|
61
|
+
"*Password*) printf '%s\\n' \"$GITHUB_TOKEN_VALUE\" ;;",
|
|
66
62
|
"*) printf '\\n' ;;",
|
|
67
63
|
"esac",
|
|
68
64
|
""
|
|
@@ -71,7 +67,7 @@ async function withHttpsAuthEnv(options, callback) {
|
|
|
71
67
|
try {
|
|
72
68
|
return await callback({
|
|
73
69
|
GIT_ASKPASS: askpass,
|
|
74
|
-
|
|
70
|
+
GITHUB_TOKEN_VALUE: token,
|
|
75
71
|
GIT_TERMINAL_PROMPT: "0"
|
|
76
72
|
});
|
|
77
73
|
} finally {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { commandExists, run } from "./exec.js";
|
|
4
|
+
import { download } from "./manifest.js";
|
|
5
|
+
|
|
6
|
+
const userAgent = "harness-data-installer";
|
|
7
|
+
|
|
8
|
+
export function githubToken(options = {}) {
|
|
9
|
+
return options.githubToken || process.env.GITHUB_TOKEN || "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function hasGithubAuth(options = {}) {
|
|
13
|
+
if (githubToken(options)) return true;
|
|
14
|
+
if (!(await commandExists("gh"))) return false;
|
|
15
|
+
const result = await run("gh", ["auth", "status"], { allowFailure: true });
|
|
16
|
+
return result.code === 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function githubHeaders(options = {}) {
|
|
20
|
+
const headers = { "User-Agent": userAgent };
|
|
21
|
+
const token = githubToken(options);
|
|
22
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
23
|
+
return headers;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function githubJson(url, options = {}) {
|
|
27
|
+
if (!githubToken(options) && await commandExists("gh")) {
|
|
28
|
+
const apiPath = new URL(url).pathname.replace(/^\/repos\//, "repos/");
|
|
29
|
+
const result = await run("gh", ["api", apiPath], { allowFailure: true });
|
|
30
|
+
if (result.code === 0 && result.stdout.trim()) return JSON.parse(result.stdout);
|
|
31
|
+
}
|
|
32
|
+
const tmp = path.join(fs.mkdtempSync(path.join(process.cwd(), ".github-api-")), "response.json");
|
|
33
|
+
try {
|
|
34
|
+
await download(url, tmp, githubHeaders(options));
|
|
35
|
+
return JSON.parse(fs.readFileSync(tmp, "utf8"));
|
|
36
|
+
} finally {
|
|
37
|
+
fs.rmSync(path.dirname(tmp), { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function latestRelease(repo, options = {}) {
|
|
42
|
+
return githubJson(`https://api.github.com/repos/${repo}/releases/latest`, options);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function findReleaseAsset(release, name) {
|
|
46
|
+
return (release.assets || []).find((asset) => asset.name === name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function downloadReleaseAsset(asset, file, options = {}) {
|
|
50
|
+
const headers = { ...githubHeaders(options), Accept: "application/octet-stream" };
|
|
51
|
+
await download(asset.url, file, headers);
|
|
52
|
+
}
|
package/src/lib/manifest.js
CHANGED
|
@@ -13,7 +13,7 @@ export function manifestDigest(manifest) {
|
|
|
13
13
|
return crypto.createHash("sha256").update(JSON.stringify(manifest)).digest("hex");
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function download(url, file, headers = {}) {
|
|
16
|
+
export function download(url, file, headers = {}) {
|
|
17
17
|
return new Promise((resolve, reject) => {
|
|
18
18
|
const request = https.get(url, { headers }, (response) => {
|
|
19
19
|
if ([301, 302, 307, 308].includes(response.statusCode) && response.headers.location) {
|
|
@@ -77,10 +77,7 @@ async function downloadPrivateWithGh(asset, file) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function githubToken(options = {}) {
|
|
80
|
-
|
|
81
|
-
if (!name) return "";
|
|
82
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error("--github-token-env must be a valid environment variable name");
|
|
83
|
-
return process.env[name] || "";
|
|
80
|
+
return options.githubToken || process.env.GITHUB_TOKEN || "";
|
|
84
81
|
}
|
|
85
82
|
|
|
86
83
|
async function githubAssetApiUrl(asset, token) {
|
|
@@ -121,7 +118,7 @@ async function downloadAsset(tool, asset, file, options = {}) {
|
|
|
121
118
|
console.log(`Downloading private ${tool.name} asset from ${tool.repo}`);
|
|
122
119
|
if (await downloadPrivateWithGh(asset, file)) return;
|
|
123
120
|
if (await downloadPrivateWithToken(asset, file, options)) return;
|
|
124
|
-
throw new Error(`private GitHub Release asset requires gh auth login or --github-token
|
|
121
|
+
throw new Error(`private GitHub Release asset requires gh auth login, GITHUB_TOKEN, or --github-token: ${assetName(asset)}`);
|
|
125
122
|
}
|
|
126
123
|
|
|
127
124
|
async function expectedSha256(tool, asset, options = {}) {
|
|
@@ -140,21 +137,25 @@ function fileSha256(file) {
|
|
|
140
137
|
}
|
|
141
138
|
|
|
142
139
|
export async function installToolsFromManifest(workspace, manifestPath, options = {}) {
|
|
143
|
-
const manifest = readManifest(manifestPath);
|
|
140
|
+
const manifest = options.manifestOverride || readManifest(manifestPath);
|
|
141
|
+
const only = options.tools ? new Set(options.tools) : null;
|
|
144
142
|
const key = platformKey();
|
|
145
143
|
const cacheDir = path.join(workspace, ".bootstrap-cache");
|
|
146
144
|
const binDir = path.join(workspace, "bin");
|
|
147
145
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
148
146
|
fs.mkdirSync(binDir, { recursive: true });
|
|
147
|
+
const installedTools = {};
|
|
149
148
|
|
|
150
149
|
for (const tool of manifest.tools || []) {
|
|
150
|
+
if (only && !only.has(tool.name)) continue;
|
|
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
|
console.log(`Downloading ${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
|
+
if (sha && actualSha !== sha) throw new Error(`${tool.name} sha256 mismatch`);
|
|
158
159
|
if (!sha) console.warn(`warning: ${tool.name} has no sha256; continuing without checksum`);
|
|
159
160
|
if (archive.endsWith(".zip")) {
|
|
160
161
|
await run("unzip", ["-o", archive, "-d", binDir], { stdio: "inherit" });
|
|
@@ -164,6 +165,12 @@ export async function installToolsFromManifest(workspace, manifestPath, options
|
|
|
164
165
|
const binary = path.join(binDir, tool.binary);
|
|
165
166
|
if (!fs.existsSync(binary)) throw new Error(`${tool.binary} was not extracted to bin/`);
|
|
166
167
|
fs.chmodSync(binary, 0o755);
|
|
168
|
+
installedTools[tool.name] = {
|
|
169
|
+
version: tool.version || "",
|
|
170
|
+
asset: assetName(asset),
|
|
171
|
+
sha256: sha || actualSha
|
|
172
|
+
};
|
|
167
173
|
}
|
|
174
|
+
Object.defineProperty(manifest, "installedTools", { value: installedTools, enumerable: false });
|
|
168
175
|
return manifest;
|
|
169
176
|
}
|
package/src/lib/paths.js
CHANGED
|
@@ -2,7 +2,6 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
const appDir = path.join("lumi-ai-lab", "harness-data");
|
|
6
5
|
const stateDir = path.join("lumi-ai-lab", "harness-data-installer");
|
|
7
6
|
|
|
8
7
|
export function expandHome(value) {
|
|
@@ -13,9 +12,7 @@ export function expandHome(value) {
|
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
export function defaultWorkspaceDir() {
|
|
16
|
-
|
|
17
|
-
if (process.platform === "win32") return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), appDir);
|
|
18
|
-
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"), appDir);
|
|
15
|
+
return process.cwd();
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
export function userStatePath() {
|
|
@@ -62,6 +59,6 @@ export function findWorkspaceDir(explicitDir) {
|
|
|
62
59
|
|
|
63
60
|
export function looksLikeWorkspace(dir) {
|
|
64
61
|
return fs.existsSync(path.join(dir, "bootstrap", "cli-manifest.json")) &&
|
|
65
|
-
fs.existsSync(path.join(dir, "
|
|
62
|
+
fs.existsSync(path.join(dir, "agents")) &&
|
|
66
63
|
fs.existsSync(path.join(dir, "wikis"));
|
|
67
64
|
}
|
package/src/lib/platform.js
CHANGED
|
@@ -2,8 +2,12 @@ export function platformKey() {
|
|
|
2
2
|
const os = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "windows" : process.platform;
|
|
3
3
|
const arch = process.arch === "x64" ? "amd64" : process.arch;
|
|
4
4
|
const key = `${os}-${arch}`;
|
|
5
|
-
if (!["darwin-arm64", "darwin-amd64", "linux-
|
|
5
|
+
if (!["darwin-arm64", "darwin-amd64", "linux-amd64", "windows-amd64"].includes(key)) {
|
|
6
6
|
throw new Error(`unsupported platform: ${key}`);
|
|
7
7
|
}
|
|
8
8
|
return key;
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
export function binaryName(name) {
|
|
12
|
+
return process.platform === "win32" ? `${name}.exe` : name;
|
|
13
|
+
}
|
package/src/lib/prompt.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import readline from "node:readline/promises";
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { Writable } from "node:stream";
|
|
3
4
|
|
|
4
|
-
const agentChoices = ["claude", "codex", "pi", "
|
|
5
|
+
const agentChoices = ["claude", "codex", "pi", "all"];
|
|
5
6
|
|
|
6
7
|
export async function confirm(message, options = {}) {
|
|
7
8
|
if (options.yes) return true;
|
|
@@ -19,17 +20,47 @@ export async function confirm(message, options = {}) {
|
|
|
19
20
|
export async function chooseAgent(options = {}) {
|
|
20
21
|
if (options.agent) {
|
|
21
22
|
const value = String(options.agent).trim().toLowerCase();
|
|
22
|
-
if (!agentChoices.includes(value)) throw new Error("agent must be claude, codex, pi,
|
|
23
|
+
if (!agentChoices.includes(value)) throw new Error("agent must be claude, codex, pi, or all");
|
|
23
24
|
return value;
|
|
24
25
|
}
|
|
25
|
-
if (options.yes)
|
|
26
|
+
if (options.yes) return "all";
|
|
26
27
|
const rl = readline.createInterface({ input, output });
|
|
27
28
|
try {
|
|
28
|
-
const answer = (await rl.question("Choose Agent: claude, codex, pi,
|
|
29
|
-
const value = answer || "
|
|
30
|
-
if (!agentChoices.includes(value)) throw new Error("agent must be claude, codex, pi,
|
|
29
|
+
const answer = (await rl.question("Choose Agent: claude, codex, pi, all [all] ")).trim().toLowerCase();
|
|
30
|
+
const value = answer || "all";
|
|
31
|
+
if (!agentChoices.includes(value)) throw new Error("agent must be claude, codex, pi, or all");
|
|
31
32
|
return value;
|
|
32
33
|
} finally {
|
|
33
34
|
rl.close();
|
|
34
35
|
}
|
|
35
36
|
}
|
|
37
|
+
|
|
38
|
+
export async function ask(message, options = {}) {
|
|
39
|
+
if (options.value) return String(options.value);
|
|
40
|
+
if (options.yes) throw new Error(`${message} is required`);
|
|
41
|
+
const rl = readline.createInterface({ input, output });
|
|
42
|
+
try {
|
|
43
|
+
return (await rl.question(`${message} `)).trim();
|
|
44
|
+
} finally {
|
|
45
|
+
rl.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function askSecret(message, options = {}) {
|
|
50
|
+
if (options.value) return String(options.value);
|
|
51
|
+
if (options.yes) throw new Error(`${message} is required`);
|
|
52
|
+
const muted = new Writable({
|
|
53
|
+
write(_chunk, _encoding, callback) {
|
|
54
|
+
callback();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
output.write(`${message} `);
|
|
58
|
+
const rl = readline.createInterface({ input, output: muted, terminal: true });
|
|
59
|
+
try {
|
|
60
|
+
const answer = (await rl.question("")).trim();
|
|
61
|
+
output.write("\n");
|
|
62
|
+
return answer;
|
|
63
|
+
} finally {
|
|
64
|
+
rl.close();
|
|
65
|
+
}
|
|
66
|
+
}
|