opencode-windows-encoding 3.1.0 → 4.0.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,51 @@ 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
+ OpenCode V2 is in beta and uses a different plugin contract (`{ id, setup }`). This package ships a dedicated V2 line on the npm `beta` dist-tag:
72
+
73
+ ```bash
74
+ opencode plugin add opencode-windows-encoding@beta
75
+ ```
76
+
77
+ Or configure it in `opencode.jsonc`:
78
+
79
+ ```jsonc
80
+ {
81
+ "plugins": [
82
+ "opencode-windows-encoding@beta"
83
+ ]
84
+ }
85
+ ```
86
+
87
+ The V2 build injects the encoding prefix via the `shell create.before` hook. The resolved shell is provided directly on the event (`event.shell`, possibly a full path with `.exe`), so the plugin does not need to read `config.shell`.
88
+
89
+ > Note: the V2 plugin API is still in beta and its contract may change. If anything breaks after upgrading opencode2, please report it.
90
+
69
91
  ## Local Usage (Copy & Go)
70
92
 
71
- This is a single-file plugin. Copy `src/utf8-encoding.ts` directly to OpenCode's plugins directory no dependencies required:
93
+ The built V1 plugin is a single self-contained JavaScript file (the shared core is bundled inline). From a clone of this repo:
94
+
95
+ ```bash
96
+ npm install && npm run build
97
+ ```
98
+
99
+ Then copy `dist/v1.js` to OpenCode's plugins directory:
72
100
 
73
101
  **PowerShell:**
74
102
  ```powershell
75
- Copy-Item src/utf8-encoding.ts $env:USERPROFILE/.config/opencode/plugins/utf8-encoding.ts
103
+ Copy-Item dist/v1.js $env:USERPROFILE/.config/opencode/plugins/utf8-encoding.js
76
104
  ```
77
105
 
78
106
  **Bash / WSL:**
79
107
  ```bash
80
- cp src/utf8-encoding.ts ~/.config/opencode/plugins/utf8-encoding.ts
108
+ cp dist/v1.js ~/.config/opencode/plugins/utf8-encoding.js
81
109
  ```
82
110
 
83
- Restart OpenCode to apply. No `npm install`, no build step.
111
+ Restart OpenCode to apply.
84
112
 
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.
113
+ 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
114
  ## Requirements
87
115
 
88
116
  - **OpenCode** (any recent version with plugin support)
@@ -111,7 +139,7 @@ Reference the source file directly:
111
139
  ```jsonc
112
140
  {
113
141
  "plugin": [
114
- "/path/to/opencode-windows-encoding/src/utf8-encoding.ts"
142
+ "/path/to/opencode-windows-encoding/src/v1.ts"
115
143
  ]
116
144
  }
117
145
  ```
@@ -1,9 +1,9 @@
1
1
  import { PluginInput } from '@opencode-ai/plugin';
2
2
 
3
3
  /**
4
- * OpenCode Plugin — UTF-8 Encoding Fix for Windows
4
+ * OpenCode Plugin — UTF-8 Encoding Fix for Windows (V1)
5
5
  *
6
- * 单文件插件,可直接复制到 ~/.config/opencode/plugins/ 使用,无需 npm install
6
+ * V1 入口(tool.execute.before 契约),共享逻辑见 ./encoding-core.ts
7
7
  *
8
8
  * 工作原理:拦截所有 bash/shell 工具调用,读取 opencode 配置的 shell
9
9
  * (pwsh / bash / cmd),按对应 shell 在命令前注入对应的 UTF-8 编码配置,
@@ -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,23 @@ 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/v1.ts
39
56
  function withTimeout(p, ms) {
40
57
  return new Promise((resolve, reject) => {
41
58
  const t = setTimeout(() => reject(new Error("timeout")), ms);
@@ -60,11 +77,6 @@ async function resolveShellKind(client) {
60
77
  return void 0;
61
78
  }
62
79
  }
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
80
  var Utf8EncodingPlugin = async (input) => {
69
81
  flog("=== LOADED ===");
70
82
  let kindPromise = null;
@@ -84,27 +96,22 @@ var Utf8EncodingPlugin = async (input) => {
84
96
  flog(` args keys: ${JSON.stringify(Object.keys(args))}`);
85
97
  return;
86
98
  }
87
- const { prefixes, cleanCmd } = stripSetPrefixes(cmd);
88
- flog(` orig: ${cleanCmd.slice(0, 120)}`);
89
99
  const kind = await getKind();
90
100
  if (!kind) {
91
101
  flog(" skip (no shell configured)");
92
102
  return;
93
103
  }
94
- const { prefix, sep, marker } = ENC[kind];
95
104
  flog(` shell kind: ${kind}`);
96
- if (cleanCmd.includes(marker)) {
97
- flog(" skip (idempotent)");
98
- return;
99
- }
100
- args.command = prefixes + prefix + sep + cleanCmd;
105
+ const next = injectUtf8Prefix(cmd, kind);
106
+ if (next === void 0) return;
107
+ args.command = next;
101
108
  flog(" INJECTED");
102
109
  }
103
110
  };
104
111
  };
105
- var utf8_encoding_default = Utf8EncodingPlugin;
112
+ var v1_default = Utf8EncodingPlugin;
106
113
  export {
107
114
  Utf8EncodingPlugin,
108
- utf8_encoding_default as default
115
+ v1_default as default
109
116
  };
110
- //# sourceMappingURL=utf8-encoding.js.map
117
+ //# sourceMappingURL=v1.js.map
package/dist/v1.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/encoding-core.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 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\"\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\nexport default Utf8EncodingPlugin\n"],"mappings":"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","names":["input"]}
package/dist/v2.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * OpenCode V2 (opencode2) Plugin — UTF-8 Encoding Fix for Windows
3
+ *
4
+ * V2 入口({ id, setup } 契约)。类型为自包含最小结构声明,
5
+ * 已对照 @opencode-ai/plugin@0.0.0-beta-18866(npm dist-tag beta)验证;
6
+ * V2 插件 API 在 beta 期间可能变动,升级时需重新核对。
7
+ */
8
+ interface ShellCreateBeforeEvent {
9
+ command: string;
10
+ cwd: string;
11
+ timeout: number;
12
+ shell: string;
13
+ env: Record<string, string | undefined>;
14
+ }
15
+ interface HookRegistration {
16
+ dispose(): Promise<void>;
17
+ }
18
+ interface PluginContextV2 {
19
+ shell: {
20
+ hook(name: "create.before", cb: (event: ShellCreateBeforeEvent) => void | Promise<void>): Promise<HookRegistration>;
21
+ };
22
+ }
23
+ interface PluginV2 {
24
+ id: string;
25
+ setup(ctx: PluginContextV2): void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>;
26
+ }
27
+ declare const Utf8EncodingPluginV2: PluginV2;
28
+
29
+ export { Utf8EncodingPluginV2 as default };
package/dist/v2.js ADDED
@@ -0,0 +1,80 @@
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
+ var Utf8EncodingPluginV2 = {
57
+ id: "utf8-encoding",
58
+ async setup(ctx) {
59
+ flog("=== LOADED (v2) ===");
60
+ const registration = await ctx.shell.hook("create.before", (event) => {
61
+ flog(`[shell.create.before] shell="${event.shell}"`);
62
+ const kind = detectShellKind(event.shell);
63
+ if (!kind) {
64
+ flog(" skip (unknown shell)");
65
+ return;
66
+ }
67
+ flog(` shell kind: ${kind}`);
68
+ const next = injectUtf8Prefix(event.command, kind);
69
+ if (next === void 0) return;
70
+ event.command = next;
71
+ flog(" INJECTED");
72
+ });
73
+ return () => registration.dispose();
74
+ }
75
+ };
76
+ var v2_default = Utf8EncodingPluginV2;
77
+ export {
78
+ v2_default as default
79
+ };
80
+ //# 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 (opencode2) Plugin — UTF-8 Encoding Fix for Windows\n *\n * V2 入口({ id, setup } 契约)。类型为自包含最小结构声明,\n * 已对照 @opencode-ai/plugin@0.0.0-beta-18866(npm dist-tag beta)验证;\n * V2 插件 API 在 beta 期间可能变动,升级时需重新核对。\n */\n\nimport { detectShellKind, flog, injectUtf8Prefix } from \"./encoding-core.js\"\n\ninterface ShellCreateBeforeEvent {\n command: string\n cwd: string\n timeout: number\n shell: string\n env: Record<string, string | undefined>\n}\ninterface HookRegistration { dispose(): Promise<void> }\ninterface PluginContextV2 {\n shell: {\n hook(name: \"create.before\", cb: (event: ShellCreateBeforeEvent) => void | Promise<void>): Promise<HookRegistration>\n }\n}\ninterface PluginV2 {\n id: string\n setup(ctx: PluginContextV2): void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>\n}\n\nconst Utf8EncodingPluginV2: PluginV2 = {\n id: \"utf8-encoding\",\n async setup(ctx) {\n flog(\"=== LOADED (v2) ===\")\n const registration = await ctx.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 () => registration.dispose()\n },\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;;;AC/CA,IAAM,uBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,MAAM,MAAM,KAAK;AACf,SAAK,qBAAqB;AAC1B,UAAM,eAAe,MAAM,IAAI,MAAM,KAAK,iBAAiB,CAAC,UAAU;AACpE,WAAK,gCAAgC,MAAM,KAAK,GAAG;AACnD,YAAM,OAAO,gBAAgB,MAAM,KAAK;AACxC,UAAI,CAAC,MAAM;AAAE,aAAK,wBAAwB;AAAG;AAAA,MAAO;AACpD,WAAK,iBAAiB,IAAI,EAAE;AAC5B,YAAM,OAAO,iBAAiB,MAAM,SAAS,IAAI;AACjD,UAAI,SAAS,OAAW;AACxB,YAAM,UAAU;AAChB,WAAK,YAAY;AAAA,IACnB,CAAC;AACD,WAAO,MAAM,aAAa,QAAQ;AAAA,EACpC;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.0.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 +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":";AAUA,SAAS,sBAAsB;AAC/B,SAAS,cAAc;AACvB,SAAS,YAAY;AAIrB,IAAM,QAAQ,QAAQ,IAAI,wBAAwB;AAClD,IAAM,MAAM,KAAK,OAAO,GAAG,iBAAiB;AAC5C,SAAS,KAAK,KAAa;AACzB,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;AAKA,IAAM,MAA0E;AAAA,EAC9E,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;AAGA,SAAS,gBAAgB,OAAkD;AACzE,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;AAGA,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;AAGA,SAAS,iBAAiB,KAAqD;AAC7E,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;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,EAAE,UAAU,SAAS,IAAI,iBAAiB,GAAG;AACnD,WAAK,WAAW,SAAS,MAAM,GAAG,GAAG,CAAC,EAAE;AAExC,YAAM,OAAO,MAAM,QAAQ;AAC3B,UAAI,CAAC,MAAM;AAAE,aAAK,8BAA8B;AAAG;AAAA,MAAO;AAC1D,YAAM,EAAE,QAAQ,KAAK,OAAO,IAAI,IAAI,IAAI;AACxC,WAAK,iBAAiB,IAAI,EAAE;AAG5B,UAAI,SAAS,SAAS,MAAM,GAAG;AAAE,aAAK,qBAAqB;AAAG;AAAA,MAAO;AAErE,WAAK,UAAU,WAAW,SAAS,MAAM;AACzC,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AACF;AAEA,IAAO,wBAAQ;","names":["input"]}