@bash0816/copilot-termux 1.0.68 → 1.0.69

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/bin/copilot CHANGED
@@ -40,6 +40,7 @@ fi
40
40
  _GLIBC_LD="${PREFIX}/glibc/lib/ld-linux-aarch64.so.1"
41
41
  _GLIBC_LIBS="${PREFIX}/glibc/lib"
42
42
  _GLIBC_RUNTIME="${CURRENT}/prebuilds/linux-arm64/runtime.node"
43
+ _GLIBC_CLI_NATIVE="${CURRENT}/prebuilds/linux-arm64/cli-native.node"
43
44
  _GLIBC_NODE_AUTO="${CACHE_DIR}/glibc-node/node"
44
45
 
45
46
  _USE_GLIBC_NODE=""
@@ -49,7 +50,19 @@ elif [ -x "${_GLIBC_NODE_AUTO}" ]; then
49
50
  _USE_GLIBC_NODE="${_GLIBC_NODE_AUTO}"
50
51
  fi
51
52
 
52
- if [ -n "${_USE_GLIBC_NODE}" ] && [ -f "${_GLIBC_RUNTIME}" ] && [ -f "${_GLIBC_LD}" ]; then
53
+ _GLIBC_READY=1
54
+ [ -n "${_USE_GLIBC_NODE}" ] || _GLIBC_READY=0
55
+ [ -f "${_GLIBC_RUNTIME}" ] || _GLIBC_READY=0
56
+ [ -f "${_GLIBC_CLI_NATIVE}" ] || _GLIBC_READY=0
57
+ [ -f "${_GLIBC_LD}" ] || _GLIBC_READY=0
58
+
59
+ _GLIBC_ANY=0
60
+ [ -n "${_USE_GLIBC_NODE}" ] && _GLIBC_ANY=1
61
+ [ -f "${_GLIBC_RUNTIME}" ] && _GLIBC_ANY=1
62
+ [ -f "${_GLIBC_CLI_NATIVE}" ] && _GLIBC_ANY=1
63
+ [ -f "${_GLIBC_LD}" ] && _GLIBC_ANY=1
64
+
65
+ if [ "${_GLIBC_READY}" = "1" ]; then
53
66
  _SSL_CERT="${PREFIX}/etc/tls/cert.pem"
54
67
  _SSL_CERT_DIR="${PREFIX}/etc/tls"
55
68
  if [ ! -r "${_SSL_CERT}" ]; then
@@ -65,9 +78,21 @@ if [ -n "${_USE_GLIBC_NODE}" ] && [ -f "${_GLIBC_RUNTIME}" ] && [ -f "${_GLIBC_L
65
78
  --require "${PACKAGE_DIR}/lib/platform-patch.js" \
66
79
  "${COPILOT_INDEX}" \
67
80
  "$@"
81
+ elif [ "${_GLIBC_ANY}" = "1" ]; then
82
+ # 注: システムの glibc-repo パッケージ(_GLIBC_LD)を別目的で入れているだけで
83
+ # copilot-termux 自身の glibc mode セットアップ(setup.js の setupGlibcRuntime/setupGlibcNode)が
84
+ # 未完の場合も、ここでは「glibc mode 設定が部分的」としてエラー扱いにする。
85
+ # これは意図的な判断(Free auto mode の既知の壊れた bionic 経路への黙示的フォールバックを防ぐため)。
86
+ echo "[copilot-termux] ERROR: glibc mode partially configured. Missing:" >&2
87
+ [ -n "${_USE_GLIBC_NODE}" ] || echo " - glibc node (${_GLIBC_NODE_AUTO})" >&2
88
+ [ -f "${_GLIBC_RUNTIME}" ] || echo " - glibc runtime.node (${_GLIBC_RUNTIME})" >&2
89
+ [ -f "${_GLIBC_CLI_NATIVE}" ] || echo " - glibc cli-native.node (${_GLIBC_CLI_NATIVE})" >&2
90
+ [ -f "${_GLIBC_LD}" ] || echo " - glibc loader (${_GLIBC_LD})" >&2
91
+ echo "[copilot-termux] Run: copilot-termux setup" >&2
92
+ exit 1
68
93
  fi
69
94
 
70
- # bionic fallback
95
+ # bionic fallback(glibc関連ファイルが一つも無い場合のみ。意図的なbionic-onlyの正当な状態)
71
96
  LD_PRELOAD="${PRELOAD}" exec node \
72
97
  --require "${PACKAGE_DIR}/lib/platform-patch.js" \
73
98
  "${COPILOT_INDEX}" \
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "manifest_version": 1,
3
3
  "package_name": "@bash0816/copilot-termux",
4
- "copilot_version": "1.0.68",
5
- "latest_audited_version": "1.0.65-1",
6
- "latest_candidate_version": "1.0.68",
7
- "previous_stable_version": "1.0.63",
4
+ "copilot_version": "1.0.69",
5
+ "latest_audited_version": "1.0.68-1",
6
+ "latest_candidate_version": null,
7
+ "previous_stable_version": "1.0.68",
8
8
  "candidate_state": "none",
9
9
  "canonical_package_status": "not_published",
10
10
  "public_distribution_status": "staged",
11
11
  "nodejs_glibc_version": "node-glibc-v24.15.0",
12
12
  "build_run_id": null,
13
- "last_updated": "2026-07-02",
13
+ "last_updated": "2026-07-07",
14
14
  "manifest_url": "https://raw.githubusercontent.com/bash0816/Github-Copilot-Termux/main/packages/copilot-termux/config/copilot-termux-release-manifest.json"
15
15
  }
@@ -2,8 +2,8 @@
2
2
  "wrapperVersion": "2.0.0",
3
3
  "copilot": {
4
4
  "package": "@github/copilot-linuxmusl-arm64",
5
- "version": "1.0.68",
6
- "integrity": "sha512-LTYZFOHpeLg4rCtsq3A/LMZxxRKFcCLmhnt8F7ovNYLNDJMkh3xdYanoXP0C0PO3uyUMiNJ+p5YXhZiBuo2yfw=="
5
+ "version": "1.0.69",
6
+ "integrity": "sha512-Y6DY9RkyWV6l+S0cNz4d+AkUa38goNV5StQH3j8OoVrndK3+RgQeUjG1XpJ//PzXEXi4vQ0cZuc9XzFj/TY7tw=="
7
7
  },
8
8
  "glibcNode": {
9
9
  "version": "26.2.0",
@@ -31,8 +31,10 @@ function parseVer(v) {
31
31
  return { nums, pre };
32
32
  }
33
33
 
34
- // このプロジェクトの prerelease 表記は x.y.z-N(N は数字のみ)を前提としており、
35
- // 汎用 semver のような英数混在 prerelease 識別子の比較は想定していません。
34
+ // このプロジェクトの `-N` サフィックスは、汎用 semver のプレリリース識別子ではなく、
35
+ // 「正式版リリース後に追加したfork独自パッチのリビジョン番号」を意味する。
36
+ // suffixなし = revision 0、`-N` = revision N として扱い、Nが大きいほど新しい。
37
+ // (例: 1.0.68 < 1.0.68-1 < 1.0.68-2)
36
38
  // returns negative if a < b, 0 if equal, positive if a > b
37
39
  function compareVersions(a, b) {
38
40
  const pa = parseVer(a);
@@ -42,28 +44,14 @@ function compareVersions(a, b) {
42
44
  const nb = pb.nums[i] || 0;
43
45
  if (na !== nb) return na - nb;
44
46
  }
45
- // prerelease: no pre = stable > has pre (semver spec)
46
- if (pa.pre === null && pb.pre !== null) return 1;
47
- if (pa.pre !== null && pb.pre === null) return -1;
48
- if (pa.pre !== null && pb.pre !== null) {
49
- const aParts = pa.pre.split('.');
50
- const bParts = pb.pre.split('.');
51
- const len = Math.max(aParts.length, bParts.length);
52
- for (let j = 0; j < len; j++) {
53
- const ap = aParts[j], bp = bParts[j];
54
- if (ap === undefined) return -1;
55
- if (bp === undefined) return 1;
56
- const an = Number(ap), bn = Number(bp);
57
- if (!isNaN(an) && !isNaN(bn)) {
58
- if (an !== bn) return an - bn;
59
- } else {
60
- if (ap < bp) return -1;
61
- if (ap > bp) return 1;
62
- }
63
- }
64
- return 0;
65
- }
66
- return 0;
47
+ // このフォークの `-N` サフィックスは一般semverの「正式版より前のプレリリース」ではなく、
48
+ // 「正式版リリース後に追加したfork独自パッチのリビジョン番号」を意味する。
49
+ // suffixなし = revision 0、`-N` = revision N として扱い、Nが大きいほど新しいとみなす。
50
+ // (例: 1.0.68 < 1.0.68-1 < 1.0.68-2)
51
+ // parseVer前提: N は数字のみ(本ファイル冒頭コメントの前提を踏襲)。
52
+ const ra = pa.pre === null ? 0 : Number(pa.pre);
53
+ const rb = pb.pre === null ? 0 : Number(pb.pre);
54
+ return ra - rb;
67
55
  }
68
56
 
69
57
  function isPrerelease(v) {
@@ -172,6 +160,9 @@ async function runNotify() {
172
160
 
173
161
  module.exports = { runUpdate, runNotify };
174
162
 
163
+ // テスト専用export・CLI実行経路には影響しない(node:test用)
164
+ module.exports.__test = { compareVersions };
165
+
175
166
  // CLI entry
176
167
  if (require.main === module) {
177
168
  const mode = process.argv[2] || 'notify';
@@ -56,6 +56,16 @@ Module._resolveFilename = function (request, parent, isMain, options) {
56
56
  return origResolve(request, parent, isMain, options);
57
57
  };
58
58
 
59
+ // [glibc mode の設計方針]
60
+ // glibc mode(COPILOT_TERMUX_GLIBC_MODE=1)は「native実装に全面的に任せるモード」ではない。
61
+ // 正しくは「glibc addon のロードを可能にしつつ、Copilot-Termux が検証済みの
62
+ // JS通信/認証/モデル解決の経路は両モード共通で維持するモード」である。
63
+ // 常時適用されるJSスタブ(isGlibcMode分岐がない箇所)は、bionic回避だけでなく
64
+ // Copilot token交換・Free/Enterprise endpoint補正・モデルstale対策・
65
+ // tools payload補正・stream互換維持を兼ねているため、glibc modeでも意図的に残す。
66
+ // native実装に戻す変更は、スタブ単位ではなくauth/network/model/streamの連鎖単位で
67
+ // 実機検証してから行う(2026-07-06 GPT-5.5レビュー結論)。
68
+
59
69
  // [Android bionic 対応] linuxmusl-arm64/runtime.node は Rust tokio を使う。
60
70
  // bionic 上で musl pthread ABI でスレッドを生成すると TUI 起動時に SIGSEGV。
61
71
  // runtime.node ロード後に Rust async 初期化関数を no-op に差し替えて阻止する。
@@ -74,7 +84,7 @@ Module._load = function (request, parent, isMain) {
74
84
  // jsonrpcServer* は拡張 JSON-RPC サーバー (ThreadsafeFunction)、
75
85
  // lspClient* は LSP クライアント (ThreadsafeFunction)。
76
86
  // featureFlagService* は同期 Rust のため除外(no-op にすると .handle クラッシュ)。
77
- const TOKIO_PATTERN = /^(ahpRelay|ahpRelayAuthenticate|ahpRelayCancelTurn|ahpRelayCompleteInput|ahpRelayCompletions|ahpRelayConfirmToolCall|ahpRelayConnect|ahpRelayCreate|ahpRelayCreateSession|ahpRelayDispose|ahpRelayGetPlan|ahpRelayListCheckpoints|ahpRelayListWorkspaceFiles|ahpRelayPendingRequiredResourcesJson|ahpRelayReadCheckpoint|ahpRelayRefreshSessions|ahpRelayReleaseSession|ahpRelayRemovePendingMessage|ahpRelaySelectAgent|ahpRelaySessionDiff|ahpRelaySetMode|ahpRelaySetModel|ahpRelaySetPendingMessage|ahpRelaySetSessionApproveAll|ahpRelaySetTitle|ahpRelayStartTurn|ahpRelayStateJson|ahpRelaySubscribeSession|ahpRelayTerminalDispose|ahpRelayTerminalEnsure|ahpRelayTerminalWrite|jsonrpcServer|jsonrpcServerAddConnection|jsonrpcServerBeginShutdown|jsonrpcServerConnectionClose|jsonrpcServerConnectionNotify|jsonrpcServerConnectionNotifyAfterResponse|jsonrpcServerConnectionRequest|jsonrpcServerConnectionWrite|jsonrpcServerCreate|jsonrpcServerDispatchComplete|jsonrpcServerRegisterSession|jsonrpcServerRemove|jsonrpcServerRemoveSession|jsonrpcServerStartTcpListener|jsonrpcServerStopTcpListener|lspClient|lspClientCloseDocument|lspClientCreateOwned|lspClientCreateOwnedSandboxed|lspClientDispose|lspClientEnhanceStartupErrorMessage|lspClientFindSourceFile|lspClientInitialize|lspClientInitialized|lspClientOpenDocument|lspClientOwnedShutdown|lspClientRequest|lspClientTakeExitInfo|lspClientWaitForDiagnostics|lspClientWaitForProjectLoad|modelHttp|modelHttpCancelRequest|modelHttpRegisterCancellation|modelHttpResetNetworking|networkFetch|networkFetchResetClients|sessionSqlite|sessionSqliteClose|sessionSqliteExec|sessionStore|sessionStoreBeginForgeSkillProposalGeneration|sessionStoreClose|sessionStoreCompleteForgeSkillProposalGeneration|sessionStoreDefaultPath|sessionStoreDeleteDynamicContextItem|sessionStoreEnsureSession|sessionStoreExec|sessionStoreExecuteReadOnly|sessionStoreFailStaleGeneratingForgeSkillProposals|sessionStoreGetCheckpoints|sessionStoreGetDynamicContextBoard|sessionStoreGetDynamicContextItem|sessionStoreGetFiles|sessionStoreGetForgeSkillProposalByFingerprint|sessionStoreGetForgeSkillProposalById|sessionStoreGetForgeSkillProposalWorkspaceBefore|sessionStoreGetForgeTrajectoryEvents|sessionStoreGetForgeTrajectoryEventsForScope|sessionStoreGetMaxTurnIndex|sessionStoreGetRefs|sessionStoreGetSession|sessionStoreGetStats|sessionStoreGetTurns|sessionStoreIncrementDynamicContextCount|sessionStoreIncrementDynamicContextReadCount|sessionStoreIndexWorkspaceArtifact|sessionStoreInsertCheckpointWithRuntimeDefaults|sessionStoreInsertDynamicContextItem|sessionStoreInsertFileWithRuntimeDefaults|sessionStoreInsertForgeTrajectoryEventWithRuntimeDefaults|sessionStoreInsertRefWithRuntimeDefaults|sessionStoreInsertTurnWithRuntimeDefaults|sessionStoreListForgeSkillProposals|sessionStoreOpen|sessionStoreSearch|sessionStoreTrackingEventOperations|sessionStoreTrackingExtractFilePath|sessionStoreTrackingExtractForgeTrajectoryEvents|sessionStoreTrackingExtractRefsFromBash|sessionStoreTrackingExtractRefsFromMcpTool|sessionStoreTrackingExtractRepoFromMcpTool|sessionStoreTrackingFlushOperations|sessionStoreTrackingInitialState|sessionStoreTransitionForgeSkillProposalStatus|sessionStoreUpsertDynamicContextItem|sessionStoreUpsertSessionWithRuntimeDefaults|websocketResponses)/;
87
+ const TOKIO_PATTERN = /^(ahpRelay|ahpRelayAuthenticate|ahpRelayCancelTurn|ahpRelayCompleteInput|ahpRelayCompletions|ahpRelayConfirmToolCall|ahpRelayConnect|ahpRelayCreate|ahpRelayCreateSession|ahpRelayDispose|ahpRelayGetPlan|ahpRelayListCheckpoints|ahpRelayListWorkspaceFiles|ahpRelayPendingRequiredResourcesJson|ahpRelayReadCheckpoint|ahpRelayRefreshSessions|ahpRelayReleaseSession|ahpRelayRemovePendingMessage|ahpRelaySelectAgent|ahpRelaySessionDiff|ahpRelaySetMode|ahpRelaySetModel|ahpRelaySetPendingMessage|ahpRelaySetSessionApproveAll|ahpRelaySetTitle|ahpRelayStartTurn|ahpRelayStateJson|ahpRelaySubscribeSession|ahpRelayTerminalDispose|ahpRelayTerminalEnsure|ahpRelayTerminalWrite|jsonrpcServer|jsonrpcServerAddConnection|jsonrpcServerBeginShutdown|jsonrpcServerConnectionClose|jsonrpcServerConnectionNotify|jsonrpcServerConnectionNotifyAfterResponse|jsonrpcServerConnectionRequest|jsonrpcServerConnectionWrite|jsonrpcServerCreate|jsonrpcServerDispatchComplete|jsonrpcServerRegisterSession|jsonrpcServerRemove|jsonrpcServerRemoveSession|jsonrpcServerStartTcpListener|jsonrpcServerStopTcpListener|lspClient|lspClientCloseDocument|lspClientCreateOwned|lspClientCreateOwnedSandboxed|lspClientDispose|lspClientEnhanceStartupErrorMessage|lspClientFindSourceFile|lspClientInitialize|lspClientInitialized|lspClientOpenDocument|lspClientOwnedShutdown|lspClientRequest|lspClientTakeExitInfo|lspClientWaitForDiagnostics|lspClientWaitForProjectLoad|modelHttp|modelHttpCancelRequest|modelHttpRegisterCancellation|modelHttpResetNetworking|networkFetch|networkFetchNextRequestId|networkFetchResetClients|sessionSqlite|sessionSqliteClose|sessionSqliteExec|sessionStore|sessionStoreBeginForgeSkillProposalGeneration|sessionStoreClose|sessionStoreCompleteForgeSkillProposalGeneration|sessionStoreDefaultPath|sessionStoreDeleteDynamicContextItem|sessionStoreEnsureSession|sessionStoreExec|sessionStoreExecuteReadOnly|sessionStoreExecuteReadOnlyAsync|sessionStoreFailStaleGeneratingForgeSkillProposals|sessionStoreGetCheckpoints|sessionStoreGetDynamicContextBoard|sessionStoreGetDynamicContextItem|sessionStoreGetFiles|sessionStoreGetForgeSkillProposalByFingerprint|sessionStoreGetForgeSkillProposalById|sessionStoreGetForgeSkillProposalWorkspaceBefore|sessionStoreGetForgeTrajectoryEvents|sessionStoreGetForgeTrajectoryEventsForScope|sessionStoreGetMaxTurnIndex|sessionStoreGetRefs|sessionStoreGetSession|sessionStoreGetStats|sessionStoreGetTurns|sessionStoreIncrementDynamicContextCount|sessionStoreIncrementDynamicContextReadCount|sessionStoreIndexWorkspaceArtifact|sessionStoreInsertAssistantUsageEventWithRuntimeDefaults|sessionStoreInsertCheckpointWithRuntimeDefaults|sessionStoreInsertDynamicContextItem|sessionStoreInsertFileWithRuntimeDefaults|sessionStoreInsertForgeTrajectoryEventWithRuntimeDefaults|sessionStoreInsertRefWithRuntimeDefaults|sessionStoreInsertTurnWithRuntimeDefaults|sessionStoreListForgeSkillProposals|sessionStoreOpen|sessionStoreSearch|sessionStoreTrackingEventOperations|sessionStoreTrackingExtractFilePath|sessionStoreTrackingExtractForgeTrajectoryEvents|sessionStoreTrackingExtractRefsFromBash|sessionStoreTrackingExtractRefsFromMcpTool|sessionStoreTrackingExtractRepoFromMcpTool|sessionStoreTrackingFlushOperations|sessionStoreTrackingInitialState|sessionStoreTransitionForgeSkillProposalStatus|sessionStoreUpsertDynamicContextItem|sessionStoreUpsertSessionWithRuntimeDefaults|websocketResponses)/;
78
88
  for (const key of Object.keys(result)) {
79
89
  if (TOKIO_PATTERN.test(key) && typeof result[key] === 'function') {
80
90
  result[key] = () => undefined;
@@ -827,6 +837,7 @@ Module._load = function (request, parent, isMain) {
827
837
  };
828
838
  return { json: JSON.stringify({ kind: 'ok', completion, copilotUsage: null, ttftMs: null, interTokenLatencyMs: null }) };
829
839
  };
840
+ // glibc modeでも維持(isGlibcMode分岐なし): bionic回避に加えCopilot token交換・アカウント切替キャッシュ管理を兼ねるため
830
841
  // === authManager* JS stubs (1.0.64: tokio thread spawn → SIGSEGV on bionic) ===
831
842
  const _authMgr = new Map(); // uuid → { cachedInfo, pendingInfo, cachedToken, cachedHost, gen }
832
843
  let _modelListCache = null; // capiClientListModels が取得したモデルリストキャッシュ(modelResolver fallback 用)
@@ -1271,6 +1282,10 @@ Module._load = function (request, parent, isMain) {
1271
1282
  // bionic 上の実 I/O で動作しないため、Node.js ビルトイン fetch(動作確認済み)に固定する。
1272
1283
  // GitHub OAuth token via env var or gh CLI (keychain unavailable on bionic)
1273
1284
 
1285
+ // glibc modeでも維持(isGlibcMode分岐なし、かつファイルスコープのためそもそも分岐不可):
1286
+ // JS実装のnetworkFetch*/modelHttp*/capiClientListModelsがglobalThis.fetchに依存しているため、
1287
+ // ここだけglibc modeでnative fetchに戻すと、JSスタブがRust fetch経由になり前提が崩れる。
1288
+
1274
1289
  const _COPILOT_TOKEN_DEFAULT_TTL_MS = 28 * 60 * 1000;
1275
1290
  function _normalizeCopilotTokenExpiry(expiresAt, now = Date.now()) {
1276
1291
  const parsed = expiresAt ? Date.parse(expiresAt) : NaN;
package/lib/setup.js CHANGED
@@ -91,15 +91,16 @@ async function setup() {
91
91
  }
92
92
  }
93
93
 
94
+ await setupGlibcWrap();
95
+ await setupGlibcRuntime(versionDir, version);
96
+ await setupGlibcNode();
97
+
94
98
  const currentLink = path.join(CACHE_DIR, 'current');
95
99
  const tmp = path.join(CACHE_DIR, `current.tmp.${process.pid}`);
96
100
  try { fs.unlinkSync(tmp); } catch (_) {}
97
101
  fs.symlinkSync(versionDir, tmp);
98
102
  fs.renameSync(tmp, currentLink);
99
103
 
100
- await setupGlibcWrap();
101
- await setupGlibcRuntime(versionDir, version);
102
- await setupGlibcNode();
103
104
  console.log(`✓ copilot ${version} ready`);
104
105
  }
105
106
 
@@ -144,7 +145,9 @@ async function setupGlibcWrap() {
144
145
 
145
146
  async function setupGlibcRuntime(versionDir, version) {
146
147
  const linux64Dir = path.join(versionDir, 'prebuilds', 'linux-arm64');
147
- if (fs.existsSync(path.join(linux64Dir, 'runtime.node'))) {
148
+ const runtimeNode = path.join(linux64Dir, 'runtime.node');
149
+ const cliNativeNode = path.join(linux64Dir, 'cli-native.node');
150
+ if (fs.existsSync(runtimeNode) && fs.existsSync(cliNativeNode)) {
148
151
  return;
149
152
  }
150
153
 
@@ -178,39 +181,102 @@ async function setupGlibcRuntime(versionDir, version) {
178
181
  }
179
182
 
180
183
  async function setupGlibcNode() {
181
- const nodeDir = path.join(CACHE_DIR, 'glibc-node');
182
- const nodeBin = path.join(nodeDir, 'node');
184
+ const { version, sha256: expectedSha256 } = manifest.glibcNode;
185
+ const nodeVersionDir = path.join(CACHE_DIR, `glibc-node-${version}`);
186
+ const nodeBin = path.join(nodeVersionDir, 'node');
187
+ const currentLink = path.join(CACHE_DIR, 'glibc-node');
188
+
189
+ const PREFIX = process.env.PREFIX || '/data/data/com.termux/files/usr';
190
+ const glibcLd = path.join(PREFIX, 'glibc', 'lib', 'ld-linux-aarch64.so.1');
191
+ const glibcLibs = path.join(PREFIX, 'glibc', 'lib');
192
+
193
+ // glibc動的リンカ経由でないとTermux(bionic)上ではglibc Nodeバイナリを実行できないため、
194
+ // ld-linux 経由で `node -v` を実行しバージョン文字列を確認する。
195
+ function verifyNodeBinary(binPath) {
196
+ // bin/copilot と同様、Termux由来の LD_PRELOAD (bionic向けexecフック) を明示的にクリアしないと
197
+ // glibc loaderがロードするlibc.soがinvalid ELF headerエラーになる(2026-07-06実機確認)。
198
+ const out = execFileSync(glibcLd, ['--library-path', glibcLibs, binPath, '-v'], {
199
+ encoding: 'utf8',
200
+ env: { ...process.env, LD_PRELOAD: '' },
201
+ }).trim();
202
+ return out === `v${version}`;
203
+ }
204
+
205
+ let needsDownload = true;
183
206
  if (fs.existsSync(nodeBin)) {
184
- return;
207
+ try {
208
+ if (verifyNodeBinary(nodeBin)) {
209
+ needsDownload = false;
210
+ } else {
211
+ console.log(`glibc node at ${nodeVersionDir} failed version check, re-downloading...`);
212
+ fs.rmSync(nodeVersionDir, { recursive: true, force: true });
213
+ }
214
+ } catch (e) {
215
+ // 破損バイナリ・実行不可などpartial stateからの自己修復
216
+ console.log(`glibc node at ${nodeVersionDir} failed to execute (${e.message}), re-downloading...`);
217
+ fs.rmSync(nodeVersionDir, { recursive: true, force: true });
218
+ }
185
219
  }
186
220
 
187
- const { version, sha256: expectedSha256 } = manifest.glibcNode;
188
- const tarballUrl = `https://nodejs.org/dist/v${version}/node-v${version}-linux-arm64.tar.gz`;
221
+ if (needsDownload) {
222
+ const tarballUrl = `https://nodejs.org/dist/v${version}/node-v${version}-linux-arm64.tar.gz`;
223
+ console.log(`Downloading Node.js v${version} (glibc)...`);
224
+ const tarball = await httpsGet(tarballUrl);
189
225
 
190
- console.log(`Downloading Node.js v${version} (glibc)...`);
191
- const tarball = await httpsGet(tarballUrl);
226
+ const actualSha256 = crypto.createHash('sha256').update(tarball).digest('hex');
227
+ if (actualSha256 !== expectedSha256) {
228
+ throw new Error(`Node.js integrity check failed: expected ${expectedSha256}, got ${actualSha256}`);
229
+ }
192
230
 
193
- const actualSha256 = crypto.createHash('sha256').update(tarball).digest('hex');
194
- if (actualSha256 !== expectedSha256) {
195
- throw new Error(`Node.js integrity check failed: expected ${expectedSha256}, got ${actualSha256}`);
196
- }
231
+ const stagingDir = `${nodeVersionDir}.staging.${process.pid}`;
232
+ const tarballPath = path.join(CACHE_DIR, `node-glibc-${version}.tgz`);
233
+ try {
234
+ fs.rmSync(stagingDir, { recursive: true, force: true });
235
+ fs.writeFileSync(tarballPath, tarball);
236
+ fs.mkdirSync(stagingDir, { recursive: true });
197
237
 
198
- const tarballPath = path.join(CACHE_DIR, `node-glibc-${version}.tgz`);
199
- try {
200
- fs.writeFileSync(tarballPath, tarball);
201
- fs.mkdirSync(nodeDir, { recursive: true });
238
+ console.log('Extracting node binary...');
239
+ execFileSync('tar', [
240
+ '-xzf', tarballPath,
241
+ '-C', stagingDir,
242
+ '--strip-components=2',
243
+ `node-v${version}-linux-arm64/bin/node`,
244
+ ]);
245
+ const stagingBin = path.join(stagingDir, 'node');
246
+ fs.chmodSync(stagingBin, 0o755);
247
+
248
+ // staging側で実行検証(glibcローダー経由)。失敗すれば例外がそのまま
249
+ // 呼び出し元に伝播し、stagingはcatchで削除、nodeVersionDirには一切触れない。
250
+ if (!verifyNodeBinary(stagingBin)) {
251
+ throw new Error(`Downloaded glibc node binary failed version check (expected v${version})`);
252
+ }
202
253
 
203
- console.log('Extracting node binary...');
204
- execFileSync('tar', [
205
- '-xzf', tarballPath,
206
- '-C', nodeDir,
207
- '--strip-components=2',
208
- `node-v${version}-linux-arm64/bin/node`,
209
- ]);
210
- fs.chmodSync(nodeBin, 0o755);
211
- } finally {
212
- fs.rmSync(tarballPath, { force: true });
254
+ // 既存の nodeVersionDir が壊れた状態で残っている場合に備え、rename前にクリア
255
+ // (ここまで到達した時点で staging 側の検証は完了済みなので安全)
256
+ fs.rmSync(nodeVersionDir, { recursive: true, force: true });
257
+ fs.renameSync(stagingDir, nodeVersionDir);
258
+ } catch (err) {
259
+ fs.rmSync(stagingDir, { recursive: true, force: true });
260
+ throw err;
261
+ } finally {
262
+ fs.rmSync(tarballPath, { force: true });
263
+ }
264
+ }
265
+
266
+ // Migration: 1.0.68以前の glibc-node は実ディレクトリだった。
267
+ // 新バージョンの用意(既存流用 or 新規ダウンロード、どちらもverifyNodeBinary通過済み)が
268
+ // 完全に成功した後(ここまで到達した時点)でのみ、旧実ディレクトリを削除する。
269
+ let currentLinkStat = null;
270
+ try { currentLinkStat = fs.lstatSync(currentLink); } catch (_) {}
271
+ if (currentLinkStat && currentLinkStat.isDirectory() && !currentLinkStat.isSymbolicLink()) {
272
+ fs.rmSync(currentLink, { recursive: true, force: true });
213
273
  }
274
+
275
+ const tmpLink = path.join(CACHE_DIR, `glibc-node.tmp.${process.pid}`);
276
+ try { fs.unlinkSync(tmpLink); } catch (_) {}
277
+ fs.symlinkSync(nodeVersionDir, tmpLink);
278
+ fs.renameSync(tmpLink, currentLink);
279
+
214
280
  console.log('✓ glibc node ready');
215
281
  }
216
282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash0816/copilot-termux",
3
- "version": "1.0.68",
3
+ "version": "1.0.69",
4
4
  "description": "GitHub Copilot CLI for Termux (Android aarch64)",
5
5
  "license": "GPL-3.0-only",
6
6
  "readme": "README.md",