opencode-windows-encoding 3.1.0 → 4.1.0

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/opencode-windows-encoding)](https://www.npmjs.com/package/opencode-windows-encoding)
4
4
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
5
5
 
6
- OpenCode plugin that fixes UTF-8 encoding issues when executing shell commands on Windows. Single-file, zero npm runtime dependencies.
6
+ OpenCode plugin that fixes UTF-8 encoding issues when executing shell commands on Windows. Zero npm runtime dependencies.
7
7
 
8
8
  ## The Problem
9
9
 
@@ -36,13 +36,13 @@ chcp 65001 >nul
36
36
  - **Zero config** — works out of the box with no options
37
37
  - **Debug logging off by default** — set `OPENCODE_UTF8_DEBUG=1` to enable diagnostic logging to `$TMP/utf8-plugin.log`
38
38
 
39
- ## Installation
39
+ ## Installation (OpenCode V1)
40
40
 
41
41
  ```bash
42
42
  npm install opencode-windows-encoding
43
43
  ```
44
44
 
45
- ## Usage
45
+ ## Usage (OpenCode V1)
46
46
 
47
47
  Add the plugin to your `opencode.jsonc`:
48
48
 
@@ -66,23 +66,48 @@ Or with a specific version:
66
66
 
67
67
  After adding the plugin, restart OpenCode. All subsequent shell commands will use UTF-8 encoding automatically.
68
68
 
69
+ ## OpenCode V2 (opencode2)
70
+
71
+ Since v4.1.0 the main line works on both V1 and V2 hosts — no separate dist-tag is needed:
72
+
73
+ ```jsonc
74
+ {
75
+ "plugin": [
76
+ "opencode-windows-encoding@latest"
77
+ ]
78
+ }
79
+ ```
80
+
81
+ The default export is a combined `{ id, server, setup }` module:
82
+
83
+ - **V1 hosts** (opencode ≤ 1.17, or a local path reference on 1.18.x) call `server` and inject via `tool.execute.before`.
84
+ - **V2 hosts** validate `id` + `setup`. The setup registers the `shell create.before` hook on hosts that expose a shell hook domain (contract verified against the opencode dev branch; the resolved shell is provided directly on `event.shell`, so no `config.shell` lookup is needed). On V2 hosts without the shell domain (e.g. opencode 1.18.x) the setup degrades to a no-op — the plugin loads cleanly and activates automatically once the host gains the hook.
85
+
86
+ > Note: the legacy `beta` dist-tag (`opencode-windows-encoding@beta`) is deprecated — the main line supersedes it.
87
+
69
88
  ## Local Usage (Copy & Go)
70
89
 
71
- This is a single-file plugin. Copy `src/utf8-encoding.ts` directly to OpenCode's plugins directory no dependencies required:
90
+ The built V1 plugin is a single self-contained JavaScript file (the shared core is bundled inline). From a clone of this repo:
91
+
92
+ ```bash
93
+ npm install && npm run build
94
+ ```
95
+
96
+ Then copy `dist/v1.js` to OpenCode's plugins directory **with a `.ts` extension** (opencode 1.18.x auto-discovery resolves `.js` files unreliably on Windows; the bundled output is plain ESM JavaScript, which loads fine as `.ts`):
72
97
 
73
98
  **PowerShell:**
74
99
  ```powershell
75
- Copy-Item src/utf8-encoding.ts $env:USERPROFILE/.config/opencode/plugins/utf8-encoding.ts
100
+ Copy-Item dist/v1.js $env:USERPROFILE/.config/opencode/plugins/utf8-encoding.ts
76
101
  ```
77
102
 
78
103
  **Bash / WSL:**
79
104
  ```bash
80
- cp src/utf8-encoding.ts ~/.config/opencode/plugins/utf8-encoding.ts
105
+ cp dist/v1.js ~/.config/opencode/plugins/utf8-encoding.ts
81
106
  ```
82
107
 
83
- Restart OpenCode to apply. No `npm install`, no build step.
108
+ Restart OpenCode to apply.
84
109
 
85
- `src/utf8-encoding.ts` uses only Node.js built-ins (`node:fs`, `node:os`, `node:path`) and a compile-time-only `import type` from `@opencode-ai/plugin` — zero npm runtime dependencies.
110
+ The built file uses only Node.js built-ins (`node:fs`, `node:os`, `node:path`) and a compile-time-only `import type` from `@opencode-ai/plugin` — zero npm runtime dependencies.
86
111
  ## Requirements
87
112
 
88
113
  - **OpenCode** (any recent version with plugin support)
@@ -111,7 +136,7 @@ Reference the source file directly:
111
136
  ```jsonc
112
137
  {
113
138
  "plugin": [
114
- "/path/to/opencode-windows-encoding/src/utf8-encoding.ts"
139
+ "/path/to/opencode-windows-encoding/src/v1.ts"
115
140
  ]
116
141
  }
117
142
  ```
package/dist/v1.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { PluginInput } from '@opencode-ai/plugin';
2
+ import { setup } from './v2.js';
3
+
4
+ /**
5
+ * OpenCode Plugin — UTF-8 Encoding Fix for Windows (V1)
6
+ *
7
+ * V1 入口(tool.execute.before 契约),共享逻辑见 ./encoding-core.ts。
8
+ *
9
+ * 工作原理:拦截所有 bash/shell 工具调用,读取 opencode 配置的 shell
10
+ * (pwsh / bash / cmd),按对应 shell 在命令前注入对应的 UTF-8 编码配置,
11
+ * 解决中文/非 ASCII 字符乱码问题。未配置 shell 时不注入。
12
+ */
13
+
14
+ declare const Utf8EncodingPlugin: (input: PluginInput) => Promise<{
15
+ "tool.execute.before": (input: {
16
+ tool: string;
17
+ sessionID: string;
18
+ callID: string;
19
+ }, output: {
20
+ args: any;
21
+ }) => Promise<void>;
22
+ }>;
23
+ /**
24
+ * 三合一模块导出:
25
+ * - V1 宿主(含 opencode ≤1.17、1.18.x 本地路径引用):调用 `server`(V1 裸函数亦可,
26
+ * 具名导出 Utf8EncodingPlugin 保留);
27
+ * - V2 宿主(npm 包规格 / plugins 目录自动发现):校验 `id` + `setup`,
28
+ * 1.18.x 无 shell 域时 setup 降级 no-op,未来含 shell hook 的宿主自动启用注入。
29
+ */
30
+ declare const pluginModule: {
31
+ id: string;
32
+ server: (input: PluginInput) => Promise<{
33
+ "tool.execute.before": (input: {
34
+ tool: string;
35
+ sessionID: string;
36
+ callID: string;
37
+ }, output: {
38
+ args: any;
39
+ }) => Promise<void>;
40
+ }>;
41
+ setup: typeof setup;
42
+ };
43
+
44
+ export { Utf8EncodingPlugin, pluginModule as default };
@@ -1,4 +1,4 @@
1
- // src/utf8-encoding.ts
1
+ // src/encoding-core.ts
2
2
  import { appendFileSync } from "fs";
3
3
  import { tmpdir } from "os";
4
4
  import { join } from "path";
@@ -36,6 +36,49 @@ function detectShellKind(shell) {
36
36
  if (base === "cmd") return "cmd";
37
37
  return "bash";
38
38
  }
39
+ function stripSetPrefixes(cmd) {
40
+ const m = cmd.match(/^((?:set\s+\w+="[^"]*"\s*&&\s*)+)/);
41
+ if (m) return { prefixes: m[1], cleanCmd: cmd.slice(m[1].length) };
42
+ return { prefixes: "", cleanCmd: cmd };
43
+ }
44
+ function injectUtf8Prefix(cmd, kind) {
45
+ const { prefixes, cleanCmd } = stripSetPrefixes(cmd);
46
+ flog(` orig: ${cleanCmd.slice(0, 120)}`);
47
+ const { prefix, sep, marker } = ENC[kind];
48
+ if (cleanCmd.includes(marker)) {
49
+ flog(" skip (idempotent)");
50
+ return void 0;
51
+ }
52
+ return prefixes + prefix + sep + cleanCmd;
53
+ }
54
+
55
+ // src/v2.ts
56
+ async function setup(ctx) {
57
+ flog("=== LOADED (v2) ===");
58
+ const shell = ctx?.shell;
59
+ if (!shell || typeof shell.hook !== "function") {
60
+ flog(" host has no shell hook domain; degrade to no-op");
61
+ return;
62
+ }
63
+ const registration = await shell.hook("create.before", (event) => {
64
+ flog(`[shell.create.before] shell="${event.shell}"`);
65
+ const kind = detectShellKind(event.shell);
66
+ if (!kind) {
67
+ flog(" skip (unknown shell)");
68
+ return;
69
+ }
70
+ flog(` shell kind: ${kind}`);
71
+ const next = injectUtf8Prefix(event.command, kind);
72
+ if (next === void 0) return;
73
+ event.command = next;
74
+ flog(" INJECTED");
75
+ });
76
+ return async () => {
77
+ await registration.dispose?.();
78
+ };
79
+ }
80
+
81
+ // src/v1.ts
39
82
  function withTimeout(p, ms) {
40
83
  return new Promise((resolve, reject) => {
41
84
  const t = setTimeout(() => reject(new Error("timeout")), ms);
@@ -60,11 +103,6 @@ async function resolveShellKind(client) {
60
103
  return void 0;
61
104
  }
62
105
  }
63
- function stripSetPrefixes(cmd) {
64
- const m = cmd.match(/^((?:set\s+\w+="[^"]*"\s*&&\s*)+)/);
65
- if (m) return { prefixes: m[1], cleanCmd: cmd.slice(m[1].length) };
66
- return { prefixes: "", cleanCmd: cmd };
67
- }
68
106
  var Utf8EncodingPlugin = async (input) => {
69
107
  flog("=== LOADED ===");
70
108
  let kindPromise = null;
@@ -84,27 +122,27 @@ var Utf8EncodingPlugin = async (input) => {
84
122
  flog(` args keys: ${JSON.stringify(Object.keys(args))}`);
85
123
  return;
86
124
  }
87
- const { prefixes, cleanCmd } = stripSetPrefixes(cmd);
88
- flog(` orig: ${cleanCmd.slice(0, 120)}`);
89
125
  const kind = await getKind();
90
126
  if (!kind) {
91
127
  flog(" skip (no shell configured)");
92
128
  return;
93
129
  }
94
- const { prefix, sep, marker } = ENC[kind];
95
130
  flog(` shell kind: ${kind}`);
96
- if (cleanCmd.includes(marker)) {
97
- flog(" skip (idempotent)");
98
- return;
99
- }
100
- args.command = prefixes + prefix + sep + cleanCmd;
131
+ const next = injectUtf8Prefix(cmd, kind);
132
+ if (next === void 0) return;
133
+ args.command = next;
101
134
  flog(" INJECTED");
102
135
  }
103
136
  };
104
137
  };
105
- var utf8_encoding_default = Utf8EncodingPlugin;
138
+ var pluginModule = {
139
+ id: "utf8-encoding",
140
+ server: Utf8EncodingPlugin,
141
+ setup
142
+ };
143
+ var v1_default = pluginModule;
106
144
  export {
107
145
  Utf8EncodingPlugin,
108
- utf8_encoding_default as default
146
+ v1_default as default
109
147
  };
110
- //# sourceMappingURL=utf8-encoding.js.map
148
+ //# sourceMappingURL=v1.js.map
package/dist/v1.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/encoding-core.ts","../src/v2.ts","../src/v1.ts"],"sourcesContent":["/**\n * 共享核心 — UTF-8 编码前缀注入逻辑\n *\n * 由 V1(v1.ts)与 V2(v2.ts)入口共同复用;\n * 仅依赖 Node.js 内置模块,不引用任何插件包类型。\n */\n\nimport { appendFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\n\n// 调试日志(写入临时目录,默认关闭,设 OPENCODE_UTF8_DEBUG=1 开启)\nconst DEBUG = process.env.OPENCODE_UTF8_DEBUG === \"1\"\nconst LOG = join(tmpdir(), \"utf8-plugin.log\")\nexport function flog(msg: string) {\n if (!DEBUG) return\n try { appendFileSync(LOG, `[${new Date().toISOString()}] ${msg}\\n`, \"utf8\") } catch {}\n}\n\nexport type ShellKind = \"pwsh\" | \"bash\" | \"cmd\"\n\n// ── 各 shell 的 UTF-8 编码前缀与命令分隔符 ──\nexport const ENC: Record<ShellKind, { prefix: string; sep: string; marker: string }> = {\n pwsh: {\n prefix:\n \"[Console]::OutputEncoding=[Console]::InputEncoding=[Text.Encoding]::UTF8;\" +\n \"$OutputEncoding=[Text.Encoding]::UTF8;\" +\n \"$env:PYTHONIOENCODING='utf-8';\",\n sep: \"\\n\",\n marker: \"OutputEncoding\",\n },\n bash: {\n prefix: \"export LC_ALL=C.UTF-8; export LANG=C.UTF-8; export PYTHONIOENCODING=utf-8;\",\n sep: \"\\n\",\n marker: \"LC_ALL\",\n },\n cmd: {\n prefix: \"chcp 65001 >nul\",\n sep: \" & \",\n marker: \"chcp\",\n },\n}\n\n/** 由 shell 路径/名称(可能含目录与 .exe 后缀)归一化为 shell 类型;无法识别返回 undefined(不注入) */\nexport function detectShellKind(shell: string | undefined): ShellKind | undefined {\n if (!shell) return undefined\n const base = shell\n .replace(/\\\\/g, \"/\")\n .split(\"/\")\n .pop()!\n .toLowerCase()\n .replace(/\\.exe$/, \"\")\n if (base === \"pwsh\" || base === \"powershell\") return \"pwsh\"\n if (base === \"cmd\") return \"cmd\"\n return \"bash\" // bash / zsh / sh / dash / ksh 等 POSIX shell\n}\n\n/** 提取 opencode 在命令前追加的 set VAR=\"value\" && 前缀 */\nexport function stripSetPrefixes(cmd: string): { prefixes: string; cleanCmd: string } {\n const m = cmd.match(/^((?:set\\s+\\w+=\"[^\"]*\"\\s*&&\\s*)+)/)\n if (m) return { prefixes: m[1], cleanCmd: cmd.slice(m[1].length) }\n return { prefixes: \"\", cleanCmd: cmd }\n}\n\n/** 在命令前注入 UTF-8 编码前缀;已含 marker(幂等)时返回 undefined */\nexport function injectUtf8Prefix(cmd: string, kind: ShellKind): string | undefined {\n const { prefixes, cleanCmd } = stripSetPrefixes(cmd)\n flog(` orig: ${cleanCmd.slice(0, 120)}`)\n\n const { prefix, sep, marker } = ENC[kind]\n\n // 防止重复注入\n if (cleanCmd.includes(marker)) { flog(\" skip (idempotent)\"); return undefined }\n\n return prefixes + prefix + sep + cleanCmd\n}\n","/**\n * OpenCode V2 Plugin — UTF-8 Encoding Fix for Windows\n *\n * V2 契约({ id, setup })。setup 在含 shell hook 域的 V2 宿主上注册\n * shell create.before hook(事件契约对照 opencode dev 分支\n * packages/plugin/src/promise/shell.ts 核实);宿主缺少 shell 域\n * (如 opencode 1.18.x)时降级为 no-op——保证插件始终可加载、不报错,\n * 待宿主升级后自动生效。类型为自包含最小结构声明,仅依赖结构匹配。\n */\n\nimport { detectShellKind, flog, injectUtf8Prefix } from \"./encoding-core.js\"\n\n/** shell create.before 事件(对照 dev 分支 ShellCreateBefore) */\nexport interface ShellCreateBeforeEvent {\n command: string\n cwd: string\n timeout: number\n shell: string\n env: Record<string, string | undefined>\n}\nexport interface HookRegistration { dispose(): Promise<void> | void }\ninterface ShellDomain {\n hook(\n name: \"create.before\",\n cb: (event: ShellCreateBeforeEvent) => void | Promise<void>,\n ): Promise<HookRegistration> | HookRegistration\n}\n/** V2 宿主上下文的最小结构声明:仅声明本插件用到的(可选)shell 域 */\nexport interface PluginContextV2 {\n shell?: ShellDomain\n}\ninterface PluginV2 {\n id: string\n setup(ctx: PluginContextV2): void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>\n}\n\n/** V2 setup:有 shell hook 域则注册注入,否则静默降级(no-op) */\nexport async function setup(ctx: PluginContextV2) {\n flog(\"=== LOADED (v2) ===\")\n const shell = ctx?.shell\n if (!shell || typeof shell.hook !== \"function\") {\n flog(\" host has no shell hook domain; degrade to no-op\")\n return\n }\n const registration = await shell.hook(\"create.before\", (event) => {\n flog(`[shell.create.before] shell=\"${event.shell}\"`)\n const kind = detectShellKind(event.shell)\n if (!kind) { flog(\" skip (unknown shell)\"); return }\n flog(` shell kind: ${kind}`)\n const next = injectUtf8Prefix(event.command, kind)\n if (next === undefined) return // 幂等 skip 日志已由 core 输出\n event.command = next\n flog(\" INJECTED\")\n })\n return async () => { await registration.dispose?.() }\n}\n\nconst Utf8EncodingPluginV2: PluginV2 = {\n id: \"utf8-encoding\",\n setup,\n}\nexport default Utf8EncodingPluginV2\n","/**\n * OpenCode Plugin — UTF-8 Encoding Fix for Windows (V1)\n *\n * V1 入口(tool.execute.before 契约),共享逻辑见 ./encoding-core.ts。\n *\n * 工作原理:拦截所有 bash/shell 工具调用,读取 opencode 配置的 shell\n * (pwsh / bash / cmd),按对应 shell 在命令前注入对应的 UTF-8 编码配置,\n * 解决中文/非 ASCII 字符乱码问题。未配置 shell 时不注入。\n */\n\nimport type { PluginInput } from \"@opencode-ai/plugin\"\nimport { detectShellKind, flog, injectUtf8Prefix, type ShellKind } from \"./encoding-core.js\"\nimport { setup } from \"./v2.js\"\n\n/** 带超时的 Promise(插件内不引入额外运行时依赖) */\nfunction withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {\n return new Promise((resolve, reject) => {\n const t = setTimeout(() => reject(new Error(\"timeout\")), ms)\n p.then(\n (v) => { clearTimeout(t); resolve(v) },\n (e) => { clearTimeout(t); reject(e) },\n )\n })\n}\n\n/** 读取 opencode 配置中的 shell,失败/超时/未配置返回 undefined(不注入,永不 reject) */\nasync function resolveShellKind(client: PluginInput[\"client\"]): Promise<ShellKind | undefined> {\n try {\n const res = await withTimeout(client.config.get(), 3000)\n const shell = (res.data as unknown as { shell?: string } | undefined)?.shell\n return detectShellKind(shell)\n } catch {\n return undefined\n }\n}\n\nexport const Utf8EncodingPlugin = async (input: PluginInput) => {\n flog(\"=== LOADED ===\")\n\n // 惰性检测:绝不在插件加载阶段调用 client.config.get()\n //(会因 httpapi 未就绪 + 请求无超时而永久挂起,导致 opencode 启动死锁)。\n // 首次 tool hook 触发时再查,结果缓存。\n let kindPromise: Promise<ShellKind | undefined> | null = null\n const getKind = () => (kindPromise ??= resolveShellKind(input.client))\n\n return {\n \"tool.execute.before\": async (input: { tool: string; sessionID: string; callID: string }, output: { args: any }) => {\n const tool = String(input?.tool ?? \"\")\n flog(`[tool.before] tool=\"${tool}\"`)\n\n if (tool !== \"bash\" && tool !== \"shell\") return\n\n const args = output.args\n if (!args) { flog(\" no args\"); return }\n\n const cmd = args.command\n if (typeof cmd !== \"string\" || !cmd) {\n flog(` args keys: ${JSON.stringify(Object.keys(args))}`)\n return\n }\n\n const kind = await getKind()\n if (!kind) { flog(\" skip (no shell configured)\"); return }\n flog(` shell kind: ${kind}`)\n\n const next = injectUtf8Prefix(cmd, kind)\n if (next === undefined) return // 幂等 skip 日志已由 core 输出\n\n args.command = next\n flog(\" INJECTED\")\n },\n }\n}\n\n/**\n * 三合一模块导出:\n * - V1 宿主(含 opencode ≤1.17、1.18.x 本地路径引用):调用 `server`(V1 裸函数亦可,\n * 具名导出 Utf8EncodingPlugin 保留);\n * - V2 宿主(npm 包规格 / plugins 目录自动发现):校验 `id` + `setup`,\n * 1.18.x 无 shell 域时 setup 降级 no-op,未来含 shell hook 的宿主自动启用注入。\n */\nconst pluginModule = {\n id: \"utf8-encoding\",\n server: Utf8EncodingPlugin,\n setup,\n}\nexport default pluginModule\n"],"mappings":";AAOA,SAAS,sBAAsB;AAC/B,SAAS,cAAc;AACvB,SAAS,YAAY;AAGrB,IAAM,QAAQ,QAAQ,IAAI,wBAAwB;AAClD,IAAM,MAAM,KAAK,OAAO,GAAG,iBAAiB;AACrC,SAAS,KAAK,KAAa;AAChC,MAAI,CAAC,MAAO;AACZ,MAAI;AAAE,mBAAe,KAAK,KAAI,oBAAI,KAAK,GAAE,YAAY,CAAC,KAAK,GAAG;AAAA,GAAM,MAAM;AAAA,EAAE,QAAQ;AAAA,EAAC;AACvF;AAKO,IAAM,MAA0E;AAAA,EACrF,MAAM;AAAA,IACJ,QACE;AAAA,IAGF,KAAK;AAAA,IACL,QAAQ;AAAA,EACV;AAAA,EACA,MAAM;AAAA,IACJ,QAAQ;AAAA,IACR,KAAK;AAAA,IACL,QAAQ;AAAA,EACV;AAAA,EACA,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,KAAK;AAAA,IACL,QAAQ;AAAA,EACV;AACF;AAGO,SAAS,gBAAgB,OAAkD;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,MACV,QAAQ,OAAO,GAAG,EAClB,MAAM,GAAG,EACT,IAAI,EACJ,YAAY,EACZ,QAAQ,UAAU,EAAE;AACvB,MAAI,SAAS,UAAU,SAAS,aAAc,QAAO;AACrD,MAAI,SAAS,MAAO,QAAO;AAC3B,SAAO;AACT;AAGO,SAAS,iBAAiB,KAAqD;AACpF,QAAM,IAAI,IAAI,MAAM,mCAAmC;AACvD,MAAI,EAAG,QAAO,EAAE,UAAU,EAAE,CAAC,GAAG,UAAU,IAAI,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE;AACjE,SAAO,EAAE,UAAU,IAAI,UAAU,IAAI;AACvC;AAGO,SAAS,iBAAiB,KAAa,MAAqC;AACjF,QAAM,EAAE,UAAU,SAAS,IAAI,iBAAiB,GAAG;AACnD,OAAK,WAAW,SAAS,MAAM,GAAG,GAAG,CAAC,EAAE;AAExC,QAAM,EAAE,QAAQ,KAAK,OAAO,IAAI,IAAI,IAAI;AAGxC,MAAI,SAAS,SAAS,MAAM,GAAG;AAAE,SAAK,qBAAqB;AAAG,WAAO;AAAA,EAAU;AAE/E,SAAO,WAAW,SAAS,MAAM;AACnC;;;ACtCA,eAAsB,MAAM,KAAsB;AAChD,OAAK,qBAAqB;AAC1B,QAAM,QAAQ,KAAK;AACnB,MAAI,CAAC,SAAS,OAAO,MAAM,SAAS,YAAY;AAC9C,SAAK,mDAAmD;AACxD;AAAA,EACF;AACA,QAAM,eAAe,MAAM,MAAM,KAAK,iBAAiB,CAAC,UAAU;AAChE,SAAK,gCAAgC,MAAM,KAAK,GAAG;AACnD,UAAM,OAAO,gBAAgB,MAAM,KAAK;AACxC,QAAI,CAAC,MAAM;AAAE,WAAK,wBAAwB;AAAG;AAAA,IAAO;AACpD,SAAK,iBAAiB,IAAI,EAAE;AAC5B,UAAM,OAAO,iBAAiB,MAAM,SAAS,IAAI;AACjD,QAAI,SAAS,OAAW;AACxB,UAAM,UAAU;AAChB,SAAK,YAAY;AAAA,EACnB,CAAC;AACD,SAAO,YAAY;AAAE,UAAM,aAAa,UAAU;AAAA,EAAE;AACtD;;;ACxCA,SAAS,YAAe,GAAe,IAAwB;AAC7D,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,IAAI,WAAW,MAAM,OAAO,IAAI,MAAM,SAAS,CAAC,GAAG,EAAE;AAC3D,MAAE;AAAA,MACA,CAAC,MAAM;AAAE,qBAAa,CAAC;AAAG,gBAAQ,CAAC;AAAA,MAAE;AAAA,MACrC,CAAC,MAAM;AAAE,qBAAa,CAAC;AAAG,eAAO,CAAC;AAAA,MAAE;AAAA,IACtC;AAAA,EACF,CAAC;AACH;AAGA,eAAe,iBAAiB,QAA+D;AAC7F,MAAI;AACF,UAAM,MAAM,MAAM,YAAY,OAAO,OAAO,IAAI,GAAG,GAAI;AACvD,UAAM,QAAS,IAAI,MAAoD;AACvE,WAAO,gBAAgB,KAAK;AAAA,EAC9B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,IAAM,qBAAqB,OAAO,UAAuB;AAC9D,OAAK,gBAAgB;AAKrB,MAAI,cAAqD;AACzD,QAAM,UAAU,MAAO,gBAAgB,iBAAiB,MAAM,MAAM;AAEpE,SAAO;AAAA,IACL,uBAAuB,OAAOA,QAA4D,WAA0B;AAClH,YAAM,OAAO,OAAOA,QAAO,QAAQ,EAAE;AACrC,WAAK,uBAAuB,IAAI,GAAG;AAEnC,UAAI,SAAS,UAAU,SAAS,QAAS;AAEzC,YAAM,OAAO,OAAO;AACpB,UAAI,CAAC,MAAM;AAAE,aAAK,WAAW;AAAG;AAAA,MAAO;AAEvC,YAAM,MAAM,KAAK;AACjB,UAAI,OAAO,QAAQ,YAAY,CAAC,KAAK;AACnC,aAAK,gBAAgB,KAAK,UAAU,OAAO,KAAK,IAAI,CAAC,CAAC,EAAE;AACxD;AAAA,MACF;AAEA,YAAM,OAAO,MAAM,QAAQ;AAC3B,UAAI,CAAC,MAAM;AAAE,aAAK,8BAA8B;AAAG;AAAA,MAAO;AAC1D,WAAK,iBAAiB,IAAI,EAAE;AAE5B,YAAM,OAAO,iBAAiB,KAAK,IAAI;AACvC,UAAI,SAAS,OAAW;AAExB,WAAK,UAAU;AACf,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AACF;AASA,IAAM,eAAe;AAAA,EACnB,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR;AACF;AACA,IAAO,aAAQ;","names":["input"]}
package/dist/v2.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * OpenCode V2 Plugin — UTF-8 Encoding Fix for Windows
3
+ *
4
+ * V2 契约({ id, setup })。setup 在含 shell hook 域的 V2 宿主上注册
5
+ * shell create.before hook(事件契约对照 opencode dev 分支
6
+ * packages/plugin/src/promise/shell.ts 核实);宿主缺少 shell 域
7
+ * (如 opencode 1.18.x)时降级为 no-op——保证插件始终可加载、不报错,
8
+ * 待宿主升级后自动生效。类型为自包含最小结构声明,仅依赖结构匹配。
9
+ */
10
+ /** shell create.before 事件(对照 dev 分支 ShellCreateBefore) */
11
+ interface ShellCreateBeforeEvent {
12
+ command: string;
13
+ cwd: string;
14
+ timeout: number;
15
+ shell: string;
16
+ env: Record<string, string | undefined>;
17
+ }
18
+ interface HookRegistration {
19
+ dispose(): Promise<void> | void;
20
+ }
21
+ interface ShellDomain {
22
+ hook(name: "create.before", cb: (event: ShellCreateBeforeEvent) => void | Promise<void>): Promise<HookRegistration> | HookRegistration;
23
+ }
24
+ /** V2 宿主上下文的最小结构声明:仅声明本插件用到的(可选)shell 域 */
25
+ interface PluginContextV2 {
26
+ shell?: ShellDomain;
27
+ }
28
+ interface PluginV2 {
29
+ id: string;
30
+ setup(ctx: PluginContextV2): void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>;
31
+ }
32
+ /** V2 setup:有 shell hook 域则注册注入,否则静默降级(no-op) */
33
+ declare function setup(ctx: PluginContextV2): Promise<(() => Promise<void>) | undefined>;
34
+ declare const Utf8EncodingPluginV2: PluginV2;
35
+
36
+ export { type HookRegistration, type PluginContextV2, type ShellCreateBeforeEvent, Utf8EncodingPluginV2 as default, setup };
package/dist/v2.js ADDED
@@ -0,0 +1,89 @@
1
+ // src/encoding-core.ts
2
+ import { appendFileSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ var DEBUG = process.env.OPENCODE_UTF8_DEBUG === "1";
6
+ var LOG = join(tmpdir(), "utf8-plugin.log");
7
+ function flog(msg) {
8
+ if (!DEBUG) return;
9
+ try {
10
+ appendFileSync(LOG, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
11
+ `, "utf8");
12
+ } catch {
13
+ }
14
+ }
15
+ var ENC = {
16
+ pwsh: {
17
+ prefix: "[Console]::OutputEncoding=[Console]::InputEncoding=[Text.Encoding]::UTF8;$OutputEncoding=[Text.Encoding]::UTF8;$env:PYTHONIOENCODING='utf-8';",
18
+ sep: "\n",
19
+ marker: "OutputEncoding"
20
+ },
21
+ bash: {
22
+ prefix: "export LC_ALL=C.UTF-8; export LANG=C.UTF-8; export PYTHONIOENCODING=utf-8;",
23
+ sep: "\n",
24
+ marker: "LC_ALL"
25
+ },
26
+ cmd: {
27
+ prefix: "chcp 65001 >nul",
28
+ sep: " & ",
29
+ marker: "chcp"
30
+ }
31
+ };
32
+ function detectShellKind(shell) {
33
+ if (!shell) return void 0;
34
+ const base = shell.replace(/\\/g, "/").split("/").pop().toLowerCase().replace(/\.exe$/, "");
35
+ if (base === "pwsh" || base === "powershell") return "pwsh";
36
+ if (base === "cmd") return "cmd";
37
+ return "bash";
38
+ }
39
+ function stripSetPrefixes(cmd) {
40
+ const m = cmd.match(/^((?:set\s+\w+="[^"]*"\s*&&\s*)+)/);
41
+ if (m) return { prefixes: m[1], cleanCmd: cmd.slice(m[1].length) };
42
+ return { prefixes: "", cleanCmd: cmd };
43
+ }
44
+ function injectUtf8Prefix(cmd, kind) {
45
+ const { prefixes, cleanCmd } = stripSetPrefixes(cmd);
46
+ flog(` orig: ${cleanCmd.slice(0, 120)}`);
47
+ const { prefix, sep, marker } = ENC[kind];
48
+ if (cleanCmd.includes(marker)) {
49
+ flog(" skip (idempotent)");
50
+ return void 0;
51
+ }
52
+ return prefixes + prefix + sep + cleanCmd;
53
+ }
54
+
55
+ // src/v2.ts
56
+ async function setup(ctx) {
57
+ flog("=== LOADED (v2) ===");
58
+ const shell = ctx?.shell;
59
+ if (!shell || typeof shell.hook !== "function") {
60
+ flog(" host has no shell hook domain; degrade to no-op");
61
+ return;
62
+ }
63
+ const registration = await shell.hook("create.before", (event) => {
64
+ flog(`[shell.create.before] shell="${event.shell}"`);
65
+ const kind = detectShellKind(event.shell);
66
+ if (!kind) {
67
+ flog(" skip (unknown shell)");
68
+ return;
69
+ }
70
+ flog(` shell kind: ${kind}`);
71
+ const next = injectUtf8Prefix(event.command, kind);
72
+ if (next === void 0) return;
73
+ event.command = next;
74
+ flog(" INJECTED");
75
+ });
76
+ return async () => {
77
+ await registration.dispose?.();
78
+ };
79
+ }
80
+ var Utf8EncodingPluginV2 = {
81
+ id: "utf8-encoding",
82
+ setup
83
+ };
84
+ var v2_default = Utf8EncodingPluginV2;
85
+ export {
86
+ v2_default as default,
87
+ setup
88
+ };
89
+ //# sourceMappingURL=v2.js.map
package/dist/v2.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/encoding-core.ts","../src/v2.ts"],"sourcesContent":["/**\n * 共享核心 — UTF-8 编码前缀注入逻辑\n *\n * 由 V1(v1.ts)与 V2(v2.ts)入口共同复用;\n * 仅依赖 Node.js 内置模块,不引用任何插件包类型。\n */\n\nimport { appendFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\n\n// 调试日志(写入临时目录,默认关闭,设 OPENCODE_UTF8_DEBUG=1 开启)\nconst DEBUG = process.env.OPENCODE_UTF8_DEBUG === \"1\"\nconst LOG = join(tmpdir(), \"utf8-plugin.log\")\nexport function flog(msg: string) {\n if (!DEBUG) return\n try { appendFileSync(LOG, `[${new Date().toISOString()}] ${msg}\\n`, \"utf8\") } catch {}\n}\n\nexport type ShellKind = \"pwsh\" | \"bash\" | \"cmd\"\n\n// ── 各 shell 的 UTF-8 编码前缀与命令分隔符 ──\nexport const ENC: Record<ShellKind, { prefix: string; sep: string; marker: string }> = {\n pwsh: {\n prefix:\n \"[Console]::OutputEncoding=[Console]::InputEncoding=[Text.Encoding]::UTF8;\" +\n \"$OutputEncoding=[Text.Encoding]::UTF8;\" +\n \"$env:PYTHONIOENCODING='utf-8';\",\n sep: \"\\n\",\n marker: \"OutputEncoding\",\n },\n bash: {\n prefix: \"export LC_ALL=C.UTF-8; export LANG=C.UTF-8; export PYTHONIOENCODING=utf-8;\",\n sep: \"\\n\",\n marker: \"LC_ALL\",\n },\n cmd: {\n prefix: \"chcp 65001 >nul\",\n sep: \" & \",\n marker: \"chcp\",\n },\n}\n\n/** 由 shell 路径/名称(可能含目录与 .exe 后缀)归一化为 shell 类型;无法识别返回 undefined(不注入) */\nexport function detectShellKind(shell: string | undefined): ShellKind | undefined {\n if (!shell) return undefined\n const base = shell\n .replace(/\\\\/g, \"/\")\n .split(\"/\")\n .pop()!\n .toLowerCase()\n .replace(/\\.exe$/, \"\")\n if (base === \"pwsh\" || base === \"powershell\") return \"pwsh\"\n if (base === \"cmd\") return \"cmd\"\n return \"bash\" // bash / zsh / sh / dash / ksh 等 POSIX shell\n}\n\n/** 提取 opencode 在命令前追加的 set VAR=\"value\" && 前缀 */\nexport function stripSetPrefixes(cmd: string): { prefixes: string; cleanCmd: string } {\n const m = cmd.match(/^((?:set\\s+\\w+=\"[^\"]*\"\\s*&&\\s*)+)/)\n if (m) return { prefixes: m[1], cleanCmd: cmd.slice(m[1].length) }\n return { prefixes: \"\", cleanCmd: cmd }\n}\n\n/** 在命令前注入 UTF-8 编码前缀;已含 marker(幂等)时返回 undefined */\nexport function injectUtf8Prefix(cmd: string, kind: ShellKind): string | undefined {\n const { prefixes, cleanCmd } = stripSetPrefixes(cmd)\n flog(` orig: ${cleanCmd.slice(0, 120)}`)\n\n const { prefix, sep, marker } = ENC[kind]\n\n // 防止重复注入\n if (cleanCmd.includes(marker)) { flog(\" skip (idempotent)\"); return undefined }\n\n return prefixes + prefix + sep + cleanCmd\n}\n","/**\n * OpenCode V2 Plugin — UTF-8 Encoding Fix for Windows\n *\n * V2 契约({ id, setup })。setup 在含 shell hook 域的 V2 宿主上注册\n * shell create.before hook(事件契约对照 opencode dev 分支\n * packages/plugin/src/promise/shell.ts 核实);宿主缺少 shell 域\n * (如 opencode 1.18.x)时降级为 no-op——保证插件始终可加载、不报错,\n * 待宿主升级后自动生效。类型为自包含最小结构声明,仅依赖结构匹配。\n */\n\nimport { detectShellKind, flog, injectUtf8Prefix } from \"./encoding-core.js\"\n\n/** shell create.before 事件(对照 dev 分支 ShellCreateBefore) */\nexport interface ShellCreateBeforeEvent {\n command: string\n cwd: string\n timeout: number\n shell: string\n env: Record<string, string | undefined>\n}\nexport interface HookRegistration { dispose(): Promise<void> | void }\ninterface ShellDomain {\n hook(\n name: \"create.before\",\n cb: (event: ShellCreateBeforeEvent) => void | Promise<void>,\n ): Promise<HookRegistration> | HookRegistration\n}\n/** V2 宿主上下文的最小结构声明:仅声明本插件用到的(可选)shell 域 */\nexport interface PluginContextV2 {\n shell?: ShellDomain\n}\ninterface PluginV2 {\n id: string\n setup(ctx: PluginContextV2): void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>\n}\n\n/** V2 setup:有 shell hook 域则注册注入,否则静默降级(no-op) */\nexport async function setup(ctx: PluginContextV2) {\n flog(\"=== LOADED (v2) ===\")\n const shell = ctx?.shell\n if (!shell || typeof shell.hook !== \"function\") {\n flog(\" host has no shell hook domain; degrade to no-op\")\n return\n }\n const registration = await shell.hook(\"create.before\", (event) => {\n flog(`[shell.create.before] shell=\"${event.shell}\"`)\n const kind = detectShellKind(event.shell)\n if (!kind) { flog(\" skip (unknown shell)\"); return }\n flog(` shell kind: ${kind}`)\n const next = injectUtf8Prefix(event.command, kind)\n if (next === undefined) return // 幂等 skip 日志已由 core 输出\n event.command = next\n flog(\" INJECTED\")\n })\n return async () => { await registration.dispose?.() }\n}\n\nconst Utf8EncodingPluginV2: PluginV2 = {\n id: \"utf8-encoding\",\n setup,\n}\nexport default Utf8EncodingPluginV2\n"],"mappings":";AAOA,SAAS,sBAAsB;AAC/B,SAAS,cAAc;AACvB,SAAS,YAAY;AAGrB,IAAM,QAAQ,QAAQ,IAAI,wBAAwB;AAClD,IAAM,MAAM,KAAK,OAAO,GAAG,iBAAiB;AACrC,SAAS,KAAK,KAAa;AAChC,MAAI,CAAC,MAAO;AACZ,MAAI;AAAE,mBAAe,KAAK,KAAI,oBAAI,KAAK,GAAE,YAAY,CAAC,KAAK,GAAG;AAAA,GAAM,MAAM;AAAA,EAAE,QAAQ;AAAA,EAAC;AACvF;AAKO,IAAM,MAA0E;AAAA,EACrF,MAAM;AAAA,IACJ,QACE;AAAA,IAGF,KAAK;AAAA,IACL,QAAQ;AAAA,EACV;AAAA,EACA,MAAM;AAAA,IACJ,QAAQ;AAAA,IACR,KAAK;AAAA,IACL,QAAQ;AAAA,EACV;AAAA,EACA,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,KAAK;AAAA,IACL,QAAQ;AAAA,EACV;AACF;AAGO,SAAS,gBAAgB,OAAkD;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,MACV,QAAQ,OAAO,GAAG,EAClB,MAAM,GAAG,EACT,IAAI,EACJ,YAAY,EACZ,QAAQ,UAAU,EAAE;AACvB,MAAI,SAAS,UAAU,SAAS,aAAc,QAAO;AACrD,MAAI,SAAS,MAAO,QAAO;AAC3B,SAAO;AACT;AAGO,SAAS,iBAAiB,KAAqD;AACpF,QAAM,IAAI,IAAI,MAAM,mCAAmC;AACvD,MAAI,EAAG,QAAO,EAAE,UAAU,EAAE,CAAC,GAAG,UAAU,IAAI,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE;AACjE,SAAO,EAAE,UAAU,IAAI,UAAU,IAAI;AACvC;AAGO,SAAS,iBAAiB,KAAa,MAAqC;AACjF,QAAM,EAAE,UAAU,SAAS,IAAI,iBAAiB,GAAG;AACnD,OAAK,WAAW,SAAS,MAAM,GAAG,GAAG,CAAC,EAAE;AAExC,QAAM,EAAE,QAAQ,KAAK,OAAO,IAAI,IAAI,IAAI;AAGxC,MAAI,SAAS,SAAS,MAAM,GAAG;AAAE,SAAK,qBAAqB;AAAG,WAAO;AAAA,EAAU;AAE/E,SAAO,WAAW,SAAS,MAAM;AACnC;;;ACtCA,eAAsB,MAAM,KAAsB;AAChD,OAAK,qBAAqB;AAC1B,QAAM,QAAQ,KAAK;AACnB,MAAI,CAAC,SAAS,OAAO,MAAM,SAAS,YAAY;AAC9C,SAAK,mDAAmD;AACxD;AAAA,EACF;AACA,QAAM,eAAe,MAAM,MAAM,KAAK,iBAAiB,CAAC,UAAU;AAChE,SAAK,gCAAgC,MAAM,KAAK,GAAG;AACnD,UAAM,OAAO,gBAAgB,MAAM,KAAK;AACxC,QAAI,CAAC,MAAM;AAAE,WAAK,wBAAwB;AAAG;AAAA,IAAO;AACpD,SAAK,iBAAiB,IAAI,EAAE;AAC5B,UAAM,OAAO,iBAAiB,MAAM,SAAS,IAAI;AACjD,QAAI,SAAS,OAAW;AACxB,UAAM,UAAU;AAChB,SAAK,YAAY;AAAA,EACnB,CAAC;AACD,SAAO,YAAY;AAAE,UAAM,aAAa,UAAU;AAAA,EAAE;AACtD;AAEA,IAAM,uBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ;AACF;AACA,IAAO,aAAQ;","names":[]}
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "opencode-windows-encoding",
3
- "version": "3.1.0",
3
+ "version": "4.1.0",
4
4
  "description": "OpenCode plugin to fix UTF-8 encoding issues in PowerShell on Windows",
5
5
  "type": "module",
6
- "main": "./dist/utf8-encoding.js",
7
- "types": "./dist/utf8-encoding.d.ts",
6
+ "main": "./dist/v1.js",
7
+ "types": "./dist/v1.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./dist/utf8-encoding.d.ts",
11
- "import": "./dist/utf8-encoding.js"
10
+ "types": "./dist/v1.d.ts",
11
+ "import": "./dist/v1.js"
12
12
  }
13
13
  },
14
14
  "files": [
@@ -18,7 +18,8 @@
18
18
  "build": "tsup",
19
19
  "dev": "tsup --watch",
20
20
  "typecheck": "tsc --noEmit",
21
- "prepublishOnly": "npm run build"
21
+ "prepublishOnly": "npm run build",
22
+ "publish:beta": "node scripts/publish-v2.mjs"
22
23
  },
23
24
  "keywords": [
24
25
  "opencode",
@@ -34,7 +35,6 @@
34
35
  "url": "git+https://github.com/Cle2ment/opencode-windows-encoding.git"
35
36
  },
36
37
  "license": "AGPL-3.0",
37
- "dependencies": {},
38
38
  "devDependencies": {
39
39
  "@opencode-ai/plugin": "^1.17.11",
40
40
  "@types/node": "^22.0.0",
@@ -1,23 +0,0 @@
1
- import { PluginInput } from '@opencode-ai/plugin';
2
-
3
- /**
4
- * OpenCode Plugin — UTF-8 Encoding Fix for Windows
5
- *
6
- * 单文件插件,可直接复制到 ~/.config/opencode/plugins/ 使用,无需 npm install。
7
- *
8
- * 工作原理:拦截所有 bash/shell 工具调用,读取 opencode 配置的 shell
9
- * (pwsh / bash / cmd),按对应 shell 在命令前注入对应的 UTF-8 编码配置,
10
- * 解决中文/非 ASCII 字符乱码问题。未配置 shell 时不注入。
11
- */
12
-
13
- declare const Utf8EncodingPlugin: (input: PluginInput) => Promise<{
14
- "tool.execute.before": (input: {
15
- tool: string;
16
- sessionID: string;
17
- callID: string;
18
- }, output: {
19
- args: any;
20
- }) => Promise<void>;
21
- }>;
22
-
23
- export { Utf8EncodingPlugin, Utf8EncodingPlugin as default };
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utf8-encoding.ts"],"sourcesContent":["/**\n * OpenCode Plugin — UTF-8 Encoding Fix for Windows\n *\n * 单文件插件,可直接复制到 ~/.config/opencode/plugins/ 使用,无需 npm install。\n *\n * 工作原理:拦截所有 bash/shell 工具调用,读取 opencode 配置的 shell\n * (pwsh / bash / cmd),按对应 shell 在命令前注入对应的 UTF-8 编码配置,\n * 解决中文/非 ASCII 字符乱码问题。未配置 shell 时不注入。\n */\n\nimport { appendFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\nimport type { PluginInput } from \"@opencode-ai/plugin\"\n\n// 调试日志(写入临时目录,默认关闭,设 OPENCODE_UTF8_DEBUG=1 开启)\nconst DEBUG = process.env.OPENCODE_UTF8_DEBUG === \"1\"\nconst LOG = join(tmpdir(), \"utf8-plugin.log\")\nfunction flog(msg: string) {\n if (!DEBUG) return\n try { appendFileSync(LOG, `[${new Date().toISOString()}] ${msg}\\n`, \"utf8\") } catch {}\n}\n\ntype ShellKind = \"pwsh\" | \"bash\" | \"cmd\"\n\n// ── 各 shell 的 UTF-8 编码前缀与命令分隔符 ──\nconst ENC: Record<ShellKind, { prefix: string; sep: string; marker: string }> = {\n pwsh: {\n prefix:\n \"[Console]::OutputEncoding=[Console]::InputEncoding=[Text.Encoding]::UTF8;\" +\n \"$OutputEncoding=[Text.Encoding]::UTF8;\" +\n \"$env:PYTHONIOENCODING='utf-8';\",\n sep: \"\\n\",\n marker: \"OutputEncoding\",\n },\n bash: {\n prefix: \"export LC_ALL=C.UTF-8; export LANG=C.UTF-8; export PYTHONIOENCODING=utf-8;\",\n sep: \"\\n\",\n marker: \"LC_ALL\",\n },\n cmd: {\n prefix: \"chcp 65001 >nul\",\n sep: \" & \",\n marker: \"chcp\",\n },\n}\n\n/** 由 config.shell 的值归一化为 shell 类型;未配置返回 undefined(不注入) */\nfunction detectShellKind(shell: string | undefined): ShellKind | undefined {\n if (!shell) return undefined\n const base = shell\n .replace(/\\\\/g, \"/\")\n .split(\"/\")\n .pop()!\n .toLowerCase()\n .replace(/\\.exe$/, \"\")\n if (base === \"pwsh\" || base === \"powershell\") return \"pwsh\"\n if (base === \"cmd\") return \"cmd\"\n return \"bash\" // bash / zsh / sh / dash / ksh 等 POSIX shell\n}\n\n/** 带超时的 Promise(插件内不引入额外运行时依赖) */\nfunction withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {\n return new Promise((resolve, reject) => {\n const t = setTimeout(() => reject(new Error(\"timeout\")), ms)\n p.then(\n (v) => { clearTimeout(t); resolve(v) },\n (e) => { clearTimeout(t); reject(e) },\n )\n })\n}\n\n/** 读取 opencode 配置中的 shell,失败/超时/未配置返回 undefined(不注入,永不 reject) */\nasync function resolveShellKind(client: PluginInput[\"client\"]): Promise<ShellKind | undefined> {\n try {\n const res = await withTimeout(client.config.get(), 3000)\n const shell = (res.data as unknown as { shell?: string } | undefined)?.shell\n return detectShellKind(shell)\n } catch {\n return undefined\n }\n}\n\n/** 提取 opencode 在命令前追加的 set VAR=\"value\" && 前缀 */\nfunction stripSetPrefixes(cmd: string): { prefixes: string; cleanCmd: string } {\n const m = cmd.match(/^((?:set\\s+\\w+=\"[^\"]*\"\\s*&&\\s*)+)/)\n if (m) return { prefixes: m[1], cleanCmd: cmd.slice(m[1].length) }\n return { prefixes: \"\", cleanCmd: cmd }\n}\n\nexport const Utf8EncodingPlugin = async (input: PluginInput) => {\n flog(\"=== LOADED ===\")\n\n // 惰性检测:绝不在插件加载阶段调用 client.config.get()\n //(会因 httpapi 未就绪 + 请求无超时而永久挂起,导致 opencode 启动死锁)。\n // 首次 tool hook 触发时再查,结果缓存。\n let kindPromise: Promise<ShellKind | undefined> | null = null\n const getKind = () => (kindPromise ??= resolveShellKind(input.client))\n\n return {\n \"tool.execute.before\": async (input: { tool: string; sessionID: string; callID: string }, output: { args: any }) => {\n const tool = String(input?.tool ?? \"\")\n flog(`[tool.before] tool=\"${tool}\"`)\n\n if (tool !== \"bash\" && tool !== \"shell\") return\n\n const args = output.args\n if (!args) { flog(\" no args\"); return }\n\n const cmd = args.command\n if (typeof cmd !== \"string\" || !cmd) {\n flog(` args keys: ${JSON.stringify(Object.keys(args))}`)\n return\n }\n\n const { prefixes, cleanCmd } = stripSetPrefixes(cmd)\n flog(` orig: ${cleanCmd.slice(0, 120)}`)\n\n const kind = await getKind()\n if (!kind) { flog(\" skip (no shell configured)\"); return }\n const { prefix, sep, marker } = ENC[kind]\n flog(` shell kind: ${kind}`)\n\n // 防止重复注入\n if (cleanCmd.includes(marker)) { flog(\" skip (idempotent)\"); return }\n\n args.command = prefixes + prefix + sep + cleanCmd\n flog(\" INJECTED\")\n },\n }\n}\n\nexport default Utf8EncodingPlugin\n"],"mappings":"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","names":["input"]}