@nightowne/tas-cli 3.0.0 → 3.0.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/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to TAS (Telegram as Storage) will be documented in this file.
4
4
 
5
+ ## [3.0.2] - 2026-09-25
6
+
7
+ ### Fixed — Apple Silicon install recovery
8
+ - **Optional native source recovery** — if the legacy optional addon fails before TAS can patch it on arm64, postinstall restores only its source without lifecycle scripts, then rebuilds it against the user's installed macFUSE library. Failed rebuilds still leave every non-mount TAS command usable.
9
+
10
+ ## [3.0.1] - 2026-09-25
11
+
12
+ ### Fixed — current macFUSE mount support (#4)
13
+ - **System macFUSE build** — TAS no longer accepts the bundled OSXFUSE 3 library. On macOS it detects `/Library/Filesystems/macfuse.fs`, selects the installed `libfuse` headers and library, and rebuilds the optional `fuse-native` addon for the active CPU.
14
+ - **Apple Silicon support path** — the rebuilt addon uses macFUSE's supported v2 API surface rather than the legacy binary with no arm64 slice. TAS never installs, replaces, or removes a FUSE kernel extension.
15
+ - **Strict runtime guard** — TAS inspects the loaded native addon and refuses mount if it still links to `libosxfuse` or lacks a system-macFUSE build. `tas doctor` then performs the real mount → readdir → unmount smoke test.
16
+ - **Install guidance** — macOS requires current macFUSE and Xcode Command Line Tools before TAS is installed. A failed native build leaves push, pull, sync, and share available while mount stays disabled.
17
+
5
18
  ## [3.0.0] - 2026-09-24
6
19
 
7
20
  ### Fixed — FUSE and path correctness
package/FAQ.md CHANGED
@@ -30,4 +30,4 @@ No. Each chunk has one owning bot. TAS records that bot so reads and deletes rou
30
30
 
31
31
  ## Does mount work on macOS?
32
32
 
33
- Not in this release. The current `fuse-native@2.x` dependency targets obsolete OSXFUSE APIs and is not validated against current macFUSE or Apple Silicon. Linux users should run `tas doctor` for a real native FUSE smoke test.
33
+ Yes with current macFUSE, Xcode Command Line Tools, and a locally rebuilt native addon. TAS never installs or replaces macFUSE; its install hook selects the system `libfuse` and compiles the optional addon for the active CPU. Run `tas doctor` before use. If the real mount/readdir/unmount smoke test does not pass, do not mount data.
package/QUICKSTART.md CHANGED
@@ -44,4 +44,4 @@ Multi-bot mode distributes chunks. It is not redundancy, quota assurance, ban pr
44
44
 
45
45
  ## Mount support
46
46
 
47
- `tas mount` is supported only on validated Linux/libfuse hosts in this release. Run `tas doctor` first; it performs a real mount/readdir/unmount smoke test. macOS mount is disabled until TAS has a maintained current-macFUSE backend and macOS CI.
47
+ `tas mount` needs a native FUSE runtime. On Linux install `fuse` and `libfuse-dev`. On macOS install current macFUSE and Xcode Command Line Tools before installing TAS; TAS rebuilds its optional native addon against the installed macFUSE library. Run `tas doctor` first and mount only when its real mount/readdir/unmount smoke test passes.
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
  <a href="https://github.com/ixchio/tas/network/members"><img src="https://img.shields.io/github/forks/ixchio/tas?style=social" alt="GitHub Forks"></a>
19
19
  <img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen?logo=node.js" alt="Node.js >= 18">
20
20
  <img src="https://img.shields.io/badge/encryption-AES--256--GCM-blueviolet?logo=shield" alt="AES-256-GCM">
21
- <img src="https://img.shields.io/badge/tests-97%20passing-success" alt="97 Tests Passing">
21
+ <img src="https://img.shields.io/badge/tests-101%20passing-success" alt="101 Tests Passing">
22
22
  </p>
23
23
 
24
24
  <p align="center">
@@ -113,7 +113,7 @@ cp report.pdf ~/cloud/ # Drop files in — auto-encrypted and uploaded
113
113
  tas unmount ~/cloud # Clean unmount when done
114
114
  ```
115
115
 
116
- > **Linux only for this release:** install `fuse`/`libfuse-dev`, then run `tas doctor` for a real mount → readdir → unmount smoke test. macOS mount is explicitly unsupported because `fuse-native@2.x` targets obsolete OSXFUSE APIs and is not validated with current macFUSE or Apple Silicon. Push, pull, sync, and share still work on macOS.
116
+ > **Native FUSE is verified, not assumed:** Linux needs `fuse`/`libfuse-dev`. macOS uses current macFUSE and rebuilds the optional native addon against the installed system library on first install. Install Xcode Command Line Tools before TAS, then run `tas doctor`; mount is available only when its real mount → readdir → unmount smoke test passes.
117
117
 
118
118
  ---
119
119
 
@@ -193,7 +193,7 @@ tas doctor
193
193
  # ✓ Database: 42 files, 1.3 GB total across 28 chunks
194
194
  # ✓ Disk space: 50 GB free (32% used)
195
195
  # ✓ Encryption: AES-256-GCM · PBKDF2-SHA512 · 600,000 iterations (OWASP 2025)
196
- # ✓ FUSE runtime: mount → readdir → unmount passed (Linux)
196
+ # ✓ FUSE runtime: mount → readdir → unmount passed
197
197
  # ✓ Telegram connectivity: 2/2 bots OK
198
198
  # ✨ All systems go!
199
199
  ```
@@ -319,7 +319,7 @@ Reliability mechanisms implemented by TAS (not an SLA):
319
319
  | **Resume Uploads** | Network-stage chunks are staged on disk and persisted in `pending_uploads`; `tas resume` continues them |
320
320
  | **Index Recovery** | Authenticated encrypted remote manifest; `tas index rebuild` restores file/chunk ownership |
321
321
  | **Graceful Shutdown** | SIGINT/SIGTERM handled; staged chunks and SQLite WAL reduce partial-state risk |
322
- | **Self-Diagnostics** | Checks config/database/chunk limits, all bots, and a real Linux FUSE smoke mount |
322
+ | **Self-Diagnostics** | Checks config/database/chunk limits, all bots, and a real native FUSE smoke mount |
323
323
 
324
324
  ---
325
325
 
@@ -351,7 +351,7 @@ tas bot add|list|enable|disable|remove # 🤖 Manage experimental bot pool
351
351
  <summary><strong>Mount & Sync</strong></summary>
352
352
 
353
353
  ```bash
354
- # FUSE Mount (Linux only in this release)
354
+ # FUSE Mount (Linux/libfuse or macOS/current macFUSE)
355
355
  tas mount <path> # Mount Telegram storage as a local folder
356
356
  tas unmount <path> # Clean unmount
357
357
 
@@ -462,7 +462,7 @@ Use TAS only at your own risk, do not use multiple bots to evade limits, follow
462
462
  | 📌 **Not a replacement for backups** | Telegram can purge old messages. Use TAS alongside, not instead of, real backup solutions. |
463
463
  | 📌 **19 MiB payload chunks** | Hosted Bot API uploads permit more, but `getFile` documents only 20 MB downloads. TAS stays below the read limit. |
464
464
  | 📌 **Multi-bot is experimental** | It distributes chunks and preserves ownership mapping; it is not redundancy or ban protection. |
465
- | 📌 **FUSE = Linux only for now** | macOS mount is disabled until a maintained modern macFUSE backend and macOS CI exist. |
465
+ | 📌 **macOS needs a native build** | Install current macFUSE and Xcode Command Line Tools before installing TAS. `tas doctor` must pass its real FUSE smoke test before you mount data. |
466
466
  | 📌 **Recovery needs config** | `index.db` can be rebuilt from the encrypted manifest only if `config.json`, password, manifest message, and owning bot survive. |
467
467
  | 📌 **No versioning (yet)** | Overwriting a file replaces the previous version. |
468
468
  | 📌 **Internet required** | Telegram-backed — offline access requires files pulled locally first. |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nightowne/tas-cli",
3
- "version": "3.0.0",
4
- "description": "Experimental local-first encrypted file transport over Telegram bots with resumable chunks, index recovery, and Linux FUSE mount",
3
+ "version": "3.0.2",
4
+ "description": "Experimental local-first encrypted file transport over Telegram bots with resumable chunks, index recovery, and FUSE mount",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -11,7 +11,8 @@
11
11
  "start": "node src/cli.js",
12
12
  "init": "node src/cli.js init",
13
13
  "test": "node --test tests/*.test.js",
14
- "prepublishOnly": "npm test"
14
+ "prepublishOnly": "npm test",
15
+ "postinstall": "node src/fuse/macfuse.cjs"
15
16
  },
16
17
  "keywords": [
17
18
  "telegram",
package/src/cli.js CHANGED
@@ -814,7 +814,7 @@ program
814
814
  // ============== MOUNT COMMAND ==============
815
815
  program
816
816
  .command('mount <mountpoint>')
817
- .description('🔥 Mount Telegram storage as a local folder (Linux FUSE only)')
817
+ .description('🔥 Mount Telegram storage as a local folder (Linux FUSE or current macFUSE)')
818
818
  .option('-p, --password <password>', 'Encryption password (uses TAS_PASSWORD env var if not provided)')
819
819
  .action(async (mountpoint, options) => {
820
820
  console.log(chalk.cyan('\n🗂️ Mounting Telegram as filesystem...\n'));
@@ -872,7 +872,7 @@ program
872
872
  console.log(chalk.dim('\nNote: FUSE requires libfuse to be installed:'));
873
873
  console.log(chalk.dim(' Ubuntu/Debian: sudo apt install fuse libfuse-dev'));
874
874
  console.log(chalk.dim(' Fedora: sudo dnf install fuse fuse-devel'));
875
- console.log(chalk.dim(' macOS: TAS mount is currently unsupported (push/pull/sync still work)\n'));
875
+ console.log(chalk.dim(' macOS: install current macFUSE and Xcode Command Line Tools, then reinstall TAS\n'));
876
876
  process.exit(1);
877
877
  }
878
878
  });
@@ -890,16 +890,17 @@ program
890
890
  try {
891
891
  const { execFileSync } = await import('child_process');
892
892
 
893
- // Use fusermount on Linux, umount on macOS
893
+ // Match fuse-native's platform unmount mechanism without invoking a shell.
894
894
  const isMac = process.platform === 'darwin';
895
- const executable = isMac ? 'umount' : 'fusermount';
896
- const args = isMac ? [absMount] : ['-u', absMount];
895
+ const executable = isMac ? 'diskutil' : 'fusermount';
896
+ const args = isMac ? ['unmount', 'force', absMount] : ['-u', absMount];
897
897
  execFileSync(executable, args, { stdio: 'pipe' });
898
898
 
899
899
  spinner.succeed(`Unmounted ${chalk.green(absMount)}`);
900
900
  } catch (err) {
901
901
  spinner.fail(`Unmount failed: ${err.message}`);
902
- console.log(chalk.dim('\nTry: fusermount -u ' + absMount));
902
+ const retry = process.platform === 'darwin' ? `diskutil unmount force ${absMount}` : `fusermount -u ${absMount}`;
903
+ console.log(chalk.dim('\nTry: ' + retry));
903
904
  process.exit(1);
904
905
  }
905
906
  });
@@ -0,0 +1,265 @@
1
+ 'use strict'
2
+
3
+ // Modern macFUSE integration for fuse-native 2.x.
4
+ //
5
+ // fuse-native bundles an OSXFUSE 3 dylib on Darwin. That dylib has no arm64
6
+ // slice and its setup helper installs an obsolete kernel extension. We never
7
+ // install or load it. Instead, when macFUSE is already installed by the user,
8
+ // the TAS postinstall hook rebuilds fuse-native against that system library.
9
+
10
+ const childProcess = require('child_process')
11
+ const fs = require('fs')
12
+ const path = require('path')
13
+
14
+ const MACFUSE_BUNDLE = '/Library/Filesystems/macfuse.fs'
15
+
16
+ const LIBRARY_CANDIDATES = [
17
+ '/usr/local/lib/libfuse.dylib',
18
+ '/opt/homebrew/lib/libfuse.dylib',
19
+ '/usr/local/lib/libfuse.2.dylib',
20
+ '/opt/homebrew/lib/libfuse.2.dylib',
21
+ '/Library/Frameworks/macfUSE.framework/Versions/Current/lib/libfuse.dylib',
22
+ '/Library/Frameworks/macfUSE.framework/lib/libfuse.dylib'
23
+ ]
24
+
25
+ const INCLUDE_ROOTS = [
26
+ '/usr/local/include',
27
+ '/opt/homebrew/include',
28
+ '/Library/Frameworks/macfUSE.framework/Headers'
29
+ ]
30
+
31
+ function findMacFuseInstallation ({ exists = fs.existsSync } = {}) {
32
+ if (!exists(MACFUSE_BUNDLE)) {
33
+ return {
34
+ ready: false,
35
+ reason: 'current macFUSE is not installed at /Library/Filesystems/macfuse.fs'
36
+ }
37
+ }
38
+
39
+ const library = LIBRARY_CANDIDATES.find(exists)
40
+ if (!library) {
41
+ return {
42
+ ready: false,
43
+ reason: 'macFUSE is installed but its libfuse.dylib was not found'
44
+ }
45
+ }
46
+
47
+ const include = findIncludeDirectory(exists)
48
+ if (!include) {
49
+ return {
50
+ ready: false,
51
+ reason: 'macFUSE is installed but its FUSE development headers were not found'
52
+ }
53
+ }
54
+
55
+ return { ready: true, bundle: MACFUSE_BUNDLE, library, include }
56
+ }
57
+
58
+ function findIncludeDirectory (exists) {
59
+ for (const root of INCLUDE_ROOTS) {
60
+ for (const candidate of [
61
+ path.join(root, 'fuse.h'),
62
+ path.join(root, 'osxfuse', 'fuse.h'),
63
+ path.join(root, 'fuse', 'fuse.h')
64
+ ]) {
65
+ if (exists(candidate)) return path.dirname(candidate)
66
+ }
67
+ }
68
+ return null
69
+ }
70
+
71
+ function getFuseNativeDirectory (packageRoot) {
72
+ try {
73
+ return path.dirname(require.resolve('fuse-native/package.json', { paths: [packageRoot] }))
74
+ } catch {
75
+ return null
76
+ }
77
+ }
78
+
79
+ function getDarwinLibraryDirectory (packageRoot) {
80
+ try {
81
+ return path.dirname(require.resolve('fuse-shared-library-darwin/package.json', { paths: [packageRoot] }))
82
+ } catch {
83
+ return null
84
+ }
85
+ }
86
+
87
+ function installOptionalFuseNative (packageRoot) {
88
+ const npmCli = process.env.npm_execpath
89
+ if (!npmCli) return { ok: false, reason: 'npm did not provide its install executable' }
90
+
91
+ const install = childProcess.spawnSync(
92
+ process.execPath,
93
+ [npmCli, 'install', 'fuse-native@2.2.6', '--no-save', '--ignore-scripts', '--no-package-lock'],
94
+ {
95
+ cwd: packageRoot,
96
+ stdio: 'inherit',
97
+ env: { ...process.env, npm_config_ignore_scripts: 'true' }
98
+ }
99
+ )
100
+ if (install.status !== 0) return { ok: false, reason: 'could not restore optional fuse-native source' }
101
+ return { ok: true }
102
+ }
103
+
104
+ function validateMacFuseNativeBinding ({
105
+ packageRoot = path.resolve(__dirname, '..', '..'),
106
+ exists = fs.existsSync,
107
+ execFileSync = childProcess.execFileSync
108
+ } = {}) {
109
+ const installation = findMacFuseInstallation({ exists })
110
+ if (!installation.ready) return installation
111
+
112
+ const fuseNativeDir = getFuseNativeDirectory(packageRoot)
113
+ if (!fuseNativeDir) {
114
+ return { ready: false, reason: 'fuse-native is not installed' }
115
+ }
116
+
117
+ const addon = path.join(fuseNativeDir, 'build', 'Release', 'fuse.node')
118
+ if (!exists(addon)) {
119
+ return {
120
+ ready: false,
121
+ reason: 'the macFUSE native addon was not rebuilt from source; reinstall TAS after installing Xcode Command Line Tools'
122
+ }
123
+ }
124
+
125
+ let linkedLibraries
126
+ try {
127
+ linkedLibraries = execFileSync('/usr/bin/otool', ['-L', addon], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
128
+ } catch {
129
+ return { ready: false, reason: 'could not inspect the macFUSE native addon' }
130
+ }
131
+
132
+ if (linkedLibraries.includes('libosxfuse')) {
133
+ return { ready: false, reason: 'the legacy OSXFUSE binary is still loaded; reinstall TAS to rebuild against macFUSE' }
134
+ }
135
+ if (!linkedLibraries.includes('libfuse')) {
136
+ return { ready: false, reason: 'the native addon is not linked to macFUSE libfuse' }
137
+ }
138
+
139
+ return { ready: true, ...installation, addon }
140
+ }
141
+
142
+ function macFuseAdapterSource () {
143
+ return `'use strict'
144
+ const path = require('path')
145
+ const macfuse = require(path.join(__dirname, '..', '..', 'src', 'fuse', 'macfuse.cjs'))
146
+ const installation = macfuse.findMacFuseInstallation()
147
+
148
+ function unavailable () {
149
+ return new Error('Current macFUSE is required for TAS mount: ' + installation.reason)
150
+ }
151
+
152
+ module.exports = {
153
+ get lib () {
154
+ if (!installation.ready) throw unavailable()
155
+ return installation.library
156
+ },
157
+ get include () {
158
+ if (!installation.ready) throw unavailable()
159
+ return installation.include
160
+ },
161
+ beforeMount (cb) { process.nextTick(cb || (() => {})) },
162
+ beforeUnmount (cb) { process.nextTick(cb || (() => {})) },
163
+ configure (cb) { process.nextTick(() => (cb || (() => {}))(installation.ready ? null : unavailable())) },
164
+ unconfigure (cb) { process.nextTick(() => (cb || (() => {}))(new Error('TAS never installs or removes macFUSE'))) },
165
+ isConfigured (cb) { process.nextTick(() => cb(null, installation.ready)) }
166
+ }
167
+ `
168
+ }
169
+
170
+ function patchFuseSource (fuseNativeDir) {
171
+ const sourcePath = path.join(fuseNativeDir, 'fuse-native.c')
172
+ const source = fs.readFileSync(sourcePath, 'utf8')
173
+ const oldDefine = '#define FUSE_USE_VERSION 29'
174
+ const newDefine = '#ifdef __APPLE__\n#define FUSE_USE_VERSION 26\n#else\n#define FUSE_USE_VERSION 29\n#endif'
175
+
176
+ if (source.includes(newDefine)) return
177
+ if (!source.includes(oldDefine)) throw new Error('Unsupported fuse-native source layout')
178
+ fs.writeFileSync(sourcePath, source.replace(oldDefine, newDefine))
179
+ }
180
+
181
+ function addLibraryRpath (addon, library) {
182
+ const result = childProcess.spawnSync(
183
+ '/usr/bin/install_name_tool',
184
+ ['-add_rpath', path.dirname(library), addon],
185
+ { stdio: 'ignore' }
186
+ )
187
+ // A duplicate rpath produces a nonzero exit code and is already safe.
188
+ return result.status === 0 || result.status === 1
189
+ }
190
+
191
+ function installMacFuseAdapter ({
192
+ packageRoot = path.resolve(__dirname, '..', '..'),
193
+ platform = process.platform,
194
+ log = console
195
+ } = {}) {
196
+ if (platform !== 'darwin') return { skipped: true, reason: 'not macOS' }
197
+
198
+ const installation = findMacFuseInstallation()
199
+ if (!installation.ready) {
200
+ log.warn(`[tas] macOS FUSE was not built: ${installation.reason}`)
201
+ return { skipped: true, ...installation }
202
+ }
203
+
204
+ let fuseNativeDir = getFuseNativeDirectory(packageRoot)
205
+ let darwinLibraryDir = getDarwinLibraryDirectory(packageRoot)
206
+ if (!fuseNativeDir || !darwinLibraryDir) {
207
+ // The legacy optional addon can fail to build on arm64 before our root
208
+ // postinstall runs. Restore its source without lifecycle scripts, then
209
+ // patch and rebuild it against the system macFUSE library below.
210
+ const restored = installOptionalFuseNative(packageRoot)
211
+ if (!restored.ok) {
212
+ log.warn(`[tas] macOS FUSE was not built: ${restored.reason}`)
213
+ return { ready: false, reason: restored.reason }
214
+ }
215
+ fuseNativeDir = getFuseNativeDirectory(packageRoot)
216
+ darwinLibraryDir = getDarwinLibraryDirectory(packageRoot)
217
+ }
218
+ if (!fuseNativeDir || !darwinLibraryDir) {
219
+ const reason = 'optional fuse-native dependencies are not installed'
220
+ log.warn(`[tas] macOS FUSE was not built: ${reason}`)
221
+ return { ready: false, reason }
222
+ }
223
+
224
+ try {
225
+ fs.writeFileSync(path.join(darwinLibraryDir, 'index.js'), macFuseAdapterSource())
226
+ patchFuseSource(fuseNativeDir)
227
+
228
+ const npmCli = process.env.npm_execpath
229
+ if (!npmCli) throw new Error('npm did not provide its rebuild executable')
230
+
231
+ const rebuild = childProcess.spawnSync(
232
+ process.execPath,
233
+ [npmCli, 'rebuild', 'fuse-native', '--build-from-source', '--foreground-scripts'],
234
+ {
235
+ cwd: packageRoot,
236
+ stdio: 'inherit',
237
+ env: { ...process.env, npm_config_build_from_source: 'true' }
238
+ }
239
+ )
240
+ if (rebuild.status !== 0) throw new Error('fuse-native could not compile against macFUSE')
241
+
242
+ const addon = path.join(fuseNativeDir, 'build', 'Release', 'fuse.node')
243
+ addLibraryRpath(addon, installation.library)
244
+
245
+ const status = validateMacFuseNativeBinding({ packageRoot })
246
+ if (!status.ready) throw new Error(status.reason)
247
+ log.log(`[tas] macFUSE native addon rebuilt for ${process.arch}`)
248
+ return status
249
+ } catch (error) {
250
+ const reason = error instanceof Error ? error.message : String(error)
251
+ log.warn(`[tas] macOS FUSE was not built: ${reason}`)
252
+ return { ready: false, reason }
253
+ }
254
+ }
255
+
256
+ module.exports = {
257
+ MACFUSE_BUNDLE,
258
+ findMacFuseInstallation,
259
+ getFuseNativeDirectory,
260
+ installOptionalFuseNative,
261
+ validateMacFuseNativeBinding,
262
+ installMacFuseAdapter
263
+ }
264
+
265
+ if (require.main === module) installMacFuseAdapter()
package/src/fuse/mount.js CHANGED
@@ -9,6 +9,7 @@ import path from 'path';
9
9
  import fs from 'fs';
10
10
  import os from 'os';
11
11
  import { pipeline } from 'stream/promises';
12
+ import macFuse from './macfuse.cjs';
12
13
 
13
14
  let Fuse;
14
15
  try {
@@ -47,10 +48,8 @@ function withTimeout(promise, ms, label) {
47
48
  /** Verify the native FUSE stack, not merely that the JS module imports. */
48
49
  export async function checkFuseRuntime() {
49
50
  if (process.platform === 'darwin') {
50
- return {
51
- supported: false,
52
- reason: 'macOS mount is unsupported: fuse-native 2.x targets obsolete OSXFUSE APIs and current macFUSE/Apple Silicon is not validated'
53
- };
51
+ const macStatus = macFuse.validateMacFuseNativeBinding();
52
+ if (!macStatus.ready) return { supported: false, reason: macStatus.reason };
54
53
  }
55
54
  if (!Fuse) return { supported: false, reason: 'fuse-native is not installed or failed to load' };
56
55
 
@@ -94,10 +93,13 @@ export async function checkFuseRuntime() {
94
93
  export class TelegramFS {
95
94
  constructor(options) {
96
95
  if (process.platform === 'darwin') {
97
- throw new Error(
98
- 'TAS mount is currently unsupported on macOS. fuse-native 2.x targets obsolete OSXFUSE APIs ' +
99
- 'and is not compatible with current macFUSE on Apple Silicon. Use push/pull/sync/share instead.'
100
- );
96
+ const macStatus = macFuse.validateMacFuseNativeBinding();
97
+ if (!macStatus.ready) {
98
+ throw new Error(
99
+ 'TAS mount needs current macFUSE plus a rebuilt native addon: ' + macStatus.reason +
100
+ '. Install Xcode Command Line Tools, reinstall TAS, then run tas doctor.'
101
+ );
102
+ }
101
103
  }
102
104
  if (!Fuse) {
103
105
  throw new Error(