@lumi-ai-lab/harness-data 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumi-ai-lab/harness-data",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Installer and updater for Harness Data",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,9 +3,10 @@ import path from "node:path";
3
3
  import { commandExists, run } from "../lib/exec.js";
4
4
  import { writeLocalConfig, linkAgents } from "../lib/config.js";
5
5
  import { ask, askSecret, chooseAgent } from "../lib/prompt.js";
6
- import { resolveWorkspaceDir, writeState } from "../lib/paths.js";
6
+ import { readUserState, resolveWorkspaceDir, writeState } from "../lib/paths.js";
7
7
  import { installToolsFromManifest, manifestDigest, readManifest } from "../lib/manifest.js";
8
8
  import { binaryName, platformKey } from "../lib/platform.js";
9
+ import { resolveLatestManifest } from "../lib/tool-release.js";
9
10
  import { collectDoctor } from "./doctor.js";
10
11
  import { packageVersion } from "../lib/package.js";
11
12
  import { downloadReleaseAsset, findReleaseAsset, githubToken, hasGithubAuth, latestRelease } from "../lib/github.js";
@@ -15,6 +16,14 @@ import { gitUrls, runGitWithProtocol } from "../lib/git-auth.js";
15
16
  const runtimeRepo = "lumi-ai-lab/harness-data";
16
17
  const wikisRepo = "lumi-ai-lab/harness-data-wikis";
17
18
 
19
+ function readInstallState(runtimeDir) {
20
+ try {
21
+ return JSON.parse(fs.readFileSync(path.join(runtimeDir, ".harness", "installer-state.json"), "utf8"));
22
+ } catch {
23
+ return readUserState();
24
+ }
25
+ }
26
+
18
27
  async function requireCommands(commands) {
19
28
  for (const command of commands) {
20
29
  if (!(await commandExists(command))) throw new Error(`missing required command: ${command}`);
@@ -232,14 +241,18 @@ export async function installCommand(options = {}) {
232
241
  step(3, 8, "安装 CLI 工具");
233
242
  const manifestPath = path.resolve(options.manifest || path.join(runtimeDir, "bootstrap", "cli-manifest.json"));
234
243
  const tokenMode = await hasGithubAuth(options);
244
+ const installState = readInstallState(runtimeDir);
235
245
 
236
246
  let manifest;
237
247
  let localTools = {};
238
248
  if (tokenMode) {
239
- manifest = await installToolsFromManifest(runtimeDir, manifestPath, options);
249
+ manifest = readManifest(manifestPath);
250
+ const latestManifest = await resolveLatestManifest(manifest, key, options);
251
+ manifest = await installToolsFromManifest(runtimeDir, manifestPath, { ...options, state: installState, manifestOverride: latestManifest });
240
252
  } else {
241
253
  manifest = readManifest(manifestPath);
242
- await installToolsFromManifest(runtimeDir, manifestPath, { ...options, tools: ["data-harness-cli"] });
254
+ const latestManifest = await resolveLatestManifest(manifest, key, { ...options, tools: ["data-harness-cli"] });
255
+ manifest = await installToolsFromManifest(runtimeDir, manifestPath, { ...options, state: installState, manifestOverride: latestManifest });
243
256
  localTools = await installLocalTools(runtimeDir, options);
244
257
  }
245
258
  ok(`${Object.keys(manifest.installedTools || {}).length + Object.keys(localTools).length} 个 CLI 已安装到 bin/`);
@@ -7,6 +7,7 @@ import { installToolsFromManifest, manifestDigest, readManifest } from "../lib/m
7
7
  import { packageVersion } from "../lib/package.js";
8
8
  import { platformKey } from "../lib/platform.js";
9
9
  import { githubToken, latestRelease } from "../lib/github.js";
10
+ import { resolveLatestTool } from "../lib/tool-release.js";
10
11
  import { protocolFromUrl, runGitWithProtocol } from "../lib/git-auth.js";
11
12
  import { buildAndCheck, installRuntimeBundle, printDoctorSummary } from "./install.js";
12
13
  import { collectDoctor } from "./doctor.js";
@@ -23,48 +24,17 @@ async function npmLatest() {
23
24
  }
24
25
  }
25
26
 
26
- function archiveSuffix(url) {
27
- if (url.endsWith(".zip")) return "zip";
28
- if (url.endsWith(".tar.gz")) return "tar.gz";
29
- return "";
30
- }
31
-
32
- function toolAssetName(tool, tag, key) {
33
- const current = tool.platforms?.[key]?.url || "";
34
- const suffix = archiveSuffix(current) || (key.startsWith("windows-") ? "zip" : "tar.gz");
35
- return `${tool.binary}-${tag}-${key}.${suffix}`;
36
- }
37
-
38
- function releaseAsset(release, name) {
39
- return (release.assets || []).find((asset) => asset.name === name);
40
- }
41
-
42
- function oneToolManifest(manifest, tool, tag, asset, key) {
27
+ function oneToolManifest(manifest, tool) {
43
28
  return {
44
29
  ...manifest,
45
- tools: [{
46
- ...tool,
47
- version: tag,
48
- platforms: {
49
- [key]: {
50
- url: asset.browser_download_url || `https://github.com/${tool.repo}/releases/download/${tag}/${asset.name}`,
51
- sha256: ""
52
- }
53
- }
54
- }]
30
+ tools: [tool]
55
31
  };
56
32
  }
57
33
 
58
34
  async function maybeUpdateTool(runtimeDir, manifest, tool, options, state) {
59
35
  const key = platformKey();
60
- const release = await latestRelease(tool.repo, options);
61
- const tag = release.tag_name;
62
- const assetName = toolAssetName(tool, tag, key);
63
- const asset = releaseAsset(release, assetName);
64
- if (!asset) {
65
- warn(`${tool.name} 最新 release 缺少 ${assetName},已跳过`);
66
- return null;
67
- }
36
+ const latestTool = await resolveLatestTool(tool, key, options);
37
+ const tag = latestTool.version;
68
38
  const current = state.tools?.[tool.name] || {};
69
39
  const tagChanged = current.version && current.version !== tag;
70
40
  const firstInstall = !current.version;
@@ -78,12 +48,12 @@ async function maybeUpdateTool(runtimeDir, manifest, tool, options, state) {
78
48
  options.skippedUpdates?.push(`${tool.name} ${tag}`);
79
49
  return null;
80
50
  }
81
- const updatedManifest = oneToolManifest(manifest, tool, tag, asset, key);
51
+ const updatedManifest = oneToolManifest(manifest, latestTool);
82
52
  const installed = await installToolsFromManifest(runtimeDir, path.join(runtimeDir, ".bootstrap-cache", `${tool.name}-manifest.json`), {
83
53
  ...options,
84
54
  manifestOverride: updatedManifest
85
55
  });
86
- const result = installed.installedTools?.[tool.name] || { version: tag, asset: assetName };
56
+ const result = installed.installedTools?.[tool.name] || { version: tag };
87
57
  ok(`${tool.name} 已更新到 ${tag}`);
88
58
  return result;
89
59
  }
@@ -4,7 +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
+ import { action, skip, warn } from "./log.js";
8
8
 
9
9
  export function readManifest(file) {
10
10
  return JSON.parse(fs.readFileSync(file, "utf8"));
@@ -136,6 +136,35 @@ function fileSha256(file) {
136
136
  return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
137
137
  }
138
138
 
139
+ function executable(file) {
140
+ try {
141
+ fs.accessSync(file, fs.constants.X_OK);
142
+ return true;
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ function reusableInstalledTool(workspace, tool, asset, options = {}) {
149
+ const current = tool.version ? optionsStateTool(options, tool.name) : null;
150
+ if (!current?.version || current.version !== tool.version) return null;
151
+ if (!current.sha256) return null;
152
+ const binary = path.join(workspace, "bin", tool.binary);
153
+ if (!executable(binary)) return null;
154
+ const actualSha = fileSha256(binary);
155
+ if (actualSha !== current.sha256) return null;
156
+ return {
157
+ version: current.version,
158
+ asset: current.asset || assetName(asset),
159
+ sha256: current.sha256,
160
+ ...(current.assetSha256 ? { assetSha256: current.assetSha256 } : {})
161
+ };
162
+ }
163
+
164
+ function optionsStateTool(options, name) {
165
+ return options?.state?.tools?.[name] || null;
166
+ }
167
+
139
168
  export async function installToolsFromManifest(workspace, manifestPath, options = {}) {
140
169
  const manifest = options.manifestOverride || readManifest(manifestPath);
141
170
  const only = options.tools ? new Set(options.tools) : null;
@@ -150,6 +179,12 @@ export async function installToolsFromManifest(workspace, manifestPath, options
150
179
  if (only && !only.has(tool.name)) continue;
151
180
  const asset = tool.platforms?.[key];
152
181
  if (!asset?.url) throw new Error(`manifest missing ${tool.name} asset for ${key}`);
182
+ const reusable = !options.force ? reusableInstalledTool(workspace, tool, asset, options) : null;
183
+ if (reusable) {
184
+ if (options.log !== false) skip(`${tool.name} 已是最新 ${tool.version}`);
185
+ installedTools[tool.name] = reusable;
186
+ continue;
187
+ }
153
188
  const archive = path.join(cacheDir, assetName(asset));
154
189
  if (options.log !== false) action(`下载 ${tool.name} ${tool.version} (${key})`);
155
190
  await downloadAsset(tool, asset, archive, options);
@@ -165,10 +200,12 @@ export async function installToolsFromManifest(workspace, manifestPath, options
165
200
  const binary = path.join(binDir, tool.binary);
166
201
  if (!fs.existsSync(binary)) throw new Error(`${tool.binary} was not extracted to bin/`);
167
202
  fs.chmodSync(binary, 0o755);
203
+ const binarySha = fileSha256(binary);
168
204
  installedTools[tool.name] = {
169
205
  version: tool.version || "",
170
206
  asset: assetName(asset),
171
- sha256: sha || actualSha
207
+ sha256: binarySha,
208
+ assetSha256: sha || actualSha
172
209
  };
173
210
  }
174
211
  Object.defineProperty(manifest, "installedTools", { value: installedTools, enumerable: false });
@@ -0,0 +1,47 @@
1
+ import { latestRelease } from "./github.js";
2
+
3
+ function archiveSuffix(asset, key) {
4
+ if (asset?.archive) return asset.archive;
5
+ if (asset?.url?.endsWith(".zip")) return "zip";
6
+ if (asset?.url?.endsWith(".tar.gz")) return "tar.gz";
7
+ return key.startsWith("windows-") ? "zip" : "tar.gz";
8
+ }
9
+
10
+ export function toolAssetName(tool, tag, key) {
11
+ const suffix = archiveSuffix(tool.platforms?.[key], key);
12
+ return `${tool.binary}-${tag}-${key}.${suffix}`;
13
+ }
14
+
15
+ export function releaseAsset(release, name) {
16
+ return (release.assets || []).find((asset) => asset.name === name);
17
+ }
18
+
19
+ export async function resolveLatestTool(tool, key, options = {}) {
20
+ const release = await latestRelease(tool.repo, options);
21
+ const tag = release.tag_name;
22
+ const name = toolAssetName(tool, tag, key);
23
+ const asset = releaseAsset(release, name);
24
+ if (!asset) {
25
+ throw new Error(`${tool.name} latest release ${tag} missing ${key} asset in ${tool.repo}: ${name}`);
26
+ }
27
+ return {
28
+ ...tool,
29
+ version: tag,
30
+ platforms: {
31
+ [key]: {
32
+ url: asset.browser_download_url || `https://github.com/${tool.repo}/releases/download/${tag}/${asset.name}`,
33
+ sha256: ""
34
+ }
35
+ }
36
+ };
37
+ }
38
+
39
+ export async function resolveLatestManifest(manifest, key, options = {}) {
40
+ const only = options.tools ? new Set(options.tools) : null;
41
+ const tools = [];
42
+ for (const tool of manifest.tools || []) {
43
+ if (only && !only.has(tool.name)) continue;
44
+ tools.push(await resolveLatestTool(tool, key, options));
45
+ }
46
+ return { ...manifest, tools };
47
+ }