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