@ecmaos/coreutils 0.3.1 → 0.4.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.
Files changed (199) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +48 -0
  3. package/dist/commands/awk.d.ts +4 -0
  4. package/dist/commands/awk.d.ts.map +1 -0
  5. package/dist/commands/awk.js +324 -0
  6. package/dist/commands/awk.js.map +1 -0
  7. package/dist/commands/chgrp.d.ts +4 -0
  8. package/dist/commands/chgrp.d.ts.map +1 -0
  9. package/dist/commands/chgrp.js +187 -0
  10. package/dist/commands/chgrp.js.map +1 -0
  11. package/dist/commands/chmod.d.ts.map +1 -1
  12. package/dist/commands/chmod.js +139 -2
  13. package/dist/commands/chmod.js.map +1 -1
  14. package/dist/commands/chown.d.ts +4 -0
  15. package/dist/commands/chown.d.ts.map +1 -0
  16. package/dist/commands/chown.js +257 -0
  17. package/dist/commands/chown.js.map +1 -0
  18. package/dist/commands/cksum.d.ts +4 -0
  19. package/dist/commands/cksum.d.ts.map +1 -0
  20. package/dist/commands/cksum.js +124 -0
  21. package/dist/commands/cksum.js.map +1 -0
  22. package/dist/commands/cmp.d.ts +4 -0
  23. package/dist/commands/cmp.d.ts.map +1 -0
  24. package/dist/commands/cmp.js +120 -0
  25. package/dist/commands/cmp.js.map +1 -0
  26. package/dist/commands/column.d.ts +4 -0
  27. package/dist/commands/column.d.ts.map +1 -0
  28. package/dist/commands/column.js +274 -0
  29. package/dist/commands/column.js.map +1 -0
  30. package/dist/commands/cp.d.ts.map +1 -1
  31. package/dist/commands/cp.js +81 -4
  32. package/dist/commands/cp.js.map +1 -1
  33. package/dist/commands/cron.d.ts.map +1 -1
  34. package/dist/commands/cron.js +116 -23
  35. package/dist/commands/cron.js.map +1 -1
  36. package/dist/commands/curl.d.ts +4 -0
  37. package/dist/commands/curl.d.ts.map +1 -0
  38. package/dist/commands/curl.js +238 -0
  39. package/dist/commands/curl.js.map +1 -0
  40. package/dist/commands/du.d.ts +4 -0
  41. package/dist/commands/du.d.ts.map +1 -0
  42. package/dist/commands/du.js +168 -0
  43. package/dist/commands/du.js.map +1 -0
  44. package/dist/commands/echo.d.ts.map +1 -1
  45. package/dist/commands/echo.js +125 -2
  46. package/dist/commands/echo.js.map +1 -1
  47. package/dist/commands/env.d.ts +4 -0
  48. package/dist/commands/env.d.ts.map +1 -0
  49. package/dist/commands/env.js +129 -0
  50. package/dist/commands/env.js.map +1 -0
  51. package/dist/commands/expand.d.ts +4 -0
  52. package/dist/commands/expand.d.ts.map +1 -0
  53. package/dist/commands/expand.js +197 -0
  54. package/dist/commands/expand.js.map +1 -0
  55. package/dist/commands/factor.d.ts +4 -0
  56. package/dist/commands/factor.d.ts.map +1 -0
  57. package/dist/commands/factor.js +141 -0
  58. package/dist/commands/factor.js.map +1 -0
  59. package/dist/commands/fmt.d.ts +4 -0
  60. package/dist/commands/fmt.d.ts.map +1 -0
  61. package/dist/commands/fmt.js +278 -0
  62. package/dist/commands/fmt.js.map +1 -0
  63. package/dist/commands/fold.d.ts +4 -0
  64. package/dist/commands/fold.d.ts.map +1 -0
  65. package/dist/commands/fold.js +253 -0
  66. package/dist/commands/fold.js.map +1 -0
  67. package/dist/commands/groups.d.ts +4 -0
  68. package/dist/commands/groups.d.ts.map +1 -0
  69. package/dist/commands/groups.js +61 -0
  70. package/dist/commands/groups.js.map +1 -0
  71. package/dist/commands/head.d.ts.map +1 -1
  72. package/dist/commands/head.js +184 -77
  73. package/dist/commands/head.js.map +1 -1
  74. package/dist/commands/hostname.d.ts +4 -0
  75. package/dist/commands/hostname.d.ts.map +1 -0
  76. package/dist/commands/hostname.js +80 -0
  77. package/dist/commands/hostname.js.map +1 -0
  78. package/dist/commands/less.d.ts.map +1 -1
  79. package/dist/commands/less.js +1 -0
  80. package/dist/commands/less.js.map +1 -1
  81. package/dist/commands/man.d.ts.map +1 -1
  82. package/dist/commands/man.js +3 -1
  83. package/dist/commands/man.js.map +1 -1
  84. package/dist/commands/mount.d.ts +4 -0
  85. package/dist/commands/mount.d.ts.map +1 -0
  86. package/dist/commands/mount.js +1136 -0
  87. package/dist/commands/mount.js.map +1 -0
  88. package/dist/commands/od.d.ts +4 -0
  89. package/dist/commands/od.d.ts.map +1 -0
  90. package/dist/commands/od.js +342 -0
  91. package/dist/commands/od.js.map +1 -0
  92. package/dist/commands/pr.d.ts +4 -0
  93. package/dist/commands/pr.d.ts.map +1 -0
  94. package/dist/commands/pr.js +298 -0
  95. package/dist/commands/pr.js.map +1 -0
  96. package/dist/commands/printf.d.ts +4 -0
  97. package/dist/commands/printf.d.ts.map +1 -0
  98. package/dist/commands/printf.js +271 -0
  99. package/dist/commands/printf.js.map +1 -0
  100. package/dist/commands/readlink.d.ts +4 -0
  101. package/dist/commands/readlink.d.ts.map +1 -0
  102. package/dist/commands/readlink.js +104 -0
  103. package/dist/commands/readlink.js.map +1 -0
  104. package/dist/commands/realpath.d.ts +4 -0
  105. package/dist/commands/realpath.d.ts.map +1 -0
  106. package/dist/commands/realpath.js +111 -0
  107. package/dist/commands/realpath.js.map +1 -0
  108. package/dist/commands/rev.d.ts +4 -0
  109. package/dist/commands/rev.d.ts.map +1 -0
  110. package/dist/commands/rev.js +134 -0
  111. package/dist/commands/rev.js.map +1 -0
  112. package/dist/commands/shuf.d.ts +4 -0
  113. package/dist/commands/shuf.d.ts.map +1 -0
  114. package/dist/commands/shuf.js +221 -0
  115. package/dist/commands/shuf.js.map +1 -0
  116. package/dist/commands/sleep.d.ts +4 -0
  117. package/dist/commands/sleep.d.ts.map +1 -0
  118. package/dist/commands/sleep.js +102 -0
  119. package/dist/commands/sleep.js.map +1 -0
  120. package/dist/commands/strings.d.ts +4 -0
  121. package/dist/commands/strings.d.ts.map +1 -0
  122. package/dist/commands/strings.js +170 -0
  123. package/dist/commands/strings.js.map +1 -0
  124. package/dist/commands/tac.d.ts +4 -0
  125. package/dist/commands/tac.d.ts.map +1 -0
  126. package/dist/commands/tac.js +130 -0
  127. package/dist/commands/tac.js.map +1 -0
  128. package/dist/commands/time.d.ts +4 -0
  129. package/dist/commands/time.d.ts.map +1 -0
  130. package/dist/commands/time.js +126 -0
  131. package/dist/commands/time.js.map +1 -0
  132. package/dist/commands/umount.d.ts +4 -0
  133. package/dist/commands/umount.d.ts.map +1 -0
  134. package/dist/commands/umount.js +103 -0
  135. package/dist/commands/umount.js.map +1 -0
  136. package/dist/commands/uname.d.ts +4 -0
  137. package/dist/commands/uname.d.ts.map +1 -0
  138. package/dist/commands/uname.js +149 -0
  139. package/dist/commands/uname.js.map +1 -0
  140. package/dist/commands/unexpand.d.ts +4 -0
  141. package/dist/commands/unexpand.d.ts.map +1 -0
  142. package/dist/commands/unexpand.js +286 -0
  143. package/dist/commands/unexpand.js.map +1 -0
  144. package/dist/commands/uptime.d.ts +4 -0
  145. package/dist/commands/uptime.d.ts.map +1 -0
  146. package/dist/commands/uptime.js +62 -0
  147. package/dist/commands/uptime.js.map +1 -0
  148. package/dist/commands/view.d.ts +1 -0
  149. package/dist/commands/view.d.ts.map +1 -1
  150. package/dist/commands/view.js +408 -66
  151. package/dist/commands/view.js.map +1 -1
  152. package/dist/commands/yes.d.ts +4 -0
  153. package/dist/commands/yes.d.ts.map +1 -0
  154. package/dist/commands/yes.js +58 -0
  155. package/dist/commands/yes.js.map +1 -0
  156. package/dist/index.d.ts +24 -0
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +82 -0
  159. package/dist/index.js.map +1 -1
  160. package/package.json +12 -3
  161. package/src/commands/awk.ts +340 -0
  162. package/src/commands/chmod.ts +141 -2
  163. package/src/commands/chown.ts +321 -0
  164. package/src/commands/cksum.ts +133 -0
  165. package/src/commands/cmp.ts +126 -0
  166. package/src/commands/column.ts +273 -0
  167. package/src/commands/cp.ts +93 -4
  168. package/src/commands/cron.ts +115 -23
  169. package/src/commands/curl.ts +231 -0
  170. package/src/commands/echo.ts +122 -2
  171. package/src/commands/env.ts +143 -0
  172. package/src/commands/expand.ts +207 -0
  173. package/src/commands/factor.ts +151 -0
  174. package/src/commands/fmt.ts +293 -0
  175. package/src/commands/fold.ts +257 -0
  176. package/src/commands/groups.ts +72 -0
  177. package/src/commands/head.ts +176 -77
  178. package/src/commands/hostname.ts +81 -0
  179. package/src/commands/less.ts +1 -0
  180. package/src/commands/man.ts +4 -1
  181. package/src/commands/mount.ts +1302 -0
  182. package/src/commands/od.ts +327 -0
  183. package/src/commands/pr.ts +291 -0
  184. package/src/commands/printf.ts +271 -0
  185. package/src/commands/readlink.ts +102 -0
  186. package/src/commands/realpath.ts +126 -0
  187. package/src/commands/rev.ts +143 -0
  188. package/src/commands/shuf.ts +218 -0
  189. package/src/commands/sleep.ts +109 -0
  190. package/src/commands/strings.ts +176 -0
  191. package/src/commands/tac.ts +138 -0
  192. package/src/commands/time.ts +144 -0
  193. package/src/commands/umount.ts +116 -0
  194. package/src/commands/uname.ts +130 -0
  195. package/src/commands/unexpand.ts +305 -0
  196. package/src/commands/uptime.ts +73 -0
  197. package/src/commands/view.ts +463 -73
  198. package/src/index.ts +82 -0
  199. package/tsconfig.json +4 -0
@@ -0,0 +1,1302 @@
1
+ // TODO: Dropbox and S3 are WIP
2
+
3
+ import path from 'path'
4
+ import chalk from 'chalk'
5
+ import { Fetch, InMemory, resolveMountConfig, SingleBuffer } from '@zenfs/core'
6
+ import { IndexedDB, WebStorage, WebAccess, /* XML */ } from '@zenfs/dom'
7
+ import { Iso, Zip } from '@zenfs/archives'
8
+ import { Dropbox, /* S3Bucket, */ GoogleDrive } from '@zenfs/cloud'
9
+
10
+ import type { Kernel, Process, Shell, Terminal, FstabEntry } from '@ecmaos/types'
11
+ import { TerminalCommand } from '../shared/terminal-command.js'
12
+ import { writelnStdout, writelnStderr } from '../shared/helpers.js'
13
+
14
+ /**
15
+ * Parse a single fstab line
16
+ * @param line - The line to parse
17
+ * @returns Parsed entry or null if line is empty/comment
18
+ */
19
+ function parseFstabLine(line: string): FstabEntry | null {
20
+ const trimmed = line.trim()
21
+
22
+ // Skip empty lines and comments
23
+ if (trimmed === '' || trimmed.startsWith('#')) {
24
+ return null
25
+ }
26
+
27
+ // Split by whitespace (space or tab)
28
+ // Format: source target type [options]
29
+ const parts = trimmed.split(/\s+/)
30
+
31
+ if (parts.length < 3) {
32
+ // Need at least source, target, and type
33
+ return null
34
+ }
35
+
36
+ const source = parts[0] || ''
37
+ const target = parts[1] || ''
38
+ const type = parts[2] || ''
39
+ const options = parts.slice(3).join(' ') || undefined
40
+
41
+ // Validate required fields
42
+ if (!target || !type) {
43
+ return null
44
+ }
45
+
46
+ return {
47
+ source: source || '',
48
+ target,
49
+ type,
50
+ options
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Parse a complete fstab file
56
+ * @param content - The fstab file content
57
+ * @returns Array of parsed fstab entries
58
+ */
59
+ function parseFstabFile(content: string): FstabEntry[] {
60
+ const lines = content.split('\n')
61
+ const entries: FstabEntry[] = []
62
+
63
+ for (const line of lines) {
64
+ if (!line) continue
65
+ const parsed = parseFstabLine(line)
66
+ if (parsed) {
67
+ entries.push(parsed)
68
+ }
69
+ }
70
+
71
+ return entries
72
+ }
73
+
74
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
75
+ const usage = `Usage: mount [OPTIONS] [SOURCE] TARGET
76
+ mount [-a|--all]
77
+ mount [-l|--list]
78
+
79
+ Mount a filesystem.
80
+
81
+ Options:
82
+ -t, --type TYPE filesystem type (fetch, indexeddb, webstorage, webaccess, memory, singlebuffer, zip, iso, googledrive)
83
+ -o, --options OPTS mount options (comma-separated key=value pairs)
84
+ -a, --all mount all filesystems listed in /etc/fstab
85
+ -l, --list list all mounted filesystems
86
+ --help display this help and exit
87
+
88
+ Filesystem types:
89
+ fetch mount a remote filesystem via HTTP fetch
90
+ indexeddb mount an IndexedDB-backed filesystem
91
+ webstorage mount a WebStorage-backed filesystem (localStorage or sessionStorage)
92
+ webaccess mount a filesystem using the File System Access API
93
+ memory mount an in-memory filesystem
94
+ singlebuffer mount a filesystem backed by a single buffer
95
+ zip mount a readonly filesystem from a zip archive (requires SOURCE file or URL)
96
+ iso mount a readonly filesystem from an ISO image (requires SOURCE file or URL)
97
+ googledrive mount a Google Drive filesystem (requires apiKey via -o apiKey, optionally clientId for OAuth)
98
+
99
+ Mount options:
100
+ baseUrl=URL base URL for fetch operations (fetch type)
101
+ size=BYTES buffer size in bytes for singlebuffer type (default: 1048576)
102
+ storage=TYPE storage type for webstorage (localStorage or sessionStorage, default: localStorage)
103
+ apiKey=KEY Google API key (googledrive type, required)
104
+ clientId=ID Google OAuth client ID (googledrive type, optional)
105
+ scope=SCOPE OAuth scope (googledrive type, default: https://www.googleapis.com/auth/drive)
106
+ cacheTTL=SECONDS cache TTL in seconds for cloud backends (optional)
107
+
108
+ Examples:
109
+ mount -l list all mounted filesystems
110
+ mount -t memory /mnt/tmp mount memory filesystem at /mnt/tmp
111
+ mount -t indexeddb mydb /mnt/db mount IndexedDB store 'mydb' at /mnt/db
112
+ mount -t webstorage /mnt/storage mount WebStorage filesystem using localStorage
113
+ mount -t webstorage /mnt/storage -o storage=sessionStorage
114
+ mount -t webaccess /mnt/access mount File System Access API filesystem
115
+ mount -t fetch /api /mnt/api mount fetch filesystem at /mnt/api
116
+ mount -t fetch /api /mnt/api -o baseUrl=https://example.com
117
+ mount -t singlebuffer /mnt/buf mount singlebuffer filesystem at /mnt/buf
118
+ mount -t singlebuffer /mnt/buf -o size=2097152
119
+ mount -t zip https://example.com/archive.zip /mnt/zip
120
+ mount -t zip /tmp/archive.zip /mnt/zip
121
+ mount -t iso https://example.com/image.iso /mnt/iso
122
+ mount -t iso /tmp/image.iso /mnt/iso
123
+ mount -t googledrive /mnt/gdrive -o apiKey=YOUR_API_KEY # readonly/public
124
+ mount -t googledrive /mnt/gdrive -o clientId=YOUR_CLIENT_ID # rw/private`
125
+ writelnStderr(process, terminal, usage)
126
+ }
127
+
128
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
129
+ return new TerminalCommand({
130
+ command: 'mount',
131
+ description: 'Mount a filesystem',
132
+ kernel,
133
+ shell,
134
+ terminal,
135
+ run: async (pid: number, argv: string[]) => {
136
+ const process = kernel.processes.get(pid) as Process | undefined
137
+
138
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
139
+ printUsage(process, terminal)
140
+ return 0
141
+ }
142
+
143
+ let listMode = false
144
+ let allMode = false
145
+ let type: string | undefined
146
+ let options: string | undefined
147
+ const positionalArgs: string[] = []
148
+
149
+ for (let i = 0; i < argv.length; i++) {
150
+ const arg = argv[i]
151
+ if (arg === '-l' || arg === '--list') {
152
+ listMode = true
153
+ } else if (arg === '-a' || arg === '--all') {
154
+ allMode = true
155
+ } else if (arg === '-t' || arg === '--type') {
156
+ if (i + 1 < argv.length) {
157
+ type = argv[i + 1]
158
+ i++
159
+ } else {
160
+ await writelnStderr(process, terminal, chalk.red('mount: option requires an argument -- \'t\''))
161
+ return 1
162
+ }
163
+ } else if (arg === '-o' || arg === '--options') {
164
+ if (i + 1 < argv.length) {
165
+ options = argv[i + 1]
166
+ i++
167
+ } else {
168
+ await writelnStderr(process, terminal, chalk.red('mount: option requires an argument -- \'o\''))
169
+ return 1
170
+ }
171
+ } else if (arg && !arg.startsWith('-')) {
172
+ positionalArgs.push(arg)
173
+ }
174
+ }
175
+
176
+ if (listMode || (argv.length === 0 && !allMode)) {
177
+ const mountList = Array.from(kernel.filesystem.mounts.entries())
178
+
179
+ if (mountList.length === 0) {
180
+ await writelnStdout(process, terminal, 'No filesystems mounted.')
181
+ return 0
182
+ }
183
+
184
+ const mountRows = mountList.map(([target, mount]: [string, unknown]) => {
185
+ const mountObj = mount as { store?: { constructor?: { name?: string } }; constructor?: { name?: string }; metadata?: () => { name?: string } }
186
+ const store = mountObj.store
187
+ const backendName = store?.constructor?.name || mountObj.constructor?.name || 'Unknown'
188
+ const metadata = mountObj.metadata?.()
189
+ const name = metadata?.name || backendName
190
+
191
+ return {
192
+ target: chalk.blue(target),
193
+ name: chalk.gray(name)
194
+ }
195
+ })
196
+
197
+ for (const row of mountRows) {
198
+ await writelnStdout(process, terminal, `${row.target.padEnd(30)} ${row.name}`)
199
+ }
200
+
201
+ return 0
202
+ }
203
+
204
+ if (allMode) {
205
+ try {
206
+ const fstabPath = '/etc/fstab'
207
+ if (!(await shell.context.fs.promises.exists(fstabPath))) {
208
+ await writelnStderr(process, terminal, chalk.yellow(`mount: ${fstabPath} not found`))
209
+ return 1
210
+ }
211
+
212
+ const content = await shell.context.fs.promises.readFile(fstabPath, 'utf-8')
213
+ const entries = parseFstabFile(content)
214
+
215
+ if (entries.length === 0) {
216
+ await writelnStdout(process, terminal, 'No entries found in /etc/fstab')
217
+ return 0
218
+ }
219
+
220
+ await writelnStdout(process, terminal, `Mounting ${entries.length} filesystem(s) from /etc/fstab...`)
221
+
222
+ let successCount = 0
223
+ let failCount = 0
224
+
225
+ for (const entry of entries) {
226
+ try {
227
+ const entryType = entry.type
228
+ const entrySource = entry.source || ''
229
+ const entryTarget = path.resolve('/', entry.target)
230
+ const entryOptions = entry.options
231
+
232
+ // Validate entry
233
+ if (!entryType) {
234
+ await writelnStderr(process, terminal, chalk.yellow(`mount: skipping entry for ${entryTarget}: missing type`))
235
+ failCount++
236
+ continue
237
+ }
238
+
239
+ if (!entryTarget) {
240
+ await writelnStderr(process, terminal, chalk.yellow(`mount: skipping entry: missing target`))
241
+ failCount++
242
+ continue
243
+ }
244
+
245
+ // Check if filesystem type doesn't require source but one is provided
246
+ const noSourceTypes = ['memory', 'singlebuffer', 'webstorage', 'webaccess', 'xml', 'dropbox', 'googledrive']
247
+ if (entrySource && noSourceTypes.includes(entryType.toLowerCase())) {
248
+ await writelnStderr(process, terminal, chalk.yellow(`mount: ${entryType} filesystem does not require a source, ignoring source for ${entryTarget}`))
249
+ }
250
+
251
+ // Check if filesystem type requires source but none is provided
252
+ const requiresSourceTypes = ['zip', 'iso', 'fetch', 'indexeddb']
253
+ if (!entrySource && requiresSourceTypes.includes(entryType.toLowerCase())) {
254
+ await writelnStderr(process, terminal, chalk.yellow(`mount: skipping ${entryTarget}: ${entryType} filesystem requires a source`))
255
+ failCount++
256
+ continue
257
+ }
258
+
259
+ // Create target directory if needed
260
+ const parentDir = path.dirname(entryTarget)
261
+ if (parentDir !== entryTarget && !(await shell.context.fs.promises.exists(parentDir))) {
262
+ await shell.context.fs.promises.mkdir(parentDir, { recursive: true })
263
+ }
264
+
265
+ if (!(await shell.context.fs.promises.exists(entryTarget))) {
266
+ await shell.context.fs.promises.mkdir(entryTarget, { recursive: true })
267
+ }
268
+
269
+ // Parse mount options
270
+ const mountOptions = entryOptions?.split(',').reduce((acc, option) => {
271
+ const [key, value] = option.split('=')
272
+ if (key && value) {
273
+ acc[key.trim()] = value.trim()
274
+ }
275
+ return acc
276
+ }, {} as Record<string, string>) || {}
277
+
278
+ // Perform the mount based on type
279
+ switch (entryType.toLowerCase()) {
280
+ case 'fetch': {
281
+ let fetchBaseUrl = mountOptions.baseUrl || ''
282
+ let indexUrl: string
283
+
284
+ if (entrySource && /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(entrySource)) {
285
+ indexUrl = entrySource
286
+ } else {
287
+ if (!fetchBaseUrl) {
288
+ throw new Error('fetch filesystem requires either a full URL as source or baseUrl option')
289
+ }
290
+ fetchBaseUrl = new URL(fetchBaseUrl).toString()
291
+ indexUrl = new URL(entrySource || 'index.json', fetchBaseUrl).toString()
292
+ }
293
+
294
+ await kernel.filesystem.fsSync.mount(
295
+ entryTarget,
296
+ await resolveMountConfig({
297
+ backend: Fetch,
298
+ index: indexUrl,
299
+ baseUrl: fetchBaseUrl,
300
+ disableAsyncCache: true,
301
+ })
302
+ )
303
+ break
304
+ }
305
+ case 'indexeddb':
306
+ await kernel.filesystem.fsSync.mount(
307
+ entryTarget,
308
+ await resolveMountConfig({
309
+ backend: IndexedDB,
310
+ storeName: entrySource || entryTarget
311
+ })
312
+ )
313
+ break
314
+ case 'webstorage': {
315
+ const storageType = mountOptions.storage?.toLowerCase() || 'localstorage'
316
+ let storage: Storage
317
+
318
+ if (storageType === 'sessionstorage') {
319
+ if (typeof sessionStorage === 'undefined') {
320
+ throw new Error('sessionStorage is not available in this environment')
321
+ }
322
+ storage = sessionStorage
323
+ } else if (storageType === 'localstorage') {
324
+ if (typeof localStorage === 'undefined') {
325
+ throw new Error('localStorage is not available in this environment')
326
+ }
327
+ storage = localStorage
328
+ } else {
329
+ throw new Error(`invalid storage type '${storageType}'. Use 'localStorage' or 'sessionStorage'`)
330
+ }
331
+
332
+ await kernel.filesystem.fsSync.mount(
333
+ entryTarget,
334
+ await resolveMountConfig({
335
+ backend: WebStorage,
336
+ storage
337
+ } as { backend: typeof WebStorage; storage: Storage })
338
+ )
339
+ break
340
+ }
341
+ case 'webaccess': {
342
+ if (typeof window === 'undefined') {
343
+ throw new Error('File System Access API is not available in this environment')
344
+ }
345
+
346
+ const win = window as unknown as { showDirectoryPicker?: () => Promise<FileSystemDirectoryHandle> }
347
+ if (!win.showDirectoryPicker) {
348
+ throw new Error('File System Access API is not available in this environment')
349
+ }
350
+
351
+ // For fstab, we can't interactively pick a directory, so skip
352
+ await writelnStderr(process, terminal, chalk.yellow(`mount: skipping ${entryTarget}: webaccess requires interactive directory selection`))
353
+ failCount++
354
+ continue
355
+ }
356
+ case 'memory':
357
+ await kernel.filesystem.fsSync.mount(
358
+ entryTarget,
359
+ await resolveMountConfig({
360
+ backend: InMemory
361
+ })
362
+ )
363
+ break
364
+ case 'singlebuffer': {
365
+ const bufferSize = mountOptions.size
366
+ ? parseInt(mountOptions.size, 10)
367
+ : 1048576
368
+
369
+ if (isNaN(bufferSize) || bufferSize <= 0) {
370
+ throw new Error('invalid buffer size for singlebuffer type')
371
+ }
372
+
373
+ let buffer: ArrayBuffer | SharedArrayBuffer
374
+ try {
375
+ buffer = new SharedArrayBuffer(bufferSize)
376
+ } catch {
377
+ buffer = new ArrayBuffer(bufferSize)
378
+ }
379
+
380
+ await kernel.filesystem.fsSync.mount(
381
+ entryTarget,
382
+ await resolveMountConfig({
383
+ backend: SingleBuffer,
384
+ buffer
385
+ })
386
+ )
387
+ break
388
+ }
389
+ case 'zip': {
390
+ if (!entrySource) {
391
+ throw new Error('zip filesystem requires a source file or URL')
392
+ }
393
+
394
+ let arrayBuffer: ArrayBuffer
395
+
396
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(entrySource)) {
397
+ const response = await fetch(entrySource)
398
+ if (!response.ok) {
399
+ throw new Error(`failed to fetch archive: ${response.status} ${response.statusText}`)
400
+ }
401
+ arrayBuffer = await response.arrayBuffer()
402
+ } else {
403
+ const sourcePath = path.resolve('/', entrySource)
404
+ if (!(await shell.context.fs.promises.exists(sourcePath))) {
405
+ throw new Error(`archive file not found: ${sourcePath}`)
406
+ }
407
+ const fileData = await shell.context.fs.promises.readFile(sourcePath)
408
+ arrayBuffer = new Uint8Array(fileData).buffer
409
+ }
410
+
411
+ await kernel.filesystem.fsSync.mount(
412
+ entryTarget,
413
+ await resolveMountConfig({
414
+ backend: Zip,
415
+ data: arrayBuffer
416
+ })
417
+ )
418
+ break
419
+ }
420
+ case 'iso': {
421
+ if (!entrySource) {
422
+ throw new Error('iso filesystem requires a source file or URL')
423
+ }
424
+
425
+ let uint8Array: Uint8Array
426
+
427
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(entrySource)) {
428
+ const response = await fetch(entrySource)
429
+ if (!response.ok) {
430
+ throw new Error(`failed to fetch ISO image: ${response.status} ${response.statusText}`)
431
+ }
432
+ const arrayBuffer = await response.arrayBuffer()
433
+ uint8Array = new Uint8Array(arrayBuffer)
434
+ } else {
435
+ const sourcePath = path.resolve('/', entrySource)
436
+ if (!(await shell.context.fs.promises.exists(sourcePath))) {
437
+ throw new Error(`ISO image file not found: ${sourcePath}`)
438
+ }
439
+ uint8Array = await shell.context.fs.promises.readFile(sourcePath)
440
+ }
441
+
442
+ await kernel.filesystem.fsSync.mount(
443
+ entryTarget,
444
+ await resolveMountConfig({
445
+ backend: Iso,
446
+ data: uint8Array
447
+ })
448
+ )
449
+ break
450
+ }
451
+ case 'dropbox': {
452
+ if (!mountOptions.client) {
453
+ throw new Error('dropbox filesystem requires client configuration')
454
+ }
455
+
456
+ let clientConfig: { accessToken: string; [key: string]: unknown }
457
+ try {
458
+ clientConfig = JSON.parse(mountOptions.client)
459
+ } catch {
460
+ throw new Error('invalid JSON in client option')
461
+ }
462
+
463
+ if (!clientConfig.accessToken) {
464
+ throw new Error('client configuration must include accessToken')
465
+ }
466
+
467
+ const dropboxModule = await import('dropbox')
468
+ const DropboxClient = dropboxModule.Dropbox
469
+ const client = new DropboxClient(clientConfig)
470
+ const cacheTTL = mountOptions.cacheTTL ? parseInt(mountOptions.cacheTTL, 10) : undefined
471
+
472
+ await kernel.filesystem.fsSync.mount(
473
+ entryTarget,
474
+ await resolveMountConfig({
475
+ backend: Dropbox,
476
+ client,
477
+ ...(cacheTTL && !isNaN(cacheTTL) ? { cacheTTL } : {})
478
+ })
479
+ )
480
+ break
481
+ }
482
+ case 'googledrive': {
483
+ if (typeof window === 'undefined') {
484
+ throw new Error('Google Drive API requires a browser environment')
485
+ }
486
+
487
+ if (!mountOptions.apiKey) {
488
+ throw new Error('googledrive filesystem requires apiKey option')
489
+ }
490
+
491
+ // Google Drive mounting is complex and requires interactive auth
492
+ // For fstab, we'll skip it with a warning
493
+ await writelnStderr(process, terminal, chalk.yellow(`mount: skipping ${entryTarget}: googledrive requires interactive authentication`))
494
+ failCount++
495
+ continue
496
+ }
497
+ default:
498
+ throw new Error(`unknown filesystem type '${entryType}'`)
499
+ }
500
+
501
+ const successMessage = entrySource
502
+ ? chalk.green(`Mounted ${entryType} filesystem from ${entrySource} to ${entryTarget}`)
503
+ : chalk.green(`Mounted ${entryType} filesystem at ${entryTarget}`)
504
+ await writelnStdout(process, terminal, successMessage)
505
+ successCount++
506
+ } catch (error) {
507
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
508
+ await writelnStderr(process, terminal, chalk.red(`mount: failed to mount ${entry.target}: ${errorMessage}`))
509
+ failCount++
510
+ }
511
+ }
512
+
513
+ await writelnStdout(process, terminal, `\nMount summary: ${successCount} succeeded, ${failCount} failed`)
514
+ return failCount > 0 ? 1 : 0
515
+ } catch (error) {
516
+ await writelnStderr(process, terminal, chalk.red(`mount: failed to process /etc/fstab: ${error instanceof Error ? error.message : 'Unknown error'}`))
517
+ return 1
518
+ }
519
+ }
520
+
521
+ if (positionalArgs.length === 0) {
522
+ await writelnStderr(process, terminal, chalk.red('mount: missing target argument'))
523
+ await writelnStderr(process, terminal, 'Try \'mount --help\' for more information.')
524
+ return 1
525
+ }
526
+
527
+ if (positionalArgs.length > 2) {
528
+ await writelnStderr(process, terminal, chalk.red('mount: too many arguments'))
529
+ await writelnStderr(process, terminal, 'Try \'mount --help\' for more information.')
530
+ return 1
531
+ }
532
+
533
+ if (!type) {
534
+ await writelnStderr(process, terminal, chalk.red('mount: filesystem type must be specified'))
535
+ await writelnStderr(process, terminal, 'Try \'mount --help\' for more information.')
536
+ return 1
537
+ }
538
+
539
+ const source = positionalArgs.length === 2 ? positionalArgs[0] : ''
540
+ const targetArg = positionalArgs[positionalArgs.length - 1]
541
+
542
+ if (!targetArg) {
543
+ await writelnStderr(process, terminal, chalk.red('mount: missing target argument'))
544
+ return 1
545
+ }
546
+
547
+ const target = path.resolve(shell.cwd, targetArg)
548
+
549
+ if (positionalArgs.length === 2 && (type.toLowerCase() === 'memory' || type.toLowerCase() === 'singlebuffer' || type.toLowerCase() === 'webstorage' || type.toLowerCase() === 'webaccess' || type.toLowerCase() === 'xml' || type.toLowerCase() === 'dropbox' /* || type.toLowerCase() === 's3' */ || type.toLowerCase() === 'googledrive')) {
550
+ await writelnStderr(process, terminal, chalk.yellow(`mount: ${type.toLowerCase()} filesystem does not require a source`))
551
+ await writelnStderr(process, terminal, `Usage: mount -t ${type.toLowerCase()} TARGET`)
552
+ return 1
553
+ }
554
+
555
+ if (positionalArgs.length === 1 && (type.toLowerCase() === 'zip' || type.toLowerCase() === 'iso')) {
556
+ await writelnStderr(process, terminal, chalk.red(`mount: ${type.toLowerCase()} filesystem requires a source file or URL`))
557
+ await writelnStderr(process, terminal, `Usage: mount -t ${type.toLowerCase()} SOURCE TARGET`)
558
+ return 1
559
+ }
560
+
561
+ try {
562
+ const parentDir = path.dirname(target)
563
+ if (parentDir !== target && !(await shell.context.fs.promises.exists(parentDir))) {
564
+ await shell.context.fs.promises.mkdir(parentDir, { recursive: true })
565
+ }
566
+
567
+ if (!(await shell.context.fs.promises.exists(target))) {
568
+ await shell.context.fs.promises.mkdir(target, { recursive: true })
569
+ }
570
+
571
+ const mountOptions = options?.split(',').reduce((acc, option) => {
572
+ const [key, value] = option.split('=')
573
+ if (key && value) {
574
+ acc[key.trim()] = value.trim()
575
+ }
576
+ return acc
577
+ }, {} as Record<string, string>) || {}
578
+
579
+ switch (type.toLowerCase()) {
580
+ case 'fetch': {
581
+ let fetchBaseUrl = new URL(mountOptions.baseUrl || '').toString()
582
+ let indexUrl: string
583
+
584
+ if (source && /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(source)) {
585
+ indexUrl = source
586
+ } else {
587
+ indexUrl = new URL(source || 'index.json', fetchBaseUrl).toString()
588
+ }
589
+
590
+ await kernel.filesystem.fsSync.mount(
591
+ target,
592
+ await resolveMountConfig({
593
+ backend: Fetch,
594
+ index: indexUrl,
595
+ baseUrl: fetchBaseUrl,
596
+ disableAsyncCache: true,
597
+ })
598
+ )
599
+ break
600
+ }
601
+ case 'indexeddb':
602
+ await kernel.filesystem.fsSync.mount(
603
+ target,
604
+ await resolveMountConfig({
605
+ backend: IndexedDB,
606
+ storeName: source || target
607
+ })
608
+ )
609
+ break
610
+ case 'webstorage': {
611
+ const storageType = mountOptions.storage?.toLowerCase() || 'localstorage'
612
+ let storage: Storage
613
+
614
+ if (storageType === 'sessionstorage') {
615
+ if (typeof sessionStorage === 'undefined') {
616
+ await writelnStderr(process, terminal, chalk.red('mount: sessionStorage is not available in this environment'))
617
+ return 1
618
+ }
619
+ storage = sessionStorage
620
+ } else if (storageType === 'localstorage') {
621
+ if (typeof localStorage === 'undefined') {
622
+ await writelnStderr(process, terminal, chalk.red('mount: localStorage is not available in this environment'))
623
+ return 1
624
+ }
625
+ storage = localStorage
626
+ } else {
627
+ await writelnStderr(process, terminal, chalk.red(`mount: invalid storage type '${storageType}'. Use 'localStorage' or 'sessionStorage'`))
628
+ return 1
629
+ }
630
+
631
+ await kernel.filesystem.fsSync.mount(
632
+ target,
633
+ await resolveMountConfig({
634
+ backend: WebStorage,
635
+ storage
636
+ } as { backend: typeof WebStorage; storage: Storage })
637
+ )
638
+ break
639
+ }
640
+ case 'webaccess': {
641
+ if (typeof window === 'undefined') {
642
+ await writelnStderr(process, terminal, chalk.red('mount: File System Access API is not available in this environment'))
643
+ return 1
644
+ }
645
+
646
+ const win = window as unknown as { showDirectoryPicker?: () => Promise<FileSystemDirectoryHandle> }
647
+ if (!win.showDirectoryPicker) {
648
+ await writelnStderr(process, terminal, chalk.red('mount: File System Access API is not available in this environment'))
649
+ return 1
650
+ }
651
+
652
+ try {
653
+ const directoryHandle = await win.showDirectoryPicker()
654
+
655
+ await kernel.filesystem.fsSync.mount(
656
+ target,
657
+ await resolveMountConfig({
658
+ backend: WebAccess,
659
+ handle: directoryHandle
660
+ } as { backend: typeof WebAccess; handle: FileSystemDirectoryHandle })
661
+ )
662
+ } catch (error) {
663
+ if (error instanceof Error && error.name === 'AbortError') {
664
+ await writelnStderr(process, terminal, chalk.yellow('mount: directory selection cancelled'))
665
+ return 1
666
+ }
667
+ throw error
668
+ }
669
+ break
670
+ }
671
+ // TODO: Some more work needs to be done with the XML backend
672
+ // case 'xml': {
673
+ // if (typeof document === 'undefined') {
674
+ // await writelnStderr(process, terminal, chalk.red('mount: XML backend requires DOM APIs (document) which are not available in this environment'))
675
+ // return 1
676
+ // }
677
+
678
+ // let root: Element | undefined
679
+
680
+ // if (mountOptions.root) {
681
+ // const rootSelector = mountOptions.root
682
+ // const element = document.querySelector(rootSelector)
683
+ // if (!element) {
684
+ // await writelnStderr(process, terminal, chalk.yellow(`mount: root element '${rootSelector}' not found, creating new element`))
685
+ // root = new DOMParser().parseFromString('<fs></fs>', 'application/xml').documentElement
686
+ // root.setAttribute('id', 'xmlfs-' + Math.random().toString(36).substring(2, 15))
687
+ // root.setAttribute('style', 'display: none')
688
+ // } else {
689
+ // root = element as Element
690
+ // }
691
+ // } else {
692
+ // root = new DOMParser().parseFromString('<fs></fs>', 'application/xml').documentElement
693
+ // root.setAttribute('id', 'xmlfs-' + Math.random().toString(36).substring(2, 15))
694
+ // root.setAttribute('style', 'display: none')
695
+ // }
696
+
697
+ // if (!root) throw new Error('Failed to create root element')
698
+
699
+ // const rootNode = document.createElement('file')
700
+ // rootNode.setAttribute('paths', JSON.stringify(['/']))
701
+ // rootNode.setAttribute('nlink', '1')
702
+ // rootNode.setAttribute('mode', (constants.S_IFDIR | 0o777).toString(16))
703
+ // rootNode.setAttribute('uid', (0).toString(16))
704
+ // rootNode.setAttribute('gid', (0).toString(16))
705
+ // rootNode.textContent = '[]'
706
+
707
+ // root.appendChild(rootNode)
708
+
709
+ // try {
710
+ // const config = {
711
+ // backend: XML,
712
+ // root
713
+ // } as { backend: typeof XML; root: Element }
714
+
715
+ // document.body.appendChild(root)
716
+ // const mountConfig = await resolveMountConfig(config)
717
+ // await kernel.filesystem.fsSync.mount(target, mountConfig)
718
+ // } catch (error) {
719
+ // const errorMessage = error instanceof Error ? error.message : String(error)
720
+ // await writelnStderr(process, terminal, chalk.red(`mount: failed to mount XML filesystem: ${errorMessage}`))
721
+ // if (error instanceof Error && error.stack) {
722
+ // await writelnStderr(process, terminal, chalk.gray(`Stack: ${error.stack}`))
723
+ // }
724
+ // return 1
725
+ // }
726
+ // break
727
+ // }
728
+ case 'memory':
729
+ await kernel.filesystem.fsSync.mount(
730
+ target,
731
+ await resolveMountConfig({
732
+ backend: InMemory
733
+ })
734
+ )
735
+ break
736
+ case 'singlebuffer': {
737
+ const bufferSize = mountOptions.size
738
+ ? parseInt(mountOptions.size, 10)
739
+ : 1048576
740
+
741
+ if (isNaN(bufferSize) || bufferSize <= 0) {
742
+ await writelnStderr(process, terminal, chalk.red('mount: invalid buffer size for singlebuffer type'))
743
+ return 1
744
+ }
745
+
746
+ let buffer: ArrayBuffer | SharedArrayBuffer
747
+ try {
748
+ buffer = new SharedArrayBuffer(bufferSize)
749
+ } catch {
750
+ buffer = new ArrayBuffer(bufferSize)
751
+ }
752
+
753
+ await kernel.filesystem.fsSync.mount(
754
+ target,
755
+ await resolveMountConfig({
756
+ backend: SingleBuffer,
757
+ buffer
758
+ })
759
+ )
760
+ break
761
+ }
762
+ case 'zip': {
763
+ if (!source) {
764
+ await writelnStderr(process, terminal, chalk.red('mount: zip filesystem requires a source file or URL'))
765
+ return 1
766
+ }
767
+
768
+ let arrayBuffer: ArrayBuffer
769
+
770
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(source)) {
771
+ await writelnStdout(process, terminal, chalk.gray(`Fetching archive from ${source}...`))
772
+ const response = await fetch(source)
773
+ if (!response.ok) {
774
+ await writelnStderr(process, terminal, chalk.red(`mount: failed to fetch archive: ${response.status} ${response.statusText}`))
775
+ return 1
776
+ }
777
+ arrayBuffer = await response.arrayBuffer()
778
+ } else {
779
+ const sourcePath = path.resolve(shell.cwd, source)
780
+ if (!(await shell.context.fs.promises.exists(sourcePath))) {
781
+ await writelnStderr(process, terminal, chalk.red(`mount: archive file not found: ${sourcePath}`))
782
+ return 1
783
+ }
784
+ await writelnStdout(process, terminal, chalk.gray(`Reading archive from ${sourcePath}...`))
785
+ const fileData = await shell.context.fs.promises.readFile(sourcePath)
786
+ arrayBuffer = new Uint8Array(fileData).buffer
787
+ }
788
+
789
+ await kernel.filesystem.fsSync.mount(
790
+ target,
791
+ await resolveMountConfig({
792
+ backend: Zip,
793
+ data: arrayBuffer
794
+ })
795
+ )
796
+ break
797
+ }
798
+ case 'iso': {
799
+ if (!source) {
800
+ await writelnStderr(process, terminal, chalk.red('mount: iso filesystem requires a source file or URL'))
801
+ return 1
802
+ }
803
+
804
+ let uint8Array: Uint8Array
805
+
806
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(source)) {
807
+ await writelnStdout(process, terminal, chalk.gray(`Fetching ISO image from ${source}...`))
808
+ const response = await fetch(source)
809
+ if (!response.ok) {
810
+ await writelnStderr(process, terminal, chalk.red(`mount: failed to fetch ISO image: ${response.status} ${response.statusText}`))
811
+ return 1
812
+ }
813
+ const arrayBuffer = await response.arrayBuffer()
814
+ uint8Array = new Uint8Array(arrayBuffer)
815
+ } else {
816
+ const sourcePath = path.resolve(shell.cwd, source)
817
+ if (!(await shell.context.fs.promises.exists(sourcePath))) {
818
+ await writelnStderr(process, terminal, chalk.red(`mount: ISO image file not found: ${sourcePath}`))
819
+ return 1
820
+ }
821
+ await writelnStdout(process, terminal, chalk.gray(`Reading ISO image from ${sourcePath}...`))
822
+ uint8Array = await shell.context.fs.promises.readFile(sourcePath)
823
+ }
824
+
825
+ await kernel.filesystem.fsSync.mount(
826
+ target,
827
+ await resolveMountConfig({
828
+ backend: Iso,
829
+ data: uint8Array
830
+ })
831
+ )
832
+ break
833
+ }
834
+ case 'dropbox': {
835
+ if (!mountOptions.client) {
836
+ await writelnStderr(process, terminal, chalk.red('mount: dropbox filesystem requires client configuration'))
837
+ await writelnStderr(process, terminal, 'Usage: mount -t dropbox TARGET -o client=\'{"accessToken":"..."}\'')
838
+ return 1
839
+ }
840
+
841
+ try {
842
+ let clientConfig: { accessToken: string; [key: string]: unknown }
843
+ try {
844
+ clientConfig = JSON.parse(mountOptions.client)
845
+ } catch {
846
+ await writelnStderr(process, terminal, chalk.red('mount: invalid JSON in client option'))
847
+ return 1
848
+ }
849
+
850
+ if (!clientConfig.accessToken) {
851
+ await writelnStderr(process, terminal, chalk.red('mount: client configuration must include accessToken'))
852
+ return 1
853
+ }
854
+
855
+ const dropboxModule = await import('dropbox')
856
+ const DropboxClient = dropboxModule.Dropbox
857
+ const client = new DropboxClient(clientConfig)
858
+ const cacheTTL = mountOptions.cacheTTL ? parseInt(mountOptions.cacheTTL, 10) : undefined
859
+
860
+ await kernel.filesystem.fsSync.mount(
861
+ target,
862
+ await resolveMountConfig({
863
+ backend: Dropbox,
864
+ client,
865
+ ...(cacheTTL && !isNaN(cacheTTL) ? { cacheTTL } : {})
866
+ })
867
+ )
868
+ } catch (error) {
869
+ await writelnStderr(process, terminal, chalk.red(`mount: failed to mount dropbox filesystem: ${error instanceof Error ? error.message : 'Unknown error'}`))
870
+ return 1
871
+ }
872
+ break
873
+ }
874
+ /* case 's3': {
875
+ if (!mountOptions.bucket) {
876
+ await writelnStderr(process, terminal, chalk.red('mount: s3 filesystem requires bucket option'))
877
+ await writelnStderr(process, terminal, 'Usage: mount -t s3 TARGET -o bucket=my-bucket')
878
+ return 1
879
+ }
880
+
881
+ try {
882
+ // Start with default config
883
+ let clientConfigRaw: { region?: string; credentials?: { accessKeyId?: string; secretAccessKey?: string; sessionToken?: string }; [key: string]: unknown } = {}
884
+
885
+ // Parse client config if provided
886
+ if (mountOptions.client) {
887
+ try {
888
+ clientConfigRaw = JSON.parse(mountOptions.client)
889
+ } catch {
890
+ await writelnStderr(process, terminal, chalk.red('mount: invalid JSON in client option'))
891
+ return 1
892
+ }
893
+ }
894
+
895
+ // Set region: use from config, then env var, then default
896
+ if (!clientConfigRaw.region) {
897
+ clientConfigRaw.region = shell.env.get('AWS_DEFAULT_REGION') || 'us-east-1'
898
+ }
899
+
900
+ // Use environment variables as defaults if credentials not provided
901
+ if (!clientConfigRaw.credentials) {
902
+ const accessKeyId = shell.env.get('AWS_ACCESS_KEY_ID')
903
+ const secretAccessKey = shell.env.get('AWS_SECRET_ACCESS_KEY')
904
+ const sessionToken = shell.env.get('AWS_SESSION_TOKEN')
905
+
906
+ if (accessKeyId && secretAccessKey) {
907
+ clientConfigRaw.credentials = {
908
+ accessKeyId,
909
+ secretAccessKey,
910
+ ...(sessionToken ? { sessionToken } : {})
911
+ }
912
+ }
913
+ } else {
914
+ // Validate credentials if provided
915
+ if (!clientConfigRaw.credentials.accessKeyId || !clientConfigRaw.credentials.secretAccessKey) {
916
+ await writelnStderr(process, terminal, chalk.yellow('mount: credentials object should include both accessKeyId and secretAccessKey'))
917
+ await writelnStderr(process, terminal, 'Note: If credentials are not provided, AWS SDK will use default credential chain (env vars, IAM role, etc.)')
918
+ }
919
+ }
920
+
921
+ // Configure for browser environment if needed
922
+ if (typeof window !== 'undefined') {
923
+ // Ensure we're using fetch for browser requests
924
+ if (!clientConfigRaw.requestHandler) {
925
+ // The AWS SDK v3 uses fetch by default in browsers, but we can explicitly set it
926
+ // This helps ensure CORS is handled properly
927
+ clientConfigRaw.requestHandler = undefined // Let SDK use default browser fetch
928
+ }
929
+ }
930
+
931
+ const s3Module = await import('@aws-sdk/client-s3')
932
+ const S3Client = s3Module.S3
933
+ const client = new S3Client(clientConfigRaw as never)
934
+ const bucketName = mountOptions.bucket
935
+ const prefix = mountOptions.prefix
936
+ const cacheTTL = mountOptions.cacheTTL ? parseInt(mountOptions.cacheTTL, 10) : undefined
937
+
938
+ try {
939
+ await kernel.filesystem.fsSync.mount(
940
+ target,
941
+ await resolveMountConfig({
942
+ backend: S3Bucket,
943
+ client,
944
+ bucketName,
945
+ ...(prefix ? { prefix } : {}),
946
+ ...(cacheTTL && !isNaN(cacheTTL) ? { cacheTTL } : {})
947
+ })
948
+ )
949
+ } catch (mountError) {
950
+ const errorMessage = mountError instanceof Error ? mountError.message : String(mountError)
951
+ // Provide helpful guidance for common S3 errors
952
+ await writelnStderr(process, terminal, chalk.red(`mount: failed to mount s3 filesystem: ${errorMessage}`))
953
+ await writelnStderr(process, terminal, chalk.yellow('\nS3 CORS configuration may be required:'))
954
+ await writelnStderr(process, terminal, 'For browser access, your S3 bucket needs CORS configuration:')
955
+ await writelnStderr(process, terminal, ' {')
956
+ await writelnStderr(process, terminal, ' "CORSRules": [{')
957
+ await writelnStderr(process, terminal, ' "AllowedOrigins": ["*"],')
958
+ await writelnStderr(process, terminal, ' "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],')
959
+ await writelnStderr(process, terminal, ' "AllowedHeaders": ["*"],')
960
+ await writelnStderr(process, terminal, ' "ExposeHeaders": ["ETag"],')
961
+ await writelnStderr(process, terminal, ' "MaxAgeSeconds": 3000')
962
+ await writelnStderr(process, terminal, ' }]')
963
+ await writelnStderr(process, terminal, ' }')
964
+ await writelnStderr(process, terminal, chalk.gray('\nAlso ensure your bucket policy allows the required operations.'))
965
+ throw mountError
966
+ }
967
+ } catch (error) {
968
+ await writelnStderr(process, terminal, chalk.red(`mount: failed to mount s3 filesystem: ${error instanceof Error ? error.message : 'Unknown error'}`))
969
+ if (error instanceof Error && error.stack) {
970
+ await writelnStderr(process, terminal, chalk.gray(error.stack))
971
+ }
972
+ return 1
973
+ }
974
+ break
975
+ } */
976
+ case 'googledrive': {
977
+ try {
978
+ if (typeof window === 'undefined') {
979
+ await writelnStderr(process, terminal, chalk.red('mount: Google Drive API requires a browser environment'))
980
+ return 1
981
+ }
982
+
983
+ // if (!mountOptions.apiKey) {
984
+ // await writelnStderr(process, terminal, chalk.red('mount: googledrive filesystem requires apiKey option'))
985
+ // await writelnStderr(process, terminal, 'Usage: mount -t googledrive TARGET -o apiKey=YOUR_API_KEY')
986
+ // return 1
987
+ // }
988
+
989
+ const win = window as unknown as {
990
+ gapi?: {
991
+ load?: (module: string, callback: () => void) => void
992
+ client?: {
993
+ init?: (config: { apiKey: string; discoveryDocs?: string[] }) => Promise<void>
994
+ request?: (config: { path: string }) => Promise<unknown>
995
+ setToken?: (token: { access_token: string }) => void
996
+ drive?: unknown
997
+ }
998
+ }
999
+ google?: {
1000
+ accounts?: {
1001
+ id?: {
1002
+ initialize: (config: { client_id: string; callback: (response: { credential: string }) => void; scope?: string }) => void
1003
+ prompt: (callback?: (notification: { isNotDisplayed: boolean; isSkippedMoment: boolean; isDismissedMoment: boolean }) => void) => void
1004
+ renderButton: (element: HTMLElement, config: { theme?: string; size?: string; text?: string; width?: number; locale?: string }) => void
1005
+ }
1006
+ oauth2?: {
1007
+ initTokenClient: (config: { client_id: string; scope: string; callback: (response: { access_token: string }) => void }) => { requestAccessToken: () => void }
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ // Load Google Identity Services library if not already loaded
1014
+ if (!win.google?.accounts) {
1015
+ await writelnStdout(process, terminal, chalk.gray('Loading Google Identity Services library...'))
1016
+
1017
+ await new Promise<void>((resolve, reject) => {
1018
+ const script = document.createElement('script')
1019
+ script.src = 'https://accounts.google.com/gsi/client'
1020
+ script.async = true
1021
+ script.defer = true
1022
+ script.onload = () => resolve()
1023
+ script.onerror = () => reject(new Error('Failed to load Google Identity Services script'))
1024
+ document.head.appendChild(script)
1025
+ })
1026
+ }
1027
+
1028
+ // Wait for google.accounts to be available
1029
+ let attempts = 0
1030
+ while (!win.google?.accounts && attempts < 50) {
1031
+ await new Promise(resolve => setTimeout(resolve, 100))
1032
+ attempts++
1033
+ }
1034
+
1035
+ if (!win.google?.accounts) {
1036
+ await writelnStderr(process, terminal, chalk.red('mount: Failed to load Google Identity Services library'))
1037
+ return 1
1038
+ }
1039
+
1040
+ // Load Google API script if not already loaded
1041
+ if (!win.gapi) {
1042
+ await writelnStdout(process, terminal, chalk.gray('Loading Google API client library...'))
1043
+
1044
+ await new Promise<void>((resolve, reject) => {
1045
+ const script = document.createElement('script')
1046
+ script.src = 'https://apis.google.com/js/api.js'
1047
+ script.onload = () => resolve()
1048
+ script.onerror = () => reject(new Error('Failed to load Google API script'))
1049
+ document.head.appendChild(script)
1050
+ })
1051
+ }
1052
+
1053
+ // Wait for gapi to be available
1054
+ attempts = 0
1055
+ while (!win.gapi && attempts < 50) {
1056
+ await new Promise(resolve => setTimeout(resolve, 100))
1057
+ attempts++
1058
+ }
1059
+
1060
+ if (!win.gapi) {
1061
+ await writelnStderr(process, terminal, chalk.red('mount: Failed to load Google API client library'))
1062
+ return 1
1063
+ }
1064
+
1065
+ if (!win.gapi.client || !win.gapi.client.drive) {
1066
+ await writelnStdout(process, terminal, chalk.gray('Initializing Google API client...'))
1067
+
1068
+ const initConfig: {
1069
+ apiKey: string
1070
+ discoveryDocs?: string[]
1071
+ } = {
1072
+ apiKey: mountOptions.apiKey!,
1073
+ discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest']
1074
+ }
1075
+
1076
+ // Load the client module
1077
+ await new Promise<void>((resolve, reject) => {
1078
+ if (!win.gapi?.load) {
1079
+ reject(new Error('gapi.load is not available'))
1080
+ return
1081
+ }
1082
+ win.gapi.load('client', () => {
1083
+ if (!win.gapi?.client?.init) {
1084
+ reject(new Error('gapi.client.init is not available'))
1085
+ return
1086
+ }
1087
+ win.gapi.client.init(initConfig)
1088
+ .then(() => {
1089
+ setTimeout(() => {
1090
+ if (win.gapi?.client?.drive) {
1091
+ resolve()
1092
+ } else {
1093
+ reject(new Error('API discovery response missing required fields. The Drive API discovery document may have failed to load. This could be due to: network issues, invalid API key, or the Drive API not being enabled in your Google Cloud project.'))
1094
+ }
1095
+ }, 1000)
1096
+ })
1097
+ .catch((error: Error & { error?: string; details?: string }) => {
1098
+ const err = error as Error & { error?: string; details?: string }
1099
+ let errorMessage = 'Unknown error'
1100
+
1101
+ errorMessage = JSON.stringify({
1102
+ error: err.error,
1103
+ details: err.details
1104
+ }, null, 2)
1105
+
1106
+ const lowerError = err.error?.toLowerCase()
1107
+ if (lowerError?.includes('discovery') || lowerError?.includes('API') || lowerError?.includes('required fields')) {
1108
+ reject(new Error(`API discovery failed: ${errorMessage}. This could be due to: network issues, invalid API key, or the Drive API not being enabled in your Google Cloud project.`))
1109
+ } else {
1110
+ reject(new Error(err.error || err.details || 'Unknown error'))
1111
+ }
1112
+ })
1113
+ })
1114
+ })
1115
+
1116
+ // Verify Drive API is loaded
1117
+ if (!win.gapi?.client?.drive) {
1118
+ // Try one more time with a longer wait
1119
+ await new Promise<void>((resolve, reject) => {
1120
+ let attempts = 0
1121
+ const checkDrive = () => {
1122
+ if (win.gapi?.client?.drive) {
1123
+ resolve()
1124
+ } else if (attempts < 10) {
1125
+ attempts++
1126
+ setTimeout(checkDrive, 200)
1127
+ } else {
1128
+ reject(new Error('Failed to load Drive API. The discovery document may have failed to load. Check the browser console for network errors (502 Bad Gateway suggests a network issue).'))
1129
+ }
1130
+ }
1131
+ checkDrive()
1132
+ })
1133
+ }
1134
+ }
1135
+
1136
+ if (!win.gapi?.client?.drive) {
1137
+ await writelnStderr(process, terminal, chalk.red('mount: Google Drive API is not available'))
1138
+ await writelnStderr(process, terminal, chalk.yellow('Troubleshooting steps:'))
1139
+ await writelnStderr(process, terminal, ' 1. Check the browser console for network errors (502 Bad Gateway suggests a network/server issue)')
1140
+ await writelnStderr(process, terminal, ' 2. Verify your API key is valid and has the Drive API enabled')
1141
+ await writelnStderr(process, terminal, ' 3. Ensure the Drive API is enabled in your Google Cloud project')
1142
+ await writelnStderr(process, terminal, ' 4. Check if there are any API key restrictions (HTTP referrers, IP addresses, etc.)')
1143
+ await writelnStderr(process, terminal, ' 5. Try refreshing the page and mounting again')
1144
+ return 1
1145
+ }
1146
+
1147
+ // Handle OAuth authentication if clientId is provided
1148
+ if (mountOptions.clientId) {
1149
+ await writelnStdout(process, terminal, chalk.gray('Checking authentication status...'))
1150
+
1151
+ const driveScope = mountOptions.scope || 'https://www.googleapis.com/auth/drive'
1152
+
1153
+ // Check if user is already authenticated
1154
+ try {
1155
+ const client = win.gapi?.client
1156
+ if (client?.request) {
1157
+ await (client.request as (config: { path: string }) => Promise<unknown>)({
1158
+ path: 'https://www.googleapis.com/drive/v3/about?fields=user'
1159
+ })
1160
+ }
1161
+ } catch (error) {
1162
+ // User needs to authenticate using Google Identity Services
1163
+ await writelnStdout(process, terminal, chalk.gray('Authentication required. Please sign in to Google...'))
1164
+
1165
+ try {
1166
+ if (!win.google?.accounts?.oauth2) {
1167
+ throw new Error('Google Identity Services OAuth2 is not available')
1168
+ }
1169
+
1170
+ await new Promise<void>((resolve, reject) => {
1171
+ const tokenClient = win.google?.accounts?.oauth2?.initTokenClient({
1172
+ client_id: mountOptions.clientId!,
1173
+ scope: driveScope,
1174
+ callback: (response: { access_token: string }) => {
1175
+ if (response.access_token && win.gapi?.client?.setToken) {
1176
+ win.gapi.client.setToken({ access_token: response.access_token })
1177
+ resolve()
1178
+ } else {
1179
+ reject(new Error('Failed to obtain access token'))
1180
+ }
1181
+ },
1182
+ })
1183
+
1184
+ tokenClient?.requestAccessToken()
1185
+ })
1186
+ } catch (authError) {
1187
+ const authErrMsg = authError instanceof Error ? authError.message : String(authError)
1188
+ if (authErrMsg.toLowerCase().includes('popup') || authErrMsg.toLowerCase().includes('blocked')) {
1189
+ throw new Error(`OAuth authentication failed: ${authErrMsg}. Please allow popups for this site.`)
1190
+ } else if (authErrMsg.toLowerCase().includes('origin') || authErrMsg.toLowerCase().includes('authorized')) {
1191
+ throw new Error(`OAuth authentication failed: ${authErrMsg}. Your origin may not be authorized in Google Cloud Console. Add your current origin to the OAuth client's authorized JavaScript origins.`)
1192
+ }
1193
+ throw authError
1194
+ }
1195
+ }
1196
+ }
1197
+
1198
+ const drive = win.gapi.client.drive
1199
+ const cacheTTL = mountOptions.cacheTTL ? parseInt(mountOptions.cacheTTL) : undefined
1200
+
1201
+ await kernel.filesystem.fsSync.mount(
1202
+ target,
1203
+ await resolveMountConfig({
1204
+ backend: GoogleDrive,
1205
+ drive: drive as never,
1206
+ disableAsyncCache: true,
1207
+ ...(cacheTTL && !isNaN(cacheTTL) ? { cacheTTL } : {})
1208
+ })
1209
+ )
1210
+ } catch (error) {
1211
+ let errorMessage = 'Unknown error'
1212
+ if (error instanceof Error) {
1213
+ errorMessage = error.message || error.toString()
1214
+ } else if (typeof error === 'string') {
1215
+ errorMessage = error
1216
+ } else if (error && typeof error === 'object') {
1217
+ // Try to extract error message from object
1218
+ const err = error as Record<string, unknown>
1219
+ errorMessage =
1220
+ (typeof err.message === 'string' ? err.message : '') ||
1221
+ (typeof err.error === 'string' ? err.error : '') ||
1222
+ (typeof err.details === 'string' ? err.details : '') ||
1223
+ (typeof err.reason === 'string' ? err.reason : '') ||
1224
+ (err.toString && typeof err.toString === 'function' ? err.toString() : '') ||
1225
+ JSON.stringify(error)
1226
+ } else {
1227
+ errorMessage = String(error)
1228
+ }
1229
+
1230
+ await writelnStderr(process, terminal, chalk.red(`mount: failed to mount googledrive filesystem: ${errorMessage}`))
1231
+
1232
+ // Provide specific guidance for common errors
1233
+ const lowerMessage = errorMessage.toLowerCase()
1234
+ if (lowerMessage.includes('popup') || lowerMessage.includes('blocked')) {
1235
+ await writelnStderr(process, terminal, chalk.yellow('\nOAuth popup was blocked. Common causes:'))
1236
+ await writelnStderr(process, terminal, ' • Browser popup blocker is enabled')
1237
+ await writelnStderr(process, terminal, ' • Browser security restrictions')
1238
+ await writelnStderr(process, terminal, chalk.gray('\nTo fix this:'))
1239
+ await writelnStderr(process, terminal, ' 1. Allow popups for this site in your browser settings')
1240
+ await writelnStderr(process, terminal, ' 2. Try the mount command again')
1241
+ } else if (lowerMessage.includes('origin') || lowerMessage.includes('authorized')) {
1242
+ await writelnStderr(process, terminal, chalk.yellow('\nOAuth authentication failed. Common causes:'))
1243
+ await writelnStderr(process, terminal, ' • Your domain/origin is not authorized in Google Cloud Console')
1244
+ await writelnStderr(process, terminal, ' • Invalid or incorrect OAuth client ID')
1245
+ await writelnStderr(process, terminal, chalk.gray('\nTo fix this:'))
1246
+ await writelnStderr(process, terminal, ' 1. Go to Google Cloud Console > APIs & Services > Credentials')
1247
+ await writelnStderr(process, terminal, ' 2. Find your OAuth 2.0 Client ID')
1248
+ await writelnStderr(process, terminal, ' 3. Add your current origin to "Authorized JavaScript origins"')
1249
+ await writelnStderr(process, terminal, ' (e.g., http://localhost:30443 or your domain)')
1250
+ await writelnStderr(process, terminal, ' 4. If you only need read-only access, try mounting without clientId:')
1251
+ await writelnStderr(process, terminal, ' mount -t googledrive /mnt/gdrive -o apiKey=YOUR_API_KEY')
1252
+ } else if (lowerMessage.includes('discovery') || lowerMessage.includes('api discovery') || lowerMessage.includes('required fields')) {
1253
+ await writelnStderr(process, terminal, chalk.yellow('\nThe Drive API discovery document failed to load. Common causes:'))
1254
+ await writelnStderr(process, terminal, ' • Network connectivity issues (check for 502 Bad Gateway in console)')
1255
+ await writelnStderr(process, terminal, ' • Invalid or restricted API key')
1256
+ await writelnStderr(process, terminal, ' • Drive API not enabled in Google Cloud project')
1257
+ await writelnStderr(process, terminal, ' • CORS or browser security restrictions')
1258
+ await writelnStderr(process, terminal, chalk.gray('\nCheck the browser console for detailed network error messages.'))
1259
+ } else if (lowerMessage.includes('network') || lowerMessage.includes('fetch') || lowerMessage.includes('502') || lowerMessage.includes('bad gateway')) {
1260
+ await writelnStderr(process, terminal, chalk.yellow('\nNetwork error detected. This may be a temporary issue with Google\'s servers.'))
1261
+ await writelnStderr(process, terminal, ' • Wait a few moments and try again')
1262
+ await writelnStderr(process, terminal, ' • Check your internet connection')
1263
+ await writelnStderr(process, terminal, ' • Verify the API key is correct')
1264
+ }
1265
+
1266
+ // Show full error details if it's an object (for debugging)
1267
+ if (error && typeof error === 'object' && !(error instanceof Error)) {
1268
+ try {
1269
+ const errorStr = JSON.stringify(error, null, 2)
1270
+ if (errorStr !== '{}' && errorStr.length < 500) {
1271
+ await writelnStderr(process, terminal, chalk.gray(`\nError details:\n${errorStr}`))
1272
+ }
1273
+ } catch {
1274
+ // Ignore JSON stringify errors
1275
+ }
1276
+ }
1277
+
1278
+ if (error instanceof Error && error.stack && !lowerMessage.includes('discovery') && !lowerMessage.includes('network')) {
1279
+ await writelnStderr(process, terminal, chalk.gray(`\nStack trace:\n${error.stack}`))
1280
+ }
1281
+ return 1
1282
+ }
1283
+ break
1284
+ }
1285
+ default:
1286
+ await writelnStderr(process, terminal, chalk.red(`mount: unknown filesystem type '${type}'`))
1287
+ await writelnStderr(process, terminal, 'Supported types: fetch, indexeddb, webstorage, webaccess, memory, singlebuffer, zip, iso, dropbox, s3, googledrive')
1288
+ return 1
1289
+ }
1290
+
1291
+ const successMessage = source
1292
+ ? chalk.green(`Mounted ${type} filesystem from ${source} to ${target}`)
1293
+ : chalk.green(`Mounted ${type} filesystem at ${target}`)
1294
+ await writelnStdout(process, terminal, successMessage)
1295
+ return 0
1296
+ } catch (error) {
1297
+ await writelnStderr(process, terminal, chalk.red(`mount: failed to mount filesystem: ${error instanceof Error ? error.message : 'Unknown error'}`))
1298
+ return 1
1299
+ }
1300
+ }
1301
+ })
1302
+ }