@lumi-ai-lab/harness-data 0.0.13 → 0.0.15

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.13",
3
+ "version": "0.0.15",
4
4
  "description": "Installer and updater for Harness Data",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,10 +40,15 @@ function configPathsValid(workspace) {
40
40
  return matches.length >= 3 && matches.every((entry) => fs.existsSync(entry));
41
41
  }
42
42
 
43
- function jsonFileValid(file) {
43
+ function casCredentialsValid(dir) {
44
+ const encrypted = path.join(dir, "credentials.enc");
44
45
  try {
45
- JSON.parse(fs.readFileSync(file, "utf8"));
46
- return true;
46
+ if (fs.statSync(encrypted).size > 0) return true;
47
+ } catch {}
48
+
49
+ try {
50
+ const config = JSON.parse(fs.readFileSync(path.join(dir, "config.json"), "utf8"));
51
+ return Boolean(config?.cas?.username && config?.cas?.password);
47
52
  } catch {
48
53
  return false;
49
54
  }
@@ -65,7 +70,7 @@ export async function collectDoctor(workspace, options = {}) {
65
70
  add("config/harness-config.yaml", fs.existsSync(path.join(workspace, "config", "harness-config.yaml")));
66
71
  add("config/qdm-cli-paths.env", fs.existsSync(path.join(workspace, "config", "qdm-cli-paths.env")));
67
72
  add("config CLI paths", configPathsValid(workspace));
68
- add("CAS credentials file", jsonFileValid(path.join(casConfigDir, "config.json")), casConfigDir);
73
+ add("CAS credentials file", casCredentialsValid(casConfigDir), casConfigDir);
69
74
  add("CMR token", await tokenCheck(workspace, "qdm-cmr-cli", env));
70
75
  add("Indicators token", await tokenCheck(workspace, "qdm-indicators-cli", env));
71
76
  add("Agent hook", concreteAgentNames.some((name) => agentOk(workspace, name)));
@@ -3,7 +3,7 @@ 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
9
  import { resolveLatestManifest } from "../lib/tool-release.js";
@@ -16,6 +16,14 @@ import { gitUrls, runGitWithProtocol } from "../lib/git-auth.js";
16
16
  const runtimeRepo = "lumi-ai-lab/harness-data";
17
17
  const wikisRepo = "lumi-ai-lab/harness-data-wikis";
18
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
+
19
27
  async function requireCommands(commands) {
20
28
  for (const command of commands) {
21
29
  if (!(await commandExists(command))) throw new Error(`missing required command: ${command}`);
@@ -162,8 +170,9 @@ async function writeCasCredentials(runtimeDir, options = {}) {
162
170
  if (!username || !password) throw new Error("CAS username and password are required");
163
171
  const dir = casConfigDir(runtimeDir);
164
172
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
165
- fs.writeFileSync(path.join(dir, "config.json"), `${JSON.stringify({ cas: { username, password } }, null, 2)}\n`, { mode: 0o600 });
166
- ok("CAS 凭证已保存到 .qdm-auth/cas/config.json");
173
+ const env = { QDM_CAS_CONFIG_DIR: dir };
174
+ await run(path.join(runtimeDir, "bin", binaryName("cas-cli")), ["config", "set-credentials", "--username", username, "--password", password], { cwd: runtimeDir, env });
175
+ ok("CAS 凭证已加密保存到 .qdm-auth/cas/credentials.enc");
167
176
  return dir;
168
177
  }
169
178
 
@@ -233,17 +242,18 @@ export async function installCommand(options = {}) {
233
242
  step(3, 8, "安装 CLI 工具");
234
243
  const manifestPath = path.resolve(options.manifest || path.join(runtimeDir, "bootstrap", "cli-manifest.json"));
235
244
  const tokenMode = await hasGithubAuth(options);
245
+ const installState = readInstallState(runtimeDir);
236
246
 
237
247
  let manifest;
238
248
  let localTools = {};
239
249
  if (tokenMode) {
240
250
  manifest = readManifest(manifestPath);
241
251
  const latestManifest = await resolveLatestManifest(manifest, key, options);
242
- manifest = await installToolsFromManifest(runtimeDir, manifestPath, { ...options, manifestOverride: latestManifest });
252
+ manifest = await installToolsFromManifest(runtimeDir, manifestPath, { ...options, state: installState, manifestOverride: latestManifest });
243
253
  } else {
244
254
  manifest = readManifest(manifestPath);
245
255
  const latestManifest = await resolveLatestManifest(manifest, key, { ...options, tools: ["data-harness-cli"] });
246
- await installToolsFromManifest(runtimeDir, manifestPath, { ...options, manifestOverride: latestManifest });
256
+ manifest = await installToolsFromManifest(runtimeDir, manifestPath, { ...options, state: installState, manifestOverride: latestManifest });
247
257
  localTools = await installLocalTools(runtimeDir, options);
248
258
  }
249
259
  ok(`${Object.keys(manifest.installedTools || {}).length + Object.keys(localTools).length} 个 CLI 已安装到 bin/`);
package/src/lib/config.js CHANGED
@@ -36,16 +36,17 @@ export function writeLocalConfig(workspace, options = {}) {
36
36
  }
37
37
 
38
38
  export function validateCasConfigDir(dir) {
39
- const file = path.join(dir, "config.json");
40
- let config;
39
+ const encrypted = path.join(dir, "credentials.enc");
41
40
  try {
42
- config = JSON.parse(fs.readFileSync(file, "utf8"));
43
- } catch {
44
- throw new Error(`CAS config is missing or invalid: ${file}`);
45
- }
46
- if (!config?.cas?.username || !config?.cas?.password) {
47
- throw new Error(`CAS config must contain non-empty cas.username and cas.password: ${file}`);
48
- }
41
+ if (fs.statSync(encrypted).size > 0) return;
42
+ } catch {}
43
+
44
+ const legacy = path.join(dir, "config.json");
45
+ try {
46
+ const config = JSON.parse(fs.readFileSync(legacy, "utf8"));
47
+ if (config?.cas?.username && config?.cas?.password) return;
48
+ } catch {}
49
+ throw new Error(`CAS credentials are missing or invalid in: ${dir}`);
49
50
  }
50
51
 
51
52
  export function linkAgents(workspace, agent) {
@@ -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 });