@ecmaos/coreutils 0.5.2 → 0.5.3

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.
@@ -0,0 +1,843 @@
1
+ import path from 'path'
2
+ import chalk from 'chalk'
3
+ import * as git from 'isomorphic-git'
4
+ import http from 'isomorphic-git/http/web'
5
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
6
+ import { TerminalCommand } from '../shared/terminal-command.js'
7
+ import { writelnStdout, writelnStderr } from '../shared/helpers.js'
8
+
9
+ const CORS_PROXY = 'https://cors.isomorphic-git.org'
10
+
11
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
12
+ const usage = `Usage: git [COMMAND] [OPTIONS] [ARGS...]
13
+
14
+ Common Git commands:
15
+ init Initialize a new repository
16
+ clone <url> Clone a repository
17
+ add <file>... Add files to staging
18
+ commit -m <msg> Commit staged changes
19
+ status Show working tree status
20
+ log Show commit logs
21
+ branch List or create branches
22
+ checkout <branch> Switch branches
23
+ push Push to remote
24
+ pull Pull from remote
25
+ fetch Fetch from remote
26
+ diff Show changes
27
+ rm <file>... Remove files from git
28
+ config Get/set configuration
29
+ remote Manage remotes
30
+
31
+ --help display this help and exit`
32
+ writelnStderr(process, terminal, usage)
33
+ }
34
+
35
+ async function findGitDir(fs: typeof import('@zenfs/core').fs.promises, startDir: string): Promise<string | null> {
36
+ let currentDir = startDir
37
+ const root = '/'
38
+
39
+ while (currentDir !== root && currentDir !== '') {
40
+ const gitDir = path.join(currentDir, '.git')
41
+ try {
42
+ await fs.stat(gitDir)
43
+ return gitDir
44
+ } catch {
45
+ const parent = path.dirname(currentDir)
46
+ if (parent === currentDir) break
47
+ currentDir = parent
48
+ }
49
+ }
50
+
51
+ return null
52
+ }
53
+
54
+ async function getGitDir(fs: typeof import('@zenfs/core').fs.promises, cwd: string): Promise<string> {
55
+ const gitDir = await findGitDir(fs, cwd)
56
+ if (!gitDir) {
57
+ throw new Error('not a git repository (or any of the parent directories)')
58
+ }
59
+ return path.dirname(gitDir)
60
+ }
61
+
62
+ async function handleInit(
63
+ fs: typeof import('@zenfs/core').fs.promises,
64
+ shell: Shell,
65
+ terminal: Terminal,
66
+ process: Process | undefined,
67
+ args: string[]
68
+ ): Promise<number> {
69
+ const dir = args.length > 0 && args[0] ? path.resolve(shell.cwd, args[0]) : shell.cwd
70
+
71
+ try {
72
+ await git.init({ fs, dir })
73
+ await writelnStdout(process, terminal, `Initialized empty Git repository in ${dir}/.git/`)
74
+ return 0
75
+ } catch (error) {
76
+ const errorMessage = error instanceof Error ? error.message : String(error)
77
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
78
+ return 1
79
+ }
80
+ }
81
+
82
+ function convertSshToHttps(url: string): string {
83
+ const sshPattern = /^git@([^:]+):(.+)$/
84
+ const match = url.match(sshPattern)
85
+
86
+ if (match) {
87
+ const host = match[1]
88
+ const path = match[2]
89
+ return `https://${host}/${path}`
90
+ }
91
+
92
+ return url
93
+ }
94
+
95
+ async function handleClone(
96
+ fs: typeof import('@zenfs/core').fs.promises,
97
+ shell: Shell,
98
+ terminal: Terminal,
99
+ process: Process | undefined,
100
+ args: string[]
101
+ ): Promise<number> {
102
+ if (args.length === 0) {
103
+ await writelnStderr(process, terminal, 'fatal: You must specify a repository to clone.')
104
+ return 1
105
+ }
106
+
107
+ const url = args[0]
108
+ if (!url) {
109
+ await writelnStderr(process, terminal, 'fatal: You must specify a repository to clone.')
110
+ return 1
111
+ }
112
+
113
+ const httpsUrl = convertSshToHttps(url)
114
+ const dir = args.length > 1 && args[1]
115
+ ? path.resolve(shell.cwd, args[1])
116
+ : path.resolve(shell.cwd, path.basename(httpsUrl.replace(/\.git$/, '')))
117
+
118
+ try {
119
+ await writelnStdout(process, terminal, `Cloning into '${path.basename(dir)}'...`)
120
+ const token = shell.env.get('GITHUB_TOKEN')
121
+ await git.clone({
122
+ fs,
123
+ http,
124
+ dir,
125
+ url: httpsUrl,
126
+ corsProxy: shell.env.get('GIT_CORS_PROXY') || CORS_PROXY,
127
+ onAuth: token ? () => ({ username: token }) : undefined
128
+ })
129
+ await writelnStdout(process, terminal, 'done.')
130
+ return 0
131
+ } catch (error) {
132
+ const errorMessage = error instanceof Error ? error.message : String(error)
133
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
134
+ return 1
135
+ }
136
+ }
137
+
138
+ async function collectFilesRecursively(
139
+ fs: typeof import('@zenfs/core').fs.promises,
140
+ searchDir: string,
141
+ gitDir: string
142
+ ): Promise<string[]> {
143
+ const files: string[] = []
144
+
145
+ try {
146
+ const entries = await fs.readdir(searchDir)
147
+ for (const entry of entries) {
148
+ if (entry === '.git') continue
149
+
150
+ const entryPath = path.join(searchDir, entry)
151
+ let entryStats
152
+ try {
153
+ entryStats = await fs.stat(entryPath)
154
+ } catch {
155
+ continue
156
+ }
157
+
158
+ if (entryStats.isDirectory()) {
159
+ const subFiles = await collectFilesRecursively(fs, entryPath, gitDir)
160
+ files.push(...subFiles)
161
+ } else if (entryStats.isFile()) {
162
+ const gitRelativePath = path.relative(gitDir, entryPath).replace(/\\/g, '/')
163
+ if (gitRelativePath && !gitRelativePath.startsWith('..') && gitRelativePath !== '.git' && !gitRelativePath.startsWith('.git/')) {
164
+ files.push(gitRelativePath)
165
+ }
166
+ }
167
+ }
168
+ } catch {
169
+ // Skip directories that can't be accessed
170
+ }
171
+
172
+ return files
173
+ }
174
+
175
+ async function handleAdd(
176
+ fs: typeof import('@zenfs/core').fs.promises,
177
+ shell: Shell,
178
+ terminal: Terminal,
179
+ process: Process | undefined,
180
+ args: string[]
181
+ ): Promise<number> {
182
+ if (args.length === 0) {
183
+ await writelnStderr(process, terminal, 'Nothing specified, nothing added.')
184
+ return 0
185
+ }
186
+
187
+ try {
188
+ const dir = await getGitDir(fs, shell.cwd)
189
+ const filesToAdd = new Set<string>()
190
+
191
+ for (const file of args) {
192
+ if (!file) continue
193
+
194
+ const targetPath = path.resolve(shell.cwd, file)
195
+ try {
196
+ const stats = await fs.stat(targetPath)
197
+
198
+ if (stats.isDirectory()) {
199
+ const collectedFiles = await collectFilesRecursively(fs, targetPath, dir)
200
+ for (const filePath of collectedFiles) {
201
+ if (filePath && filePath !== '.git' && !filePath.startsWith('.git/')) {
202
+ filesToAdd.add(filePath)
203
+ }
204
+ }
205
+ } else if (stats.isFile()) {
206
+ const gitRelativePath = path.relative(dir, targetPath).replace(/\\/g, '/')
207
+ if (gitRelativePath && !gitRelativePath.startsWith('..') && gitRelativePath !== '.git' && !gitRelativePath.startsWith('.git/')) {
208
+ filesToAdd.add(gitRelativePath)
209
+ }
210
+ }
211
+ } catch {
212
+ continue
213
+ }
214
+ }
215
+
216
+ let hasError = false
217
+ for (const filePath of filesToAdd) {
218
+ try {
219
+ await git.add({ fs, dir, filepath: filePath })
220
+ } catch (error) {
221
+ const errorMessage = error instanceof Error ? error.message : String(error)
222
+ await writelnStderr(process, terminal, `error: ${errorMessage}`)
223
+ hasError = true
224
+ }
225
+ }
226
+
227
+ return hasError ? 1 : 0
228
+ } catch (error) {
229
+ const errorMessage = error instanceof Error ? error.message : String(error)
230
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
231
+ return 1
232
+ }
233
+ }
234
+
235
+ async function handleCommit(
236
+ fs: typeof import('@zenfs/core').fs.promises,
237
+ shell: Shell,
238
+ terminal: Terminal,
239
+ process: Process | undefined,
240
+ args: string[]
241
+ ): Promise<number> {
242
+ let message: string | undefined
243
+
244
+ for (let i = 0; i < args.length; i++) {
245
+ if (args[i] === '-m' && i + 1 < args.length) {
246
+ message = args[i + 1]
247
+ i++
248
+ } else if (args[i] === '--message' && i + 1 < args.length) {
249
+ message = args[i + 1]
250
+ i++
251
+ } else if (args[i]?.startsWith('-m')) {
252
+ message = args[i]?.slice(2) || undefined
253
+ }
254
+ }
255
+
256
+ if (!message) {
257
+ await writelnStderr(process, terminal, 'Aborting commit due to empty commit message.')
258
+ return 1
259
+ }
260
+
261
+ try {
262
+ const dir = await getGitDir(fs, shell.cwd)
263
+
264
+ const username = shell.env.get('USER') || 'root'
265
+ const email = shell.env.get('EMAIL') || `${username}@${shell.env.get('HOSTNAME') || 'localhost'}`
266
+
267
+ const sha = await git.commit({
268
+ fs,
269
+ dir,
270
+ message,
271
+ author: {
272
+ name: username,
273
+ email
274
+ }
275
+ })
276
+
277
+ await writelnStdout(process, terminal, `[${sha.slice(0, 7)}] ${message}`)
278
+ return 0
279
+ } catch (error) {
280
+ const errorMessage = error instanceof Error ? error.message : String(error)
281
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
282
+ return 1
283
+ }
284
+ }
285
+
286
+ async function handleStatus(
287
+ fs: typeof import('@zenfs/core').fs.promises,
288
+ shell: Shell,
289
+ terminal: Terminal,
290
+ process: Process | undefined,
291
+ _args: string[]
292
+ ): Promise<number> {
293
+ try {
294
+ const dir = await getGitDir(fs, shell.cwd)
295
+ const statusMatrix = await git.statusMatrix({ fs, dir })
296
+
297
+ const modified: string[] = []
298
+ const added: string[] = []
299
+ const deleted: string[] = []
300
+ const untracked: string[] = []
301
+
302
+ for (const [filepath, headStatus, workdirStatus, stageStatus] of statusMatrix) {
303
+ if (headStatus === 0 && stageStatus === 2) {
304
+ added.push(filepath)
305
+ } else if (headStatus === 1 && workdirStatus === 0) {
306
+ deleted.push(filepath)
307
+ } else if (headStatus === 1 && workdirStatus === 2) {
308
+ modified.push(filepath)
309
+ } else if (headStatus === 0 && workdirStatus === 2 && stageStatus === 0) {
310
+ untracked.push(filepath)
311
+ }
312
+ }
313
+
314
+ if (modified.length === 0 && added.length === 0 && deleted.length === 0 && untracked.length === 0) {
315
+ await writelnStdout(process, terminal, 'nothing to commit, working tree clean')
316
+ return 0
317
+ }
318
+
319
+ if (modified.length > 0) {
320
+ await writelnStdout(process, terminal, chalk.red('modified: ') + modified.join(' '))
321
+ }
322
+ if (added.length > 0) {
323
+ await writelnStdout(process, terminal, chalk.green('new file: ') + added.join(' '))
324
+ }
325
+ if (deleted.length > 0) {
326
+ await writelnStdout(process, terminal, chalk.red('deleted: ') + deleted.join(' '))
327
+ }
328
+ if (untracked.length > 0) {
329
+ await writelnStdout(process, terminal, chalk.yellow('untracked: ') + untracked.join(' '))
330
+ }
331
+
332
+ return 0
333
+ } catch (error) {
334
+ const errorMessage = error instanceof Error ? error.message : String(error)
335
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
336
+ return 1
337
+ }
338
+ }
339
+
340
+ async function handleLog(
341
+ fs: typeof import('@zenfs/core').fs.promises,
342
+ shell: Shell,
343
+ terminal: Terminal,
344
+ process: Process | undefined,
345
+ args: string[]
346
+ ): Promise<number> {
347
+ let depth = 10
348
+ let oneline = false
349
+
350
+ for (let i = 0; i < args.length; i++) {
351
+ if (args[i] === '--oneline') {
352
+ oneline = true
353
+ } else if (args[i] === '-n' && i + 1 < args.length) {
354
+ depth = parseInt(args[i + 1] || '10', 10) || depth
355
+ i++
356
+ } else if (args[i]?.startsWith('-n')) {
357
+ depth = parseInt(args[i]?.slice(2) || '10', 10) || depth
358
+ }
359
+ }
360
+
361
+ try {
362
+ const dir = await getGitDir(fs, shell.cwd)
363
+ const commits = await git.log({ fs, dir, depth })
364
+
365
+ for (const commit of commits) {
366
+ const commitObj = await git.readCommit({ fs, dir, oid: commit.oid })
367
+ if (oneline) {
368
+ await writelnStdout(process, terminal, `${chalk.yellow(commit.oid.slice(0, 7))} ${commitObj.commit.message.split('\n')[0]}`)
369
+ } else {
370
+ await writelnStdout(process, terminal, `commit ${chalk.yellow(commit.oid)}`)
371
+ await writelnStdout(process, terminal, `Author: ${commitObj.commit.author.name} <${commitObj.commit.author.email}>`)
372
+ await writelnStdout(process, terminal, `Date: ${new Date(commitObj.commit.author.timestamp * 1000).toLocaleString()}`)
373
+ await writelnStdout(process, terminal, '')
374
+ for (const line of commitObj.commit.message.split('\n')) {
375
+ await writelnStdout(process, terminal, ` ${line}`)
376
+ }
377
+ await writelnStdout(process, terminal, '')
378
+ }
379
+ }
380
+
381
+ return 0
382
+ } catch (error) {
383
+ const errorMessage = error instanceof Error ? error.message : String(error)
384
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
385
+ return 1
386
+ }
387
+ }
388
+
389
+ async function handleBranch(
390
+ fs: typeof import('@zenfs/core').fs.promises,
391
+ shell: Shell,
392
+ terminal: Terminal,
393
+ process: Process | undefined,
394
+ args: string[]
395
+ ): Promise<number> {
396
+ try {
397
+ const dir = await getGitDir(fs, shell.cwd)
398
+
399
+ if (args.length === 0) {
400
+ const branches = await git.listBranches({ fs, dir })
401
+ const currentBranch = await git.currentBranch({ fs, dir })
402
+
403
+ for (const branch of branches) {
404
+ if (branch === currentBranch) {
405
+ await writelnStdout(process, terminal, chalk.green(`* ${branch}`))
406
+ } else {
407
+ await writelnStdout(process, terminal, ` ${branch}`)
408
+ }
409
+ }
410
+ return 0
411
+ }
412
+
413
+ const branchName = args[0]
414
+ if (!branchName) {
415
+ await writelnStderr(process, terminal, 'fatal: branch name required')
416
+ return 1
417
+ }
418
+ await git.branch({ fs, dir, ref: branchName, checkout: false })
419
+ await writelnStdout(process, terminal, `Created branch '${branchName}'`)
420
+ return 0
421
+ } catch (error) {
422
+ const errorMessage = error instanceof Error ? error.message : String(error)
423
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
424
+ return 1
425
+ }
426
+ }
427
+
428
+ async function handleCheckout(
429
+ fs: typeof import('@zenfs/core').fs.promises,
430
+ shell: Shell,
431
+ terminal: Terminal,
432
+ process: Process | undefined,
433
+ args: string[]
434
+ ): Promise<number> {
435
+ if (args.length === 0) {
436
+ await writelnStderr(process, terminal, 'fatal: You must specify a branch to checkout.')
437
+ return 1
438
+ }
439
+
440
+ try {
441
+ const dir = await getGitDir(fs, shell.cwd)
442
+ const branch = args[0]
443
+
444
+ await git.checkout({ fs, dir, ref: branch })
445
+ await writelnStdout(process, terminal, `Switched to branch '${branch}'`)
446
+ return 0
447
+ } catch (error) {
448
+ const errorMessage = error instanceof Error ? error.message : String(error)
449
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
450
+ return 1
451
+ }
452
+ }
453
+
454
+ async function handlePush(
455
+ fs: typeof import('@zenfs/core').fs.promises,
456
+ shell: Shell,
457
+ terminal: Terminal,
458
+ process: Process | undefined,
459
+ args: string[]
460
+ ): Promise<number> {
461
+ try {
462
+ const dir = await getGitDir(fs, shell.cwd)
463
+ const remote = args[0] || 'origin'
464
+ const currentBranch = await git.currentBranch({ fs, dir })
465
+ const ref = args[1] || currentBranch || 'main'
466
+
467
+ if (!ref) {
468
+ await writelnStderr(process, terminal, 'fatal: No branch specified and unable to determine current branch.')
469
+ return 1
470
+ }
471
+
472
+ await writelnStdout(process, terminal, `Pushing to ${remote}...`)
473
+ const token = shell.env.get('GITHUB_TOKEN')
474
+ await git.push({
475
+ fs,
476
+ http,
477
+ dir,
478
+ remote,
479
+ ref,
480
+ corsProxy: CORS_PROXY,
481
+ onAuth: token ? () => ({ username: token }) : undefined
482
+ })
483
+ await writelnStdout(process, terminal, 'done.')
484
+ return 0
485
+ } catch (error) {
486
+ const errorMessage = error instanceof Error ? error.message : String(error)
487
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
488
+ return 1
489
+ }
490
+ }
491
+
492
+ async function handlePull(
493
+ fs: typeof import('@zenfs/core').fs.promises,
494
+ shell: Shell,
495
+ terminal: Terminal,
496
+ process: Process | undefined,
497
+ args: string[]
498
+ ): Promise<number> {
499
+ try {
500
+ const dir = await getGitDir(fs, shell.cwd)
501
+ const remote = args[0] || 'origin'
502
+ const currentBranch = await git.currentBranch({ fs, dir })
503
+ const ref = args[1] || currentBranch || 'main'
504
+
505
+ if (!ref) {
506
+ await writelnStderr(process, terminal, 'fatal: No branch specified and unable to determine current branch.')
507
+ return 1
508
+ }
509
+
510
+ await writelnStdout(process, terminal, `Pulling from ${remote}...`)
511
+ const token = shell.env.get('GITHUB_TOKEN')
512
+ await git.pull({
513
+ fs,
514
+ http,
515
+ dir,
516
+ remote,
517
+ ref,
518
+ corsProxy: CORS_PROXY,
519
+ onAuth: token ? () => ({ username: token }) : undefined
520
+ })
521
+ await writelnStdout(process, terminal, 'done.')
522
+ return 0
523
+ } catch (error) {
524
+ const errorMessage = error instanceof Error ? error.message : String(error)
525
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
526
+ return 1
527
+ }
528
+ }
529
+
530
+ async function handleFetch(
531
+ fs: typeof import('@zenfs/core').fs.promises,
532
+ shell: Shell,
533
+ terminal: Terminal,
534
+ process: Process | undefined,
535
+ args: string[]
536
+ ): Promise<number> {
537
+ try {
538
+ const dir = await getGitDir(fs, shell.cwd)
539
+ const remote = args[0] || 'origin'
540
+
541
+ await writelnStdout(process, terminal, `Fetching from ${remote}...`)
542
+ const token = shell.env.get('GITHUB_TOKEN')
543
+ await git.fetch({
544
+ fs,
545
+ http,
546
+ dir,
547
+ remote,
548
+ corsProxy: CORS_PROXY,
549
+ onAuth: token ? () => ({ username: token }) : undefined
550
+ })
551
+ await writelnStdout(process, terminal, 'done.')
552
+ return 0
553
+ } catch (error) {
554
+ const errorMessage = error instanceof Error ? error.message : String(error)
555
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
556
+ return 1
557
+ }
558
+ }
559
+
560
+ async function handleDiff(
561
+ fs: typeof import('@zenfs/core').fs.promises,
562
+ shell: Shell,
563
+ terminal: Terminal,
564
+ process: Process | undefined,
565
+ args: string[]
566
+ ): Promise<number> {
567
+ try {
568
+ const dir = await getGitDir(fs, shell.cwd)
569
+
570
+ if (args.length > 0 && args[0]) {
571
+ const filepath = path.relative(dir, path.resolve(shell.cwd, args[0]))
572
+ const status = await git.status({ fs, dir, filepath })
573
+ if (status === '*modified' || status === '*added' || status === '*deleted') {
574
+ await writelnStdout(process, terminal, `diff --git a/${filepath} b/${filepath}`)
575
+ await writelnStdout(process, terminal, `--- a/${filepath}`)
576
+ await writelnStdout(process, terminal, `+++ b/${filepath}`)
577
+ await writelnStdout(process, terminal, `Status: ${status}`)
578
+ } else {
579
+ await writelnStdout(process, terminal, `No changes to ${filepath}`)
580
+ }
581
+ } else {
582
+ const statusMatrix = await git.statusMatrix({ fs, dir })
583
+ for (const [filepath] of statusMatrix) {
584
+ await writelnStdout(process, terminal, `diff --git a/${filepath} b/${filepath}`)
585
+ }
586
+ }
587
+
588
+ return 0
589
+ } catch (error) {
590
+ const errorMessage = error instanceof Error ? error.message : String(error)
591
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
592
+ return 1
593
+ }
594
+ }
595
+
596
+ async function handleRm(
597
+ fs: typeof import('@zenfs/core').fs.promises,
598
+ shell: Shell,
599
+ terminal: Terminal,
600
+ process: Process | undefined,
601
+ args: string[]
602
+ ): Promise<number> {
603
+ if (args.length === 0) {
604
+ await writelnStderr(process, terminal, 'Nothing specified, nothing removed.')
605
+ return 0
606
+ }
607
+
608
+ try {
609
+ const dir = await getGitDir(fs, shell.cwd)
610
+
611
+ for (const file of args) {
612
+ if (!file) continue
613
+ const filePath = path.relative(dir, path.resolve(shell.cwd, file))
614
+ try {
615
+ await git.remove({ fs, dir, filepath: filePath })
616
+ } catch (error) {
617
+ const errorMessage = error instanceof Error ? error.message : String(error)
618
+ await writelnStderr(process, terminal, `error: ${errorMessage}`)
619
+ }
620
+ }
621
+ return 0
622
+ } catch (error) {
623
+ const errorMessage = error instanceof Error ? error.message : String(error)
624
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
625
+ return 1
626
+ }
627
+ }
628
+
629
+ async function handleRemote(
630
+ fs: typeof import('@zenfs/core').fs.promises,
631
+ shell: Shell,
632
+ terminal: Terminal,
633
+ process: Process | undefined,
634
+ args: string[]
635
+ ): Promise<number> {
636
+ try {
637
+ const dir = await getGitDir(fs, shell.cwd)
638
+
639
+ if (args.length === 0) {
640
+ const remotes = await git.listRemotes({ fs, dir })
641
+ for (const remote of remotes) {
642
+ await writelnStdout(process, terminal, remote.remote)
643
+ }
644
+ return 0
645
+ }
646
+
647
+ if (args[0] === '-v' || args[0] === '--verbose') {
648
+ const remotes = await git.listRemotes({ fs, dir })
649
+ for (const remote of remotes) {
650
+ await writelnStdout(process, terminal, `${remote.remote}\t${remote.url} (fetch)`)
651
+ await writelnStdout(process, terminal, `${remote.remote}\t${remote.url} (push)`)
652
+ }
653
+ return 0
654
+ }
655
+
656
+ if (args[0] === 'add' && args.length === 3 && args[1] && args[2]) {
657
+ const httpsUrl = convertSshToHttps(args[2])
658
+ await git.setConfig({ fs, dir, path: `remote.${args[1]}.url`, value: httpsUrl })
659
+ return 0
660
+ }
661
+
662
+ if ((args[0] === 'remove' || args[0] === 'rm') && args.length === 2 && args[1]) {
663
+ try {
664
+ const configFile = path.join(dir, '.git', 'config')
665
+ const configContent = await fs.readFile(configFile, 'utf-8')
666
+ const lines = configContent.split('\n')
667
+ const newLines: string[] = []
668
+ let skipSection = false
669
+
670
+ for (let i = 0; i < lines.length; i++) {
671
+ const line = lines[i]
672
+ if (!line) continue
673
+ if (line.trim() === `[remote "${args[1]}"]`) {
674
+ skipSection = true
675
+ continue
676
+ }
677
+ if (skipSection && line.trim().startsWith('[')) {
678
+ skipSection = false
679
+ }
680
+ if (!skipSection) {
681
+ newLines.push(line)
682
+ }
683
+ }
684
+
685
+ await fs.writeFile(configFile, newLines.join('\n'), 'utf-8')
686
+ } catch (error) {
687
+ const errorMessage = error instanceof Error ? error.message : String(error)
688
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
689
+ return 1
690
+ }
691
+ return 0
692
+ }
693
+
694
+ if (args[0] === 'set-url' && args.length === 3 && args[1] && args[2]) {
695
+ const httpsUrl = convertSshToHttps(args[2])
696
+ await git.setConfig({ fs, dir, path: `remote.${args[1]}.url`, value: httpsUrl })
697
+ return 0
698
+ }
699
+
700
+ if (args[0] === 'show' && args.length === 2 && args[1]) {
701
+ try {
702
+ const url = await git.getConfig({ fs, dir, path: `remote.${args[1]}.url` })
703
+ if (url) {
704
+ await writelnStdout(process, terminal, `* remote ${args[1]}`)
705
+ await writelnStdout(process, terminal, ` Fetch URL: ${url}`)
706
+ await writelnStdout(process, terminal, ` Push URL: ${url}`)
707
+ } else {
708
+ await writelnStderr(process, terminal, `fatal: No such remote '${args[1]}'`)
709
+ return 1
710
+ }
711
+ } catch (error) {
712
+ const errorMessage = error instanceof Error ? error.message : String(error)
713
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
714
+ return 1
715
+ }
716
+ return 0
717
+ }
718
+
719
+ await writelnStderr(process, terminal, 'usage: git remote [-v | --verbose]')
720
+ await writelnStderr(process, terminal, ' or: git remote add <name> <url>')
721
+ await writelnStderr(process, terminal, ' or: git remote remove <name>')
722
+ await writelnStderr(process, terminal, ' or: git remote set-url <name> <url>')
723
+ await writelnStderr(process, terminal, ' or: git remote show <name>')
724
+ return 1
725
+ } catch (error) {
726
+ const errorMessage = error instanceof Error ? error.message : String(error)
727
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
728
+ return 1
729
+ }
730
+ }
731
+
732
+ async function handleConfig(
733
+ fs: typeof import('@zenfs/core').fs.promises,
734
+ shell: Shell,
735
+ terminal: Terminal,
736
+ process: Process | undefined,
737
+ args: string[]
738
+ ): Promise<number> {
739
+ try {
740
+ const dir = await getGitDir(fs, shell.cwd)
741
+
742
+ if (args.length === 0) {
743
+ try {
744
+ const configFile = path.join(dir, '.git', 'config')
745
+ const configContent = await fs.readFile(configFile, 'utf-8')
746
+ const lines = configContent.split('\n')
747
+ for (const line of lines) {
748
+ const trimmed = line.trim()
749
+ if (trimmed && !trimmed.startsWith('[') && !trimmed.startsWith('#') && trimmed.includes('=')) {
750
+ await writelnStdout(process, terminal, trimmed)
751
+ }
752
+ }
753
+ } catch {
754
+ await writelnStdout(process, terminal, 'No configuration found')
755
+ }
756
+ return 0
757
+ }
758
+
759
+ if (args.length === 1 && args[0]) {
760
+ const value = await git.getConfig({ fs, dir, path: args[0] })
761
+ if (value) {
762
+ await writelnStdout(process, terminal, value)
763
+ }
764
+ return 0
765
+ }
766
+
767
+ if (args.length === 2 && args[0] && args[1]) {
768
+ await git.setConfig({ fs, dir, path: args[0], value: args[1] })
769
+ return 0
770
+ }
771
+
772
+ await writelnStderr(process, terminal, 'usage: git config <key> [value]')
773
+ return 1
774
+ } catch (error) {
775
+ const errorMessage = error instanceof Error ? error.message : String(error)
776
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
777
+ return 1
778
+ }
779
+ }
780
+
781
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
782
+ return new TerminalCommand({
783
+ command: 'git',
784
+ description: 'Git version control system',
785
+ kernel,
786
+ shell,
787
+ terminal,
788
+ run: async (pid: number, argv: string[]) => {
789
+ const process = kernel.processes.get(pid) as Process | undefined
790
+
791
+ if (argv.length === 0 || (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h'))) {
792
+ printUsage(process, terminal)
793
+ return 0
794
+ }
795
+
796
+ const subcommand = argv[0]
797
+ const args = argv.slice(1)
798
+ const fs = shell.context.fs.promises
799
+
800
+ try {
801
+ switch (subcommand) {
802
+ case 'init':
803
+ return await handleInit(fs, shell, terminal, process, args)
804
+ case 'clone':
805
+ return await handleClone(fs, shell, terminal, process, args)
806
+ case 'add':
807
+ return await handleAdd(fs, shell, terminal, process, args)
808
+ case 'commit':
809
+ return await handleCommit(fs, shell, terminal, process, args)
810
+ case 'status':
811
+ return await handleStatus(fs, shell, terminal, process, args)
812
+ case 'log':
813
+ return await handleLog(fs, shell, terminal, process, args)
814
+ case 'branch':
815
+ return await handleBranch(fs, shell, terminal, process, args)
816
+ case 'checkout':
817
+ return await handleCheckout(fs, shell, terminal, process, args)
818
+ case 'push':
819
+ return await handlePush(fs, shell, terminal, process, args)
820
+ case 'pull':
821
+ return await handlePull(fs, shell, terminal, process, args)
822
+ case 'fetch':
823
+ return await handleFetch(fs, shell, terminal, process, args)
824
+ case 'diff':
825
+ return await handleDiff(fs, shell, terminal, process, args)
826
+ case 'rm':
827
+ return await handleRm(fs, shell, terminal, process, args)
828
+ case 'config':
829
+ return await handleConfig(fs, shell, terminal, process, args)
830
+ case 'remote':
831
+ return await handleRemote(fs, shell, terminal, process, args)
832
+ default:
833
+ await writelnStderr(process, terminal, `git: '${subcommand}' is not a git command. See 'git --help'.`)
834
+ return 1
835
+ }
836
+ } catch (error) {
837
+ const errorMessage = error instanceof Error ? error.message : String(error)
838
+ await writelnStderr(process, terminal, `fatal: ${errorMessage}`)
839
+ return 1
840
+ }
841
+ }
842
+ })
843
+ }