@ecmaos/coreutils 0.3.0 → 0.3.1

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 (46) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/dist/commands/cron.d.ts +4 -0
  4. package/dist/commands/cron.d.ts.map +1 -0
  5. package/dist/commands/cron.js +439 -0
  6. package/dist/commands/cron.js.map +1 -0
  7. package/dist/commands/man.d.ts.map +1 -1
  8. package/dist/commands/man.js +10 -0
  9. package/dist/commands/man.js.map +1 -1
  10. package/dist/commands/open.d.ts +4 -0
  11. package/dist/commands/open.d.ts.map +1 -0
  12. package/dist/commands/open.js +74 -0
  13. package/dist/commands/open.js.map +1 -0
  14. package/dist/commands/play.d.ts +4 -0
  15. package/dist/commands/play.d.ts.map +1 -0
  16. package/dist/commands/play.js +231 -0
  17. package/dist/commands/play.js.map +1 -0
  18. package/dist/commands/tar.d.ts.map +1 -1
  19. package/dist/commands/tar.js +67 -17
  20. package/dist/commands/tar.js.map +1 -1
  21. package/dist/commands/video.d.ts +4 -0
  22. package/dist/commands/video.d.ts.map +1 -0
  23. package/dist/commands/video.js +250 -0
  24. package/dist/commands/video.js.map +1 -0
  25. package/dist/commands/view.d.ts +4 -0
  26. package/dist/commands/view.d.ts.map +1 -0
  27. package/dist/commands/view.js +488 -0
  28. package/dist/commands/view.js.map +1 -0
  29. package/dist/commands/web.d.ts +4 -0
  30. package/dist/commands/web.d.ts.map +1 -0
  31. package/dist/commands/web.js +348 -0
  32. package/dist/commands/web.js.map +1 -0
  33. package/dist/index.d.ts +6 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +20 -2
  36. package/dist/index.js.map +1 -1
  37. package/package.json +3 -2
  38. package/src/commands/cron.ts +499 -0
  39. package/src/commands/man.ts +8 -0
  40. package/src/commands/open.ts +84 -0
  41. package/src/commands/play.ts +249 -0
  42. package/src/commands/tar.ts +62 -19
  43. package/src/commands/video.ts +267 -0
  44. package/src/commands/view.ts +526 -0
  45. package/src/commands/web.ts +377 -0
  46. package/src/index.ts +20 -2
@@ -0,0 +1,499 @@
1
+ import path from 'path'
2
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
3
+ import { TerminalCommand } from '../shared/terminal-command.js'
4
+ import { writelnStdout, writelnStderr } from '../shared/helpers.js'
5
+ import { parseCronExpression } from 'cron-schedule'
6
+
7
+ interface CrontabEntry {
8
+ expression: string
9
+ command: string
10
+ lineNumber: number
11
+ }
12
+
13
+ /**
14
+ * Parse a single crontab line
15
+ * @param line - The line to parse
16
+ * @returns Parsed entry or null if line is empty/comment
17
+ */
18
+ function parseCrontabLine(line: string): { expression: string, command: string } | null {
19
+ const trimmed = line.trim()
20
+
21
+ // Skip empty lines and comments
22
+ if (trimmed === '' || trimmed.startsWith('#')) {
23
+ return null
24
+ }
25
+
26
+ // Split by whitespace - cron expression is first 5 or 6 fields
27
+ const parts = trimmed.split(/\s+/)
28
+
29
+ if (parts.length < 6) {
30
+ // Need at least 5 fields for cron expression + command
31
+ return null
32
+ }
33
+
34
+ // Standard format: minute hour day month weekday command (5 fields + command)
35
+ // Extended format: second minute hour day month weekday command (6 fields + command)
36
+ // We need to determine which format is being used
37
+ let expression: string
38
+ let command: string
39
+
40
+ // If we have exactly 6 parts, it must be 5-field format (5 cron fields + 1 command word)
41
+ if (parts.length === 6) {
42
+ // 5-field format: minute hour day month weekday command
43
+ expression = parts.slice(0, 5).join(' ')
44
+ command = parts[5] || ''
45
+ } else {
46
+ // 7+ parts: could be 5-field with multi-word command or 6-field format
47
+ // Try parsing as 5-field first (more common), then 6-field if that fails
48
+ const potential5Field = parts.slice(0, 5).join(' ')
49
+ const potential6Field = parts.slice(0, 6).join(' ')
50
+
51
+ let valid5Field = false
52
+ let valid6Field = false
53
+
54
+ // Try to validate 5-field format
55
+ try {
56
+ parseCronExpression(potential5Field)
57
+ valid5Field = true
58
+ } catch {
59
+ // Not a valid 5-field expression
60
+ }
61
+
62
+ // Try to validate 6-field format
63
+ try {
64
+ parseCronExpression(potential6Field)
65
+ valid6Field = true
66
+ } catch {
67
+ // Not a valid 6-field expression
68
+ }
69
+
70
+ // Prefer 5-field format if both are valid (more common)
71
+ // Only use 6-field if 5-field is invalid but 6-field is valid
72
+ if (valid5Field) {
73
+ expression = potential5Field
74
+ command = parts.slice(5).join(' ')
75
+ } else if (valid6Field) {
76
+ expression = potential6Field
77
+ command = parts.slice(6).join(' ')
78
+ } else {
79
+ // Neither format is valid, return null
80
+ return null
81
+ }
82
+ }
83
+
84
+ // Validate the expression
85
+ try {
86
+ parseCronExpression(expression)
87
+ } catch {
88
+ return null
89
+ }
90
+
91
+ command = command.trim()
92
+ return { expression, command }
93
+ }
94
+
95
+ /**
96
+ * Parse a complete crontab file
97
+ * @param content - The crontab file content
98
+ * @returns Array of parsed crontab entries
99
+ */
100
+ function parseCrontabFile(content: string): CrontabEntry[] {
101
+ const lines = content.split('\n')
102
+ const entries: CrontabEntry[] = []
103
+
104
+ for (let i = 0; i < lines.length; i++) {
105
+ const line = lines[i]?.trim()
106
+ if (!line) continue
107
+ const parsed = parseCrontabLine(line)
108
+
109
+ if (parsed) {
110
+ entries.push({
111
+ expression: parsed.expression,
112
+ command: parsed.command,
113
+ lineNumber: i + 1
114
+ })
115
+ }
116
+ }
117
+
118
+ return entries
119
+ }
120
+
121
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
122
+ const usage = `Usage: cron [COMMAND] [OPTIONS]
123
+
124
+ Manage scheduled tasks (crontabs).
125
+
126
+ Commands:
127
+ list List all active cron jobs
128
+ add <schedule> <cmd> Add a new cron job to user crontab
129
+ remove <id> Remove a cron job by ID
130
+ edit Open user crontab in editor
131
+ validate <expression> Validate a cron expression
132
+ next <expression> [N] Show next N execution times (default: 1)
133
+ test <expression> Test if expression matches current time
134
+ reload Reload crontabs from files
135
+
136
+ Examples:
137
+ cron list List all cron jobs
138
+ cron add "*/5 * * * *" "echo hello" Add job to run every 5 minutes
139
+ cron add "0 0 * * *" "echo daily" Add daily job at midnight
140
+ cron remove cron:user:1 Remove user cron job #1
141
+ cron validate "*/5 * * * *" Validate cron expression
142
+ cron next "*/5 * * * *" 5 Show next 5 execution times
143
+ cron test "*/5 * * * *" Test if expression matches now
144
+
145
+ Crontab format:
146
+ Standard: minute hour day month weekday command
147
+ Extended: second minute hour day month weekday command
148
+
149
+ --help display this help and exit`
150
+ writelnStderr(process, terminal, usage)
151
+ }
152
+
153
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
154
+ return new TerminalCommand({
155
+ command: 'cron',
156
+ description: 'Manage scheduled tasks (crontabs)',
157
+ kernel,
158
+ shell,
159
+ terminal,
160
+ run: async (pid: number, argv: string[]) => {
161
+ const process = kernel.processes.get(pid) as Process | undefined
162
+
163
+ if (argv.length === 0 || (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h'))) {
164
+ printUsage(process, terminal)
165
+ return 0
166
+ }
167
+
168
+ const subcommand = argv[0]
169
+
170
+ try {
171
+ switch (subcommand) {
172
+ case 'list': {
173
+ const cronJobs = kernel.intervals.listCrons()
174
+ if (cronJobs.length === 0) {
175
+ await writelnStdout(process, terminal, 'No active cron jobs.')
176
+ return 0
177
+ }
178
+
179
+ await writelnStdout(process, terminal, 'Active cron jobs:')
180
+ for (const jobName of cronJobs) {
181
+ await writelnStdout(process, terminal, ` ${jobName}`)
182
+ }
183
+ return 0
184
+ }
185
+
186
+ case 'add': {
187
+ if (argv.length < 3) {
188
+ await writelnStderr(process, terminal, 'cron add: missing arguments')
189
+ await writelnStderr(process, terminal, 'Usage: cron add <schedule> <command>')
190
+ return 1
191
+ }
192
+
193
+ const schedule = argv[1]
194
+ if (!schedule) {
195
+ await writelnStderr(process, terminal, 'cron add: missing schedule')
196
+ await writelnStderr(process, terminal, 'Usage: cron add <schedule> <command>')
197
+ return 1
198
+ }
199
+ const command = argv.slice(2).join(' ')
200
+
201
+ // Validate the cron expression
202
+ try {
203
+ parseCronExpression(schedule)
204
+ } catch (error) {
205
+ await writelnStderr(process, terminal, `cron add: invalid cron expression: ${schedule}`)
206
+ return 1
207
+ }
208
+
209
+ // Get user home directory
210
+ const home = shell.env.get('HOME') ?? '/root'
211
+ const configDir = path.join(home, '.config')
212
+ const crontabPath = path.join(configDir, 'crontab')
213
+
214
+ // Ensure .config directory exists
215
+ try {
216
+ if (!await shell.context.fs.promises.exists(configDir)) {
217
+ await shell.context.fs.promises.mkdir(configDir, { recursive: true })
218
+ }
219
+ } catch (error) {
220
+ await writelnStderr(process, terminal, `cron add: failed to create .config directory: ${error}`)
221
+ return 1
222
+ }
223
+
224
+ // Read existing crontab or create new
225
+ let crontabContent = ''
226
+ try {
227
+ if (await shell.context.fs.promises.exists(crontabPath)) {
228
+ crontabContent = await shell.context.fs.promises.readFile(crontabPath, 'utf-8')
229
+ if (!crontabContent.endsWith('\n') && crontabContent.length > 0) {
230
+ crontabContent += '\n'
231
+ }
232
+ }
233
+ } catch (error) {
234
+ // File doesn't exist or can't be read, start fresh
235
+ crontabContent = ''
236
+ }
237
+
238
+ // Add new entry
239
+ crontabContent += `${schedule} ${command}\n`
240
+
241
+ // Write back to file
242
+ try {
243
+ await shell.context.fs.promises.writeFile(crontabPath, crontabContent, { encoding: 'utf-8' })
244
+ } catch (error) {
245
+ await writelnStderr(process, terminal, `cron add: failed to write crontab: ${error}`)
246
+ return 1
247
+ }
248
+
249
+ await writelnStdout(process, terminal, `Added cron job: ${schedule} ${command}`)
250
+ await writelnStdout(process, terminal, 'Run "cron reload" to activate the new job.')
251
+ return 0
252
+ }
253
+
254
+ case 'remove': {
255
+ if (argv.length < 2) {
256
+ await writelnStderr(process, terminal, 'cron remove: missing job ID')
257
+ await writelnStderr(process, terminal, 'Usage: cron remove <id>')
258
+ return 1
259
+ }
260
+
261
+ const jobId = argv[1]
262
+ if (!jobId) {
263
+ await writelnStderr(process, terminal, 'cron remove: missing job ID')
264
+ return 1
265
+ }
266
+
267
+ const handle = kernel.intervals.getCron(jobId)
268
+
269
+ if (!handle) {
270
+ await writelnStderr(process, terminal, `cron remove: job not found: ${jobId}`)
271
+ return 1
272
+ }
273
+
274
+ kernel.intervals.clearCron(jobId)
275
+
276
+ // Also remove from crontab file if it's a user job
277
+ if (jobId.startsWith('cron:user:')) {
278
+ const home = shell.env.get('HOME') ?? '/root'
279
+ const crontabPath = path.join(home, '.config', 'crontab')
280
+
281
+ try {
282
+ if (await shell.context.fs.promises.exists(crontabPath)) {
283
+ const content = await shell.context.fs.promises.readFile(crontabPath, 'utf-8')
284
+ const lines = content.split('\n')
285
+ const lineNumber = parseInt(jobId.replace('cron:user:', ''), 10)
286
+
287
+ if (lineNumber > 0 && lineNumber <= lines.length) {
288
+ // Remove the line (1-indexed to 0-indexed)
289
+ const entries = parseCrontabFile(content)
290
+ const entryToRemove = entries.find((e: { lineNumber: number }) => e.lineNumber === lineNumber)
291
+
292
+ if (entryToRemove) {
293
+ const newLines = lines.filter((_, idx) => idx + 1 !== lineNumber)
294
+ await shell.context.fs.promises.writeFile(crontabPath, newLines.join('\n'), { encoding: 'utf-8' })
295
+ }
296
+ }
297
+ }
298
+ } catch (error) {
299
+ // Continue even if file update fails
300
+ kernel.log.warn(`Failed to update crontab file: ${error}`)
301
+ }
302
+ }
303
+
304
+ await writelnStdout(process, terminal, `Removed cron job: ${jobId}`)
305
+ return 0
306
+ }
307
+
308
+ case 'edit': {
309
+ const home = shell.env.get('HOME') ?? '/root'
310
+ const crontabPath = path.join(home, '.config', 'crontab')
311
+
312
+ // Use view command to edit (or create if doesn't exist)
313
+ const result = await kernel.execute({
314
+ command: '/bin/view',
315
+ args: [crontabPath],
316
+ shell,
317
+ terminal
318
+ })
319
+
320
+ if (result === 0) {
321
+ await writelnStdout(process, terminal, 'Crontab edited. Run "cron reload" to apply changes.')
322
+ }
323
+
324
+ return result
325
+ }
326
+
327
+ case 'validate': {
328
+ if (argv.length < 2) {
329
+ await writelnStderr(process, terminal, 'cron validate: missing expression')
330
+ await writelnStderr(process, terminal, 'Usage: cron validate <expression>')
331
+ return 1
332
+ }
333
+
334
+ const expression = argv[1]
335
+ if (!expression) {
336
+ await writelnStderr(process, terminal, 'cron validate: missing expression')
337
+ return 1
338
+ }
339
+ try {
340
+ parseCronExpression(expression)
341
+ await writelnStdout(process, terminal, `Valid cron expression: ${expression}`)
342
+ return 0
343
+ } catch (error) {
344
+ await writelnStderr(process, terminal, `Invalid cron expression: ${expression}`)
345
+ await writelnStderr(process, terminal, `Error: ${error instanceof Error ? error.message : String(error)}`)
346
+ return 1
347
+ }
348
+ }
349
+
350
+ case 'next': {
351
+ if (argv.length < 2) {
352
+ await writelnStderr(process, terminal, 'cron next: missing expression')
353
+ await writelnStderr(process, terminal, 'Usage: cron next <expression> [count]')
354
+ return 1
355
+ }
356
+
357
+ const expression = argv[1]
358
+ if (!expression) {
359
+ await writelnStderr(process, terminal, 'cron next: missing expression')
360
+ return 1
361
+ }
362
+ const count = argv.length > 2 && argv[2] ? parseInt(argv[2], 10) : 1
363
+
364
+ if (isNaN(count) || count < 1) {
365
+ await writelnStderr(process, terminal, 'cron next: invalid count (must be >= 1)')
366
+ return 1
367
+ }
368
+
369
+ try {
370
+ const cron = parseCronExpression(expression)
371
+ const now = new Date()
372
+ const dates = cron.getNextDates(count, now)
373
+
374
+ await writelnStdout(process, terminal, `Next ${count} execution time(s) for "${expression}":`)
375
+ for (let i = 0; i < dates.length; i++) {
376
+ const date = dates[i]
377
+ if (date) {
378
+ await writelnStdout(process, terminal, ` ${i + 1}. ${date.toISOString()}`)
379
+ }
380
+ }
381
+ return 0
382
+ } catch (error) {
383
+ await writelnStderr(process, terminal, `Invalid cron expression: ${expression}`)
384
+ await writelnStderr(process, terminal, `Error: ${error instanceof Error ? error.message : String(error)}`)
385
+ return 1
386
+ }
387
+ }
388
+
389
+ case 'test': {
390
+ if (argv.length < 2) {
391
+ await writelnStderr(process, terminal, 'cron test: missing expression')
392
+ await writelnStderr(process, terminal, 'Usage: cron test <expression>')
393
+ return 1
394
+ }
395
+
396
+ const expression = argv[1]
397
+ if (!expression) {
398
+ await writelnStderr(process, terminal, 'cron test: missing expression')
399
+ return 1
400
+ }
401
+ try {
402
+ const cron = parseCronExpression(expression)
403
+ const now = new Date()
404
+ const matches = cron.matchDate(now)
405
+
406
+ if (matches) {
407
+ await writelnStdout(process, terminal, `Expression "${expression}" matches current time: ${now.toISOString()}`)
408
+ } else {
409
+ await writelnStdout(process, terminal, `Expression "${expression}" does not match current time: ${now.toISOString()}`)
410
+ }
411
+ return 0
412
+ } catch (error) {
413
+ await writelnStderr(process, terminal, `Invalid cron expression: ${expression}`)
414
+ await writelnStderr(process, terminal, `Error: ${error instanceof Error ? error.message : String(error)}`)
415
+ return 1
416
+ }
417
+ }
418
+
419
+ case 'reload': {
420
+ // Clear all existing cron jobs
421
+ const existingJobs = kernel.intervals.listCrons()
422
+ for (const jobName of existingJobs) {
423
+ kernel.intervals.clearCron(jobName)
424
+ }
425
+
426
+ // Reload system crontab
427
+ try {
428
+ const systemCrontabPath = '/etc/crontab'
429
+ if (await kernel.filesystem.fs.exists(systemCrontabPath)) {
430
+ const content = await kernel.filesystem.fs.readFile(systemCrontabPath, 'utf-8')
431
+ const entries = parseCrontabFile(content)
432
+
433
+ for (const entry of entries) {
434
+ const jobName = `cron:system:${entry.lineNumber}`
435
+ kernel.intervals.setCron(
436
+ jobName,
437
+ entry.expression,
438
+ async () => {
439
+ await kernel.shell.execute(entry.command)
440
+ },
441
+ {
442
+ errorHandler: (err: unknown) => {
443
+ kernel.log.error(`Cron job ${jobName} failed: ${err instanceof Error ? err.message : String(err)}`)
444
+ }
445
+ }
446
+ )
447
+ }
448
+ }
449
+ } catch (error) {
450
+ kernel.log.warn(`Failed to load system crontab: ${error}`)
451
+ }
452
+
453
+ // Reload user crontab
454
+ try {
455
+ const home = shell.env.get('HOME') ?? '/root'
456
+ const userCrontabPath = path.join(home, '.config', 'crontab')
457
+
458
+ if (await shell.context.fs.promises.exists(userCrontabPath)) {
459
+ const content = await shell.context.fs.promises.readFile(userCrontabPath, 'utf-8')
460
+ const entries = parseCrontabFile(content)
461
+
462
+ for (const entry of entries) {
463
+ const jobName = `cron:user:${entry.lineNumber}`
464
+ // Ensure command is properly trimmed and preserved
465
+ const command = entry.command.trim()
466
+ kernel.intervals.setCron(
467
+ jobName,
468
+ entry.expression,
469
+ async () => {
470
+ await kernel.shell.execute(command)
471
+ },
472
+ {
473
+ errorHandler: (err: unknown) => {
474
+ kernel.log.error(`Cron job ${jobName} failed: ${err instanceof Error ? err.message : String(err)}`)
475
+ }
476
+ }
477
+ )
478
+ }
479
+ }
480
+ } catch (error) {
481
+ kernel.log.warn(`Failed to load user crontab: ${error}`)
482
+ }
483
+
484
+ await writelnStdout(process, terminal, 'Crontabs reloaded.')
485
+ return 0
486
+ }
487
+
488
+ default:
489
+ await writelnStderr(process, terminal, `cron: unknown command: ${subcommand}`)
490
+ await writelnStderr(process, terminal, "Try 'cron --help' for more information.")
491
+ return 1
492
+ }
493
+ } catch (error) {
494
+ await writelnStderr(process, terminal, `cron: error: ${error instanceof Error ? error.message : String(error)}`)
495
+ return 1
496
+ }
497
+ }
498
+ })
499
+ }
@@ -535,6 +535,14 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
535
535
  }
536
536
  } else if (arg === '--list' || arg === '-l') {
537
537
  listTopicsFlag = true
538
+ } else if (arg.startsWith('--list') && arg.length > 6) {
539
+ // Handle case where --list is concatenated with package name
540
+ listTopicsFlag = true
541
+ topicPath = arg.slice(6) // Extract everything after "--list"
542
+ } else if (arg.startsWith('-l') && arg.length > 2) {
543
+ // Handle case where -l is concatenated with package name (e.g., "-l@zenfs/core")
544
+ listTopicsFlag = true
545
+ topicPath = arg.slice(2) // Extract everything after "-l"
538
546
  } else if (!arg.startsWith('-')) {
539
547
  topicPath = arg
540
548
  }
@@ -0,0 +1,84 @@
1
+ import path from 'path'
2
+ import chalk from 'chalk'
3
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
4
+ import { TerminalCommand } from '../shared/terminal-command.js'
5
+ import { writelnStderr } from '../shared/helpers.js'
6
+
7
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
8
+ const usage = `Usage: open [FILE|URL]
9
+ Open a file or URL.
10
+
11
+ --help display this help and exit
12
+
13
+ Examples:
14
+ open file.txt open a file in the current directory
15
+ open /path/to/file.txt open a file by absolute path
16
+ open sample-1/sample-5 (1).jpg open a file with spaces in the name
17
+ open https://example.com open a URL in a new tab`
18
+ writelnStderr(process, terminal, usage)
19
+ }
20
+
21
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
22
+ return new TerminalCommand({
23
+ command: 'open',
24
+ description: 'Open a file or URL',
25
+ kernel,
26
+ shell,
27
+ terminal,
28
+ run: async (pid: number, argv: string[]) => {
29
+ const process = kernel.processes.get(pid) as Process | undefined
30
+
31
+ if (!process) return 1
32
+
33
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
34
+ printUsage(process, terminal)
35
+ return 0
36
+ }
37
+
38
+ if (argv.length === 0) {
39
+ await writelnStderr(process, terminal, `open: missing file or URL argument`)
40
+ await writelnStderr(process, terminal, `Try 'open --help' for more information.`)
41
+ return 1
42
+ }
43
+
44
+ const filePath = argv.join(' ')
45
+
46
+ if (!filePath) {
47
+ await writelnStderr(process, terminal, `open: missing file or URL argument`)
48
+ return 1
49
+ }
50
+
51
+ // Check if it's a URL by looking for URL schemes
52
+ const urlPattern = /^[a-zA-Z][a-zA-Z\d+\-.]*:/
53
+ const isURL = urlPattern.test(filePath)
54
+
55
+ if (isURL) {
56
+ window.open(filePath, '_blank')
57
+ return 0
58
+ }
59
+
60
+ // Treat as file path - resolve relative to current working directory
61
+ const fullPath = path.resolve(shell.cwd, filePath)
62
+
63
+ try {
64
+ if (!(await shell.context.fs.promises.exists(fullPath))) {
65
+ await writelnStderr(process, terminal, chalk.red(`open: file not found: ${fullPath}`))
66
+ return 1
67
+ }
68
+
69
+ const file = await shell.context.fs.promises.readFile(fullPath)
70
+ const blob = new Blob([new Uint8Array(file)], { type: 'application/octet-stream' })
71
+ const url = window.URL.createObjectURL(blob)
72
+ const a = document.createElement('a')
73
+ a.href = url
74
+ a.download = path.basename(fullPath)
75
+ a.click()
76
+ window.URL.revokeObjectURL(url)
77
+ return 0
78
+ } catch (error) {
79
+ await writelnStderr(process, terminal, chalk.red(`open: ${error instanceof Error ? error.message : 'Unknown error'}`))
80
+ return 1
81
+ }
82
+ }
83
+ })
84
+ }