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 +11 -14
- package/dist/v1.d.ts +22 -1
- package/dist/v1.js +32 -1
- package/dist/v1.js.map +1 -1
- package/dist/v2.d.ts +16 -9
- package/dist/v2.js +28 -19
- package/dist/v2.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
"
|
|
82
|
-
"opencode-windows-encoding@
|
|
75
|
+
"plugin": [
|
|
76
|
+
"opencode-windows-encoding@latest"
|
|
83
77
|
]
|
|
84
78
|
}
|
|
85
79
|
```
|
|
86
80
|
|
|
87
|
-
The
|
|
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
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
2
|
+
* OpenCode V2 Plugin — UTF-8 Encoding Fix for Windows
|
|
3
3
|
*
|
|
4
|
-
* V2
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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":[]}
|