pal-explorer-cli 0.4.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.
Files changed (156) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +314 -0
  3. package/bin/pal.js +230 -0
  4. package/extensions/@palexplorer/analytics/README.md +45 -0
  5. package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
  6. package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
  7. package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
  8. package/extensions/@palexplorer/analytics/extension.json +27 -0
  9. package/extensions/@palexplorer/analytics/index.js +186 -0
  10. package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
  11. package/extensions/@palexplorer/audit/extension.json +17 -0
  12. package/extensions/@palexplorer/audit/index.js +2 -0
  13. package/extensions/@palexplorer/auth-email/extension.json +17 -0
  14. package/extensions/@palexplorer/auth-email/index.js +102 -0
  15. package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
  16. package/extensions/@palexplorer/auth-oauth/index.js +199 -0
  17. package/extensions/@palexplorer/chat/extension.json +17 -0
  18. package/extensions/@palexplorer/chat/index.js +2 -0
  19. package/extensions/@palexplorer/discovery/extension.json +16 -0
  20. package/extensions/@palexplorer/discovery/index.js +111 -0
  21. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  22. package/extensions/@palexplorer/email-notifications/index.js +242 -0
  23. package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
  24. package/extensions/@palexplorer/explorer-integration/index.js +122 -0
  25. package/extensions/@palexplorer/groups/extension.json +17 -0
  26. package/extensions/@palexplorer/groups/index.js +2 -0
  27. package/extensions/@palexplorer/networks/extension.json +17 -0
  28. package/extensions/@palexplorer/networks/index.js +2 -0
  29. package/extensions/@palexplorer/share-links/extension.json +17 -0
  30. package/extensions/@palexplorer/share-links/index.js +2 -0
  31. package/extensions/@palexplorer/sync/extension.json +17 -0
  32. package/extensions/@palexplorer/sync/index.js +2 -0
  33. package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
  34. package/extensions/@palexplorer/user-mgmt/index.js +2 -0
  35. package/extensions/@palexplorer/vfs/extension.json +17 -0
  36. package/extensions/@palexplorer/vfs/index.js +167 -0
  37. package/lib/capabilities.js +263 -0
  38. package/lib/commands/analytics.js +175 -0
  39. package/lib/commands/api-keys.js +131 -0
  40. package/lib/commands/audit.js +235 -0
  41. package/lib/commands/auth.js +137 -0
  42. package/lib/commands/backup.js +76 -0
  43. package/lib/commands/billing.js +148 -0
  44. package/lib/commands/chat.js +217 -0
  45. package/lib/commands/cloud-backup.js +231 -0
  46. package/lib/commands/comment.js +99 -0
  47. package/lib/commands/completion.js +203 -0
  48. package/lib/commands/compliance.js +218 -0
  49. package/lib/commands/config.js +136 -0
  50. package/lib/commands/connect.js +44 -0
  51. package/lib/commands/dept.js +294 -0
  52. package/lib/commands/device.js +146 -0
  53. package/lib/commands/download.js +226 -0
  54. package/lib/commands/explorer.js +178 -0
  55. package/lib/commands/extension.js +970 -0
  56. package/lib/commands/favorite.js +90 -0
  57. package/lib/commands/federation.js +270 -0
  58. package/lib/commands/file.js +533 -0
  59. package/lib/commands/group.js +271 -0
  60. package/lib/commands/gui-share.js +29 -0
  61. package/lib/commands/init.js +61 -0
  62. package/lib/commands/invite.js +59 -0
  63. package/lib/commands/list.js +59 -0
  64. package/lib/commands/log.js +116 -0
  65. package/lib/commands/nearby.js +108 -0
  66. package/lib/commands/network.js +251 -0
  67. package/lib/commands/notify.js +198 -0
  68. package/lib/commands/org.js +273 -0
  69. package/lib/commands/pal.js +180 -0
  70. package/lib/commands/permissions.js +216 -0
  71. package/lib/commands/pin.js +97 -0
  72. package/lib/commands/protocol.js +357 -0
  73. package/lib/commands/rbac.js +147 -0
  74. package/lib/commands/recover.js +36 -0
  75. package/lib/commands/register.js +171 -0
  76. package/lib/commands/relay.js +131 -0
  77. package/lib/commands/remote.js +368 -0
  78. package/lib/commands/revoke.js +50 -0
  79. package/lib/commands/scanner.js +280 -0
  80. package/lib/commands/schedule.js +344 -0
  81. package/lib/commands/scim.js +203 -0
  82. package/lib/commands/search.js +181 -0
  83. package/lib/commands/serve.js +438 -0
  84. package/lib/commands/server.js +350 -0
  85. package/lib/commands/share-link.js +199 -0
  86. package/lib/commands/share.js +323 -0
  87. package/lib/commands/sso.js +200 -0
  88. package/lib/commands/status.js +136 -0
  89. package/lib/commands/stream.js +562 -0
  90. package/lib/commands/su.js +187 -0
  91. package/lib/commands/sync.js +827 -0
  92. package/lib/commands/transfers.js +152 -0
  93. package/lib/commands/uninstall.js +188 -0
  94. package/lib/commands/update.js +204 -0
  95. package/lib/commands/user.js +276 -0
  96. package/lib/commands/vfs.js +84 -0
  97. package/lib/commands/web.js +52 -0
  98. package/lib/commands/webhook.js +180 -0
  99. package/lib/commands/whoami.js +59 -0
  100. package/lib/commands/workspace.js +121 -0
  101. package/lib/core/accessLog.js +54 -0
  102. package/lib/core/analytics.js +99 -0
  103. package/lib/core/backup.js +84 -0
  104. package/lib/core/billing.js +336 -0
  105. package/lib/core/bitfieldStore.js +53 -0
  106. package/lib/core/connectionManager.js +182 -0
  107. package/lib/core/dhtDiscovery.js +148 -0
  108. package/lib/core/discoveryClient.js +408 -0
  109. package/lib/core/extensionAnalyzer.js +357 -0
  110. package/lib/core/extensionSandbox.js +250 -0
  111. package/lib/core/extensionWorkerHost.js +166 -0
  112. package/lib/core/extensions.js +1082 -0
  113. package/lib/core/fileDiff.js +69 -0
  114. package/lib/core/groups.js +119 -0
  115. package/lib/core/identity.js +340 -0
  116. package/lib/core/mdnsService.js +126 -0
  117. package/lib/core/networks.js +81 -0
  118. package/lib/core/permissions.js +109 -0
  119. package/lib/core/pro.js +27 -0
  120. package/lib/core/resolver.js +74 -0
  121. package/lib/core/serverList.js +224 -0
  122. package/lib/core/sharePolicy.js +69 -0
  123. package/lib/core/shares.js +325 -0
  124. package/lib/core/signalingServer.js +441 -0
  125. package/lib/core/streamTransport.js +106 -0
  126. package/lib/core/su.js +55 -0
  127. package/lib/core/syncEngine.js +264 -0
  128. package/lib/core/syncState.js +159 -0
  129. package/lib/core/transfers.js +259 -0
  130. package/lib/core/users.js +225 -0
  131. package/lib/core/vfs.js +216 -0
  132. package/lib/core/webServer.js +702 -0
  133. package/lib/core/webrtcStream.js +396 -0
  134. package/lib/crypto/chatEncryption.js +57 -0
  135. package/lib/crypto/shareEncryption.js +195 -0
  136. package/lib/crypto/sharePassword.js +35 -0
  137. package/lib/crypto/streamEncryption.js +189 -0
  138. package/lib/package.json +1 -0
  139. package/lib/protocol/envelope.js +271 -0
  140. package/lib/protocol/handler.js +191 -0
  141. package/lib/protocol/index.js +27 -0
  142. package/lib/protocol/messages.js +247 -0
  143. package/lib/protocol/negotiation.js +127 -0
  144. package/lib/protocol/policy.js +142 -0
  145. package/lib/protocol/router.js +86 -0
  146. package/lib/protocol/sync.js +122 -0
  147. package/lib/utils/cli.js +15 -0
  148. package/lib/utils/config.js +123 -0
  149. package/lib/utils/configIntegrity.js +87 -0
  150. package/lib/utils/downloadDir.js +9 -0
  151. package/lib/utils/explorer.js +83 -0
  152. package/lib/utils/format.js +12 -0
  153. package/lib/utils/help.js +357 -0
  154. package/lib/utils/logger.js +103 -0
  155. package/lib/utils/torrent.js +203 -0
  156. package/package.json +71 -0
@@ -0,0 +1,225 @@
1
+ import config from '../utils/config.js';
2
+ import { signConfigKey } from '../utils/configIntegrity.js';
3
+
4
+ const ROLES = ['owner', 'user', 'guest'];
5
+
6
+ let guestProfile = null;
7
+
8
+ export function addGuestProfile(publicKey, handle) {
9
+ guestProfile = {
10
+ publicKey,
11
+ handle: handle || null,
12
+ name: 'Guest',
13
+ role: 'guest',
14
+ isGuest: true,
15
+ createdAt: new Date().toISOString(),
16
+ lastLogin: new Date().toISOString(),
17
+ };
18
+ return guestProfile;
19
+ }
20
+
21
+ export function removeGuestProfile() {
22
+ guestProfile = null;
23
+ }
24
+
25
+ export function isGuestActive() {
26
+ return guestProfile !== null;
27
+ }
28
+
29
+ function getUserStore() {
30
+ return config.get('users') || { activeUser: null, profiles: [] };
31
+ }
32
+
33
+ function saveUserStore(store) {
34
+ config.set('users', store);
35
+ signConfigKey('users').catch(() => {});
36
+ }
37
+
38
+ export function getProfiles() {
39
+ return getUserStore().profiles;
40
+ }
41
+
42
+ export function getActiveUser() {
43
+ if (guestProfile) return guestProfile;
44
+ const store = getUserStore();
45
+ if (!store.activeUser || store.profiles.length === 0) return null;
46
+ return store.profiles.find(p => p.publicKey === store.activeUser) || null;
47
+ }
48
+
49
+ export function getActivePublicKey() {
50
+ const store = getUserStore();
51
+ return store.activeUser || null;
52
+ }
53
+
54
+ export function setActiveUser(publicKey) {
55
+ const store = getUserStore();
56
+ const profile = store.profiles.find(p => p.publicKey === publicKey);
57
+ if (!profile) throw new Error('User profile not found');
58
+ profile.lastLogin = new Date().toISOString();
59
+ store.activeUser = publicKey;
60
+ saveUserStore(store);
61
+ return profile;
62
+ }
63
+
64
+ export function addProfile(publicKey, handle, name, role = 'user') {
65
+ if (!ROLES.includes(role)) throw new Error(`Invalid role: ${role}`);
66
+ const store = getUserStore();
67
+
68
+ if (store.profiles.find(p => p.publicKey === publicKey)) {
69
+ throw new Error('Profile already exists for this key');
70
+ }
71
+
72
+ const profile = {
73
+ publicKey,
74
+ handle: handle || null,
75
+ name: name || 'User',
76
+ role,
77
+ createdAt: new Date().toISOString(),
78
+ lastLogin: new Date().toISOString(),
79
+ };
80
+
81
+ store.profiles.push(profile);
82
+
83
+ if (!store.activeUser || store.profiles.length === 1) {
84
+ store.activeUser = publicKey;
85
+ }
86
+
87
+ saveUserStore(store);
88
+ return profile;
89
+ }
90
+
91
+ export function removeProfile(publicKey) {
92
+ const store = getUserStore();
93
+ const profile = store.profiles.find(p => p.publicKey === publicKey);
94
+ if (!profile) throw new Error('Profile not found');
95
+ if (profile.role === 'owner') throw new Error('Cannot remove the owner profile');
96
+
97
+ store.profiles = store.profiles.filter(p => p.publicKey !== publicKey);
98
+
99
+ if (store.activeUser === publicKey) {
100
+ const owner = store.profiles.find(p => p.role === 'owner');
101
+ store.activeUser = owner?.publicKey || store.profiles[0]?.publicKey || null;
102
+ }
103
+
104
+ saveUserStore(store);
105
+ return true;
106
+ }
107
+
108
+ export function promoteProfile(publicKey, newRole) {
109
+ if (!['user', 'guest'].includes(newRole)) throw new Error(`Cannot set role to: ${newRole}`);
110
+ const store = getUserStore();
111
+ const profile = store.profiles.find(p => p.publicKey === publicKey);
112
+ if (!profile) throw new Error('Profile not found');
113
+ if (profile.role === 'owner') throw new Error('Cannot change owner role');
114
+ profile.role = newRole;
115
+ saveUserStore(store);
116
+ return profile;
117
+ }
118
+
119
+ export function updateProfileHandle(publicKey, handle) {
120
+ const store = getUserStore();
121
+ const profile = store.profiles.find(p => p.publicKey === publicKey);
122
+ if (!profile) return null;
123
+ profile.handle = handle;
124
+ saveUserStore(store);
125
+ return profile;
126
+ }
127
+
128
+ export function isOwner(publicKey) {
129
+ const store = getUserStore();
130
+ const profile = store.profiles.find(p => p.publicKey === publicKey);
131
+ return profile && profile.role === 'owner';
132
+ }
133
+
134
+ export function requireRole(minRole) {
135
+ const active = getActiveUser();
136
+ if (!active) throw new Error('No active user. Run `pe login` first.');
137
+
138
+ const roleLevel = { owner: 2, user: 1, guest: 0 };
139
+ if ((roleLevel[active.role] || 0) < (roleLevel[minRole] || 0)) {
140
+ throw new Error(`This action requires ${minRole} role. You are: ${active.role}`);
141
+ }
142
+ return active;
143
+ }
144
+
145
+ export function migrateToMultiUser() {
146
+ const store = getUserStore();
147
+ if (store.profiles.length > 0) return false;
148
+
149
+ const identity = config.get('identity');
150
+ if (!identity?.publicKey) return false;
151
+
152
+ const profile = {
153
+ publicKey: identity.publicKey,
154
+ handle: identity.handle || null,
155
+ name: identity.name || 'Owner',
156
+ role: 'owner',
157
+ createdAt: identity.createdAt || new Date().toISOString(),
158
+ lastLogin: new Date().toISOString(),
159
+ };
160
+
161
+ store.profiles = [profile];
162
+ store.activeUser = identity.publicKey;
163
+ saveUserStore(store);
164
+
165
+ // Migrate existing friends to be scoped to this user
166
+ const friends = config.get('friends') || [];
167
+ if (friends.length > 0) {
168
+ config.set(`userdata.${identity.publicKey}.friends`, friends);
169
+ }
170
+
171
+ return true;
172
+ }
173
+
174
+ // Per-user data accessors
175
+ export function getUserData(key) {
176
+ const pk = getActivePublicKey();
177
+ if (!pk) return null;
178
+ return config.get(`userdata.${pk}.${key}`) ?? null;
179
+ }
180
+
181
+ export function setUserData(key, value) {
182
+ const pk = getActivePublicKey();
183
+ if (!pk) throw new Error('No active user');
184
+ config.set(`userdata.${pk}.${key}`, value);
185
+ }
186
+
187
+ export function getUserFriends() {
188
+ return getUserData('friends') || [];
189
+ }
190
+
191
+ export function setUserFriends(friends) {
192
+ setUserData('friends', friends);
193
+ }
194
+
195
+ export function getUserReceivedShares() {
196
+ return getUserData('receivedShares') || [];
197
+ }
198
+
199
+ export function setUserReceivedShares(shares) {
200
+ setUserData('receivedShares', shares);
201
+ }
202
+
203
+ export function getFriends() {
204
+ const pk = getActivePublicKey();
205
+ if (pk) {
206
+ const perUser = config.get(`userdata.${pk}.friends`);
207
+ if (perUser !== undefined && perUser !== null) return perUser;
208
+ // Non-owners get empty list if no per-user data (no fallback to global)
209
+ const active = getActiveUser();
210
+ if (active && active.role !== 'owner') return [];
211
+ }
212
+ return config.get('friends') || [];
213
+ }
214
+
215
+ export function saveFriends(friends) {
216
+ const pk = getActivePublicKey();
217
+ if (pk) {
218
+ config.set(`userdata.${pk}.friends`, friends);
219
+ }
220
+ // Only write global for owner (backward compat)
221
+ const active = getActiveUser();
222
+ if (!active || active.role === 'owner') {
223
+ config.set('friends', friends);
224
+ }
225
+ }
@@ -0,0 +1,216 @@
1
+ import { v2 as webdav } from 'webdav-server';
2
+ import { exec } from 'child_process';
3
+ import crypto from 'crypto';
4
+ import path from 'path';
5
+ import config from '../utils/config.js';
6
+
7
+ export class VirtualDrive {
8
+ constructor(port = 1900) {
9
+ this.port = port;
10
+ this.token = null;
11
+ this.server = null;
12
+ this.mounted = false;
13
+ this.driveLetter = 'P';
14
+ this.shares = new Map();
15
+ }
16
+
17
+ async start() {
18
+ this.token = crypto.randomBytes(32).toString('hex');
19
+
20
+ const userManager = new webdav.SimpleUserManager();
21
+ const user = userManager.addUser('pal', this.token, false);
22
+
23
+ const privilegeManager = new webdav.SimplePathPrivilegeManager();
24
+ privilegeManager.setRights(user, '/', ['all']);
25
+
26
+ this.server = new webdav.WebDAVServer({
27
+ port: this.port,
28
+ hostname: '127.0.0.1',
29
+ requireAuthentification: true,
30
+ httpAuthentication: new webdav.HTTPBasicAuthentication(userManager, 'Palexplorer VFS'),
31
+ privilegeManager,
32
+ });
33
+
34
+ return new Promise((resolve, reject) => {
35
+ this.server.start((serverOrError) => {
36
+ if (serverOrError instanceof Error) {
37
+ return reject(new Error(`WebDAV server failed to start on port ${this.port}: ${serverOrError.message}`));
38
+ }
39
+ console.log(`WebDAV server started on http://127.0.0.1:${this.port} (auth required)`);
40
+ resolve();
41
+ });
42
+ });
43
+ }
44
+
45
+ async stop() {
46
+ if (!this.server) return;
47
+ return new Promise((resolve) => {
48
+ this.server.stop(() => {
49
+ this.server = null;
50
+ this.token = null;
51
+ resolve();
52
+ });
53
+ });
54
+ }
55
+
56
+ async addShare(shareName, localPath) {
57
+ return new Promise((resolve, reject) => {
58
+ if (!this.server) {
59
+ return reject(new Error('WebDAV server not running. Call start() first.'));
60
+ }
61
+ this.server.setFileSystem(`/${shareName}`, new webdav.PhysicalFileSystem(localPath), (success) => {
62
+ if (success) {
63
+ this.shares.set(shareName, localPath);
64
+ console.log(`Added share to VFS: /${shareName} -> ${localPath}`);
65
+ resolve(true);
66
+ } else {
67
+ reject(new Error(`Failed to add share: /${shareName}`));
68
+ }
69
+ });
70
+ });
71
+ }
72
+
73
+ async removeShare(shareName) {
74
+ if (!this.server) return;
75
+ this.server.removeFileSystem(`/${shareName}`, () => {
76
+ this.shares.delete(shareName);
77
+ });
78
+ }
79
+
80
+ async syncShares() {
81
+ const configShares = config.get('shares') || [];
82
+ const activeNames = new Set();
83
+
84
+ for (const share of configShares) {
85
+ if (share.status !== 'active') continue;
86
+ const name = path.basename(share.path);
87
+ activeNames.add(name);
88
+ if (!this.shares.has(name)) {
89
+ try {
90
+ await this.addShare(name, share.path);
91
+ } catch {
92
+ // Skip shares that fail to add (e.g., path doesn't exist)
93
+ }
94
+ }
95
+ }
96
+
97
+ // Remove shares no longer in config
98
+ for (const [name] of this.shares) {
99
+ if (!activeNames.has(name)) {
100
+ await this.removeShare(name);
101
+ }
102
+ }
103
+ }
104
+
105
+ status() {
106
+ return {
107
+ running: !!this.server,
108
+ mounted: this.mounted,
109
+ driveLetter: this.driveLetter,
110
+ port: this.port,
111
+ shareCount: this.shares.size,
112
+ shares: Array.from(this.shares.entries()).map(([name, localPath]) => ({ name, localPath })),
113
+ };
114
+ }
115
+
116
+ async mount(driveLetter = 'P') {
117
+ this.driveLetter = driveLetter;
118
+
119
+ if (process.platform === 'darwin') {
120
+ const mountPoint = `/Volumes/Palexplorer`;
121
+ const cmd = `mkdir -p "${mountPoint}" && mount_webdav -s -S http://127.0.0.1:${this.port} "${mountPoint}"`;
122
+ return new Promise((resolve, reject) => {
123
+ exec(cmd, (err) => {
124
+ if (err) {
125
+ console.warn('VFS mount failed on macOS. Ensure webdav support is available.');
126
+ return resolve(); // non-fatal
127
+ }
128
+ this.mounted = true;
129
+ console.log(`Mounted Virtual Drive at ${mountPoint}`);
130
+ resolve();
131
+ });
132
+ });
133
+ }
134
+
135
+ if (process.platform === 'linux') {
136
+ const mountPoint = `${process.env.HOME}/Palexplorer`;
137
+ const cmd = `mkdir -p "${mountPoint}" && mount -t davfs http://127.0.0.1:${this.port} "${mountPoint}"`;
138
+ return new Promise((resolve, reject) => {
139
+ exec(cmd, (err) => {
140
+ if (err) {
141
+ console.warn('VFS mount failed on Linux. Install davfs2: sudo apt install davfs2');
142
+ return resolve(); // non-fatal
143
+ }
144
+ this.mounted = true;
145
+ console.log(`Mounted Virtual Drive at ${mountPoint}`);
146
+ resolve();
147
+ });
148
+ });
149
+ }
150
+
151
+ if (process.platform !== 'win32') return;
152
+
153
+ if (!/^[A-Za-z]$/.test(driveLetter)) throw new Error('Invalid drive letter');
154
+ const cmd = `net use ${driveLetter}: http://127.0.0.1:${this.port} /user:pal ${this.token} /persistent:yes`;
155
+
156
+ return new Promise((resolve, reject) => {
157
+ exec(cmd, (err, stdout, stderr) => {
158
+ if (err) {
159
+ if (stderr && stderr.includes('85')) {
160
+ this.mounted = true;
161
+ return resolve();
162
+ }
163
+ if (stderr && stderr.includes('67')) {
164
+ console.warn(`Virtual Drive: WebClient service not running, ${driveLetter}: drive skipped.`);
165
+ return resolve();
166
+ }
167
+ return reject(new Error(`Mount failed: ${stderr || err.message}`));
168
+ }
169
+ this.mounted = true;
170
+ console.log(`Mounted Virtual Drive at ${driveLetter}:`);
171
+ resolve();
172
+ });
173
+ });
174
+ }
175
+
176
+ async unmount(driveLetter = 'P') {
177
+ if (process.platform === 'darwin') {
178
+ const mountPoint = `/Volumes/Palexplorer`;
179
+ return new Promise((resolve) => {
180
+ exec(`umount "${mountPoint}"`, () => {
181
+ this.mounted = false;
182
+ resolve();
183
+ });
184
+ });
185
+ }
186
+
187
+ if (process.platform === 'linux') {
188
+ const mountPoint = `${process.env.HOME}/Palexplorer`;
189
+ return new Promise((resolve) => {
190
+ exec(`umount "${mountPoint}"`, () => {
191
+ this.mounted = false;
192
+ resolve();
193
+ });
194
+ });
195
+ }
196
+
197
+ if (process.platform !== 'win32') return;
198
+
199
+ if (!/^[A-Za-z]$/.test(driveLetter)) throw new Error('Invalid drive letter');
200
+ const cmd = `net use ${driveLetter}: /delete /yes`;
201
+
202
+ return new Promise((resolve, reject) => {
203
+ exec(cmd, (err) => {
204
+ if (err) return reject(new Error(`Unmount failed: ${err.message}`));
205
+ this.mounted = false;
206
+ resolve();
207
+ });
208
+ });
209
+ }
210
+ }
211
+
212
+ export const vfs = new VirtualDrive();
213
+
214
+ export function getMountStatus() {
215
+ return vfs.status();
216
+ }