opencode-windows-encoding 4.0.0 → 5.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
@@ -68,25 +68,22 @@ After adding the plugin, restart OpenCode. All subsequent shell commands will us
68
68
 
69
69
  ## OpenCode V2 (opencode2)
70
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`:
71
+ Since v4.1.0 the main line works on both V1 and V2 hosts no separate dist-tag is needed:
78
72
 
79
73
  ```jsonc
80
74
  {
81
- "plugins": [
82
- "opencode-windows-encoding@beta"
75
+ "plugin": [
76
+ "opencode-windows-encoding@latest"
83
77
  ]
84
78
  }
85
79
  ```
86
80
 
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`.
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.
88
85
 
89
- > Note: the V2 plugin API is still in beta and its contract may change. If anything breaks after upgrading opencode2, please report it.
86
+ > Note: the legacy `beta` dist-tag (`opencode-windows-encoding@beta`) is deprecated the main line supersedes it.
90
87
 
91
88
  ## Local Usage (Copy & Go)
92
89
 
@@ -96,16 +93,16 @@ The built V1 plugin is a single self-contained JavaScript file (the shared core
96
93
  npm install && npm run build
97
94
  ```
98
95
 
99
- Then copy `dist/v1.js` to OpenCode's plugins directory:
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`):
100
97
 
101
98
  **PowerShell:**
102
99
  ```powershell
103
- Copy-Item dist/v1.js $env:USERPROFILE/.config/opencode/plugins/utf8-encoding.js
100
+ Copy-Item dist/v1.js $env:USERPROFILE/.config/opencode/plugins/utf8-encoding.ts
104
101
  ```
105
102
 
106
103
  **Bash / WSL:**
107
104
  ```bash
108
- cp dist/v1.js ~/.config/opencode/plugins/utf8-encoding.js
105
+ cp dist/v1.js ~/.config/opencode/plugins/utf8-encoding.ts
109
106
  ```
110
107
 
111
108
  Restart OpenCode to apply.
package/dist/v1.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PluginInput } from '@opencode-ai/plugin';
2
+ import { setup } from './v2.js';
2
3
 
3
4
  /**
4
5
  * OpenCode Plugin — UTF-8 Encoding Fix for Windows (V1)
@@ -19,5 +20,25 @@ declare const Utf8EncodingPlugin: (input: PluginInput) => Promise<{
19
20
  args: any;
20
21
  }) => Promise<void>;
21
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
+ };
22
43
 
23
- export { Utf8EncodingPlugin, Utf8EncodingPlugin as default };
44
+ export { Utf8EncodingPlugin, pluginModule as default };
package/dist/v1.js CHANGED
@@ -52,6 +52,32 @@ function injectUtf8Prefix(cmd, kind) {
52
52
  return prefixes + prefix + sep + cleanCmd;
53
53
  }
54
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
+
55
81
  // src/v1.ts
56
82
  function withTimeout(p, ms) {
57
83
  return new Promise((resolve, reject) => {
@@ -109,7 +135,12 @@ var Utf8EncodingPlugin = async (input) => {
109
135
  }
110
136
  };
111
137
  };
112
- var v1_default = Utf8EncodingPlugin;
138
+ var pluginModule = {
139
+ id: "windows-encoding",
140
+ server: Utf8EncodingPlugin,
141
+ setup
142
+ };
143
+ var v1_default = pluginModule;
113
144
  export {
114
145
  Utf8EncodingPlugin,
115
146
  v1_default as default
package/dist/v1.js.map CHANGED
@@ -1 +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"]}
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: \"windows-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: \"windows-encoding\",\n server: Utf8EncodingPlugin,\n setup,\n}\nexport default pluginModule\n"],"mappings":"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","names":["input"]}
package/dist/v2.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  /**
2
- * OpenCode V2 (opencode2) Plugin — UTF-8 Encoding Fix for Windows
2
+ * OpenCode V2 Plugin — UTF-8 Encoding Fix for Windows
3
3
  *
4
- * V2 入口({ id, setup } 契约)。类型为自包含最小结构声明,
5
- * 已对照 @opencode-ai/plugin@0.0.0-beta-18866(npm dist-tag beta)验证;
6
- * V2 插件 API 在 beta 期间可能变动,升级时需重新核对。
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
+ * 待宿主升级后自动生效。类型为自包含最小结构声明,仅依赖结构匹配。
7
9
  */
10
+ /** shell create.before 事件(对照 dev 分支 ShellCreateBefore) */
8
11
  interface ShellCreateBeforeEvent {
9
12
  command: string;
10
13
  cwd: string;
@@ -13,17 +16,21 @@ interface ShellCreateBeforeEvent {
13
16
  env: Record<string, string | undefined>;
14
17
  }
15
18
  interface HookRegistration {
16
- dispose(): Promise<void>;
19
+ dispose(): Promise<void> | void;
17
20
  }
21
+ interface ShellDomain {
22
+ hook(name: "create.before", cb: (event: ShellCreateBeforeEvent) => void | Promise<void>): Promise<HookRegistration> | HookRegistration;
23
+ }
24
+ /** V2 宿主上下文的最小结构声明:仅声明本插件用到的(可选)shell 域 */
18
25
  interface PluginContextV2 {
19
- shell: {
20
- hook(name: "create.before", cb: (event: ShellCreateBeforeEvent) => void | Promise<void>): Promise<HookRegistration>;
21
- };
26
+ shell?: ShellDomain;
22
27
  }
23
28
  interface PluginV2 {
24
29
  id: string;
25
30
  setup(ctx: PluginContextV2): void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>;
26
31
  }
32
+ /** V2 setup:有 shell hook 域则注册注入,否则静默降级(no-op) */
33
+ declare function setup(ctx: PluginContextV2): Promise<(() => Promise<void>) | undefined>;
27
34
  declare const Utf8EncodingPluginV2: PluginV2;
28
35
 
29
- export { Utf8EncodingPluginV2 as default };
36
+ export { type HookRegistration, type PluginContextV2, type ShellCreateBeforeEvent, Utf8EncodingPluginV2 as default, setup };
package/dist/v2.js CHANGED
@@ -53,28 +53,37 @@ function injectUtf8Prefix(cmd, kind) {
53
53
  }
54
54
 
55
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();
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;
74
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: "windows-encoding",
82
+ setup
75
83
  };
76
84
  var v2_default = Utf8EncodingPluginV2;
77
85
  export {
78
- v2_default as default
86
+ v2_default as default,
87
+ setup
79
88
  };
80
89
  //# sourceMappingURL=v2.js.map
package/dist/v2.js.map CHANGED
@@ -1 +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":[]}
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: \"windows-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,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-windows-encoding",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "OpenCode plugin to fix UTF-8 encoding issues in PowerShell on Windows",
5
5
  "type": "module",
6
6
  "main": "./dist/v1.js",