dsh-agy-link 0.2.0 → 0.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1 (2026-08-19)
4
+
5
+ - Cross-platform hardening (Linux / macOS / Windows):
6
+ - binary discovery is platform-aware: agy / agy.exe / .cmd / .bat across
7
+ PATH, ~/.local/bin, /usr/local/bin, /opt/homebrew/bin,
8
+ %LOCALAPPDATA%\Programs, and the npm shim dir; a real executable is
9
+ always preferred over a cmd shim
10
+ - Windows .cmd/.bat shims spawn through cmd.exe with cross-spawn-style
11
+ argument quoting (unit-tested)
12
+ - tree-kill uses taskkill /T /F on Windows (Unix process groups do not
13
+ exist there); detached sessions are POSIX-only so no console window
14
+ flashes on Windows
15
+ - CRLF stdout is normalized (trailing \r stripped per line)
16
+ - the MCP bridge script path resolves via fileURLToPath (URL.pathname
17
+ would yield /C:/... on Windows and break the spawn)
18
+ - 5 new cross-platform tests (56 total).
19
+
3
20
  ## 0.2.0 (2026-08-19)
4
21
 
5
22
  - Multimodal (path-based): DSH image attachments are staged to a local media
@@ -25,4 +42,3 @@
25
42
  - README restructured into a single bilingual page: Chinese first, then
26
43
  English (README.zh.md removed; package files list updated).
27
44
  - Releases now publish to npm automatically (NPM_TOKEN secret configured).
28
-
package/README.md CHANGED
@@ -59,6 +59,8 @@ dsh plugin --profile web add dsh-agy-link
59
59
 
60
60
  > 插件绝不读取/复制/移动 `~/.gemini/antigravity-cli/antigravity-oauth-token`;登录完全通过官方 CLI 自己的流程完成。
61
61
 
62
+ > 🖥 **跨平台**:Linux / macOS / Windows 均受支持——bin 探测按平台查找 `agy`/`agy.exe`(PATH、`~/.local/bin`、`/usr/local/bin`、`/opt/homebrew/bin`、`%LOCALAPPDATA%\Programs`,npm `.cmd` shim 自动经 cmd.exe 安全引号包裹启动);中断/超时杀树在 Windows 走 `taskkill /T /F`;CRLF 输出统一剥离;媒体目录与 MCP 桥路径全部 `fileURLToPath`/`join` 构造。
63
+
62
64
  ## ⚙️ 配置
63
65
 
64
66
  配置在 `agy-link` 插件条目里(`/plugin` 或 profile patch 层编辑),环境变量优先:
@@ -145,6 +147,8 @@ dsh plugin --profile web add dsh-agy-link
145
147
 
146
148
  > The plugin never reads, copies, or moves `~/.gemini/antigravity-cli/antigravity-oauth-token`; login is driven entirely through the official CLI's own flow.
147
149
 
150
+ > 🖥 **Cross-platform**: Linux / macOS / Windows — platform-aware binary discovery (`agy` / `agy.exe` across PATH, `~/.local/bin`, `/usr/local/bin`, `/opt/homebrew/bin`, `%LOCALAPPDATA%\Programs`; npm `.cmd` shims spawn through cmd.exe with safe quoting), tree-kill via `taskkill /T /F` on Windows, CRLF output normalized, media and bridge paths built with `fileURLToPath`/`join`.
151
+
148
152
  ## ⚙️ Configuration
149
153
 
150
154
  Config lives in the `agy-link` plugin entry (edit via `/plugin` or the profile patch layer). Environment variables override the file:
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { execFileSync, spawn } from "node:child_process";
7
7
  import { defineTool } from "@deepseek-ai/dsh-tools";
8
8
  import { createServer } from "node:http";
9
9
  import { randomBytes } from "node:crypto";
10
+ import { fileURLToPath } from "node:url";
10
11
  //#region src/common/types.ts
11
12
  const PROVIDER_ID = "antigravity";
12
13
  const PLUGIN_ID = "agy-link";
@@ -1015,20 +1016,51 @@ async function sweepDir(dir, ttlMs, now = Date.now()) {
1015
1016
  function defaultMediaDir(stateDir) {
1016
1017
  return join(stateDir, "media");
1017
1018
  }
1019
+ //#endregion
1020
+ //#region src/host/runner.ts
1021
+ const IS_WIN = process.platform === "win32";
1022
+ /** Executable candidates for one PATH entry, per-platform. Exported for tests. */
1023
+ function binCandidates(dir, platform = process.platform) {
1024
+ return (platform === "win32" ? [
1025
+ ".exe",
1026
+ ".cmd",
1027
+ ".bat"
1028
+ ] : [""]).map((e) => join(dir, "agy" + e));
1029
+ }
1030
+ /** True when the resolved bin is a Windows cmd shim (needs shell wrapping). */
1031
+ function isCmdShim(bin) {
1032
+ return /\.(cmd|bat)$/i.test(bin);
1033
+ }
1034
+ /** cmd.exe argument quoting (cross-spawn rules). Exported for tests. */
1035
+ function windowsQuote(arg) {
1036
+ if (/[ \t\n\v"]/.test(arg) === false) return arg;
1037
+ let escaped = arg.replace(/(\\+)\"/g, "$1$1\\\"").replace(/(\\+)$/, "$1$1");
1038
+ escaped = "\"" + escaped.replace(/"/g, "\\\"") + "\"";
1039
+ return escaped;
1040
+ }
1018
1041
  function resolveAgyBin(cfg) {
1019
1042
  const candidates = [];
1020
1043
  if (cfg.agyBin !== "") candidates.push(cfg.agyBin);
1021
1044
  const pathEnv = process.env.PATH ?? "";
1022
- for (const dir of pathEnv.split(delimiter)) if (dir !== "") candidates.push(join(dir, "agy"));
1023
- candidates.push(join(homedir(), ".local", "bin", "agy"));
1024
- candidates.push("/usr/local/bin/agy");
1045
+ for (const dir of pathEnv.split(delimiter)) if (dir !== "") candidates.push(...binCandidates(dir));
1046
+ if (IS_WIN) {
1047
+ const local = process.env.LOCALAPPDATA ?? "";
1048
+ if (local !== "") candidates.push(join(local, "Programs", "agy", "agy.exe"));
1049
+ candidates.push(join(homedir(), ".local", "bin", "agy.exe"));
1050
+ candidates.push(join(homedir(), "AppData", "Roaming", "npm", "agy.cmd"));
1051
+ } else {
1052
+ candidates.push(join(homedir(), ".local", "bin", "agy"));
1053
+ candidates.push("/usr/local/bin/agy");
1054
+ candidates.push("/opt/homebrew/bin/agy");
1055
+ }
1056
+ const hits = [];
1025
1057
  for (const c of candidates) try {
1026
- accessSync(c, constants.X_OK);
1027
- return c;
1058
+ accessSync(c, constants.F_OK);
1059
+ hits.push(c);
1028
1060
  } catch {
1029
1061
  continue;
1030
1062
  }
1031
- return null;
1063
+ return hits.find((h) => !isCmdShim(h)) ?? hits[0] ?? null;
1032
1064
  }
1033
1065
  function compareVersions(a, b) {
1034
1066
  const pa = a.split(/\./).map(Number);
@@ -1045,6 +1077,24 @@ function parseVersion(out) {
1045
1077
  }
1046
1078
  function killTree(child) {
1047
1079
  if (child.pid === void 0) return;
1080
+ if (IS_WIN) {
1081
+ try {
1082
+ spawn("taskkill", [
1083
+ "/pid",
1084
+ String(child.pid),
1085
+ "/T",
1086
+ "/F"
1087
+ ], {
1088
+ stdio: "ignore",
1089
+ windowsHide: true
1090
+ });
1091
+ } catch {
1092
+ try {
1093
+ child.kill();
1094
+ } catch {}
1095
+ }
1096
+ return;
1097
+ }
1048
1098
  try {
1049
1099
  process.kill(-child.pid, "SIGTERM");
1050
1100
  } catch {
@@ -1055,15 +1105,31 @@ function killTree(child) {
1055
1105
  }
1056
1106
  function startAgyProcess(opts) {
1057
1107
  const started = Date.now();
1058
- const child = spawn(opts.bin, opts.args, {
1108
+ const child = IS_WIN && isCmdShim(opts.bin) ? spawn(process.env.ComSpec ?? "cmd.exe", [
1109
+ "/d",
1110
+ "/s",
1111
+ "/c",
1112
+ [opts.bin, ...opts.args].map(windowsQuote).join(" ")
1113
+ ], {
1059
1114
  cwd: opts.cwd,
1060
1115
  env: process.env,
1061
- detached: true,
1062
1116
  stdio: [
1063
1117
  "pipe",
1064
1118
  "pipe",
1065
1119
  "pipe"
1066
- ]
1120
+ ],
1121
+ windowsVerbatimArguments: true,
1122
+ windowsHide: true
1123
+ }) : spawn(opts.bin, opts.args, {
1124
+ cwd: opts.cwd,
1125
+ env: process.env,
1126
+ detached: !IS_WIN,
1127
+ stdio: [
1128
+ "pipe",
1129
+ "pipe",
1130
+ "pipe"
1131
+ ],
1132
+ windowsHide: true
1067
1133
  });
1068
1134
  let stdout = "";
1069
1135
  let stderr = "";
@@ -1088,7 +1154,7 @@ function startAgyProcess(opts) {
1088
1154
  pending += chunk;
1089
1155
  let nl;
1090
1156
  while ((nl = pending.indexOf("\n")) >= 0) {
1091
- const line = pending.slice(0, nl);
1157
+ const line = pending.slice(0, nl).replace(/\r$/, "");
1092
1158
  pending = pending.slice(nl + 1);
1093
1159
  opts.onLine?.(line);
1094
1160
  }
@@ -2715,7 +2781,7 @@ function apply(ctx, entryConfig = {}) {
2715
2781
  const want = cfg.mcpBridge && cfg.enabled;
2716
2782
  if (want && bridgeState.bridge === null) (async () => {
2717
2783
  try {
2718
- const script = new URL("./bridge.mjs", import.meta.url).pathname;
2784
+ const script = fileURLToPath(new URL("./bridge.mjs", import.meta.url));
2719
2785
  const toolsSvc = ctx.get("tools");
2720
2786
  const bridge = await startMcpBridge({
2721
2787
  bridgeScript: script,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agy-link",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Google Antigravity (agy CLI) models for DeepSeek Harness — stream Gemini/Claude/GPT-OSS subscriptions into DSH with thinking, tool activity, token usage and in-GUI Google OAuth login.",
5
5
  "type": "module",
6
6
  "license": "MIT",