@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/src/commands/update.js
CHANGED
|
@@ -4,10 +4,12 @@ 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, printDoctorSummary } from "./install.js";
|
|
9
11
|
import { collectDoctor } from "./doctor.js";
|
|
10
|
-
import {
|
|
12
|
+
import { action, blank, header, ok, shortSha, skip, step, warn } from "../lib/log.js";
|
|
11
13
|
|
|
12
14
|
async function npmLatest() {
|
|
13
15
|
try {
|
|
@@ -20,139 +22,220 @@ async function npmLatest() {
|
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
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
|
-
}
|
|
25
|
+
function archiveSuffix(url) {
|
|
26
|
+
if (url.endsWith(".zip")) return "zip";
|
|
27
|
+
if (url.endsWith(".tar.gz")) return "tar.gz";
|
|
28
|
+
return "";
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
function
|
|
36
|
-
|
|
31
|
+
function toolAssetName(tool, tag, key) {
|
|
32
|
+
const current = tool.platforms?.[key]?.url || "";
|
|
33
|
+
const suffix = archiveSuffix(current) || (key.startsWith("windows-") ? "zip" : "tar.gz");
|
|
34
|
+
return `${tool.binary}-${tag}-${key}.${suffix}`;
|
|
37
35
|
}
|
|
38
36
|
|
|
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
|
-
}
|
|
37
|
+
function releaseAsset(release, name) {
|
|
38
|
+
return (release.assets || []).find((asset) => asset.name === name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function oneToolManifest(manifest, tool, tag, asset, key) {
|
|
42
|
+
return {
|
|
43
|
+
...manifest,
|
|
44
|
+
tools: [{
|
|
45
|
+
...tool,
|
|
46
|
+
version: tag,
|
|
47
|
+
platforms: {
|
|
48
|
+
[key]: {
|
|
49
|
+
url: asset.browser_download_url || `https://github.com/${tool.repo}/releases/download/${tag}/${asset.name}`,
|
|
50
|
+
sha256: ""
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}]
|
|
62
54
|
};
|
|
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
55
|
}
|
|
67
56
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
57
|
+
async function maybeUpdateTool(runtimeDir, manifest, tool, options, state) {
|
|
58
|
+
const key = platformKey();
|
|
59
|
+
const release = await latestRelease(tool.repo, options);
|
|
60
|
+
const tag = release.tag_name;
|
|
61
|
+
const assetName = toolAssetName(tool, tag, key);
|
|
62
|
+
const asset = releaseAsset(release, assetName);
|
|
63
|
+
if (!asset) {
|
|
64
|
+
warn(`${tool.name} 最新 release 缺少 ${assetName},已跳过`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const current = state.tools?.[tool.name] || {};
|
|
68
|
+
const tagChanged = current.version && current.version !== tag;
|
|
69
|
+
const firstInstall = !current.version;
|
|
70
|
+
if (!tagChanged && !firstInstall) {
|
|
71
|
+
ok(`${tool.name} 已是最新 ${tag}`);
|
|
72
|
+
return null;
|
|
79
73
|
}
|
|
74
|
+
action(`发现更新:${tool.name} ${current.version || "unknown"} -> ${tag}`);
|
|
75
|
+
if (!(await confirm(`是否更新 ${tool.name}?`, { defaultNo: true }))) {
|
|
76
|
+
skip(tool.name);
|
|
77
|
+
options.skippedUpdates?.push(`${tool.name} ${tag}`);
|
|
78
|
+
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
|
+
const result = installed.installedTools?.[tool.name] || { version: tag, asset: assetName };
|
|
86
|
+
ok(`${tool.name} 已更新到 ${tag}`);
|
|
87
|
+
return result;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
async function
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
90
|
+
async function updateWikis(runtimeDir, options, state) {
|
|
91
|
+
const wikisDir = path.join(runtimeDir, "wikis");
|
|
92
|
+
if (!githubToken(options) && state.installMode !== "github-token") {
|
|
93
|
+
skip("harness-data-wikis 为本地路径模式,请手动检查");
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (!fs.existsSync(path.join(wikisDir, ".git"))) {
|
|
97
|
+
skip("harness-data-wikis 不是 git checkout");
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
await run("git", ["-C", wikisDir, "fetch", "origin"]);
|
|
101
|
+
const local = (await run("git", ["-C", wikisDir, "rev-parse", "HEAD"])).stdout.trim();
|
|
102
|
+
const remote = (await run("git", ["-C", wikisDir, "rev-parse", "origin/HEAD"], { allowFailure: true })).stdout.trim();
|
|
103
|
+
if (!remote || local === remote) {
|
|
104
|
+
ok(`harness-data-wikis 已是最新 ${shortSha(local)}`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
action(`发现更新:harness-data-wikis ${shortSha(local)} -> ${shortSha(remote)}`);
|
|
108
|
+
if (!(await confirm("是否更新 harness-data-wikis?", { defaultNo: true }))) {
|
|
109
|
+
skip("harness-data-wikis");
|
|
110
|
+
options.skippedUpdates?.push(`harness-data-wikis ${shortSha(remote)}`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
await run("git", ["-C", wikisDir, "pull", "--ff-only"]);
|
|
114
|
+
const commit = (await run("git", ["-C", wikisDir, "rev-parse", "HEAD"])).stdout.trim();
|
|
115
|
+
ok(`harness-data-wikis 已更新到 ${shortSha(commit)}`);
|
|
116
|
+
return { commit };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function checkUpdates(workspace, options = {}) {
|
|
85
120
|
const state = readUserState();
|
|
86
|
-
|
|
121
|
+
const latestInstaller = await npmLatest();
|
|
122
|
+
return {
|
|
123
|
+
installer: { current: packageVersion(), latest: latestInstaller, update: Boolean(latestInstaller && latestInstaller !== packageVersion()) },
|
|
124
|
+
state,
|
|
125
|
+
hasUpdates: Boolean(latestInstaller && latestInstaller !== packageVersion())
|
|
126
|
+
};
|
|
87
127
|
}
|
|
88
128
|
|
|
89
129
|
export async function updateCommand(options = {}) {
|
|
90
|
-
const
|
|
91
|
-
if (!fs.existsSync(
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
130
|
+
const runtimeDir = findWorkspaceDir(options.dir);
|
|
131
|
+
if (!fs.existsSync(runtimeDir)) throw new Error(`runtime directory does not exist: ${runtimeDir}`);
|
|
132
|
+
const key = platformKey();
|
|
133
|
+
header("Harness Data 更新器", packageVersion(), [
|
|
134
|
+
`运行目录:${runtimeDir}`,
|
|
135
|
+
`平台:${key}`
|
|
136
|
+
]);
|
|
137
|
+
const state = readUserState();
|
|
138
|
+
const manifestPath = path.join(runtimeDir, "bootstrap", "cli-manifest.json");
|
|
139
|
+
const manifest = readManifest(manifestPath);
|
|
140
|
+
let changed = false;
|
|
141
|
+
let runtimeTag = state.runtimeTag || "";
|
|
142
|
+
const nextTools = { ...(state.tools || {}) };
|
|
143
|
+
const applied = [];
|
|
144
|
+
const skipped = [];
|
|
145
|
+
const trackingOptions = { ...options, skippedUpdates: skipped };
|
|
146
|
+
|
|
147
|
+
step(1, 6, "检查 installer");
|
|
148
|
+
const latestInstaller = await npmLatest();
|
|
149
|
+
if (latestInstaller && latestInstaller !== packageVersion()) {
|
|
150
|
+
warn(`installer 有新版本 ${packageVersion()} -> ${latestInstaller}`);
|
|
151
|
+
action("请重新执行:npx @lumi-ai-lab/harness-data@latest update");
|
|
152
|
+
} else {
|
|
153
|
+
ok(`installer 已是最新 ${packageVersion()}`);
|
|
100
154
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
155
|
+
blank();
|
|
156
|
+
|
|
157
|
+
step(2, 6, "检查 runtime bundle");
|
|
158
|
+
const runtimeRelease = await latestRelease("lumi-ai-lab/harness-data", options);
|
|
159
|
+
if (state.runtimeTag && state.runtimeTag !== runtimeRelease.tag_name) {
|
|
160
|
+
action(`发现更新:runtime bundle ${state.runtimeTag} -> ${runtimeRelease.tag_name}`);
|
|
161
|
+
if (await confirm("是否更新 runtime bundle?", { defaultNo: true })) {
|
|
162
|
+
const bundle = await installRuntimeBundle(runtimeDir, { ...trackingOptions, force: true });
|
|
163
|
+
runtimeTag = bundle.tag || runtimeRelease.tag_name;
|
|
164
|
+
changed = true;
|
|
165
|
+
applied.push(`runtime bundle ${runtimeTag}`);
|
|
166
|
+
} else {
|
|
167
|
+
skip("runtime bundle");
|
|
168
|
+
skipped.push(`runtime bundle ${runtimeRelease.tag_name}`);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
ok(`runtime bundle 已是最新 ${state.runtimeTag || runtimeRelease.tag_name}`);
|
|
117
172
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
173
|
+
blank();
|
|
174
|
+
|
|
175
|
+
step(3, 6, "检查 CLI 工具");
|
|
176
|
+
for (const tool of manifest.tools || []) {
|
|
177
|
+
if (state.installMode === "local-path" && tool.name !== "data-harness-cli") {
|
|
178
|
+
skip(`${tool.name} 为本地路径模式,请手动检查`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const installed = await maybeUpdateTool(runtimeDir, manifest, tool, trackingOptions, state);
|
|
182
|
+
if (installed) {
|
|
183
|
+
nextTools[tool.name] = installed;
|
|
184
|
+
changed = true;
|
|
185
|
+
applied.push(`${tool.name} ${installed.version || ""}`.trim());
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
blank();
|
|
189
|
+
|
|
190
|
+
step(4, 6, "检查 Wikis 知识库");
|
|
191
|
+
const wikis = await updateWikis(runtimeDir, trackingOptions, state);
|
|
192
|
+
if (wikis) {
|
|
193
|
+
changed = true;
|
|
194
|
+
applied.push(`harness-data-wikis ${shortSha(wikis.commit)}`);
|
|
130
195
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
196
|
+
blank();
|
|
197
|
+
|
|
198
|
+
step(5, 6, "构建 Wikis 索引");
|
|
199
|
+
if (changed) {
|
|
200
|
+
await buildAndCheck(runtimeDir, trackingOptions);
|
|
201
|
+
} else {
|
|
202
|
+
skip("没有组件更新");
|
|
134
203
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
204
|
+
blank();
|
|
205
|
+
|
|
206
|
+
step(6, 6, "安装校验");
|
|
207
|
+
if (changed) {
|
|
208
|
+
const doctor = await collectDoctor(runtimeDir, trackingOptions);
|
|
209
|
+
printDoctorSummary(doctor);
|
|
210
|
+
if (doctor.checks.some((check) => !check.ok)) throw new Error("doctor failed; update is incomplete");
|
|
211
|
+
writeState(runtimeDir, {
|
|
212
|
+
...state,
|
|
213
|
+
runtimeTag,
|
|
214
|
+
tools: nextTools,
|
|
215
|
+
manifestSha256: manifestDigest(manifest),
|
|
216
|
+
lastCheckAt: new Date().toISOString()
|
|
217
|
+
});
|
|
218
|
+
blank();
|
|
219
|
+
console.log(`更新完成:${runtimeDir}`);
|
|
220
|
+
if (applied.length) {
|
|
221
|
+
console.log("");
|
|
222
|
+
console.log("已更新:");
|
|
223
|
+
for (const item of applied) console.log(`- ${item}`);
|
|
224
|
+
}
|
|
225
|
+
if (skipped.length) {
|
|
226
|
+
console.log("");
|
|
227
|
+
console.log("已跳过:");
|
|
228
|
+
for (const item of skipped) console.log(`- ${item}`);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
skip("没有组件更新");
|
|
232
|
+
writeState(runtimeDir, { ...state, lastCheckAt: new Date().toISOString() });
|
|
233
|
+
blank();
|
|
234
|
+
console.log(skipped.length ? "没有应用任何更新。" : "没有发现需要更新的内容。");
|
|
235
|
+
if (skipped.length) {
|
|
236
|
+
console.log("");
|
|
237
|
+
console.log("已跳过:");
|
|
238
|
+
for (const item of skipped) console.log(`- ${item}`);
|
|
141
239
|
}
|
|
142
240
|
}
|
|
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
241
|
}
|
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/exec.js
CHANGED
|
@@ -4,7 +4,7 @@ export function run(command, args = [], options = {}) {
|
|
|
4
4
|
return new Promise((resolve, reject) => {
|
|
5
5
|
const child = spawn(command, args, {
|
|
6
6
|
cwd: options.cwd,
|
|
7
|
-
env: { ...process.env, ...(options.env || {}) },
|
|
7
|
+
env: { ...process.env, GH_NO_UPDATE_NOTIFIER: "1", ...(options.env || {}) },
|
|
8
8
|
shell: options.shell || false,
|
|
9
9
|
stdio: options.stdio || "pipe"
|
|
10
10
|
});
|
package/src/lib/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/log.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function header(title, version, rows = []) {
|
|
2
|
+
console.log(`${title} ${version}`);
|
|
3
|
+
console.log("");
|
|
4
|
+
for (const row of rows) console.log(row);
|
|
5
|
+
if (rows.length) console.log("");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function step(index, total, title) {
|
|
9
|
+
console.log(`[${index}/${total}] ${title}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ok(message) {
|
|
13
|
+
console.log(`通过:${message}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function warn(message) {
|
|
17
|
+
console.warn(`提示:${message}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function skip(message) {
|
|
21
|
+
console.log(`跳过:${message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function fail(message) {
|
|
25
|
+
console.log(`失败:${message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function action(message) {
|
|
29
|
+
console.log(message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function blank() {
|
|
33
|
+
console.log("");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function shortSha(value) {
|
|
37
|
+
return value ? value.slice(0, 7) : "";
|
|
38
|
+
}
|
|
39
|
+
|
package/src/lib/manifest.js
CHANGED
|
@@ -4,6 +4,7 @@ import https from "node:https";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { commandExists, run } from "./exec.js";
|
|
6
6
|
import { platformKey } from "./platform.js";
|
|
7
|
+
import { action, warn } from "./log.js";
|
|
7
8
|
|
|
8
9
|
export function readManifest(file) {
|
|
9
10
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
@@ -13,7 +14,7 @@ export function manifestDigest(manifest) {
|
|
|
13
14
|
return crypto.createHash("sha256").update(JSON.stringify(manifest)).digest("hex");
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
function download(url, file, headers = {}) {
|
|
17
|
+
export function download(url, file, headers = {}) {
|
|
17
18
|
return new Promise((resolve, reject) => {
|
|
18
19
|
const request = https.get(url, { headers }, (response) => {
|
|
19
20
|
if ([301, 302, 307, 308].includes(response.statusCode) && response.headers.location) {
|
|
@@ -77,10 +78,7 @@ async function downloadPrivateWithGh(asset, file) {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
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] || "";
|
|
81
|
+
return options.githubToken || process.env.GITHUB_TOKEN || "";
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
async function githubAssetApiUrl(asset, token) {
|
|
@@ -118,10 +116,9 @@ async function downloadAsset(tool, asset, file, options = {}) {
|
|
|
118
116
|
await download(asset.url, file);
|
|
119
117
|
return;
|
|
120
118
|
}
|
|
121
|
-
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,30 +137,40 @@ 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
|
+
if (options.log !== false) action(`下载 ${tool.name} ${tool.version} (${key})`);
|
|
155
155
|
await downloadAsset(tool, asset, archive, options);
|
|
156
156
|
const sha = await expectedSha256(tool, asset, options);
|
|
157
|
-
|
|
158
|
-
if (
|
|
157
|
+
const actualSha = fileSha256(archive);
|
|
158
|
+
if (sha && actualSha !== sha) throw new Error(`${tool.name} sha256 mismatch`);
|
|
159
|
+
if (!sha) warn(`${tool.name} 未提供 sha256,已继续安装`);
|
|
159
160
|
if (archive.endsWith(".zip")) {
|
|
160
|
-
await run("unzip", ["-o", archive, "-d", binDir]
|
|
161
|
+
await run("unzip", ["-o", archive, "-d", binDir]);
|
|
161
162
|
} else {
|
|
162
|
-
await run("tar", ["-xzf", archive, "-C", binDir]
|
|
163
|
+
await run("tar", ["-xzf", archive, "-C", binDir]);
|
|
163
164
|
}
|
|
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
|
}
|