@ecmaos/coreutils 0.2.0 → 0.2.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.
@@ -1,5 +1,6 @@
1
1
  import path from 'path'
2
2
  import chalk from 'chalk'
3
+ import columnify from 'columnify'
3
4
  import humanFormat from 'human-format'
4
5
  import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
5
6
  import { TerminalCommand } from '../shared/terminal-command.js'
@@ -153,87 +154,100 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
153
154
  .filter((entry, index, self) => self.findIndex(e => e?.name === entry?.name) === index)
154
155
  .filter((entry): entry is NonNullable<typeof entry> => entry !== null && entry !== undefined)
155
156
 
156
- const data = [
157
- ['Name', 'Size', 'Modified', 'Mode', 'Owner', 'Info'],
158
- ...directories.sort((a, b) => a.name.localeCompare(b.name)).map(directory => {
159
- const displayName = directory.linkTarget
160
- ? `${directory.name} ${chalk.cyan('⟶')} ${directory.linkTarget}`
161
- : directory.name
162
- const modeStats = directory.linkStats && directory.linkStats.isSymbolicLink()
163
- ? directory.linkStats
164
- : directory.stats
165
- const modeString = modeStats
166
- ? getModeString(modeStats, directory.linkStats?.isSymbolicLink() ? directory.stats : undefined)
167
- : ''
168
- const linkInfo = getLinkInfo(directory.linkTarget, directory.linkStats, directory.stats)
169
- return [
170
- displayName,
171
- '',
172
- directory.stats ? getTimestampString(directory.stats.mtime) : '',
173
- modeString,
174
- directory.stats ? getOwnerString(directory.stats) : '',
175
- linkInfo
176
- ]
177
- }),
178
- ...files.sort((a, b) => a.name.localeCompare(b.name)).map(file => {
179
- const displayName = file.linkTarget
180
- ? `${file.name} ${chalk.cyan('⟶')} ${file.linkTarget}`
181
- : file.name
182
- const modeStats = file.linkStats && file.linkStats.isSymbolicLink()
183
- ? file.linkStats
184
- : file.stats
185
- const modeString = modeStats
186
- ? getModeString(modeStats, file.linkStats?.isSymbolicLink() ? file.stats : undefined)
187
- : ''
188
- return [
189
- displayName,
190
- file.stats ? humanFormat(file.stats.size) : '',
191
- file.stats ? getTimestampString(file.stats.mtime) : '',
192
- modeString,
193
- file.stats ? getOwnerString(file.stats) : '',
194
- (() => {
195
- const linkInfo = getLinkInfo(file.linkTarget, file.linkStats, file.stats)
196
- if (linkInfo) return linkInfo
197
-
198
- if (descriptions.has(path.resolve(fullPath, file.name))) return descriptions.get(path.resolve(fullPath, file.name))
199
- if (file.name.includes('.')) {
200
- const ext = file.name.split('.').pop()
201
- if (ext && descriptions.has('.' + ext)) return descriptions.get('.' + ext)
202
- }
203
- if (!file.stats) return ''
204
- if (file.stats.isBlockDevice() || file.stats.isCharacterDevice()) {
205
- // TODO: zenfs `fs.mounts` is deprecated - use a better way of getting device info
206
- }
157
+ const isDevDirectory = fullPath.startsWith('/dev')
158
+ const columns = isDevDirectory ? ['Name', 'Mode', 'Owner', 'Info'] : ['Name', 'Size', 'Modified', 'Mode', 'Owner', 'Info']
159
+
160
+ const directoryRows = directories.sort((a, b) => a.name.localeCompare(b.name)).map(directory => {
161
+ const displayName = directory.linkTarget
162
+ ? `${directory.name} ${chalk.cyan('⟶')} ${directory.linkTarget}`
163
+ : directory.name
164
+ const modeStats = directory.linkStats && directory.linkStats.isSymbolicLink()
165
+ ? directory.linkStats
166
+ : directory.stats
167
+ const modeString = modeStats
168
+ ? getModeString(modeStats, directory.linkStats?.isSymbolicLink() ? directory.stats : undefined)
169
+ : ''
170
+ const linkInfo = getLinkInfo(directory.linkTarget, directory.linkStats, directory.stats)
171
+
172
+ const modeType = modeString?.charAt(0) || ''
173
+ const coloredName = modeType === 'd' ? chalk.blue(displayName)
174
+ : modeType === 'l' ? chalk.cyan(displayName)
175
+ : chalk.green(displayName)
176
+
177
+ const row: Record<string, string> = {
178
+ Name: coloredName,
179
+ Mode: chalk.gray(modeString),
180
+ Owner: directory.stats ? chalk.gray(getOwnerString(directory.stats)) : '',
181
+ Info: chalk.gray(linkInfo)
182
+ }
183
+
184
+ if (!isDevDirectory) {
185
+ row.Size = ''
186
+ row.Modified = directory.stats ? chalk.gray(getTimestampString(directory.stats.mtime)) : ''
187
+ }
188
+
189
+ return row
190
+ })
191
+
192
+ const fileRows = files.sort((a, b) => a.name.localeCompare(b.name)).map(file => {
193
+ const displayName = file.linkTarget
194
+ ? `${file.name} ${chalk.cyan('')} ${file.linkTarget}`
195
+ : file.name
196
+ const modeStats = file.linkStats && file.linkStats.isSymbolicLink()
197
+ ? file.linkStats
198
+ : file.stats
199
+ const modeString = modeStats
200
+ ? getModeString(modeStats, file.linkStats?.isSymbolicLink() ? file.stats : undefined)
201
+ : ''
202
+
203
+ const modeType = modeString?.charAt(0) || ''
204
+ const coloredName = modeType === 'd' ? chalk.blue(displayName)
205
+ : modeType === 'l' ? chalk.cyan(displayName)
206
+ : chalk.green(displayName)
207
+
208
+ const info = (() => {
209
+ const linkInfo = getLinkInfo(file.linkTarget, file.linkStats, file.stats)
210
+ if (linkInfo) return linkInfo
211
+
212
+ if (descriptions.has(path.resolve(fullPath, file.name))) return descriptions.get(path.resolve(fullPath, file.name)) || ''
213
+ if (file.name.includes('.')) {
214
+ const ext = file.name.split('.').pop()
215
+ if (ext && descriptions.has('.' + ext)) return descriptions.get('.' + ext) || ''
216
+ }
217
+ if (!file.stats) return ''
218
+ if (file.stats.isBlockDevice() || file.stats.isCharacterDevice()) {
219
+ // TODO: zenfs `fs.mounts` is deprecated - use a better way of getting device info
220
+ }
221
+
222
+ return ''
223
+ })()
224
+
225
+ const row: Record<string, string> = {
226
+ Name: coloredName,
227
+ Mode: chalk.gray(modeString),
228
+ Owner: file.stats ? chalk.gray(getOwnerString(file.stats)) : '',
229
+ Info: chalk.gray(info)
230
+ }
207
231
 
208
- return ''
209
- })()
210
- ]
232
+ if (!isDevDirectory) {
233
+ row.Size = file.stats ? chalk.gray(humanFormat(file.stats.size)) : ''
234
+ row.Modified = file.stats ? chalk.gray(getTimestampString(file.stats.mtime)) : ''
235
+ }
236
+
237
+ return row
238
+ })
239
+
240
+ const data = [...directoryRows, ...fileRows]
241
+
242
+ if (data.length > 0) {
243
+ const table = columnify(data, {
244
+ columns,
245
+ columnSplitter: ' ',
246
+ showHeaders: true,
247
+ headingTransform: (heading: string) => chalk.bold(heading)
211
248
  })
212
- ] as string[][]
213
-
214
- // Special output for certain directories
215
- if (fullPath.startsWith('/dev')) data.forEach(row => row.splice(1, 2)) // remove size and modified columns
216
-
217
- const columnWidths = data[0]?.map((_, colIndex) => Math.max(...data.map(row => {
218
- // Remove ANSI escape sequences before calculating length
219
- const cleanedCell = row[colIndex]?.replace(/\u001b\[.*?m/g, '')
220
- // count all emojis as two characters
221
- return cleanedCell?.length || 0
222
- })))
223
-
224
- for (const [rowIndex, row] of data.entries()) {
225
- const line = row
226
- .map((cell, index) => {
227
- const paddedCell = cell.padEnd(columnWidths?.[index] ?? 0)
228
- if (index === 0 && rowIndex > 0) {
229
- if (row[3]?.startsWith('d')) return chalk.blue(paddedCell)
230
- else if (row[3]?.startsWith('l')) return chalk.cyan(paddedCell)
231
- else return chalk.green(paddedCell)
232
- } else return rowIndex === 0 ? chalk.bold(paddedCell) : chalk.gray(paddedCell)
233
- })
234
- .join(' ')
235
-
236
- if (data.length > 1) await writelnStdout(process, terminal, line)
249
+
250
+ await writelnStdout(process, terminal, table)
237
251
  }
238
252
 
239
253
  return 0
@@ -0,0 +1,436 @@
1
+ import chalk from 'chalk'
2
+ import type { Kernel, Process, Shell, Terminal, User } from '@ecmaos/types'
3
+ import { TerminalCommand } from '../shared/terminal-command.js'
4
+ import { writelnStdout, writelnStderr } from '../shared/helpers.js'
5
+
6
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
7
+ const usage = `Usage: user [COMMAND] [OPTIONS] [USERNAME]
8
+ Manage users on the system.
9
+
10
+ Commands:
11
+ add USERNAME Add a new user
12
+ del USERNAME Delete a user
13
+ mod USERNAME Modify a user
14
+ list List all users (default)
15
+
16
+ Options for 'add':
17
+ -m, --create-home Create home directory
18
+ -s, --shell SHELL Login shell (default: ecmaos)
19
+ -g, --gid GID Group ID (default: same as UID)
20
+ -u, --uid UID User ID (default: auto-assigned)
21
+ -p, --password PASS Password (will prompt if not provided)
22
+
23
+ Options for 'del':
24
+ -r, --remove-home Remove home directory
25
+
26
+ Options for 'mod':
27
+ -s, --shell SHELL Change login shell
28
+ -g, --gid GID Change group ID
29
+ -p, --password Change password (will prompt)
30
+
31
+ --help Display this help and exit`
32
+ writelnStdout(process, terminal, usage)
33
+ }
34
+
35
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
36
+ return new TerminalCommand({
37
+ command: 'user',
38
+ description: 'Manage users on the system',
39
+ kernel,
40
+ shell,
41
+ terminal,
42
+ run: async (pid: number, argv: string[]) => {
43
+ const process = kernel.processes.get(pid) as Process | undefined
44
+
45
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
46
+ printUsage(process, terminal)
47
+ return 0
48
+ }
49
+
50
+ if (shell.credentials.suid !== 0) {
51
+ await writelnStderr(process, terminal, chalk.red('user: permission denied'))
52
+ return 1
53
+ }
54
+
55
+ const command = argv.length > 0 && argv[0] !== undefined && !argv[0].startsWith('-') ? argv[0] : 'list'
56
+ const remainingArgs = command !== 'list' ? argv.slice(1) : argv
57
+
58
+ switch (command) {
59
+ case 'list': {
60
+ const users = Array.from(kernel.users.all.values()) as User[]
61
+
62
+ if (users.length === 0) {
63
+ await writelnStdout(process, terminal, 'No users found')
64
+ return 0
65
+ }
66
+
67
+ const uidWidth = Math.max(3, ...users.map(u => u.uid.toString().length))
68
+ const usernameWidth = Math.max(8, ...users.map(u => u.username.length))
69
+ const gidWidth = Math.max(3, ...users.map(u => u.gid.toString().length))
70
+
71
+ await writelnStdout(process, terminal, chalk.bold(
72
+ 'UID'.padEnd(uidWidth) + '\t' +
73
+ 'Username'.padEnd(usernameWidth) + '\t' +
74
+ 'GID'.padEnd(gidWidth) + '\t' +
75
+ 'Groups'
76
+ ))
77
+
78
+ for (const usr of users) {
79
+ await writelnStdout(process, terminal,
80
+ chalk.yellow(usr.uid.toString().padEnd(uidWidth)) + '\t' +
81
+ chalk.green(usr.username.padEnd(usernameWidth)) + '\t' +
82
+ chalk.cyan(usr.gid.toString().padEnd(gidWidth)) + '\t' +
83
+ chalk.blue(usr.groups.join(', ') || '-')
84
+ )
85
+ }
86
+
87
+ return 0
88
+ }
89
+
90
+ case 'add': {
91
+ let username = ''
92
+ let createHome = false
93
+ let shellValue = 'ecmaos'
94
+ let gid: number | undefined
95
+ let uid: number | undefined
96
+ let password: string | undefined
97
+
98
+ for (let i = 0; i < remainingArgs.length; i++) {
99
+ const arg = remainingArgs[i]
100
+ if (!arg || typeof arg !== 'string') continue
101
+ if (arg.startsWith('-')) {
102
+ if (arg === '-m' || arg === '--create-home') {
103
+ createHome = true
104
+ } else if (arg === '-s' || arg === '--shell') {
105
+ if (i + 1 < remainingArgs.length) {
106
+ const nextArg = remainingArgs[++i]
107
+ if (nextArg) {
108
+ shellValue = nextArg
109
+ } else {
110
+ await writelnStderr(process, terminal, chalk.red('user add: option requires an argument -- \'s\''))
111
+ return 1
112
+ }
113
+ } else {
114
+ await writelnStderr(process, terminal, chalk.red('user add: option requires an argument -- \'s\''))
115
+ return 1
116
+ }
117
+ } else if (arg === '-g' || arg === '--gid') {
118
+ if (i + 1 < remainingArgs.length) {
119
+ const gidStr = remainingArgs[++i]
120
+ if (gidStr) {
121
+ gid = parseInt(gidStr, 10)
122
+ if (isNaN(gid)) {
123
+ await writelnStderr(process, terminal, chalk.red(`user add: invalid GID '${gidStr}'`))
124
+ return 1
125
+ }
126
+ } else {
127
+ await writelnStderr(process, terminal, chalk.red('user add: option requires an argument -- \'g\''))
128
+ return 1
129
+ }
130
+ } else {
131
+ await writelnStderr(process, terminal, chalk.red('user add: option requires an argument -- \'g\''))
132
+ return 1
133
+ }
134
+ } else if (arg === '-u' || arg === '--uid') {
135
+ if (i + 1 < remainingArgs.length) {
136
+ const uidStr = remainingArgs[++i]
137
+ if (uidStr) {
138
+ uid = parseInt(uidStr, 10)
139
+ if (isNaN(uid)) {
140
+ await writelnStderr(process, terminal, chalk.red(`user add: invalid UID '${uidStr}'`))
141
+ return 1
142
+ }
143
+ } else {
144
+ await writelnStderr(process, terminal, chalk.red('user add: option requires an argument -- \'u\''))
145
+ return 1
146
+ }
147
+ } else {
148
+ await writelnStderr(process, terminal, chalk.red('user add: option requires an argument -- \'u\''))
149
+ return 1
150
+ }
151
+ } else if (arg === '-p' || arg === '--password') {
152
+ if (i + 1 < remainingArgs.length) {
153
+ const nextArg = remainingArgs[++i]
154
+ if (nextArg) {
155
+ password = nextArg
156
+ } else {
157
+ await writelnStderr(process, terminal, chalk.red('user add: option requires an argument -- \'p\''))
158
+ return 1
159
+ }
160
+ } else {
161
+ await writelnStderr(process, terminal, chalk.red('user add: option requires an argument -- \'p\''))
162
+ return 1
163
+ }
164
+ } else if (arg === '--help' || arg === '-h') {
165
+ printUsage(process, terminal)
166
+ return 0
167
+ } else {
168
+ await writelnStderr(process, terminal, chalk.red(`user add: invalid option -- '${arg.replace(/^-+/, '')}'`))
169
+ return 1
170
+ }
171
+ } else {
172
+ if (!username) {
173
+ username = arg
174
+ } else {
175
+ await writelnStderr(process, terminal, chalk.red(`user add: unexpected argument '${arg}'`))
176
+ return 1
177
+ }
178
+ }
179
+ }
180
+
181
+ if (!username) {
182
+ await writelnStderr(process, terminal, chalk.red('user add: username required'))
183
+ await writelnStdout(process, terminal, 'Try \'user add --help\' for more information.')
184
+ return 1
185
+ }
186
+
187
+ const allUsers = Array.from(kernel.users.all.values()) as User[]
188
+ if (allUsers.some((u: User) => u.username === username)) {
189
+ await writelnStderr(process, terminal, chalk.red(`user add: user '${username}' already exists`))
190
+ return 1
191
+ }
192
+
193
+ if (uid !== undefined && kernel.users.all.has(uid)) {
194
+ await writelnStderr(process, terminal, chalk.red(`user add: UID ${uid} already in use`))
195
+ return 1
196
+ }
197
+
198
+ if (!password) {
199
+ password = await terminal.readline(chalk.cyan(`New password: `), true)
200
+ const confirm = await terminal.readline(chalk.cyan('Retype new password: '), true)
201
+ if (password !== confirm) {
202
+ await writelnStderr(process, terminal, chalk.red('user add: password mismatch'))
203
+ return 1
204
+ }
205
+ }
206
+
207
+ try {
208
+ await kernel.users.add({
209
+ username,
210
+ password,
211
+ uid,
212
+ gid,
213
+ shell: shellValue,
214
+ home: `/home/${username}`
215
+ }, { noHome: !createHome })
216
+ await writelnStdout(process, terminal, chalk.green(`user add: user '${username}' created successfully`))
217
+ return 0
218
+ } catch (error) {
219
+ await writelnStderr(process, terminal, chalk.red(`user add: ${error instanceof Error ? error.message : 'Unknown error'}`))
220
+ return 1
221
+ }
222
+ }
223
+
224
+ case 'del': {
225
+ let username = ''
226
+ let removeHome = false
227
+
228
+ for (let i = 0; i < remainingArgs.length; i++) {
229
+ const arg = remainingArgs[i]
230
+ if (!arg || typeof arg !== 'string') continue
231
+ if (arg.startsWith('-')) {
232
+ if (arg === '-r' || arg === '--remove-home') {
233
+ removeHome = true
234
+ } else if (arg === '--help' || arg === '-h') {
235
+ printUsage(process, terminal)
236
+ return 0
237
+ } else {
238
+ await writelnStderr(process, terminal, chalk.red(`user del: invalid option -- '${arg.replace(/^-+/, '')}'`))
239
+ return 1
240
+ }
241
+ } else {
242
+ if (!username) {
243
+ username = arg
244
+ } else {
245
+ await writelnStderr(process, terminal, chalk.red(`user del: unexpected argument '${arg}'`))
246
+ return 1
247
+ }
248
+ }
249
+ }
250
+
251
+ if (!username) {
252
+ await writelnStderr(process, terminal, chalk.red('user del: username required'))
253
+ await writelnStdout(process, terminal, 'Try \'user del --help\' for more information.')
254
+ return 1
255
+ }
256
+
257
+ const allUsers = Array.from(kernel.users.all.values()) as User[]
258
+ const usr = allUsers.find((u: User) => u.username === username)
259
+ if (!usr) {
260
+ await writelnStderr(process, terminal, chalk.red(`user del: user '${username}' does not exist`))
261
+ return 1
262
+ }
263
+
264
+ if (usr.uid === 0) {
265
+ await writelnStderr(process, terminal, chalk.red('user del: cannot delete root user'))
266
+ return 1
267
+ }
268
+
269
+ try {
270
+ await kernel.users.remove(usr.uid)
271
+
272
+ if (removeHome && usr.home) {
273
+ try {
274
+ const removeDirRecursive = async (dirPath: string): Promise<void> => {
275
+ const entries = await shell.context.fs.promises.readdir(dirPath)
276
+ for (const entry of entries) {
277
+ const entryPath = `${dirPath}/${entry}`
278
+ const stat = await shell.context.fs.promises.stat(entryPath)
279
+ if (stat.isDirectory()) {
280
+ await removeDirRecursive(entryPath)
281
+ } else {
282
+ await shell.context.fs.promises.unlink(entryPath)
283
+ }
284
+ }
285
+ await shell.context.fs.promises.rmdir(dirPath)
286
+ }
287
+ await removeDirRecursive(usr.home)
288
+ } catch {
289
+ await writelnStderr(process, terminal, chalk.yellow(`user del: warning: could not remove home directory '${usr.home}'`))
290
+ }
291
+ }
292
+
293
+ await shell.context.fs.promises.writeFile(
294
+ '/etc/passwd',
295
+ (await shell.context.fs.promises.readFile('/etc/passwd', 'utf8'))
296
+ .split('\n')
297
+ .filter((line: string) => !line.startsWith(`${username}:`))
298
+ .join('\n')
299
+ )
300
+ await shell.context.fs.promises.writeFile(
301
+ '/etc/shadow',
302
+ (await shell.context.fs.promises.readFile('/etc/shadow', 'utf8'))
303
+ .split('\n')
304
+ .filter((line: string) => !line.startsWith(`${username}:`))
305
+ .join('\n')
306
+ )
307
+
308
+ await writelnStdout(process, terminal, chalk.green(`user del: user '${username}' deleted successfully`))
309
+ return 0
310
+ } catch (error) {
311
+ await writelnStderr(process, terminal, chalk.red(`user del: ${error instanceof Error ? error.message : 'Unknown error'}`))
312
+ return 1
313
+ }
314
+ }
315
+
316
+ case 'mod': {
317
+ let username = ''
318
+ let shellValue: string | undefined
319
+ let gid: number | undefined
320
+ let changePassword = false
321
+
322
+ for (let i = 0; i < remainingArgs.length; i++) {
323
+ const arg = remainingArgs[i]
324
+ if (!arg || typeof arg !== 'string') continue
325
+ if (arg.startsWith('-')) {
326
+ if (arg === '-s' || arg === '--shell') {
327
+ if (i + 1 < remainingArgs.length) {
328
+ const nextArg = remainingArgs[++i]
329
+ if (nextArg) {
330
+ shellValue = nextArg
331
+ } else {
332
+ await writelnStderr(process, terminal, chalk.red('user mod: option requires an argument -- \'s\''))
333
+ return 1
334
+ }
335
+ } else {
336
+ await writelnStderr(process, terminal, chalk.red('user mod: option requires an argument -- \'s\''))
337
+ return 1
338
+ }
339
+ } else if (arg === '-g' || arg === '--gid') {
340
+ if (i + 1 < remainingArgs.length) {
341
+ const gidStr = remainingArgs[++i]
342
+ if (gidStr) {
343
+ gid = parseInt(gidStr, 10)
344
+ if (isNaN(gid)) {
345
+ await writelnStderr(process, terminal, chalk.red(`user mod: invalid GID '${gidStr}'`))
346
+ return 1
347
+ }
348
+ } else {
349
+ await writelnStderr(process, terminal, chalk.red('user mod: option requires an argument -- \'g\''))
350
+ return 1
351
+ }
352
+ } else {
353
+ await writelnStderr(process, terminal, chalk.red('user mod: option requires an argument -- \'g\''))
354
+ return 1
355
+ }
356
+ } else if (arg === '-p' || arg === '--password') {
357
+ changePassword = true
358
+ } else if (arg === '--help' || arg === '-h') {
359
+ printUsage(process, terminal)
360
+ return 0
361
+ } else {
362
+ await writelnStderr(process, terminal, chalk.red(`user mod: invalid option -- '${arg.replace(/^-+/, '')}'`))
363
+ return 1
364
+ }
365
+ } else {
366
+ if (!username) {
367
+ username = arg
368
+ } else {
369
+ await writelnStderr(process, terminal, chalk.red(`user mod: unexpected argument '${arg}'`))
370
+ return 1
371
+ }
372
+ }
373
+ }
374
+
375
+ if (!username) {
376
+ await writelnStderr(process, terminal, chalk.red('user mod: username required'))
377
+ await writelnStdout(process, terminal, 'Try \'user mod --help\' for more information.')
378
+ return 1
379
+ }
380
+
381
+ const allUsers = Array.from(kernel.users.all.values()) as User[]
382
+ const usr = allUsers.find((u: User) => u.username === username)
383
+ if (!usr) {
384
+ await writelnStderr(process, terminal, chalk.red(`user mod: user '${username}' does not exist`))
385
+ return 1
386
+ }
387
+
388
+ const updates: Partial<User> = {}
389
+ if (shellValue !== undefined) {
390
+ updates.shell = shellValue
391
+ }
392
+ if (gid !== undefined) {
393
+ updates.gid = gid
394
+ }
395
+
396
+ if (changePassword) {
397
+ const newPassword = await terminal.readline(chalk.cyan('New password: '), true)
398
+ const confirm = await terminal.readline(chalk.cyan('Retype new password: '), true)
399
+
400
+ if (newPassword !== confirm) {
401
+ await writelnStderr(process, terminal, chalk.red('user mod: password mismatch'))
402
+ return 1
403
+ }
404
+
405
+ try {
406
+ const hashedPassword = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(newPassword.trim()))
407
+ updates.password = Array.from(new Uint8Array(hashedPassword)).map(b => b.toString(16).padStart(2, '0')).join('')
408
+ } catch (error) {
409
+ await writelnStderr(process, terminal, chalk.red(`user mod: failed to hash password: ${error instanceof Error ? error.message : 'Unknown error'}`))
410
+ return 1
411
+ }
412
+ }
413
+
414
+ if (Object.keys(updates).length === 0 && !changePassword) {
415
+ await writelnStderr(process, terminal, chalk.red('user mod: no changes specified'))
416
+ return 1
417
+ }
418
+
419
+ try {
420
+ await kernel.users.update(usr.uid, updates)
421
+ await writelnStdout(process, terminal, chalk.green(`user mod: user '${username}' modified successfully`))
422
+ return 0
423
+ } catch (error) {
424
+ await writelnStderr(process, terminal, chalk.red(`user mod: ${error instanceof Error ? error.message : 'Unknown error'}`))
425
+ return 1
426
+ }
427
+ }
428
+
429
+ default:
430
+ await writelnStderr(process, terminal, chalk.red(`user: invalid command '${command}'`))
431
+ await writelnStdout(process, terminal, 'Try \'user --help\' for more information.')
432
+ return 1
433
+ }
434
+ }
435
+ })
436
+ }
package/src/index.ts CHANGED
@@ -51,6 +51,7 @@ import { createCommand as createTest } from './commands/test.js'
51
51
  import { createCommand as createTr } from './commands/tr.js'
52
52
  import { createCommand as createTrue } from './commands/true.js'
53
53
  import { createCommand as createUniq } from './commands/uniq.js'
54
+ import { createCommand as createUser } from './commands/user.js'
54
55
  import { createCommand as createWc } from './commands/wc.js'
55
56
  import { createCommand as createWhich } from './commands/which.js'
56
57
  import { createCommand as createWhoami } from './commands/whoami.js'
@@ -86,6 +87,7 @@ export { createCommand as createSort } from './commands/sort.js'
86
87
  export { createCommand as createTest } from './commands/test.js'
87
88
  export { createCommand as createTr } from './commands/tr.js'
88
89
  export { createCommand as createUniq } from './commands/uniq.js'
90
+ export { createCommand as createUser } from './commands/user.js'
89
91
  export { createCommand as createWc } from './commands/wc.js'
90
92
  export { createCommand as createWhich } from './commands/which.js'
91
93
  export { createCommand as createSockets } from './commands/sockets.js'
@@ -140,6 +142,7 @@ export function createAllCommands(kernel: Kernel, shell: Shell, terminal: Termin
140
142
  tr: createTr(kernel, shell, terminal),
141
143
  true: createTrue(kernel, shell, terminal),
142
144
  uniq: createUniq(kernel, shell, terminal),
145
+ user: createUser(kernel, shell, terminal),
143
146
  wc: createWc(kernel, shell, terminal),
144
147
  which: createWhich(kernel, shell, terminal),
145
148
  whoami: createWhoami(kernel, shell, terminal)