@ecmaos/coreutils 0.1.3 → 0.1.5

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 +32 -0
  3. package/dist/commands/cat.d.ts.map +1 -1
  4. package/dist/commands/cat.js +23 -4
  5. package/dist/commands/cat.js.map +1 -1
  6. package/dist/commands/echo.d.ts.map +1 -1
  7. package/dist/commands/echo.js +5 -2
  8. package/dist/commands/echo.js.map +1 -1
  9. package/dist/commands/grep.d.ts +4 -0
  10. package/dist/commands/grep.d.ts.map +1 -0
  11. package/dist/commands/grep.js +140 -0
  12. package/dist/commands/grep.js.map +1 -0
  13. package/dist/commands/head.d.ts +4 -0
  14. package/dist/commands/head.d.ts.map +1 -0
  15. package/dist/commands/head.js +162 -0
  16. package/dist/commands/head.js.map +1 -0
  17. package/dist/commands/hex.d.ts.map +1 -1
  18. package/dist/commands/hex.js +69 -17
  19. package/dist/commands/hex.js.map +1 -1
  20. package/dist/commands/ls.d.ts.map +1 -1
  21. package/dist/commands/ls.js +5 -3
  22. package/dist/commands/ls.js.map +1 -1
  23. package/dist/commands/passkey.d.ts +4 -0
  24. package/dist/commands/passkey.d.ts.map +1 -0
  25. package/dist/commands/passkey.js +184 -0
  26. package/dist/commands/passkey.js.map +1 -0
  27. package/dist/commands/tail.d.ts +4 -0
  28. package/dist/commands/tail.d.ts.map +1 -0
  29. package/dist/commands/tail.js +152 -0
  30. package/dist/commands/tail.js.map +1 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +10 -0
  34. package/dist/index.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/commands/cat.ts +25 -4
  37. package/src/commands/echo.ts +5 -2
  38. package/src/commands/grep.ts +154 -0
  39. package/src/commands/head.ts +170 -0
  40. package/src/commands/hex.ts +76 -20
  41. package/src/commands/ls.ts +4 -2
  42. package/src/commands/passkey.ts +208 -0
  43. package/src/commands/tail.ts +164 -0
  44. package/src/index.ts +10 -0
@@ -0,0 +1,208 @@
1
+ import chalk from 'chalk'
2
+ import type { CommandLineOptions } from 'command-line-args'
3
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
4
+ import { TerminalCommand } from '../shared/terminal-command.js'
5
+ import { writelnStdout, writelnStderr } from '../shared/helpers.js'
6
+
7
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
8
+ return new TerminalCommand({
9
+ command: 'passkey',
10
+ description: 'Manage passkey credentials for WebAuthn authentication',
11
+ kernel,
12
+ shell,
13
+ terminal,
14
+ options: [
15
+ { name: 'help', type: Boolean, description: kernel.i18n.t('Display help') },
16
+ { name: 'subcommand', type: String, defaultOption: true, description: 'Subcommand: register, list, remove, remove-all' },
17
+ { name: 'name', type: String, description: 'Name/description for the passkey (used with register)' },
18
+ { name: 'id', type: String, description: 'Passkey ID to remove (used with remove)' }
19
+ ],
20
+ run: async (argv: CommandLineOptions, process?: Process) => {
21
+ if (!process) return 1
22
+
23
+ const currentUid = shell.credentials.uid
24
+ const user = kernel.users.get(currentUid)
25
+ if (!user) {
26
+ await writelnStderr(process, terminal, chalk.red('Error: Current user not found'))
27
+ return 1
28
+ }
29
+
30
+ const subcommand = (argv.subcommand as string)?.toLowerCase()
31
+
32
+ if (!subcommand || subcommand === 'help' || argv.help) {
33
+ await writelnStdout(process, terminal, 'Usage: passkey <subcommand> [options]')
34
+ await writelnStdout(process, terminal, '')
35
+ await writelnStdout(process, terminal, 'Subcommands:')
36
+ await writelnStdout(process, terminal, ' register [--name <name>] Register a new passkey')
37
+ await writelnStdout(process, terminal, ' list List all registered passkeys')
38
+ await writelnStdout(process, terminal, ' remove --id <id> Remove a specific passkey')
39
+ await writelnStdout(process, terminal, ' remove-all Remove all passkeys')
40
+ return 0
41
+ }
42
+
43
+ try {
44
+ switch (subcommand) {
45
+ case 'register': {
46
+ if (!kernel.auth.passkey.isSupported()) {
47
+ await writelnStderr(process, terminal, chalk.red('Error: WebAuthn is not supported in this browser'))
48
+ return 1
49
+ }
50
+
51
+ const name = (argv.name as string) || undefined
52
+ const username = user.username
53
+ const userId = new TextEncoder().encode(username)
54
+
55
+ const challenge = crypto.getRandomValues(new Uint8Array(32))
56
+ const rpId = globalThis.location.hostname || 'localhost'
57
+
58
+ const createOptions: PublicKeyCredentialCreationOptions = {
59
+ challenge,
60
+ rp: {
61
+ name: kernel.name || 'ecmaOS',
62
+ id: rpId
63
+ },
64
+ user: {
65
+ id: userId,
66
+ name: username,
67
+ displayName: username
68
+ },
69
+ pubKeyCredParams: [
70
+ { type: 'public-key', alg: -7 },
71
+ { type: 'public-key', alg: -257 }
72
+ ],
73
+ authenticatorSelection: {
74
+ userVerification: 'preferred'
75
+ },
76
+ timeout: 60000
77
+ }
78
+
79
+ await writelnStdout(process, terminal, chalk.yellow('Please interact with your authenticator to register a passkey...'))
80
+ const credential = await kernel.auth.passkey.create(createOptions)
81
+
82
+ if (!credential || !(credential instanceof PublicKeyCredential)) {
83
+ await writelnStderr(process, terminal, chalk.red('Error: Failed to create passkey. Registration cancelled or failed.'))
84
+ return 1
85
+ }
86
+
87
+ const publicKeyCredential = credential as PublicKeyCredential
88
+ const response = publicKeyCredential.response as AuthenticatorAttestationResponse
89
+
90
+ const credentialId = btoa(String.fromCharCode(...new Uint8Array(publicKeyCredential.rawId)))
91
+
92
+ let publicKeyArray: Uint8Array
93
+
94
+ try {
95
+ if (typeof response.getPublicKey === 'function') {
96
+ try {
97
+ const publicKeyCrypto = response.getPublicKey()
98
+
99
+ if (publicKeyCrypto && publicKeyCrypto instanceof CryptoKey) {
100
+ const publicKeyJwk = await crypto.subtle.exportKey('jwk', publicKeyCrypto)
101
+ publicKeyArray = new TextEncoder().encode(JSON.stringify(publicKeyJwk))
102
+ } else {
103
+ throw new Error('getPublicKey() did not return a valid CryptoKey')
104
+ }
105
+ } catch (exportError) {
106
+ await writelnStderr(process, terminal, chalk.yellow(`Warning: Could not extract public key via getPublicKey(): ${exportError instanceof Error ? exportError.message : String(exportError)}. Using attestationObject instead.`))
107
+ const attestationObject = response.attestationObject
108
+ publicKeyArray = new Uint8Array(attestationObject)
109
+ }
110
+ } else {
111
+ const attestationObject = response.attestationObject
112
+ publicKeyArray = new Uint8Array(attestationObject)
113
+ }
114
+ } catch (error) {
115
+ await writelnStderr(process, terminal, chalk.red(`Error processing credential data: ${error instanceof Error ? error.message : String(error)}`))
116
+ return 1
117
+ }
118
+
119
+ const passkey = {
120
+ id: crypto.randomUUID(),
121
+ credentialId,
122
+ publicKey: publicKeyArray,
123
+ createdAt: Date.now(),
124
+ name
125
+ }
126
+
127
+ await kernel.users.addPasskey(currentUid, passkey)
128
+ await writelnStdout(process, terminal, chalk.green(`Passkey registered successfully${name ? `: ${name}` : ''}`))
129
+ await writelnStdout(process, terminal, `Passkey ID: ${passkey.id}`)
130
+ return 0
131
+ }
132
+
133
+ case 'list': {
134
+ const passkeys = await kernel.users.getPasskeys(currentUid)
135
+
136
+ if (passkeys.length === 0) {
137
+ await writelnStdout(process, terminal, 'No passkeys registered for this user.')
138
+ return 0
139
+ }
140
+
141
+ await writelnStdout(process, terminal, `Registered passkeys (${passkeys.length}):`)
142
+ await writelnStdout(process, terminal, '')
143
+
144
+ for (const pk of passkeys) {
145
+ const createdDate = new Date(pk.createdAt).toLocaleString()
146
+ const lastUsedDate = pk.lastUsed ? new Date(pk.lastUsed).toLocaleString() : 'Never'
147
+ await writelnStdout(process, terminal, ` ID: ${pk.id}`)
148
+ if (pk.name) {
149
+ await writelnStdout(process, terminal, ` Name: ${pk.name}`)
150
+ }
151
+ await writelnStdout(process, terminal, ` Created: ${createdDate}`)
152
+ await writelnStdout(process, terminal, ` Last used: ${lastUsedDate}`)
153
+ await writelnStdout(process, terminal, '')
154
+ }
155
+ return 0
156
+ }
157
+
158
+ case 'remove': {
159
+ const id = argv.id as string
160
+ if (!id) {
161
+ await writelnStderr(process, terminal, chalk.red('Error: --id is required for remove command'))
162
+ await writelnStdout(process, terminal, 'Usage: passkey remove --id <id>')
163
+ return 1
164
+ }
165
+
166
+ const passkeys = await kernel.users.getPasskeys(currentUid)
167
+ const passkey = passkeys.find(pk => pk.id === id)
168
+
169
+ if (!passkey) {
170
+ await writelnStderr(process, terminal, chalk.red(`Error: Passkey with ID ${id} not found`))
171
+ return 1
172
+ }
173
+
174
+ await kernel.users.removePasskey(currentUid, id)
175
+ await writelnStdout(process, terminal, chalk.green(`Passkey removed successfully${passkey.name ? `: ${passkey.name}` : ''}`))
176
+ return 0
177
+ }
178
+
179
+ case 'remove-all': {
180
+ const passkeys = await kernel.users.getPasskeys(currentUid)
181
+
182
+ if (passkeys.length === 0) {
183
+ await writelnStdout(process, terminal, 'No passkeys to remove.')
184
+ return 0
185
+ }
186
+
187
+ await kernel.users.savePasskeys(currentUid, [])
188
+ const passkeysPath = `${user.home}/.passkeys`
189
+ try {
190
+ await kernel.filesystem.fs.unlink(passkeysPath)
191
+ } catch {}
192
+
193
+ await writelnStdout(process, terminal, chalk.green(`Removed ${passkeys.length} passkey(s)`))
194
+ return 0
195
+ }
196
+
197
+ default:
198
+ await writelnStderr(process, terminal, chalk.red(`Error: Unknown subcommand: ${subcommand}`))
199
+ await writelnStdout(process, terminal, 'Run "passkey help" for usage information')
200
+ return 1
201
+ }
202
+ } catch (error) {
203
+ await writelnStderr(process, terminal, chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`))
204
+ return 1
205
+ }
206
+ }
207
+ })
208
+ }
@@ -0,0 +1,164 @@
1
+ import path from 'path'
2
+ import type { CommandLineOptions } from 'command-line-args'
3
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
4
+ import { TerminalEvents } from '@ecmaos/types'
5
+ import { TerminalCommand } from '../shared/terminal-command.js'
6
+
7
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
8
+ return new TerminalCommand({
9
+ command: 'tail',
10
+ description: 'Print the last lines of files',
11
+ kernel,
12
+ shell,
13
+ terminal,
14
+ options: [
15
+ { name: 'help', type: Boolean, description: kernel.i18n.t('Display help') },
16
+ { name: 'lines', type: Number, alias: 'n', description: 'Print the last NUM lines instead of the last 10' },
17
+ { name: 'path', type: String, typeLabel: '{underline path}', defaultOption: true, multiple: true, description: 'The path(s) to the file(s) to read' }
18
+ ],
19
+ run: async (argv: CommandLineOptions, process?: Process) => {
20
+ if (!process) return 1
21
+
22
+ const writer = process.stdout.getWriter()
23
+ const numLines = (argv.lines as number) ?? 10
24
+
25
+ try {
26
+ if (!argv.path || !(argv.path as string[])[0]) {
27
+ if (!process.stdin) {
28
+ return 0
29
+ }
30
+
31
+ const reader = process.stdin.getReader()
32
+ const decoder = new TextDecoder()
33
+ const lines: string[] = []
34
+ let buffer = ''
35
+
36
+ try {
37
+ while (true) {
38
+ let readResult
39
+ try {
40
+ readResult = await reader.read()
41
+ } catch (error) {
42
+ if (error instanceof Error) {
43
+ throw error
44
+ }
45
+ break
46
+ }
47
+
48
+ const { done, value } = readResult
49
+ if (done) {
50
+ buffer += decoder.decode()
51
+ break
52
+ }
53
+ if (value) {
54
+ buffer += decoder.decode(value, { stream: true })
55
+ const newLines = buffer.split('\n')
56
+ buffer = newLines.pop() || ''
57
+ lines.push(...newLines)
58
+ }
59
+ }
60
+ if (buffer) {
61
+ lines.push(buffer)
62
+ }
63
+ } finally {
64
+ try {
65
+ reader.releaseLock()
66
+ } catch {
67
+ }
68
+ }
69
+
70
+ const output = lines.slice(-numLines).join('\n')
71
+ if (output) {
72
+ await writer.write(new TextEncoder().encode(output + '\n'))
73
+ }
74
+
75
+ return 0
76
+ }
77
+
78
+ const files = (argv.path as string[]) || []
79
+ const isMultipleFiles = files.length > 1
80
+
81
+ for (let i = 0; i < files.length; i++) {
82
+ const file = files[i]
83
+ if (!file) continue
84
+ const fullPath = path.resolve(shell.cwd, file)
85
+
86
+ if (isMultipleFiles) {
87
+ const header = i > 0 ? '\n' : ''
88
+ await writer.write(new TextEncoder().encode(`${header}==> ${file} <==\n`))
89
+ }
90
+
91
+ let interrupted = false
92
+ const interruptHandler = () => { interrupted = true }
93
+ kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler)
94
+
95
+ try {
96
+ if (!fullPath.startsWith('/dev')) {
97
+ const handle = await shell.context.fs.promises.open(fullPath, 'r')
98
+ const stat = await shell.context.fs.promises.stat(fullPath)
99
+
100
+ const decoder = new TextDecoder()
101
+ let buffer = ''
102
+ let bytesRead = 0
103
+ const chunkSize = 1024
104
+
105
+ while (bytesRead < stat.size) {
106
+ if (interrupted) break
107
+ const data = new Uint8Array(chunkSize)
108
+ const readSize = Math.min(chunkSize, stat.size - bytesRead)
109
+ await handle.read(data, 0, readSize, bytesRead)
110
+ const chunk = data.subarray(0, readSize)
111
+ buffer += decoder.decode(chunk, { stream: true })
112
+ bytesRead += readSize
113
+ }
114
+
115
+ const lines = buffer.split('\n')
116
+ if (lines[lines.length - 1] === '') {
117
+ lines.pop()
118
+ }
119
+
120
+ const output = lines.slice(-numLines).join('\n')
121
+ if (output) {
122
+ await writer.write(new TextEncoder().encode(output + '\n'))
123
+ }
124
+ } else {
125
+ const device = await shell.context.fs.promises.open(fullPath)
126
+ const decoder = new TextDecoder()
127
+ const lines: string[] = []
128
+ let buffer = ''
129
+ const chunkSize = 1024
130
+ const data = new Uint8Array(chunkSize)
131
+ let bytesRead = 0
132
+
133
+ do {
134
+ if (interrupted) break
135
+ const result = await device.read(data)
136
+ bytesRead = result.bytesRead
137
+ if (bytesRead > 0) {
138
+ buffer += decoder.decode(data.subarray(0, bytesRead), { stream: true })
139
+ }
140
+ } while (bytesRead > 0)
141
+
142
+ const allLines = buffer.split('\n')
143
+ if (allLines[allLines.length - 1] === '') {
144
+ allLines.pop()
145
+ }
146
+ lines.push(...allLines)
147
+
148
+ const output = lines.slice(-numLines).join('\n')
149
+ if (output) {
150
+ await writer.write(new TextEncoder().encode(output + '\n'))
151
+ }
152
+ }
153
+ } finally {
154
+ kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler)
155
+ }
156
+ }
157
+
158
+ return 0
159
+ } finally {
160
+ writer.releaseLock()
161
+ }
162
+ }
163
+ })
164
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,8 @@ import { createCommand as createCd } from './commands/cd.js'
12
12
  import { createCommand as createChmod } from './commands/chmod.js'
13
13
  import { createCommand as createCp } from './commands/cp.js'
14
14
  import { createCommand as createEcho } from './commands/echo.js'
15
+ import { createCommand as createGrep } from './commands/grep.js'
16
+ import { createCommand as createHead } from './commands/head.js'
15
17
  import { createCommand as createLn } from './commands/ln.js'
16
18
  import { createCommand as createLs } from './commands/ls.js'
17
19
  import { createCommand as createMkdir } from './commands/mkdir.js'
@@ -23,8 +25,10 @@ import { createCommand as createStat } from './commands/stat.js'
23
25
  import { createCommand as createTouch } from './commands/touch.js'
24
26
  import { createCommand as createHex } from './commands/hex.js'
25
27
  import { createCommand as createLess } from './commands/less.js'
28
+ import { createCommand as createPasskey } from './commands/passkey.js'
26
29
  import { createCommand as createSed } from './commands/sed.js'
27
30
  import { createCommand as createTee } from './commands/tee.js'
31
+ import { createCommand as createTail } from './commands/tail.js'
28
32
 
29
33
  // Export individual command factories
30
34
  export { createCommand as createCat } from './commands/cat.js'
@@ -32,6 +36,7 @@ export { createCommand as createCd } from './commands/cd.js'
32
36
  export { createCommand as createChmod } from './commands/chmod.js'
33
37
  export { createCommand as createCp } from './commands/cp.js'
34
38
  export { createCommand as createEcho } from './commands/echo.js'
39
+ export { createCommand as createGrep } from './commands/grep.js'
35
40
  export { createCommand as createLn } from './commands/ln.js'
36
41
  export { createCommand as createLs } from './commands/ls.js'
37
42
  export { createCommand as createMkdir } from './commands/mkdir.js'
@@ -45,6 +50,7 @@ export { createCommand as createHex } from './commands/hex.js'
45
50
  export { createCommand as createLess } from './commands/less.js'
46
51
  export { createCommand as createSed } from './commands/sed.js'
47
52
  export { createCommand as createTee } from './commands/tee.js'
53
+ export { createCommand as createTail } from './commands/tail.js'
48
54
 
49
55
  /**
50
56
  * Creates all coreutils commands.
@@ -57,6 +63,8 @@ export function createAllCommands(kernel: Kernel, shell: Shell, terminal: Termin
57
63
  chmod: createChmod(kernel, shell, terminal),
58
64
  cp: createCp(kernel, shell, terminal),
59
65
  echo: createEcho(kernel, shell, terminal),
66
+ grep: createGrep(kernel, shell, terminal),
67
+ head: createHead(kernel, shell, terminal),
60
68
  ln: createLn(kernel, shell, terminal),
61
69
  ls: createLs(kernel, shell, terminal),
62
70
  mkdir: createMkdir(kernel, shell, terminal),
@@ -68,7 +76,9 @@ export function createAllCommands(kernel: Kernel, shell: Shell, terminal: Termin
68
76
  touch: createTouch(kernel, shell, terminal),
69
77
  hex: createHex(kernel, shell, terminal),
70
78
  less: createLess(kernel, shell, terminal),
79
+ passkey: createPasskey(kernel, shell, terminal),
71
80
  sed: createSed(kernel, shell, terminal),
81
+ tail: createTail(kernel, shell, terminal),
72
82
  tee: createTee(kernel, shell, terminal)
73
83
  }
74
84
  }