orca-zh-tw-installer 1.2.1 → 1.2.2

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
@@ -51,6 +51,12 @@ npx orca-zh-tw-installer
51
51
  > 注意:套件名稱是 `orca-zh-tw-installer`。指令名稱雖然是 `orca-zh-tw`,
52
52
  > 但 `npx orca-zh-tw` 會被當成套件名去 registry 查詢而得到 404。
53
53
 
54
+ **請先完全關閉 Orca**(系統匣圖示右鍵 → Quit,不是只關閉視窗)。
55
+ 安裝腳本會偵測 Orca 是否仍在執行並直接中止——因為在執行中替換
56
+ `app.asar` 之後,那個 Orca 實例的 renderer 還握著舊的檔名,
57
+ 去載入時會拋出 `Unexpected token` 並讓側邊欄等面板顯示錯誤。
58
+ 那是一次性的、重啟即消失,但很容易被誤認為語系包壞了。
59
+
54
60
  腳本將自動執行以下流程:
55
61
  1. 自動定位作業系統對應的 Orca 安裝路徑並解包。
56
62
  2. 備份官方 `app.asar`(已含補丁時會保留原本的乾淨備份)。
package/index.js CHANGED
@@ -27,11 +27,41 @@ const asar = require('@electron/asar');
27
27
  // --dry-run:解包、修補、驗證,但不備份也不重新打包。
28
28
  // 可在 Orca 執行中安全使用,用來確認 Orca 更新後錨點是否仍然有效。
29
29
  const DRY_RUN = process.argv.includes('--dry-run');
30
+ // --force:略過「Orca 是否執行中」的檢查。不建議使用,見 countRunningOrca 的說明。
31
+ const FORCE = process.argv.includes('--force');
30
32
  const workDir = path.join(os.tmpdir(), DRY_RUN ? 'orca-zh-tw-patcher-dry' : 'orca-zh-tw-patcher');
31
33
  const unpackedDir = path.join(workDir, 'app.asar.unpacked');
32
34
 
33
35
  const MARK = 'UI_LANGUAGE_TRADITIONAL_CHINESE = "zh-TW"';
34
36
 
37
+ /**
38
+ * 偵測 Orca 是否還在執行。
39
+ *
40
+ * 為什麼要擋:重新打包 app.asar 之後,仍在執行的 Orca 其 renderer 還握著
41
+ * 舊的 lazy chunk 檔名,去載入時檔案已被換掉,會拋出
42
+ * 「Unexpected token」並讓側邊欄等面板的 error boundary 接住。
43
+ * 那是一次性的、重啟即消失的錯誤,但很容易被誤認為語系包壞了。
44
+ *
45
+ * 回傳 process 數量;無法判斷時回傳 -1(不阻擋)。
46
+ */
47
+ function countRunningOrca() {
48
+ const { execFileSync } = require('child_process');
49
+ const run = (cmd, args) => {
50
+ try {
51
+ return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
52
+ } catch { return null; }
53
+ };
54
+ if (platform === 'win32') {
55
+ const out = run('tasklist', ['/FI', 'IMAGENAME eq Orca.exe', '/NH']);
56
+ if (out === null) return -1;
57
+ return out.split(/\r?\n/).filter(l => /Orca\.exe/i.test(l)).length;
58
+ }
59
+ // macOS / Linux:-x 只比對完全相符的程序名,避免抓到自己的 npx/node
60
+ const out = run('pgrep', ['-x', platform === 'darwin' ? 'Orca' : 'orca']);
61
+ if (out === null) return 0; // pgrep 找不到時回傳非 0 退出碼,會落到 catch
62
+ return out.split('\n').filter(Boolean).length;
63
+ }
64
+
35
65
  /**
36
66
  * 修補器:每個修補都要宣告「如何判斷已完成」。
37
67
  * 若錨點找不到且尚未完成,就記為失敗——絕不靜默略過。
@@ -84,6 +114,25 @@ function patchLocaleGate(p) {
84
114
 
85
115
  async function patch() {
86
116
  try {
117
+ // dry-run 不寫檔,Orca 執行中也能安全跑,故不檢查
118
+ if (!DRY_RUN && !FORCE) {
119
+ const n = countRunningOrca();
120
+ if (n > 0) {
121
+ console.error(`❌ Orca 仍在執行中(偵測到 ${n} 個程序),已中止。\n`);
122
+ console.error(' 請先完全關閉 Orca:系統匣圖示右鍵 → Quit(不是只關閉視窗)。');
123
+ console.error(' 若在修補後才關閉,重新啟動的 Orca 可能出現「Unexpected token」');
124
+ console.error(' 造成側邊欄等面板顯示錯誤——那是舊 chunk 與新檔案不符所致。\n');
125
+ if (platform === 'win32') {
126
+ console.error(' 確認是否關乾淨:');
127
+ console.error(' Get-Process Orca,orca-terminal-daemon -ErrorAction SilentlyContinue\n');
128
+ }
129
+ console.error(' 想在 Orca 執行中檢查相容性,請改用:npm run dry-run');
130
+ console.error(' 確定要強制繼續(不建議):加上 --force');
131
+ process.exitCode = 1;
132
+ return;
133
+ }
134
+ }
135
+
87
136
  console.log('📦 1/6 正在解包 app.asar (這可能需要數十秒)...');
88
137
  if (fs.existsSync(workDir)) fs.rmSync(workDir, { recursive: true, force: true });
89
138
  fs.mkdirSync(workDir, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orca-zh-tw-installer",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Orca AI IDE 台灣繁體中文 (zh-TW) 一鍵安裝包",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -40,6 +40,28 @@ fs.writeFileSync(CJS_OUT,
40
40
  `${named}${EOL}`,
41
41
  'utf8');
42
42
 
43
+ // 產生後立刻用真正的 JS parser 驗一次。字典是 lazy chunk,語法有問題時
44
+ // Orca 只會拋「Unexpected token」並讓面板的 error boundary 接住,很難追。
45
+ // 在這裡擋掉,比事後 verify 更早。
46
+ {
47
+ const { execFileSync } = require('child_process');
48
+ const os = require('os');
49
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'orca-zh-tw-build-'));
50
+ for (const [src, name] of [[OUT, 'dict.mjs'], [CJS_OUT, 'dict.cjs']]) {
51
+ const probe = path.join(tmp, name);
52
+ fs.copyFileSync(src, probe);
53
+ try {
54
+ execFileSync(process.execPath, ['--check', probe], { stdio: ['ignore', 'ignore', 'pipe'] });
55
+ } catch (e) {
56
+ fs.rmSync(tmp, { recursive: true, force: true });
57
+ console.error(`❌ ${path.basename(src)} 語法無效,已中止:`);
58
+ console.error(' ' + String(e.stderr || e.message).split('\n').slice(0, 3).join('\n '));
59
+ process.exit(1);
60
+ }
61
+ }
62
+ fs.rmSync(tmp, { recursive: true, force: true });
63
+ }
64
+
43
65
  const n = Object.keys(flat).length;
44
- console.log(`✅ ${path.basename(OUT)}(ESM/renderer)已產生(${n} 條字串)`);
66
+ console.log(`✅ ${path.basename(OUT)}(ESM/renderer)已產生(${n} 條字串,語法驗證通過)`);
45
67
  console.log(`✅ ${path.basename(CJS_OUT)}(CJS/main process)已產生`);
@@ -85,6 +85,46 @@ if (files.includes(cjsPath)) {
85
85
  check('CJS 有 "use strict"', cjs.trimStart().startsWith('"use strict"'));
86
86
  }
87
87
 
88
+ // ── 真正的 JS 語法驗證 ──
89
+ // 字典是 lazy chunk,由 __vitePreload / require 動態載入。若語法有問題,
90
+ // Orca 會拋「Unexpected token」並讓面板的 error boundary 接住。
91
+ // JSON.parse 過不代表 JS parser 過:JSON 容許 U+2028/U+2029 等字元,
92
+ // 舊版 JS 的字串字面值不容許。故用 node --check 實際跑一次 parser。
93
+ console.log('\n=== JS 語法驗證(node --check)===');
94
+ {
95
+ const { execFileSync } = require('child_process');
96
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'orca-zh-tw-syntax-'));
97
+ const cases = [
98
+ ['ESM(renderer)', esmPath, 'dict.mjs'],
99
+ ['CJS(main process)', cjsPath, 'dict.cjs'],
100
+ ];
101
+ for (const [label, inAsarPath, name] of cases) {
102
+ if (!files.includes(inAsarPath)) { check(`${label} 存在`, false); continue; }
103
+ const f = path.join(tmp, name);
104
+ fs.writeFileSync(f, get(inAsarPath), 'utf8');
105
+ let ok = true, err = '';
106
+ try {
107
+ execFileSync(process.execPath, ['--check', f], { stdio: ['ignore', 'ignore', 'pipe'] });
108
+ } catch (e) {
109
+ ok = false;
110
+ err = String(e.stderr || e.message).split('\n').find(l => /Error|Unexpected/.test(l)) || '';
111
+ }
112
+ check(`${label} 通過 node --check${ok ? '' : ' ← ' + err}`, ok);
113
+ }
114
+ // JSON 合法但 JS 可能出問題的字元
115
+ for (const [label, inAsarPath] of [['ESM', esmPath], ['CJS', cjsPath]]) {
116
+ if (!files.includes(inAsarPath)) continue;
117
+ const s = get(inAsarPath);
118
+ // U+2028/U+2029 必須用 \u 轉義。直接把字元寫進正則字面值,JS 會把它們當成
119
+ // 行終止符而切斷字面值——正是這裡要檢查的那類問題(本檔案踩過一次)。
120
+ const bad = (s.match(/[\u2028\u2029]/g) || []).length
121
+ + (s.match(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g) || []).length
122
+ + (s.match(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g) || []).length;
123
+ check(`${label} 無 U+2028/2029、孤立代理對、控制字元`, bad === 0);
124
+ }
125
+ fs.rmSync(tmp, { recursive: true, force: true });
126
+ }
127
+
88
128
  console.log(fail === 0
89
129
  ? '\n🎉 全部通過!完全關閉並重啟 Orca,到 Settings → Appearance → Language 選「中文(繁體)」。\n'
90
130
  : `\n❌ 有 ${fail} 項未通過。可用備份還原:\n Copy-Item "${orcaPath}.bak" "${orcaPath}" -Force\n`);