@lumi-ai-lab/harness-data 0.0.16 → 0.0.17

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.16",
3
+ "version": "0.0.17",
4
4
  "description": "Installer and updater for Harness Data",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -44,6 +44,8 @@ Commands:
44
44
  Install options:
45
45
  --dir PATH Runtime directory (default: current directory)
46
46
  --agent NAME claude, codex, pi, openclaw, hermes, both, or all
47
- --github-token TOKEN GitHub token for private Release assets`);
47
+ --github-token TOKEN GitHub token for private Release assets
48
+ --cas-username USERNAME CAS username (skip interactive prompt)
49
+ --cas-password PASSWORD CAS password (skip interactive prompt)`);
48
50
  if (unknown) process.exitCode = 1;
49
51
  }
@@ -37,6 +37,26 @@ async function prepareRuntimeDir(options) {
37
37
  return runtimeDir;
38
38
  }
39
39
 
40
+ function replaceRuntimePath(runtimeDir, name, stagedRoot, backups) {
41
+ const target = path.join(runtimeDir, name);
42
+ const backup = fs.mkdtempSync(path.join(runtimeDir, `.install-backup-${name}-`));
43
+ fs.rmSync(backup, { recursive: true, force: true });
44
+ if (fs.existsSync(target)) fs.renameSync(target, backup);
45
+ backups.push({ name, target, backup });
46
+ fs.renameSync(path.join(stagedRoot, name), target);
47
+ }
48
+
49
+ function restoreRuntimeBackups(backups) {
50
+ for (const item of backups.slice().reverse()) {
51
+ fs.rmSync(item.target, { recursive: true, force: true });
52
+ if (fs.existsSync(item.backup)) fs.renameSync(item.backup, item.target);
53
+ }
54
+ }
55
+
56
+ function cleanupRuntimeBackups(backups) {
57
+ for (const item of backups) fs.rmSync(item.backup, { recursive: true, force: true });
58
+ }
59
+
40
60
  export async function installRuntimeBundle(runtimeDir, options = {}) {
41
61
  if (!options.force && fs.existsSync(path.join(runtimeDir, "agents")) &&
42
62
  fs.existsSync(path.join(runtimeDir, "config")) &&
@@ -56,7 +76,7 @@ export async function installRuntimeBundle(runtimeDir, options = {}) {
56
76
  fs.mkdirSync(cacheDir, { recursive: true });
57
77
  const archive = path.join(cacheDir, assetName);
58
78
  action(`下载 harness-data-runtime ${tag}`);
59
- await downloadReleaseAsset(asset, archive, options);
79
+ await downloadReleaseAsset(asset, archive, { ...options, progressLabel: assetName });
60
80
  if (shaAsset) {
61
81
  const shaFile = `${archive}.sha256`;
62
82
  await downloadReleaseAsset(shaAsset, shaFile, options);
@@ -69,20 +89,28 @@ export async function installRuntimeBundle(runtimeDir, options = {}) {
69
89
  }
70
90
 
71
91
  const extractDir = fs.mkdtempSync(path.join(cacheDir, "runtime-"));
72
- await run("tar", ["-xzf", archive, "-C", extractDir]);
73
- for (const dir of ["agents", "bootstrap"]) {
74
- const source = path.join(extractDir, dir);
75
- if (!fs.existsSync(source)) throw new Error(`runtime bundle missing ${dir}/`);
76
- fs.rmSync(path.join(runtimeDir, dir), { recursive: true, force: true });
77
- fs.cpSync(source, path.join(runtimeDir, dir), { recursive: true });
78
- }
79
- const configSource = path.join(extractDir, "config");
80
- if (!fs.existsSync(configSource)) throw new Error("runtime bundle missing config/");
81
- fs.mkdirSync(path.join(runtimeDir, "config"), { recursive: true });
82
- for (const file of fs.readdirSync(configSource)) {
83
- fs.copyFileSync(path.join(configSource, file), path.join(runtimeDir, "config", file));
92
+ const stagedRoot = fs.mkdtempSync(path.join(runtimeDir, ".install-new-runtime-"));
93
+ const backups = [];
94
+ try {
95
+ // Git Bash tar 无法处理 Windows 绝对路径(E:\...),使用相对路径执行
96
+ await run("tar", ["-xzf", path.relative(cacheDir, archive), "-C", path.relative(cacheDir, extractDir)], { cwd: cacheDir });
97
+ for (const dir of ["agents", "bootstrap"]) if (!fs.existsSync(path.join(extractDir, dir))) throw new Error(`runtime bundle missing ${dir}/`);
98
+ const configSource = path.join(extractDir, "config");
99
+ if (!fs.existsSync(configSource)) throw new Error("runtime bundle missing config/");
100
+
101
+ for (const dir of ["agents", "bootstrap"]) fs.cpSync(path.join(extractDir, dir), path.join(stagedRoot, dir), { recursive: true });
102
+ fs.mkdirSync(path.join(stagedRoot, "config"), { recursive: true });
103
+ for (const file of fs.readdirSync(configSource)) fs.copyFileSync(path.join(configSource, file), path.join(stagedRoot, "config", file));
104
+
105
+ for (const name of ["agents", "bootstrap", "config"]) replaceRuntimePath(runtimeDir, name, stagedRoot, backups);
106
+ cleanupRuntimeBackups(backups);
107
+ } catch (error) {
108
+ restoreRuntimeBackups(backups);
109
+ throw error;
110
+ } finally {
111
+ fs.rmSync(extractDir, { recursive: true, force: true });
112
+ fs.rmSync(stagedRoot, { recursive: true, force: true });
84
113
  }
85
- fs.rmSync(extractDir, { recursive: true, force: true });
86
114
  ok(`runtime bundle ${tag}`);
87
115
  return { tag, skipped: false };
88
116
  }
@@ -165,8 +193,8 @@ function casConfigDir(runtimeDir) {
165
193
  }
166
194
 
167
195
  async function writeCasCredentials(runtimeDir, options = {}) {
168
- const username = await ask("CAS 用户名:", options);
169
- const password = await askSecret("CAS 密码:", options);
196
+ const username = options.casUsername || await ask("CAS 用户名:", options);
197
+ const password = options.casPassword || await askSecret("CAS 密码:", options);
170
198
  if (!username || !password) throw new Error("CAS username and password are required");
171
199
  const dir = casConfigDir(runtimeDir);
172
200
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -263,6 +291,17 @@ export async function installCommand(options = {}) {
263
291
  ok(`${Object.keys(manifest.installedTools || {}).length + Object.keys(localTools).length} 个 CLI 已安装到 bin/`);
264
292
  blank();
265
293
 
294
+ // 及时持久化 CLI 安装状态,后续步骤失败时重新安装可跳过已下载的 CLI
295
+ writeState(runtimeDir, {
296
+ installMode: tokenMode ? "github-token" : "local-path",
297
+ runtimeTag: bundle.tag,
298
+ localTools,
299
+ tools: manifest.installedTools || {},
300
+ manifestSha256: manifestDigest(manifest),
301
+ packageVersion: packageVersion()
302
+ });
303
+ blank();
304
+
266
305
  step(4, 8, "同步 Wikis 知识库");
267
306
  await installWikis(runtimeDir, options);
268
307
  blank();
package/src/lib/github.js CHANGED
@@ -48,5 +48,10 @@ export function findReleaseAsset(release, name) {
48
48
 
49
49
  export async function downloadReleaseAsset(asset, file, options = {}) {
50
50
  const headers = { ...githubHeaders(options), Accept: "application/octet-stream" };
51
- await download(asset.url, file, headers);
51
+ await download(asset.url, file, headers, {
52
+ progressLabel: options.progressLabel,
53
+ log: options.log,
54
+ progress: options.progress,
55
+ progressWriter: options.progressWriter
56
+ });
52
57
  }
package/src/lib/log.js CHANGED
@@ -1,3 +1,7 @@
1
+ // 在 Git Bash (Windows) 下,Node.js stdout 连接为 pipe 时默认缓冲输出。
2
+ // setBlocking(true) 让每次写入即时刷新,避免下载进度等日志需要回车才显示。
3
+ if (process.stdout._handle?.setBlocking) process.stdout._handle.setBlocking(true);
4
+
1
5
  export function header(title, version, rows = []) {
2
6
  console.log(`${title} ${version}`);
3
7
  console.log("");
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import https from "node:https";
4
4
  import path from "node:path";
5
5
  import { commandExists, run } from "./exec.js";
6
- import { platformKey } from "./platform.js";
6
+ import { binaryName, platformKey } from "./platform.js";
7
7
  import { action, skip, warn } from "./log.js";
8
8
 
9
9
  export function readManifest(file) {
@@ -14,14 +14,107 @@ export function manifestDigest(manifest) {
14
14
  return crypto.createHash("sha256").update(JSON.stringify(manifest)).digest("hex");
15
15
  }
16
16
 
17
- export function download(url, file, headers = {}) {
17
+ function formatBytes(value) {
18
+ if (value < 1024) return `${value} B`;
19
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
20
+ return `${(value / 1024 / 1024).toFixed(1)} MB`;
21
+ }
22
+
23
+ function shouldShowProgress(options = {}) {
24
+ if (!options.progressLabel) return false;
25
+ if (options.progressWriter) return true;
26
+ if (options.log === false) return false;
27
+ if (options.progress === true) return true;
28
+ return Boolean(process.stdout.isTTY) && !process.env.CI;
29
+ }
30
+
31
+ function downloadProgress(label, total, options = {}) {
32
+ if (!shouldShowProgress(options)) return { tick() {}, done() {}, fail() {} };
33
+ const writer = options.progressWriter || process.stdout;
34
+ let downloaded = 0;
35
+ let lastLength = 0;
36
+ let lastRenderAt = 0;
37
+ let ended = false;
38
+ const width = 24;
39
+ const render = (done = false) => {
40
+ const now = Date.now();
41
+ if (!done && now - lastRenderAt < 80) return;
42
+ lastRenderAt = now;
43
+ const percent = total ? Math.min(100, Math.floor((downloaded / total) * 100)) : 0;
44
+ const filled = total ? Math.min(width, Math.floor((percent / 100) * width)) : Math.floor((downloaded / 1024) % width);
45
+ const bar = `${"=".repeat(filled)}${" ".repeat(width - filled)}`;
46
+ const size = total ? `${formatBytes(downloaded)}/${formatBytes(total)}` : formatBytes(downloaded);
47
+ const prefix = done ? "下载完成" : "下载中";
48
+ const line = `${prefix} ${label} [${bar}] ${total ? `${percent}% ` : ""}${size}`;
49
+ writer.write(`\r${line}${" ".repeat(Math.max(0, lastLength - line.length))}`);
50
+ lastLength = line.length;
51
+ if (done) writer.write("\n");
52
+ };
53
+ render();
54
+ return {
55
+ tick(chunk) {
56
+ if (ended) return;
57
+ downloaded += chunk.length;
58
+ render();
59
+ },
60
+ done() {
61
+ if (ended) return;
62
+ ended = true;
63
+ if (total) downloaded = total;
64
+ render(true);
65
+ },
66
+ fail() {
67
+ if (ended) return;
68
+ ended = true;
69
+ writer.write("\n");
70
+ }
71
+ };
72
+ }
73
+
74
+ function ghDownloadStatus(label, options = {}) {
75
+ const writer = options.progressWriter || process.stdout;
76
+ const enabled = Boolean(options.progressWriter) || (options.log !== false && Boolean(process.stdout.isTTY) && !process.env.CI);
77
+ if (!enabled) return { done() {}, fail() {} };
78
+ const frames = ["-", "\\", "|", "/"];
79
+ let index = 0;
80
+ let lastLength = 0;
81
+ let ended = false;
82
+ const render = (prefix) => {
83
+ const line = `${prefix} ${label}`;
84
+ writer.write(`\r${line}${" ".repeat(Math.max(0, lastLength - line.length))}`);
85
+ lastLength = line.length;
86
+ };
87
+ render(`下载中 ${frames[index]}`);
88
+ const timer = setInterval(() => {
89
+ index = (index + 1) % frames.length;
90
+ render(`下载中 ${frames[index]}`);
91
+ }, 120);
92
+ return {
93
+ done() {
94
+ if (ended) return;
95
+ ended = true;
96
+ clearInterval(timer);
97
+ render("下载完成");
98
+ writer.write("\n");
99
+ },
100
+ fail() {
101
+ if (ended) return;
102
+ ended = true;
103
+ clearInterval(timer);
104
+ writer.write("\n");
105
+ }
106
+ };
107
+ }
108
+
109
+ export function download(url, file, headers = {}, options = {}) {
18
110
  return new Promise((resolve, reject) => {
19
111
  const request = https.get(url, { headers }, (response) => {
20
112
  if ([301, 302, 307, 308].includes(response.statusCode) && response.headers.location) {
21
113
  const current = new URL(url);
22
114
  const next = new URL(response.headers.location, url);
23
115
  const nextHeaders = current.hostname === next.hostname ? headers : {};
24
- download(response.headers.location, file, nextHeaders).then(resolve, reject);
116
+ response.resume();
117
+ download(response.headers.location, file, nextHeaders, options).then(resolve, reject);
25
118
  return;
26
119
  }
27
120
  if (response.statusCode !== 200) {
@@ -31,9 +124,25 @@ export function download(url, file, headers = {}) {
31
124
  }
32
125
  fs.mkdirSync(path.dirname(file), { recursive: true });
33
126
  const out = fs.createWriteStream(file);
127
+ const total = Number(response.headers["content-length"]) || 0;
128
+ const progress = downloadProgress(options.progressLabel, total, options);
129
+ let settled = false;
130
+ const rejectOnce = (error) => {
131
+ if (settled) return;
132
+ settled = true;
133
+ progress.fail();
134
+ reject(error);
135
+ };
136
+ response.on("data", (chunk) => progress.tick(chunk));
137
+ response.on("error", rejectOnce);
34
138
  response.pipe(out);
35
- out.on("finish", () => out.close(resolve));
36
- out.on("error", reject);
139
+ out.on("finish", () => out.close(() => {
140
+ if (settled) return;
141
+ settled = true;
142
+ progress.done();
143
+ resolve();
144
+ }));
145
+ out.on("error", rejectOnce);
37
146
  });
38
147
  request.on("error", reject);
39
148
  });
@@ -60,19 +169,61 @@ async function ghAuthenticated() {
60
169
  return result.code === 0;
61
170
  }
62
171
 
63
- async function downloadPrivateWithGh(asset, file) {
172
+ async function downloadPrivateWithTokenValue(asset, file, token, options = {}) {
173
+ const apiUrl = await githubAssetApiUrl(asset, token);
174
+ if (!apiUrl) return false;
175
+ await download(apiUrl, file, {
176
+ Authorization: `Bearer ${token}`,
177
+ Accept: "application/octet-stream",
178
+ "User-Agent": "harness-data-installer"
179
+ }, {
180
+ progressLabel: assetName(asset),
181
+ log: options.log,
182
+ progress: options.progress,
183
+ progressWriter: options.progressWriter
184
+ });
185
+ return true;
186
+ }
187
+
188
+ async function downloadPrivateWithGh(asset, file, options = {}) {
64
189
  const parts = githubAssetParts(asset);
65
190
  if (!parts || !(await ghAuthenticated())) return false;
191
+ const failures = options.failures || [];
192
+ const tokenResult = await run("gh", ["auth", "token"], { allowFailure: true });
193
+ const token = tokenResult.code === 0 ? tokenResult.stdout.trim() : "";
194
+ if (token) {
195
+ try {
196
+ if (await downloadPrivateWithTokenValue(asset, file, token, options)) return true;
197
+ } catch (error) {
198
+ failures.push(error.message);
199
+ // If Node cannot reach GitHub directly, keep the older gh download fallback available.
200
+ }
201
+ }
66
202
  const dir = fs.mkdtempSync(path.join(path.dirname(file), "gh-download-"));
203
+ const status = ghDownloadStatus(assetName(asset), options);
67
204
  try {
68
- const result = await run("gh", ["release", "download", parts.tag, "--repo", parts.repo, "--pattern", parts.name, "--dir", dir], { allowFailure: true });
69
- if (result.code !== 0) return false;
205
+ const result = await run("gh", ["release", "download", parts.tag, "--repo", parts.repo, "--pattern", parts.name, "--dir", dir], {
206
+ allowFailure: true,
207
+ stdio: "pipe"
208
+ });
209
+ if (result.code !== 0) {
210
+ status.fail();
211
+ const detail = result.stderr.trim() || result.stdout.trim();
212
+ failures.push(`gh release download failed${detail ? `: ${detail}` : ""}`);
213
+ return false;
214
+ }
70
215
  const downloaded = path.join(dir, parts.name);
71
- if (!fs.existsSync(downloaded)) return false;
216
+ if (!fs.existsSync(downloaded)) {
217
+ status.fail();
218
+ failures.push(`gh release download did not produce ${parts.name}`);
219
+ return false;
220
+ }
72
221
  fs.mkdirSync(path.dirname(file), { recursive: true });
73
222
  fs.renameSync(downloaded, file);
223
+ status.done();
74
224
  return true;
75
225
  } finally {
226
+ status.fail();
76
227
  fs.rmSync(dir, { recursive: true, force: true });
77
228
  }
78
229
  }
@@ -101,31 +252,43 @@ async function githubAssetApiUrl(asset, token) {
101
252
  async function downloadPrivateWithToken(asset, file, options = {}) {
102
253
  const token = githubToken(options);
103
254
  if (!token) return false;
104
- const apiUrl = await githubAssetApiUrl(asset, token);
105
- if (!apiUrl) return false;
106
- await download(apiUrl, file, {
107
- Authorization: `Bearer ${token}`,
108
- Accept: "application/octet-stream",
109
- "User-Agent": "harness-data-installer"
110
- });
111
- return true;
255
+ return downloadPrivateWithTokenValue(asset, file, token, options);
112
256
  }
113
257
 
114
258
  async function downloadAsset(tool, asset, file, options = {}) {
259
+ if (!tool.private && !githubToken(options)) {
260
+ await download(asset.url, file, {}, { progressLabel: assetName(asset), log: options.log, progress: options.progress, progressWriter: options.progressWriter });
261
+ return;
262
+ }
263
+
115
264
  if (!tool.private) {
116
- await download(asset.url, file);
265
+ try {
266
+ if (await downloadPrivateWithGh(asset, file, options)) return;
267
+ if (await downloadPrivateWithToken(asset, file, options)) return;
268
+ } catch {
269
+ // Public assets must remain downloadable when an unrelated GitHub token is stale or invalid.
270
+ }
271
+ await download(asset.url, file, {}, { progressLabel: assetName(asset), log: options.log, progress: options.progress, progressWriter: options.progressWriter });
117
272
  return;
118
273
  }
119
- if (await downloadPrivateWithGh(asset, file)) return;
120
- if (await downloadPrivateWithToken(asset, file, options)) return;
121
- throw new Error(`private GitHub Release asset requires gh auth login, GITHUB_TOKEN, or --github-token: ${assetName(asset)}`);
274
+
275
+ const failures = [];
276
+ const privateOptions = { ...options, failures };
277
+ if (await downloadPrivateWithGh(asset, file, privateOptions)) return;
278
+ try {
279
+ if (await downloadPrivateWithToken(asset, file, privateOptions)) return;
280
+ } catch (error) {
281
+ failures.push(error.message);
282
+ }
283
+ const detail = failures.filter(Boolean).join("; ");
284
+ throw new Error(`private GitHub Release asset requires gh auth login, GITHUB_TOKEN, or --github-token: ${assetName(asset)}${detail ? ` (${detail})` : ""}`);
122
285
  }
123
286
 
124
287
  async function expectedSha256(tool, asset, options = {}) {
125
288
  if (asset.sha256) return asset.sha256;
126
289
  try {
127
290
  const tmp = path.join(fs.mkdtempSync(path.join(process.cwd(), ".bootstrap-cache-sha-")), "asset.sha256");
128
- await downloadAsset(tool, { ...asset, url: `${asset.url}.sha256` }, tmp, options);
291
+ await downloadAsset(tool, { ...asset, url: `${asset.url}.sha256` }, tmp, { ...options, log: false, progress: false, progressWriter: null });
129
292
  return fs.readFileSync(tmp, "utf8").trim().split(/\s+/)[0];
130
293
  } catch {
131
294
  return "";
@@ -149,7 +312,7 @@ function reusableInstalledTool(workspace, tool, asset, options = {}) {
149
312
  const current = tool.version ? optionsStateTool(options, tool.name) : null;
150
313
  if (!current?.version || current.version !== tool.version) return null;
151
314
  if (!current.sha256) return null;
152
- const binary = path.join(workspace, "bin", tool.binary);
315
+ const binary = path.join(workspace, "bin", binaryName(tool.binary));
153
316
  if (!executable(binary)) return null;
154
317
  const actualSha = fileSha256(binary);
155
318
  if (actualSha !== current.sha256) return null;
@@ -165,6 +328,25 @@ function optionsStateTool(options, name) {
165
328
  return options?.state?.tools?.[name] || null;
166
329
  }
167
330
 
331
+ async function extractArchiveBinary(workspace, cacheDir, archive, binDir, tool) {
332
+ const extractDir = fs.mkdtempSync(path.join(cacheDir, `${tool.name}-extract-`));
333
+ try {
334
+ if (archive.endsWith(".zip")) {
335
+ await run("unzip", ["-o", path.relative(workspace, archive), "-d", path.relative(workspace, extractDir)], { cwd: workspace });
336
+ } else {
337
+ await run("tar", ["-xzf", path.relative(workspace, archive), "-C", path.relative(workspace, extractDir)], { cwd: workspace });
338
+ }
339
+ const extracted = path.join(extractDir, binaryName(tool.binary));
340
+ if (!fs.existsSync(extracted)) throw new Error(`${tool.binary} was not extracted to archive root`);
341
+ fs.chmodSync(extracted, 0o755);
342
+ const binary = path.join(binDir, binaryName(tool.binary));
343
+ fs.renameSync(extracted, binary);
344
+ return binary;
345
+ } finally {
346
+ fs.rmSync(extractDir, { recursive: true, force: true });
347
+ }
348
+ }
349
+
168
350
  export async function installToolsFromManifest(workspace, manifestPath, options = {}) {
169
351
  const manifest = options.manifestOverride || readManifest(manifestPath);
170
352
  const only = options.tools ? new Set(options.tools) : null;
@@ -192,14 +374,7 @@ export async function installToolsFromManifest(workspace, manifestPath, options
192
374
  const actualSha = fileSha256(archive);
193
375
  if (sha && actualSha !== sha) throw new Error(`${tool.name} sha256 mismatch`);
194
376
  if (!sha) warn(`${tool.name} 未提供 sha256,已继续安装`);
195
- if (archive.endsWith(".zip")) {
196
- await run("unzip", ["-o", archive, "-d", binDir]);
197
- } else {
198
- await run("tar", ["-xzf", archive, "-C", binDir]);
199
- }
200
- const binary = path.join(binDir, tool.binary);
201
- if (!fs.existsSync(binary)) throw new Error(`${tool.binary} was not extracted to bin/`);
202
- fs.chmodSync(binary, 0o755);
377
+ const binary = await extractArchiveBinary(workspace, cacheDir, archive, binDir, tool);
203
378
  const binarySha = fileSha256(binary);
204
379
  installedTools[tool.name] = {
205
380
  version: tool.version || "",
package/src/lib/prompt.js CHANGED
@@ -2,10 +2,16 @@ import readline from "node:readline/promises";
2
2
  import { stdin as input, stdout as output } from "node:process";
3
3
  import { Writable } from "node:stream";
4
4
  import { agentChoices, agentChoiceText } from "./config.js";
5
+ import { run } from "./exec.js";
5
6
 
6
7
  export async function confirm(message, options = {}) {
7
8
  if (options.yes) return true;
8
9
  const suffix = options.defaultNo ? " [y/N] " : " [Y/n] ";
10
+ if (!process.stdin.isTTY) {
11
+ const answer = (await shellRead(message + suffix)).toLowerCase();
12
+ if (!answer) return !options.defaultNo;
13
+ return answer === "y" || answer === "yes";
14
+ }
9
15
  const rl = readline.createInterface({ input, output });
10
16
  try {
11
17
  const answer = (await rl.question(message + suffix)).trim().toLowerCase();
@@ -23,6 +29,12 @@ export async function chooseAgent(options = {}) {
23
29
  return value;
24
30
  }
25
31
  if (options.yes) return "all";
32
+ if (!process.stdin.isTTY) {
33
+ const answer = (await shellRead(`选择 Agent:${agentChoiceText} [all] `)).toLowerCase();
34
+ const value = answer || "all";
35
+ if (!agentChoices.includes(value)) throw new Error(`agent must be ${agentChoiceText}`);
36
+ return value;
37
+ }
26
38
  const rl = readline.createInterface({ input, output });
27
39
  try {
28
40
  const answer = (await rl.question(`选择 Agent:${agentChoiceText} [all] `)).trim().toLowerCase();
@@ -34,9 +46,22 @@ export async function chooseAgent(options = {}) {
34
46
  }
35
47
  }
36
48
 
49
+ async function shellRead(prompt) {
50
+ const escaped = prompt.replace(/'/g, "'\\''");
51
+ const result = await run("bash", ["-c", `read -p '${escaped}' -r input && echo "$input"`], { stdio: ["inherit", "pipe", "inherit"] });
52
+ return result.stdout.trim();
53
+ }
54
+
55
+ async function shellReadSecret(prompt) {
56
+ const escaped = prompt.replace(/'/g, "'\\''");
57
+ const result = await run("bash", ["-c", `read -s -p '${escaped}' -r input && echo "$input"`], { stdio: ["inherit", "pipe", "inherit"] });
58
+ return result.stdout.trim();
59
+ }
60
+
37
61
  export async function ask(message, options = {}) {
38
62
  if (options.value) return String(options.value);
39
63
  if (options.yes) throw new Error(`${message} is required`);
64
+ if (!process.stdin.isTTY) return shellRead(`${message} `);
40
65
  const rl = readline.createInterface({ input, output });
41
66
  try {
42
67
  return (await rl.question(`${message} `)).trim();
@@ -48,6 +73,7 @@ export async function ask(message, options = {}) {
48
73
  export async function askSecret(message, options = {}) {
49
74
  if (options.value) return String(options.value);
50
75
  if (options.yes) throw new Error(`${message} is required`);
76
+ if (!process.stdin.isTTY) return shellReadSecret(`${message} `);
51
77
  const muted = new Writable({
52
78
  write(_chunk, _encoding, callback) {
53
79
  callback();