neko-harness 0.4.6 → 0.4.7

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/dist/gateway.js CHANGED
@@ -16501,6 +16501,7 @@ var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
16501
16501
  --storage <path> SQLite \u5B58\u50A8\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/neko.sqlite\uFF09
16502
16502
  --credentials <path> \u7528\u6237\u51ED\u636E\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/credentials.json\uFF09
16503
16503
  --config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
16504
+ --stop-on-stdin \u6807\u51C6\u8F93\u5165\u7ED3\u675F\u65F6\u5173\u505C\u7F51\u5173\uFF08\u4F9B Windows \u4E0A\u62C9\u8D77\u7F51\u5173\u7684\u524D\u7AEF\u4F7F\u7528\uFF09
16504
16505
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
16505
16506
 
16506
16507
  \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216\u73AF\u5883\u53D8\u91CF\uFF08DEEPSEEK_API_KEY / OPENROUTER_API_KEY / OPENCODE_API_KEY / TAVILY_API_KEY \u7B49\uFF09\u914D\u7F6E\u51ED\u636E\uFF1B
@@ -16584,6 +16585,9 @@ function parseGatewayArgs(argv) {
16584
16585
  options.configDir = next;
16585
16586
  i++;
16586
16587
  break;
16588
+ case "--stop-on-stdin":
16589
+ options.stopOnStdin = true;
16590
+ break;
16587
16591
  default:
16588
16592
  throw new Error(`\u672A\u77E5\u9009\u9879: ${arg}`);
16589
16593
  }
@@ -16890,6 +16894,16 @@ function validate(config, source) {
16890
16894
  throw new Error(`neko config: ${source}: "skills.marketUrl" must be a string.`);
16891
16895
  }
16892
16896
  }
16897
+ const shell = config.shell;
16898
+ if (shell !== undefined) {
16899
+ if (typeof shell !== "object" || shell === null || Array.isArray(shell)) {
16900
+ throw new Error(`neko config: ${source}: "shell" must be an object.`);
16901
+ }
16902
+ const { path } = shell;
16903
+ if (path !== undefined && typeof path !== "string") {
16904
+ throw new Error(`neko config: ${source}: "shell.path" must be a string.`);
16905
+ }
16906
+ }
16893
16907
  const webSearch = config.webSearch;
16894
16908
  if (webSearch !== undefined) {
16895
16909
  if (typeof webSearch !== "object" || webSearch === null || Array.isArray(webSearch)) {
@@ -16909,10 +16923,36 @@ function validate(config, source) {
16909
16923
  }
16910
16924
  }
16911
16925
  // packages/core/src/credentials.ts
16912
- import { chmod, mkdir, open, readFile, rename, unlink } from "fs/promises";
16926
+ import { chmod, mkdir, open, readFile, stat, unlink } from "fs/promises";
16913
16927
  import { homedir as homedir2 } from "os";
16914
16928
  import { dirname as dirname2, join as join2 } from "path";
16915
16929
 
16930
+ // packages/core/src/rename-with-retry.ts
16931
+ import { rename } from "fs/promises";
16932
+ var TRANSIENT = new Set(["EPERM", "EBUSY", "EACCES"]);
16933
+ var FIRST_DELAY_MS = 10;
16934
+ var MAX_TOTAL_MS = 1000;
16935
+ async function renameWithRetry(from, to, deps = {}) {
16936
+ const doRename = deps.rename ?? rename;
16937
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
16938
+ let delay = FIRST_DELAY_MS;
16939
+ let waited = 0;
16940
+ for (;; ) {
16941
+ try {
16942
+ await doRename(from, to);
16943
+ return;
16944
+ } catch (error) {
16945
+ const code = error.code;
16946
+ if (typeof code !== "string" || !TRANSIENT.has(code) || waited >= MAX_TOTAL_MS)
16947
+ throw error;
16948
+ const step = Math.min(delay, MAX_TOTAL_MS - waited);
16949
+ await sleep(step);
16950
+ waited += step;
16951
+ delay *= 2;
16952
+ }
16953
+ }
16954
+ }
16955
+
16916
16956
  // packages/core/src/credential-providers.ts
16917
16957
  var model = (id, name, envVar, signupUrl) => ({ id, kind: "model", name, envVar, signupUrl, keyless: false });
16918
16958
  var MODEL_PROVIDERS = [
@@ -16981,12 +17021,15 @@ function normalizeEnabledProviders(saved) {
16981
17021
 
16982
17022
  // packages/core/src/credentials.ts
16983
17023
  import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
17024
+ var STALE_LOCK_MS = 30000;
16984
17025
  var DEFAULT_CREDENTIALS_PATH = join2(homedir2(), ".neko", "credentials.json");
16985
17026
 
16986
17027
  class FileCredentialStore {
16987
17028
  path;
16988
- constructor(path = DEFAULT_CREDENTIALS_PATH) {
17029
+ #renameDeps;
17030
+ constructor(path = DEFAULT_CREDENTIALS_PATH, renameDeps = {}) {
16989
17031
  this.path = path;
17032
+ this.#renameDeps = renameDeps;
16990
17033
  }
16991
17034
  async#read() {
16992
17035
  try {
@@ -17016,6 +17059,7 @@ class FileCredentialStore {
17016
17059
  await mkdir(dirname2(this.path), { recursive: true, mode: 448 });
17017
17060
  const lockPath = `${this.path}.lock`;
17018
17061
  let lock;
17062
+ let reclaimed = false;
17019
17063
  for (let attempt = 0;; attempt++) {
17020
17064
  try {
17021
17065
  lock = await open(lockPath, "wx", 384);
@@ -17023,6 +17067,16 @@ class FileCredentialStore {
17023
17067
  } catch (error) {
17024
17068
  if (error.code !== "EEXIST" || attempt >= 100)
17025
17069
  throw new Error("Credential storage is busy or unavailable.");
17070
+ if (!reclaimed) {
17071
+ const info = await stat(lockPath).catch(() => {
17072
+ return;
17073
+ });
17074
+ if (info && Date.now() - info.mtimeMs > STALE_LOCK_MS) {
17075
+ reclaimed = true;
17076
+ await unlink(lockPath).catch(() => {});
17077
+ continue;
17078
+ }
17079
+ }
17026
17080
  await new Promise((resolve) => setTimeout(resolve, 25));
17027
17081
  }
17028
17082
  }
@@ -17044,7 +17098,7 @@ class FileCredentialStore {
17044
17098
  } finally {
17045
17099
  await file.close();
17046
17100
  }
17047
- await rename(temporary, this.path);
17101
+ await renameWithRetry(temporary, this.path, this.#renameDeps);
17048
17102
  return result;
17049
17103
  } catch (error) {
17050
17104
  if (callbackFailed)
@@ -19069,7 +19123,7 @@ var SearchText = defineTool2({
19069
19123
  complete = false;
19070
19124
  continue;
19071
19125
  }
19072
- const lines = new TextDecoder("utf-8", { fatal: true }).decode(data).split(/\r?\n/);
19126
+ const lines = new TextDecoder("utf-8", { fatal: true }).decode(data).replace(/^\uFEFF/, "").split(/\r?\n/);
19073
19127
  for (let i = 0;i < lines.length; i++) {
19074
19128
  ctx.abortSignal?.throwIfAborted();
19075
19129
  const value = lines[i];
@@ -19502,8 +19556,11 @@ class JobManager {
19502
19556
  outcome = { status: "failed", exitCode: null, reason: "timeout" };
19503
19557
  else if (result.error.code === "aborted")
19504
19558
  outcome = { status: "stopped", exitCode: null, reason: "aborted" };
19505
- else
19559
+ else {
19560
+ entry.buffer.append(`${result.error.message}
19561
+ `);
19506
19562
  outcome = { status: "failed", exitCode: null, reason: "spawn-error" };
19563
+ }
19507
19564
  }
19508
19565
  if (this.#closing) {
19509
19566
  await new Promise((resolve) => {
@@ -19900,10 +19957,15 @@ var TOOL_GUIDELINES = {
19900
19957
  ]
19901
19958
  };
19902
19959
  var GENERIC_RULES = ["Be concise in your responses", "Show file paths clearly when working with files"];
19903
- var buildRules = (toolNames) => {
19960
+ var WINDOWS_SHELL_RULE = "On Windows, commands run in Git Bash (not PowerShell, cmd or WSL); write paths with forward slashes, e.g. D:/code or /d/code.";
19961
+ var toolRules = (name, platform) => {
19962
+ const rules = TOOL_GUIDELINES[name] ?? [];
19963
+ return platform === "win32" && (name === "bash" || name === "job") ? [...rules, WINDOWS_SHELL_RULE] : rules;
19964
+ };
19965
+ var buildRules = (toolNames, platform) => {
19904
19966
  const rules = [];
19905
19967
  const seen = new Set;
19906
- for (const rule of [...toolNames.flatMap((name) => TOOL_GUIDELINES[name] ?? []), ...GENERIC_RULES]) {
19968
+ for (const rule of [...toolNames.flatMap((name) => toolRules(name, platform)), ...GENERIC_RULES]) {
19907
19969
  if (seen.has(rule))
19908
19970
  continue;
19909
19971
  seen.add(rule);
@@ -19920,7 +19982,7 @@ ${content}
19920
19982
  ].join(`
19921
19983
 
19922
19984
  `);
19923
- var createNekoPrompt = (loadResources) => {
19985
+ var createNekoPrompt = (loadResources, platform = process.platform) => {
19924
19986
  const built = new WeakMap;
19925
19987
  const build = async (input, context) => {
19926
19988
  const cwd = input.agent.cwd;
@@ -19928,7 +19990,7 @@ var createNekoPrompt = (loadResources) => {
19928
19990
  const resources = cwd ? await loadResources(input.conversationId, cwd) : { contextFiles: [], skills: [] };
19929
19991
  const sections = {
19930
19992
  preamble: role ? ROLE_PROMPTS[role] + SUBAGENT_DELIVERY : PREAMBLE,
19931
- rules: buildRules(input.agent.tools.map((tool) => tool.name)),
19993
+ rules: buildRules(input.agent.tools.map((tool) => tool.name), platform),
19932
19994
  project_context: renderProjectContext(resources.contextFiles),
19933
19995
  skills: formatSkillsForPrompt(resources.skills) || undefined,
19934
19996
  cwd: cwd?.replace(/\\/g, "/")
@@ -19976,7 +20038,7 @@ var readResources = async (workspace, home, builtinDir, report, invocationName)
19976
20038
  const contextFiles = [];
19977
20039
  for (const path of [userAgentsFile(home), workspaceAgentsFile(workspace)]) {
19978
20040
  try {
19979
- const content = await readFile3(path, "utf8");
20041
+ const content = (await readFile3(path, "utf8")).replace(/^\uFEFF/, "");
19980
20042
  if (Buffer.byteLength(content, "utf8") > 64 * 1024)
19981
20043
  report({
19982
20044
  path,
@@ -21433,11 +21495,152 @@ function createTavilyProvider(resolveKey, fetchImpl = fetch) {
21433
21495
  };
21434
21496
  }
21435
21497
 
21498
+ // packages/core/src/windows-shell.ts
21499
+ import { spawnSync } from "child_process";
21500
+ import { statSync as statSync2 } from "fs";
21501
+ import { win32 } from "path";
21502
+ function isWslBashPath(path) {
21503
+ const normalized = path.replace(/\//g, "\\").toLowerCase();
21504
+ return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized) || /\\microsoft\\windowsapps\\(?:[^\\]+\\)*bash\.exe$/.test(normalized);
21505
+ }
21506
+ function envValue(env, name) {
21507
+ const wanted = name.toLowerCase();
21508
+ for (const [key, value] of Object.entries(env))
21509
+ if (key.toLowerCase() === wanted && value)
21510
+ return value;
21511
+ return;
21512
+ }
21513
+ function bashBesideGit(gitExe) {
21514
+ const dir = win32.dirname(gitExe);
21515
+ const parent = win32.dirname(dir);
21516
+ const name = win32.basename(dir).toLowerCase();
21517
+ if (name === "cmd")
21518
+ return win32.join(parent, "bin", "bash.exe");
21519
+ if (name === "bin") {
21520
+ const root = win32.basename(parent).toLowerCase() === "mingw64" ? win32.dirname(parent) : parent;
21521
+ return win32.join(root, "bin", "bash.exe");
21522
+ }
21523
+ return;
21524
+ }
21525
+ function configProblem(path, kind) {
21526
+ if (isWslBashPath(path))
21527
+ return "it is WSL's bash, which is not supported (commands must run in Git Bash)";
21528
+ if (kind === "missing")
21529
+ return "the file does not exist";
21530
+ if (kind === "directory")
21531
+ return "it is a directory, not bash.exe";
21532
+ return;
21533
+ }
21534
+ function resolveWindowsShell(deps, configPath) {
21535
+ if (configPath !== undefined && configPath !== "") {
21536
+ const problem = configProblem(configPath, deps.kind(configPath));
21537
+ if (problem !== undefined) {
21538
+ return {
21539
+ kind: "missing",
21540
+ reason: "invalid_config",
21541
+ detail: `shell.path "${configPath}" is invalid: ${problem}.`,
21542
+ searched: [configPath]
21543
+ };
21544
+ }
21545
+ return { kind: "found", path: configPath, source: "config" };
21546
+ }
21547
+ const searched = [];
21548
+ const check = (path) => {
21549
+ if (!searched.includes(path))
21550
+ searched.push(path);
21551
+ return !isWslBashPath(path) && deps.kind(path) === "file";
21552
+ };
21553
+ const roots = [
21554
+ envValue(deps.env, "ProgramFiles"),
21555
+ envValue(deps.env, "ProgramFiles(x86)"),
21556
+ (() => {
21557
+ const local = envValue(deps.env, "LOCALAPPDATA");
21558
+ return local === undefined ? undefined : win32.join(local, "Programs");
21559
+ })()
21560
+ ];
21561
+ for (const root of roots) {
21562
+ if (root === undefined)
21563
+ continue;
21564
+ const candidate = win32.join(root, "Git", "bin", "bash.exe");
21565
+ if (check(candidate))
21566
+ return { kind: "found", path: candidate, source: "auto" };
21567
+ }
21568
+ for (const gitExe of deps.where("git.exe")) {
21569
+ const candidate = bashBesideGit(gitExe);
21570
+ if (candidate !== undefined && check(candidate))
21571
+ return { kind: "found", path: candidate, source: "auto" };
21572
+ }
21573
+ for (const candidate of deps.where("bash.exe")) {
21574
+ if (check(candidate))
21575
+ return { kind: "found", path: candidate, source: "auto" };
21576
+ }
21577
+ return { kind: "missing", reason: "not_found", detail: "No Git Bash was found.", searched };
21578
+ }
21579
+ function summarizeWindowsShell(status) {
21580
+ return status.kind === "found" ? { status: "ready", path: status.path, source: status.source } : { status: "missing", reason: status.reason, detail: status.detail };
21581
+ }
21582
+ function windowsShellMessage(status) {
21583
+ const lines = status.reason === "invalid_config" ? [
21584
+ `Cannot run shell commands: ${status.detail}`,
21585
+ "Fix shell.path in neko.config.json, or remove it to auto-detect Git Bash."
21586
+ ] : [
21587
+ "Cannot run shell commands: Git Bash was not found. neko runs commands in Git for Windows' bash, not PowerShell, cmd or WSL.",
21588
+ "Install Git for Windows (https://git-scm.com/download/win), or set shell.path in neko.config.json to its bash.exe."
21589
+ ];
21590
+ if (status.searched.length > 0)
21591
+ lines.push("Checked:", ...status.searched.map((path) => ` ${path}`));
21592
+ lines.push("File tools (read, write, edit) still work.");
21593
+ return lines.join(`
21594
+ `);
21595
+ }
21596
+ function systemWindowsShellDeps() {
21597
+ return {
21598
+ env: process.env,
21599
+ kind(path) {
21600
+ try {
21601
+ return statSync2(path).isDirectory() ? "directory" : "file";
21602
+ } catch {
21603
+ return "missing";
21604
+ }
21605
+ },
21606
+ where(program) {
21607
+ const result = spawnSync("where", [program], { encoding: "utf8", timeout: 5000, windowsHide: true });
21608
+ if (result.status !== 0 || typeof result.stdout !== "string")
21609
+ return [];
21610
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== "");
21611
+ }
21612
+ };
21613
+ }
21614
+ var WINDOWS_SHELL_RETRY_MS = 30000;
21615
+ function createWindowsShellResolver(options) {
21616
+ const deps = options?.deps ?? systemWindowsShellDeps();
21617
+ const now = options?.now ?? Date.now;
21618
+ let cached;
21619
+ return {
21620
+ status(call) {
21621
+ if (cached !== undefined && call?.refresh !== true) {
21622
+ if (cached.status.kind === "found") {
21623
+ if (deps.kind(cached.status.path) === "file")
21624
+ return cached.status;
21625
+ } else if (now() - cached.at < WINDOWS_SHELL_RETRY_MS) {
21626
+ return cached.status;
21627
+ }
21628
+ }
21629
+ cached = { status: resolveWindowsShell(deps, options?.configPath), at: now() };
21630
+ return cached.status;
21631
+ }
21632
+ };
21633
+ }
21634
+
21436
21635
  // packages/core/src/workspace-files.ts
21437
- import { open as open2, realpath as realpath2, stat } from "fs/promises";
21636
+ import { open as open2, realpath as realpath2, stat as stat2 } from "fs/promises";
21438
21637
  import { basename as basename2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
21439
21638
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
21440
- import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
21639
+ import {
21640
+ ExecutionError,
21641
+ err,
21642
+ getOrThrow as getOrThrow2
21643
+ } from "@earendil-works/pi-durable/env";
21441
21644
  import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
21442
21645
  var BROWSE_LIMITS = { children: 1000, readBytes: 512 * 1024 };
21443
21646
  var ENV_TEMPLATE = /\.(example|sample|template)$/;
@@ -21475,6 +21678,19 @@ var FILE_CACHE_LIMITS = {
21475
21678
  concurrent: 2
21476
21679
  };
21477
21680
 
21681
+ class ShellUnavailableEnv extends LocalExecutionEnv {
21682
+ #message;
21683
+ constructor(options, message) {
21684
+ super(options);
21685
+ this.#message = message;
21686
+ }
21687
+ async exec(command, options, context) {
21688
+ if (typeof command === "string")
21689
+ return err(new ExecutionError("shell_unavailable", this.#message));
21690
+ return super.exec(command, options, context);
21691
+ }
21692
+ }
21693
+
21478
21694
  class WorkspaceFiles {
21479
21695
  #connection;
21480
21696
  #starting;
@@ -21489,8 +21705,10 @@ class WorkspaceFiles {
21489
21705
  #waiting = new Set;
21490
21706
  #timer;
21491
21707
  limits;
21492
- constructor(limits = FILE_CACHE_LIMITS) {
21708
+ #shell;
21709
+ constructor(limits = FILE_CACHE_LIMITS, shell) {
21493
21710
  this.limits = limits;
21711
+ this.#shell = shell;
21494
21712
  this.#timer = setInterval(() => {
21495
21713
  for (const [key, item] of this.#cache)
21496
21714
  if (!item.waiters && Date.now() - item.used >= this.limits.idleMs)
@@ -21509,20 +21727,31 @@ class WorkspaceFiles {
21509
21727
  queued: this.#waiting.size
21510
21728
  };
21511
21729
  }
21512
- env(cwd) {
21730
+ shellStatus(options) {
21731
+ return this.#shell?.status(options);
21732
+ }
21733
+ #open() {
21513
21734
  if (this.#closed)
21514
21735
  throw new Error("File environment closed");
21515
21736
  this.#connection ??= new Connection({ command: [localDaemon()] });
21516
- return new LocalExecutionEnv({
21517
- connection: this.#connection,
21737
+ return this.#connection;
21738
+ }
21739
+ env(cwd) {
21740
+ const options = {
21741
+ connection: this.#open(),
21518
21742
  cwd,
21519
21743
  ...process.platform === "win32" ? { shellEnv: { PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } } : {}
21520
- });
21744
+ };
21745
+ const shell = this.#shell?.status();
21746
+ if (shell === undefined)
21747
+ return new LocalExecutionEnv(options);
21748
+ if (shell.kind === "found")
21749
+ return new LocalExecutionEnv({ ...options, shellPath: shell.path });
21750
+ return new ShellUnavailableEnv(options, windowsShellMessage(shell));
21521
21751
  }
21522
21752
  async#ready() {
21523
21753
  if (!this.#starting) {
21524
- this.env(process.cwd());
21525
- const connection = this.#connection;
21754
+ const connection = this.#open();
21526
21755
  let timer;
21527
21756
  const work = Promise.race([
21528
21757
  connection.info().then(() => connection.session()),
@@ -21599,7 +21828,7 @@ class WorkspaceFiles {
21599
21828
  { path: item.root, recursive: true, exclude: { names: [".git"] } }
21600
21829
  ];
21601
21830
  const git = resolve4(item.root, ".git");
21602
- if (await stat(git).then(() => true, () => false))
21831
+ if (await stat2(git).then(() => true, () => false))
21603
21832
  targets.push({ path: git, recursive: true, exclude: { names: ["objects", "logs", "refs"] } });
21604
21833
  const result = await env.watch(targets, (change) => {
21605
21834
  for (const user of current.users) {
@@ -21707,7 +21936,7 @@ class WorkspaceFiles {
21707
21936
  const slash = query.lastIndexOf("/"), prefix = slash < 0 ? "" : query.slice(0, slash + 1), needle = query.slice(slash + 1);
21708
21937
  root = await realpath2(root);
21709
21938
  const base = await workspaceTarget(root, prefix);
21710
- if (!(await stat(base)).isDirectory())
21939
+ if (!(await stat2(base)).isDirectory())
21711
21940
  throw new Error("Workspace directory is unavailable.");
21712
21941
  return this.#withListing(root, base, signal, async (listing) => {
21713
21942
  const pattern = new RegExp([...needle].map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*"), /[A-Z]/.test(needle) ? "" : "i");
@@ -21737,7 +21966,7 @@ class WorkspaceFiles {
21737
21966
  const clean = dir === "" ? "" : relativeQuery(dir).replace(/\/+$/, "");
21738
21967
  root = await realpath2(root);
21739
21968
  const target = await workspaceTarget(root, clean);
21740
- if (!(await stat(target)).isDirectory())
21969
+ if (!(await stat2(target)).isDirectory())
21741
21970
  throw new Error("Not a directory.");
21742
21971
  const prefix = clean === "" ? "" : `${clean}/`;
21743
21972
  return this.#withListing(root, root, signal, async (listing) => {
@@ -21779,7 +22008,7 @@ class WorkspaceFiles {
21779
22008
  throw new Error("File not found.");
21780
22009
  throw error;
21781
22010
  }
21782
- const info = await stat(target);
22011
+ const info = await stat2(target);
21783
22012
  if (!info.isFile())
21784
22013
  throw new Error("Not a file.");
21785
22014
  const base = { path: clean, size: info.size, mtimeMs: info.mtimeMs };
@@ -22006,7 +22235,8 @@ async function createNekoRuntime(options = {}) {
22006
22235
  }));
22007
22236
  return modelInfoCache;
22008
22237
  };
22009
- const workspaceFiles = new WorkspaceFiles;
22238
+ const shellResolver = options.shellResolver ?? (process.platform === "win32" ? createWindowsShellResolver({ configPath: options.shellPath }) : undefined);
22239
+ const workspaceFiles = new WorkspaceFiles(undefined, shellResolver);
22010
22240
  const storage = options.storage === "memory" ? new MemoryStorage : await openNodeSqliteStorage(options.storagePath ?? DEFAULT_STORAGE_PATH);
22011
22241
  const registry = createRegistry();
22012
22242
  registry.install(createCodingTools({ images: await createNodePhotonImages() }));
@@ -22516,6 +22746,10 @@ async function createNekoRuntime(options = {}) {
22516
22746
  questions,
22517
22747
  closed: harness.closed,
22518
22748
  workspaceFiles,
22749
+ shellStatus(call) {
22750
+ const status = workspaceFiles.shellStatus(call);
22751
+ return status === undefined ? undefined : summarizeWindowsShell(status);
22752
+ },
22519
22753
  conversation,
22520
22754
  root: rootSession,
22521
22755
  submit: rootSession.submit,
@@ -23418,6 +23652,14 @@ var NekoSkillMarket = contract("neko.skill-market", {
23418
23652
  }
23419
23653
  }
23420
23654
  });
23655
+ var ShellStatusSchema = Type7.Union([
23656
+ Type7.Object({ status: Type7.Literal("ready"), path: str, source: Type7.Enum(["config", "auto"]) }),
23657
+ Type7.Object({
23658
+ status: Type7.Literal("missing"),
23659
+ reason: Type7.Enum(["not_found", "invalid_config"]),
23660
+ detail: Type7.Optional(str)
23661
+ })
23662
+ ]);
23421
23663
  var ApplicationInfoSchema = Type7.Object({
23422
23664
  runningVersion: str,
23423
23665
  installedVersion: nullableString,
@@ -23425,7 +23667,8 @@ var ApplicationInfoSchema = Type7.Object({
23425
23667
  latestVersion: nullableString,
23426
23668
  checkedAt: Type7.Union([integer, nil]),
23427
23669
  checkState: Type7.Enum(["unknown", "checking", "ok", "failed", "development"]),
23428
- serverId: str
23670
+ serverId: str,
23671
+ shell: Type7.Optional(ShellStatusSchema)
23429
23672
  });
23430
23673
  var NekoApplicationInfo = contract("neko.application-info", {
23431
23674
  version: 1,
@@ -23463,7 +23706,7 @@ function isMobilePrincipal(principal) {
23463
23706
 
23464
23707
  // packages/gateway/src/mobile/store.ts
23465
23708
  import { randomBytes } from "crypto";
23466
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile4, rename as rename2, rm, writeFile } from "fs/promises";
23709
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile4, rm, writeFile } from "fs/promises";
23467
23710
  import { join as join6 } from "path";
23468
23711
  var MOBILE_STATE_FORMAT_VERSION = 1;
23469
23712
  var MOBILE_DEFAULT_PORT = 8323;
@@ -23591,9 +23834,10 @@ async function writeFileAtomic(filePath, contents, mode) {
23591
23834
  await writeFile(tempPath, contents, { mode });
23592
23835
  if (POSIX)
23593
23836
  await chmod2(tempPath, mode);
23594
- await rename2(tempPath, filePath);
23595
- } finally {
23837
+ await renameWithRetry(tempPath, filePath);
23838
+ } catch (error) {
23596
23839
  await rm(tempPath, { force: true });
23840
+ throw error;
23597
23841
  }
23598
23842
  }
23599
23843
  async function writeSecretFile(filePath, contents) {
@@ -24149,7 +24393,7 @@ function subagentClient(connect) {
24149
24393
  import { createHash as createHash2 } from "crypto";
24150
24394
 
24151
24395
  // packages/core/src/application-version.ts
24152
- var RUNNING_VERSION = "0.4.6";
24396
+ var RUNNING_VERSION = "0.4.7";
24153
24397
  var APPLICATION_PROTOCOL = 8;
24154
24398
  function stableVersion(value) {
24155
24399
  return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value) && value.split(".").every((n) => Number.isSafeInteger(Number(n)));
@@ -26799,6 +27043,10 @@ function toMessage(error) {
26799
27043
  }
26800
27044
  return String(error);
26801
27045
  }
27046
+ function normalizeLocalAddress(address) {
27047
+ const mapped = address?.startsWith("::ffff:") ? address.slice("::ffff:".length) : undefined;
27048
+ return mapped !== undefined && isIpv4Literal(mapped) ? mapped : address;
27049
+ }
26802
27050
 
26803
27051
  class MobileGateway {
26804
27052
  serverId;
@@ -27127,11 +27375,12 @@ class MobileGateway {
27127
27375
  connection.sendNotification(method, params);
27128
27376
  }
27129
27377
  advertisedAddress(localAddress) {
27378
+ const normalized = normalizeLocalAddress(localAddress);
27130
27379
  const state = this.stateValue;
27131
- if (state.status === "listening" && state.mode === "wsl-relay" && localAddress === state.bindAddress) {
27380
+ if (state.status === "listening" && state.mode === "wsl-relay" && normalized === state.bindAddress) {
27132
27381
  return state.address;
27133
27382
  }
27134
- return localAddress;
27383
+ return normalized;
27135
27384
  }
27136
27385
  requireStore() {
27137
27386
  if (this.storeHandle === undefined)
@@ -27392,7 +27641,7 @@ import { randomUUID as randomUUID5 } from "crypto";
27392
27641
  import { ChordError as ChordError5, local as local3, method as method2 } from "@earendil-works/chord";
27393
27642
 
27394
27643
  // packages/core/src/application-updates.ts
27395
- import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
27644
+ import { mkdir as mkdir3, readFile as readFile6, rm as rm3, writeFile as writeFile3 } from "fs/promises";
27396
27645
  import { homedir as homedir5 } from "os";
27397
27646
  import { dirname as dirname5, join as join10 } from "path";
27398
27647
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -27522,7 +27771,7 @@ class UpdateChecker {
27522
27771
  try {
27523
27772
  await mkdir3(dirname5(this.#cachePath), { recursive: true });
27524
27773
  await writeFile3(temporary, JSON.stringify({ latestVersion: value.version, checkedAt: now }), { mode: 384 });
27525
- await rename3(temporary, this.#cachePath);
27774
+ await renameWithRetry(temporary, this.#cachePath);
27526
27775
  } catch {} finally {
27527
27776
  await rm3(temporary, { force: true }).catch(() => {});
27528
27777
  }
@@ -28256,7 +28505,7 @@ function serveSessions(host, runtime, context) {
28256
28505
 
28257
28506
  // packages/gateway/src/skill-market.ts
28258
28507
  import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
28259
- import { mkdir as mkdir4, readdir as readdir2, rename as rename4, rm as rm4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
28508
+ import { mkdir as mkdir4, readdir as readdir2, rm as rm4, stat as stat3, writeFile as writeFile4 } from "fs/promises";
28260
28509
  import { homedir as homedir6 } from "os";
28261
28510
  import { dirname as dirname6, join as join11 } from "path";
28262
28511
 
@@ -28401,11 +28650,11 @@ var ec = [
28401
28650
  "stream finishing",
28402
28651
  "invalid zip data"
28403
28652
  ];
28404
- var err = function(ind, msg, nt) {
28653
+ var err2 = function(ind, msg, nt) {
28405
28654
  var e = new Error(msg || ec[ind]);
28406
28655
  e.code = ind;
28407
28656
  if (Error.captureStackTrace)
28408
- Error.captureStackTrace(e, err);
28657
+ Error.captureStackTrace(e, err2);
28409
28658
  if (!nt)
28410
28659
  throw e;
28411
28660
  return e;
@@ -28438,7 +28687,7 @@ var inflt = function(dat, st, buf, dict) {
28438
28687
  var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
28439
28688
  if (t > sl) {
28440
28689
  if (noSt)
28441
- err(0);
28690
+ err2(0);
28442
28691
  break;
28443
28692
  }
28444
28693
  if (resize)
@@ -28484,10 +28733,10 @@ var inflt = function(dat, st, buf, dict) {
28484
28733
  lm = hMap(lt, lbt, 1);
28485
28734
  dm = hMap(dt, dbt, 1);
28486
28735
  } else
28487
- err(1);
28736
+ err2(1);
28488
28737
  if (pos > tbts) {
28489
28738
  if (noSt)
28490
- err(0);
28739
+ err2(0);
28491
28740
  break;
28492
28741
  }
28493
28742
  }
@@ -28500,11 +28749,11 @@ var inflt = function(dat, st, buf, dict) {
28500
28749
  pos += c & 15;
28501
28750
  if (pos > tbts) {
28502
28751
  if (noSt)
28503
- err(0);
28752
+ err2(0);
28504
28753
  break;
28505
28754
  }
28506
28755
  if (!c)
28507
- err(2);
28756
+ err2(2);
28508
28757
  if (sym < 256)
28509
28758
  buf[bt++] = sym;
28510
28759
  else if (sym == 256) {
@@ -28519,7 +28768,7 @@ var inflt = function(dat, st, buf, dict) {
28519
28768
  }
28520
28769
  var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
28521
28770
  if (!d)
28522
- err(3);
28771
+ err2(3);
28523
28772
  pos += d & 15;
28524
28773
  var dt = fd[dsym];
28525
28774
  if (dsym > 3) {
@@ -28528,7 +28777,7 @@ var inflt = function(dat, st, buf, dict) {
28528
28777
  }
28529
28778
  if (pos > tbts) {
28530
28779
  if (noSt)
28531
- err(0);
28780
+ err2(0);
28532
28781
  break;
28533
28782
  }
28534
28783
  if (resize)
@@ -28537,7 +28786,7 @@ var inflt = function(dat, st, buf, dict) {
28537
28786
  if (bt < dt) {
28538
28787
  var shift = dl - dt, dend = Math.min(dt, end);
28539
28788
  if (shift + bt < 0)
28540
- err(3);
28789
+ err2(3);
28541
28790
  for (;bt < dend; ++bt)
28542
28791
  buf[bt] = dict[shift + bt];
28543
28792
  }
@@ -28597,7 +28846,7 @@ function strFromU8(dat, latin1) {
28597
28846
  } else {
28598
28847
  var _a = dutf8(dat), { s, r } = _a;
28599
28848
  if (r.length)
28600
- err(8);
28849
+ err2(8);
28601
28850
  return s;
28602
28851
  }
28603
28852
  }
@@ -28624,7 +28873,7 @@ var z64hs = function(d, b, l, z, sc, su, off) {
28624
28873
  }
28625
28874
  }
28626
28875
  if (z < 2)
28627
- err(13);
28876
+ err2(13);
28628
28877
  }
28629
28878
  return [sc, su, off, 0];
28630
28879
  };
@@ -28633,7 +28882,7 @@ function unzipSync(data, opts) {
28633
28882
  var e = data.length - 22;
28634
28883
  for (;b4(data, e) != 101010256; --e) {
28635
28884
  if (!e || data.length - e > 65558)
28636
- err(13);
28885
+ err2(13);
28637
28886
  }
28638
28887
  var c = b2(data, e + 8);
28639
28888
  if (!c)
@@ -28663,7 +28912,7 @@ function unzipSync(data, opts) {
28663
28912
  else if (c_2 == 8)
28664
28913
  files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
28665
28914
  else
28666
- err(14, "unknown compression type " + c_2);
28915
+ err2(14, "unknown compression type " + c_2);
28667
28916
  }
28668
28917
  }
28669
28918
  return files;
@@ -28704,6 +28953,7 @@ function createSkillMarketClient(options) {
28704
28953
  const home = options.home ?? homedir6();
28705
28954
  const fetchImpl = options.fetchImpl ?? fetch;
28706
28955
  const now = options.now ?? Date.now;
28956
+ const placeRename = options.renameImpl;
28707
28957
  let base;
28708
28958
  try {
28709
28959
  base = new URL(options.marketUrl);
@@ -28808,6 +29058,8 @@ function createSkillMarketClient(options) {
28808
29058
  throw new SkillMarketError("market_unavailable", "Project-scope install needs a workspaceRoot");
28809
29059
  return projectSkillsDir(workspaceRoot);
28810
29060
  };
29061
+ const BACKUP_PREFIX = ".bak-";
29062
+ const UUID_LENGTH = 36;
28811
29063
  const sweepTempDirs = async (skillsDir) => {
28812
29064
  const parent = dirname6(skillsDir);
28813
29065
  let names = [];
@@ -28817,11 +29069,26 @@ function createSkillMarketClient(options) {
28817
29069
  return;
28818
29070
  }
28819
29071
  await Promise.all(names.filter((n) => n.startsWith(".tmp-")).map((n) => rm4(join11(parent, n), { recursive: true, force: true }).catch(() => {})));
29072
+ for (const n of names.filter((n) => n.startsWith(BACKUP_PREFIX))) {
29073
+ const backup = join11(parent, n);
29074
+ const skill = n.slice(BACKUP_PREFIX.length + UUID_LENGTH + 1);
29075
+ if (skill === "" || skill.includes("/") || skill.includes("\\") || skill === "." || skill === "..")
29076
+ continue;
29077
+ const target = join11(skillsDir, skill);
29078
+ if (await stat3(target).catch(() => {
29079
+ return;
29080
+ }) === undefined) {
29081
+ await mkdir4(skillsDir, { recursive: true }).catch(() => {});
29082
+ await renameWithRetry(backup, target).catch(() => {});
29083
+ } else {
29084
+ await rm4(backup, { recursive: true, force: true }).catch(() => {});
29085
+ }
29086
+ }
28820
29087
  };
28821
29088
  const install = async ({ name, scope, workspaceRoot, overwrite }) => {
28822
29089
  const skillsDir = skillsDirFor(scope, workspaceRoot);
28823
29090
  const target = join11(skillsDir, name);
28824
- const exists = await stat2(target).catch(() => {
29091
+ const exists = await stat3(target).catch(() => {
28825
29092
  return;
28826
29093
  }) !== undefined;
28827
29094
  if (exists && overwrite !== true)
@@ -28836,17 +29103,27 @@ function createSkillMarketClient(options) {
28836
29103
  await mkdir4(dirname6(dest), { recursive: true });
28837
29104
  await writeFile4(dest, bytes);
28838
29105
  }
28839
- if (exists)
28840
- await rm4(target, { recursive: true, force: true });
28841
29106
  await mkdir4(skillsDir, { recursive: true });
28842
- await rename4(temp, target);
29107
+ if (exists) {
29108
+ const backup = join11(dirname6(skillsDir), `${BACKUP_PREFIX}${randomUUID4()}-${name}`);
29109
+ await renameWithRetry(target, backup);
29110
+ try {
29111
+ await renameWithRetry(temp, target, placeRename ? { rename: placeRename } : {});
29112
+ } catch (error) {
29113
+ await renameWithRetry(backup, target).catch(() => {});
29114
+ throw error;
29115
+ }
29116
+ await rm4(backup, { recursive: true, force: true }).catch(() => {});
29117
+ } else {
29118
+ await renameWithRetry(temp, target, placeRename ? { rename: placeRename } : {});
29119
+ }
28843
29120
  } finally {
28844
29121
  await rm4(temp, { recursive: true, force: true }).catch(() => {});
28845
29122
  }
28846
29123
  let shadowedBy;
28847
29124
  if (scope === "user" && workspaceRoot !== undefined && workspaceRoot !== "") {
28848
29125
  const projectDir = join11(projectSkillsDir(workspaceRoot), name);
28849
- if (await stat2(projectDir).catch(() => {
29126
+ if (await stat3(projectDir).catch(() => {
28850
29127
  return;
28851
29128
  }) !== undefined)
28852
29129
  shadowedBy = "project";
@@ -28856,7 +29133,7 @@ function createSkillMarketClient(options) {
28856
29133
  const uninstall = async ({ name, scope, workspaceRoot }) => {
28857
29134
  const skillsDir = skillsDirFor(scope, workspaceRoot);
28858
29135
  const target = join11(skillsDir, name);
28859
- if (await stat2(target).catch(() => {
29136
+ if (await stat3(target).catch(() => {
28860
29137
  return;
28861
29138
  }) === undefined)
28862
29139
  throw new SkillMarketError("skill_not_installed", `Skill ${name} is not installed at ${scope} level`);
@@ -28890,7 +29167,7 @@ function createSkillMarketClient(options) {
28890
29167
 
28891
29168
  // packages/gateway/src/workspaces.ts
28892
29169
  import { constants as constants3 } from "fs";
28893
- import { access, readdir as readdir3, stat as stat3 } from "fs/promises";
29170
+ import { access, readdir as readdir3, stat as stat4 } from "fs/promises";
28894
29171
  import { dirname as dirname7, isAbsolute as isAbsolute4, join as join12 } from "path";
28895
29172
  async function browseWorkspaces(options) {
28896
29173
  const requested = options.path.replace(/^\/+([A-Za-z]:[\\/])/, "$1");
@@ -28922,7 +29199,7 @@ async function browseWorkspaces(options) {
28922
29199
  if (!entry.isDirectory() && !entry.isSymbolicLink())
28923
29200
  continue;
28924
29201
  try {
28925
- if (!(await stat3(target)).isDirectory())
29202
+ if (!(await stat4(target)).isDirectory())
28926
29203
  continue;
28927
29204
  await access(target, constants3.R_OK | constants3.X_OK);
28928
29205
  directories.push({ name: entry.name, path: target });
@@ -28971,12 +29248,15 @@ async function createNekoServerServices(options) {
28971
29248
  const updates = new UpdateChecker(options.updates);
28972
29249
  await updates.local();
28973
29250
  const updateListeners = new Set;
28974
- const info = (value) => ({ ...value, serverId: options.serverId });
28975
- let published = JSON.stringify(updates.state), closed = false;
29251
+ const info = (value) => {
29252
+ const shell = runtime.shellStatus();
29253
+ return { ...value, serverId: options.serverId, ...shell === undefined ? {} : { shell } };
29254
+ };
29255
+ let published = JSON.stringify(info(updates.state)), closed = false;
28976
29256
  const publishUpdates = () => {
28977
29257
  if (closed)
28978
29258
  return;
28979
- const next = JSON.stringify(updates.state);
29259
+ const next = JSON.stringify(info(updates.state));
28980
29260
  if (next === published)
28981
29261
  return;
28982
29262
  published = next;
@@ -28995,6 +29275,7 @@ async function createNekoServerServices(options) {
28995
29275
  variant: "ready",
28996
29276
  info: current,
28997
29277
  check: async () => {
29278
+ runtime.shellStatus({ refresh: true });
28998
29279
  await updates.check(true);
28999
29280
  publishUpdates();
29000
29281
  return info(updates.state);
@@ -29519,7 +29800,9 @@ async function startGateway(options = {}) {
29519
29800
  ...options.cwd !== undefined ? { cwd: options.cwd } : {},
29520
29801
  ...options.providers !== undefined ? { providers: options.providers } : {},
29521
29802
  ...options.extensions !== undefined ? { extensions: options.extensions } : {},
29522
- ...options.promptHome !== undefined ? { promptHome: options.promptHome } : {}
29803
+ ...options.promptHome !== undefined ? { promptHome: options.promptHome } : {},
29804
+ ...fileConfig.shell?.path !== undefined ? { shellPath: fileConfig.shell.path } : {},
29805
+ ...options.shellResolver !== undefined ? { shellResolver: options.shellResolver } : {}
29523
29806
  });
29524
29807
  try {
29525
29808
  return await bindGateway(runtime, {
@@ -29534,6 +29817,7 @@ async function startGateway(options = {}) {
29534
29817
  updates: options.updates,
29535
29818
  updateIntervalMs: options.updateIntervalMs,
29536
29819
  mobileServerName: options.mobileServerName,
29820
+ mobileNetwork: options.mobileNetwork,
29537
29821
  onError: options.onError
29538
29822
  });
29539
29823
  } catch (error) {
@@ -29568,12 +29852,13 @@ async function bindGateway(runtime, options) {
29568
29852
  wsListener = createWsListener({ host, port, onError: options.onError });
29569
29853
  listeners.push(wsListener);
29570
29854
  }
29855
+ const acceptLocal = (link) => chord.accept(link, {
29856
+ principal: { kind: "local", connectionId: randomUUID6() },
29857
+ onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
29858
+ });
29571
29859
  try {
29572
29860
  for (const listener of listeners)
29573
- await listener.start((link) => chord.accept(link, {
29574
- principal: { kind: "local", connectionId: randomUUID6() },
29575
- onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
29576
- }));
29861
+ await listener.start(acceptLocal);
29577
29862
  } catch (error) {
29578
29863
  await Promise.allSettled(listeners.map((listener) => listener.close()));
29579
29864
  await chord.close();
@@ -29589,6 +29874,7 @@ async function bindGateway(runtime, options) {
29589
29874
  serverId,
29590
29875
  serverName: options.mobileServerName,
29591
29876
  onError: options.onError,
29877
+ network: options.mobileNetwork,
29592
29878
  chord: {
29593
29879
  accept: (link, acceptOptions) => chord.accept(link, acceptOptions),
29594
29880
  invalidatePrincipal: (match) => chord.invalidatePrincipal(match)
@@ -29613,6 +29899,9 @@ async function bindGateway(runtime, options) {
29613
29899
  port: wsListener?.port,
29614
29900
  runtime,
29615
29901
  mobile,
29902
+ accept(link) {
29903
+ acceptLocal(link);
29904
+ },
29616
29905
  async close() {
29617
29906
  if (closing)
29618
29907
  return closing;
@@ -29698,14 +29987,15 @@ function installShutdown(options = {}) {
29698
29987
  exit(1);
29699
29988
  });
29700
29989
  };
29701
- const onSignal = () => {
29990
+ const begin = (reason) => {
29702
29991
  if (controller.signal.aborted) {
29703
29992
  options.log?.("\u518D\u6B21\u6536\u5230\u7EC8\u6B62\u4FE1\u53F7\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
29704
29993
  exit(130);
29705
29994
  return;
29706
29995
  }
29707
- options.log?.(`
29708
- \u6B63\u5728\u5173\u95ED\u2026`);
29996
+ options.log?.(reason === undefined ? `
29997
+ \u6B63\u5728\u5173\u95ED\u2026` : `
29998
+ \u6B63\u5728\u5173\u95ED\uFF08${reason}\uFF09\u2026`);
29709
29999
  controller.abort();
29710
30000
  timer = setTimeout(() => {
29711
30001
  options.log?.("\u5173\u95ED\u8D85\u65F6\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
@@ -29714,6 +30004,7 @@ function installShutdown(options = {}) {
29714
30004
  timer.unref?.();
29715
30005
  runCleanup();
29716
30006
  };
30007
+ const onSignal = () => begin(undefined);
29717
30008
  const installed = [];
29718
30009
  for (const signal of shutdownSignals(proc.platform)) {
29719
30010
  try {
@@ -29728,6 +30019,9 @@ function installShutdown(options = {}) {
29728
30019
  if (controller.signal.aborted)
29729
30020
  runCleanup();
29730
30021
  },
30022
+ trigger(reason) {
30023
+ begin(reason);
30024
+ },
29731
30025
  dispose() {
29732
30026
  clearTimeout(timer);
29733
30027
  for (const signal of installed)
@@ -29758,3 +30052,15 @@ if (gateway.wsUrl !== undefined)
29758
30052
  console.log(` WebSocket: ${gateway.wsUrl}`);
29759
30053
  console.log("\u6309 Ctrl+C \u505C\u6B62\u3002");
29760
30054
  shutdown.setCleanup(() => gateway.close());
30055
+ if (options.stopOnStdin === true) {
30056
+ let triggered = false;
30057
+ const onStdinEnd = () => {
30058
+ if (triggered)
30059
+ return;
30060
+ triggered = true;
30061
+ shutdown.trigger("stdin \u5DF2\u5173\u95ED");
30062
+ };
30063
+ process.stdin.once("end", onStdinEnd);
30064
+ process.stdin.once("close", onStdinEnd);
30065
+ process.stdin.resume();
30066
+ }