guito 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -1,727 +1,34 @@
1
1
  #! /usr/bin/env node
2
- import { existsSync } from 'node:fs';
3
- import { execFile } from 'node:child_process';
4
- import { readFile, rm } from 'node:fs/promises';
5
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
2
+ import { dirname, join } from 'node:path';
6
3
  import { fileURLToPath } from 'node:url';
7
- import fastify from 'fastify';
8
- import fastifyStatic from '@fastify/static';
9
- import fastifyCors from '@fastify/cors';
10
4
  import open from 'open';
11
- import simpleGit from 'simple-git';
12
- import { promisify } from 'node:util';
13
- const execFileAsync = promisify(execFile);
14
- void (async function main() {
15
- // Calculate dirname
16
- const __dirname = dirname(fileURLToPath(import.meta.url));
17
- // Get port number
18
- const portArgIndex = process.argv.findIndex((a) => a.toLowerCase() === '--port');
19
- const port = portArgIndex !== -1 ? +process.argv[portArgIndex + 1] : 8080;
20
- // Get if should not open browser
21
- const shouldOpenBrowser = process.argv.findIndex((a) => a.toLowerCase() === '--no-open') === -1;
22
- // Initialize server
23
- const app = fastify({
24
- logger: false,
25
- });
26
- // Register cors
27
- await app.register(fastifyCors);
28
- // Register static file provider (supports both `ui/` and `ui/browser/` layouts)
29
- const uiRoot = join(__dirname, 'ui');
30
- const staticRoot = existsSync(join(uiRoot, 'browser', 'index.html'))
31
- ? join(uiRoot, 'browser')
32
- : uiRoot;
33
- if (existsSync(staticRoot)) {
34
- await app.register(fastifyStatic, { root: staticRoot });
35
- // Serve UI
36
- app.get('/', (_req, resp) => resp.sendFile('index.html'));
5
+ import { startGuitoServer } from './guito-server.js';
6
+ function parsePort(args) {
7
+ const index = args.findIndex((arg) => arg.toLowerCase() === '--port');
8
+ if (index === -1) {
9
+ return 8080;
37
10
  }
38
- else {
39
- app.get('/', (_req, resp) => resp.send('Guito API is running. Build the UI with `npm run build:ui`.'));
11
+ const port = Number(args[index + 1]);
12
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
13
+ throw new Error('The --port argument must be an integer between 0 and 65535.');
40
14
  }
41
- // Initialize git lib
42
- const git = simpleGit(process.cwd());
43
- // ==================== Diff parsing ====================
44
- function parseUnifiedDiff(rawDiff) {
45
- const files = [];
46
- const lines = rawDiff.split('\n');
47
- let current = null;
48
- let inHunk = false;
49
- let oldLine = 0;
50
- let newLine = 0;
51
- const startFile = (line) => {
52
- current = {
53
- path: '',
54
- oldPath: '',
55
- status: 'modified',
56
- lines: [],
57
- additions: 0,
58
- deletions: 0,
59
- };
60
- const match = line.match(/^diff --git a\/(.*) b\/(.*)$/);
61
- if (match) {
62
- current.oldPath = match[1];
63
- current.path = match[2];
64
- }
65
- inHunk = false;
66
- };
67
- for (const line of lines) {
68
- if (line.startsWith('diff --git ')) {
69
- if (current)
70
- files.push(current);
71
- startFile(line);
72
- continue;
73
- }
74
- if (!current)
75
- continue;
76
- if (line.startsWith('@@')) {
77
- inHunk = true;
78
- const match = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
79
- if (match) {
80
- oldLine = parseInt(match[1], 10);
81
- newLine = parseInt(match[2], 10);
82
- }
83
- current.lines.push({ type: 'hunk', text: line });
84
- continue;
85
- }
86
- if (!inHunk) {
87
- // File meta headers (mode, index, rename, binary...)
88
- if (line.startsWith('new file mode'))
89
- current.status = 'added';
90
- else if (line.startsWith('deleted file mode'))
91
- current.status = 'deleted';
92
- else if (line.startsWith('rename from '))
93
- current.status = 'renamed';
94
- else if (line.startsWith('rename to '))
95
- current.path = line.slice('rename to '.length);
96
- else if (line.startsWith('Binary files') || line.startsWith('GIT binary patch'))
97
- current.status = 'binary';
98
- continue;
99
- }
100
- if (line.startsWith('\\')) {
101
- // ""
102
- continue;
103
- }
104
- if (line.startsWith('+')) {
105
- current.lines.push({ type: 'add', newLine: newLine++, text: line.slice(1) });
106
- current.additions++;
107
- }
108
- else if (line.startsWith('-')) {
109
- current.lines.push({ type: 'del', oldLine: oldLine++, text: line.slice(1) });
110
- current.deletions++;
111
- }
112
- else {
113
- current.lines.push({
114
- type: 'context',
115
- oldLine: oldLine++,
116
- newLine: newLine++,
117
- text: line.slice(1),
118
- });
119
- }
15
+ return port;
16
+ }
17
+ void (async () => {
18
+ try {
19
+ const currentDirectory = dirname(fileURLToPath(import.meta.url));
20
+ const server = await startGuitoServer({
21
+ repositoryPath: process.cwd(),
22
+ uiRoot: join(currentDirectory, 'ui'),
23
+ port: parsePort(process.argv.slice(2)),
24
+ });
25
+ console.log(`Visit "${server.address}" to see git explorer.`);
26
+ if (!process.argv.some((arg) => arg.toLowerCase() === '--no-open')) {
27
+ await open(server.address);
120
28
  }
121
- if (current)
122
- files.push(current);
123
- return files;
124
29
  }
125
- const repoRoot = async () => (await git.revparse(['--show-toplevel'])).trim();
126
- // ==================== Repository ====================
127
- app.get('/api/repo', async (_req, resp) => {
128
- try {
129
- const root = (await git.revparse(['--show-toplevel'])).trim();
130
- resp.type('application/json').send({ root, name: basename(root) });
131
- }
132
- catch (err) {
133
- resp.status(400).type('application/json').send({ error: err.message });
134
- }
135
- });
136
- // ==================== Commits ====================
137
- app.get('/api/commits', async (_req, resp) => {
138
- try {
139
- const log = await git.log({
140
- format: {
141
- hash: '%H',
142
- date: '%aI',
143
- message: '%s',
144
- refs: '%D',
145
- body: '%b',
146
- author_name: '%aN',
147
- author_email: '%aE',
148
- parents: '%P',
149
- },
150
- });
151
- const commits = log.all.map((commit) => ({
152
- ...commit,
153
- parents: String(commit.parents ?? '')
154
- .split(' ')
155
- .filter(Boolean),
156
- }));
157
- resp.type('application/json').send(commits);
158
- }
159
- catch (err) {
160
- resp.status(400).type('application/json').send({ error: err.message });
161
- }
162
- });
163
- app.post('/api/commit', async (req, resp) => {
164
- try {
165
- const { message, description } = req.body;
166
- await git.commit([message, ...(description ? [description] : [])], { '--allow-empty': null });
167
- resp.type('application/json').send({ success: true });
168
- }
169
- catch (err) {
170
- resp.status(400).type('application/json').send({ error: err.message });
171
- }
172
- });
173
- app.post('/api/amend', async (req, resp) => {
174
- try {
175
- const { message, description } = req.body;
176
- await git.commit([message, ...(description ? [description] : [])], {
177
- '--amend': null,
178
- '--no-edit': null,
179
- });
180
- resp.type('application/json').send({ success: true });
181
- }
182
- catch (err) {
183
- resp.status(400).type('application/json').send({ error: err.message });
184
- }
185
- });
186
- app.post('/api/revert', async (req, resp) => {
187
- try {
188
- const { commit } = req.body;
189
- await git.revert(commit);
190
- resp.type('application/json').send({ success: true });
191
- }
192
- catch (err) {
193
- resp.status(400).type('application/json').send({ error: err.message });
194
- }
195
- });
196
- app.post('/api/cherry-pick', async (req, resp) => {
197
- try {
198
- await git.raw(['cherry-pick', req.body.commit]);
199
- resp.type('application/json').send({ success: true });
200
- }
201
- catch (err) {
202
- resp.status(400).type('application/json').send({ error: err.message });
203
- }
204
- });
205
- app.post('/api/commit/drop', async (req, resp) => {
206
- try {
207
- await git.raw(['reset', '--hard', `${req.body.commit}^`]);
208
- resp.type('application/json').send({ success: true });
209
- }
210
- catch (err) {
211
- resp.status(400).type('application/json').send({ error: err.message });
212
- }
213
- });
214
- app.post('/api/reset-commit', async (req, resp) => {
215
- try {
216
- await git.raw(['reset', '--hard', req.body.commit]);
217
- resp.type('application/json').send({ success: true });
218
- }
219
- catch (err) {
220
- resp.status(400).type('application/json').send({ error: err.message });
221
- }
222
- });
223
- // ==================== Staging ====================
224
- app.get('/api/status', async (_req, resp) => {
225
- try {
226
- const status = await git.status();
227
- resp.type('application/json').send(status);
228
- }
229
- catch (err) {
230
- resp.status(400).type('application/json').send({ error: err.message });
231
- }
232
- });
233
- app.post('/api/stage', async (req, resp) => {
234
- try {
235
- const { files } = req.body;
236
- if (Array.isArray(files) && files.length > 0) {
237
- await git.add(files);
238
- }
239
- resp.type('application/json').send({ success: true });
240
- }
241
- catch (err) {
242
- resp.status(400).type('application/json').send({ error: err.message });
243
- }
244
- });
245
- app.post('/api/unstage', async (req, resp) => {
246
- try {
247
- const { files } = req.body;
248
- if (Array.isArray(files) && files.length > 0) {
249
- await git.reset(['HEAD', ...files]);
250
- }
251
- resp.type('application/json').send({ success: true });
252
- }
253
- catch (err) {
254
- resp.status(400).type('application/json').send({ error: err.message });
255
- }
256
- });
257
- app.post('/api/stage-lines', async (req, resp) => {
258
- try {
259
- const { file, patch } = req.body;
260
- // Stage specific lines using git add -p equivalent (patch mode)
261
- await git.raw('add --patch', { input: patch });
262
- resp.type('application/json').send({ success: true });
263
- }
264
- catch (err) {
265
- resp.status(400).type('application/json').send({ error: err.message });
266
- }
267
- });
268
- // ==================== Branches ====================
269
- app.get('/api/branches/all', async (_req, resp) => {
270
- try {
271
- const raw = await git.raw([
272
- 'for-each-ref',
273
- '--format=%(refname)%09%(objectname)%09%(HEAD)',
274
- 'refs/heads',
275
- 'refs/remotes',
276
- ]);
277
- const branches = raw
278
- .split('\n')
279
- .map((line) => line.trim())
280
- .filter(Boolean)
281
- .map((line) => {
282
- const [refname, commit, headMarker] = line.split('\t');
283
- const remote = refname.startsWith('refs/remotes/');
284
- const name = refname.replace(/^refs\/(heads|remotes)\//, '');
285
- return { name, commit, current: headMarker === '*', remote };
286
- });
287
- resp.type('application/json').send(branches);
288
- }
289
- catch (err) {
290
- resp.status(400).type('application/json').send({ error: err.message });
291
- }
292
- });
293
- app.get('/api/branches', async (_req, resp) => {
294
- try {
295
- const branchSummary = await git.branchLocal();
296
- resp.type('application/json').send(branchSummary);
297
- }
298
- catch (err) {
299
- resp.status(400).type('application/json').send({ error: err.message });
300
- }
301
- });
302
- app.post('/api/branch/create', async (req, resp) => {
303
- try {
304
- const { name, startPoint } = req.body;
305
- await git.branch([...(startPoint ? [name, startPoint] : [name])]);
306
- resp.type('application/json').send({ success: true });
307
- }
308
- catch (err) {
309
- resp.status(400).type('application/json').send({ error: err.message });
310
- }
311
- });
312
- app.post('/api/branch/delete', async (req, resp) => {
313
- try {
314
- const { name, force } = req.body;
315
- await git.deleteLocalBranch(name, force);
316
- resp.type('application/json').send({ success: true });
317
- }
318
- catch (err) {
319
- resp.status(400).type('application/json').send({ error: err.message });
320
- }
321
- });
322
- app.post('/api/branch/delete-remote', async (req, resp) => {
323
- try {
324
- const { remote, branch } = req.body;
325
- await git.push([remote, '--delete', branch]);
326
- resp.type('application/json').send({ success: true });
327
- }
328
- catch (err) {
329
- resp.status(400).type('application/json').send({ error: err.message });
330
- }
331
- });
332
- app.post('/api/branch/rename', async (req, resp) => {
333
- try {
334
- const { oldName, newName } = req.body;
335
- await git.branch(['-m', oldName, newName]);
336
- resp.type('application/json').send({ success: true });
337
- }
338
- catch (err) {
339
- resp.status(400).type('application/json').send({ error: err.message });
340
- }
341
- });
342
- app.post('/api/checkout', async (req, resp) => {
343
- try {
344
- const { ref } = req.body;
345
- await git.checkout(ref);
346
- resp.type('application/json').send({ success: true });
347
- }
348
- catch (err) {
349
- resp.status(400).type('application/json').send({ error: err.message });
350
- }
351
- });
352
- // ==================== Merge & Rebase ====================
353
- app.post('/api/merge', async (req, resp) => {
354
- try {
355
- const { branch } = req.body;
356
- await git.merge([branch]);
357
- resp.type('application/json').send({ success: true });
358
- }
359
- catch (err) {
360
- resp.status(400).type('application/json').send({ error: err.message });
361
- }
362
- });
363
- app.get('/api/archive', async (req, resp) => {
364
- try {
365
- const ref = String(req.query?.ref ?? 'HEAD');
366
- if (!/^[0-9a-fA-F]{7,40}$/.test(ref) && !/^[A-Za-z0-9._/-]+$/.test(ref)) {
367
- return resp.status(400).type('application/json').send({ error: 'invalid ref' });
368
- }
369
- const result = await execFileAsync('git', ['archive', '--format=zip', ref], {
370
- cwd: process.cwd(),
371
- encoding: 'buffer',
372
- maxBuffer: 100 * 1024 * 1024,
373
- });
374
- return resp
375
- .header('Content-Disposition', `attachment; filename="guito-${ref.slice(0, 8)}.zip"`)
376
- .type('application/zip')
377
- .send(result.stdout);
378
- }
379
- catch (err) {
380
- return resp.status(400).type('application/json').send({ error: err.message });
381
- }
382
- });
383
- app.post('/api/rebase', async (req, resp) => {
384
- try {
385
- const { branch } = req.body;
386
- await git.rebase([branch]);
387
- resp.type('application/json').send({ success: true });
388
- }
389
- catch (err) {
390
- resp.status(400).type('application/json').send({ error: err.message });
391
- }
392
- });
393
- app.post('/api/squash', async (req, resp) => {
394
- try {
395
- const { commits } = req.body;
396
- // Squash: rebase -i with squash operation
397
- await git.rebase(['-i', 'HEAD~' + commits]);
398
- resp.type('application/json').send({ success: true });
399
- }
400
- catch (err) {
401
- resp.status(400).type('application/json').send({ error: err.message });
402
- }
403
- });
404
- // ==================== Stash ====================
405
- app.get('/api/stash/list', async (_req, resp) => {
406
- try {
407
- const stashList = await git.stashList();
408
- resp.type('application/json').send(stashList);
409
- }
410
- catch (err) {
411
- resp.status(400).type('application/json').send({ error: err.message });
412
- }
413
- });
414
- app.post('/api/stash/save', async (req, resp) => {
415
- try {
416
- const { message } = req.body;
417
- await git.stash(['save', message || '']);
418
- resp.type('application/json').send({ success: true });
419
- }
420
- catch (err) {
421
- resp.status(400).type('application/json').send({ error: err.message });
422
- }
423
- });
424
- app.post('/api/stash/apply', async (req, resp) => {
425
- try {
426
- const { index } = req.body;
427
- await git.stash(['apply', `stash@{${index}}`]);
428
- resp.type('application/json').send({ success: true });
429
- }
430
- catch (err) {
431
- resp.status(400).type('application/json').send({ error: err.message });
432
- }
433
- });
434
- app.post('/api/stash/pop', async (req, resp) => {
435
- try {
436
- const { index } = req.body;
437
- await git.stash(['pop', `stash@{${index}}`]);
438
- resp.type('application/json').send({ success: true });
439
- }
440
- catch (err) {
441
- resp.status(400).type('application/json').send({ error: err.message });
442
- }
443
- });
444
- app.post('/api/stash/drop', async (req, resp) => {
445
- try {
446
- const { index } = req.body;
447
- await git.stash(['drop', `stash@{${index}}`]);
448
- resp.type('application/json').send({ success: true });
449
- }
450
- catch (err) {
451
- resp.status(400).type('application/json').send({ error: err.message });
452
- }
453
- });
454
- app.post('/api/stash/show', async (req, resp) => {
455
- try {
456
- const { index } = req.body;
457
- const show = await git.stash(['show', '-p', `stash@{${index}}`]);
458
- resp.type('application/json').send({ preview: show });
459
- }
460
- catch (err) {
461
- resp.status(400).type('application/json').send({ error: err.message });
462
- }
463
- });
464
- // ==================== Tags ====================
465
- app.get('/api/tags', async (_req, resp) => {
466
- try {
467
- const tags = await git.tags();
468
- resp.type('application/json').send(tags);
469
- }
470
- catch (err) {
471
- resp.status(400).type('application/json').send({ error: err.message });
472
- }
473
- });
474
- app.post('/api/tag/create', async (req, resp) => {
475
- try {
476
- const { name, message, commit } = req.body;
477
- if (message) {
478
- await git.tag(['-a', name, '-m', message, ...(commit ? [commit] : [])]);
479
- }
480
- else {
481
- await git.tag([name, ...(commit ? [commit] : [])]);
482
- }
483
- resp.type('application/json').send({ success: true });
484
- }
485
- catch (err) {
486
- resp.status(400).type('application/json').send({ error: err.message });
487
- }
488
- });
489
- app.post('/api/tag/delete', async (req, resp) => {
490
- try {
491
- const { name } = req.body;
492
- await git.tag(['-d', name]);
493
- resp.type('application/json').send({ success: true });
494
- }
495
- catch (err) {
496
- resp.status(400).type('application/json').send({ error: err.message });
497
- }
498
- });
499
- // ==================== Remote Operations ====================
500
- app.get('/api/fetch', async (_req, resp) => {
501
- try {
502
- await git.fetch();
503
- resp.type('application/json').send({ success: true });
504
- }
505
- catch (err) {
506
- resp.status(400).type('application/json').send({ error: err.message });
507
- }
508
- });
509
- app.post('/api/pull', async (req, resp) => {
510
- try {
511
- const { remote, branch, rebase } = req.body;
512
- await git.pull(remote || 'origin', branch || undefined, rebase ? { '--rebase': null } : {});
513
- resp.type('application/json').send({ success: true });
514
- }
515
- catch (err) {
516
- resp.status(400).type('application/json').send({ error: err.message });
517
- }
518
- });
519
- app.post('/api/push', async (req, resp) => {
520
- try {
521
- const { remote, branch, force } = req.body;
522
- await git.push(remote || 'origin', branch || undefined, force ? { '-f': null } : {});
523
- resp.type('application/json').send({ success: true });
524
- }
525
- catch (err) {
526
- resp.status(400).type('application/json').send({ error: err.message });
527
- }
528
- });
529
- app.post('/api/sync', async (_req, resp) => {
530
- try {
531
- await git.pull('origin', undefined, {});
532
- await git.push('origin', undefined, {});
533
- resp.type('application/json').send({ success: true });
534
- }
535
- catch (err) {
536
- resp.status(400).type('application/json').send({ error: err.message });
537
- }
538
- });
539
- app.post('/api/prune', async (req, resp) => {
540
- try {
541
- const { remote } = req.body;
542
- await git.remote(['prune', remote || 'origin']);
543
- resp.type('application/json').send({ success: true });
544
- }
545
- catch (err) {
546
- resp.status(400).type('application/json').send({ error: err.message });
547
- }
548
- });
549
- // ==================== Diff Viewer ====================
550
- app.post('/api/commit/diff', async (req, resp) => {
551
- try {
552
- const { hash } = req.body;
553
- if (!hash) {
554
- return resp.status(400).type('application/json').send({ error: 'hash required' });
555
- }
556
- // Get the raw unified diff (diff against the first parent for merges)
557
- const rawDiff = await git.raw([
558
- 'show',
559
- '--no-color',
560
- '--pretty=format:',
561
- '--find-renames',
562
- '--first-parent',
563
- '-m',
564
- hash,
565
- ]);
566
- resp.type('application/json').send({ hash, files: parseUnifiedDiff(rawDiff) });
567
- }
568
- catch (err) {
569
- resp.status(400).type('application/json').send({ error: err.message });
570
- }
571
- });
572
- // ==================== Working Changes ====================
573
- app.get('/api/working-changes', async (_req, resp) => {
574
- try {
575
- const root = await repoRoot();
576
- // Combined diff of the working tree + index against HEAD.
577
- const rawDiff = await git.raw(['diff', 'HEAD', '--no-color', '--find-renames']);
578
- const files = parseUnifiedDiff(rawDiff);
579
- // Untracked files are not part of `git diff` — add them as new files.
580
- const status = await git.status();
581
- for (const file of status.not_added ?? []) {
582
- let isBinary = false;
583
- let content = '';
584
- try {
585
- const buffer = await readFile(join(root, file));
586
- if (buffer.includes(0)) {
587
- isBinary = true;
588
- }
589
- else {
590
- content = buffer.toString('utf8');
591
- }
592
- }
593
- catch {
594
- content = '';
595
- }
596
- if (isBinary) {
597
- files.push({
598
- path: file,
599
- oldPath: '',
600
- status: 'binary',
601
- lines: [],
602
- additions: 0,
603
- deletions: 0,
604
- });
605
- }
606
- else {
607
- const lines = content.length > 0 ? content.split('\n') : [];
608
- files.push({
609
- path: file,
610
- oldPath: '',
611
- status: 'added',
612
- lines: lines.map((text, index) => ({ type: 'add', newLine: index + 1, text })),
613
- additions: lines.length,
614
- deletions: 0,
615
- });
616
- }
617
- }
618
- const stagedRaw = await git.raw(['diff', '--name-only', '--cached']);
619
- const unstagedRaw = await git.raw(['diff', '--name-only']);
620
- resp.type('application/json').send({
621
- files,
622
- staged: stagedRaw.split('\n').filter(Boolean),
623
- unstaged: unstagedRaw.split('\n').filter(Boolean),
624
- untracked: status.not_added ?? [],
625
- });
626
- }
627
- catch (err) {
628
- resp.status(400).type('application/json').send({ error: err.message });
629
- }
630
- });
631
- // ==================== File Content ====================
632
- app.post('/api/file-content', async (req, resp) => {
633
- try {
634
- const { path, ref } = req.body;
635
- if (!path || !ref) {
636
- return resp.status(400).type('application/json').send({ error: 'path and ref required' });
637
- }
638
- if (ref === 'WORKING') {
639
- const root = await repoRoot();
640
- const abs = resolve(root, path);
641
- const rel = relative(root, abs);
642
- if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
643
- return resp.status(400).type('application/json').send({ error: 'invalid path' });
644
- }
645
- try {
646
- const buffer = await readFile(abs);
647
- if (buffer.includes(0)) {
648
- return resp.type('application/json').send({ binary: true, content: '' });
649
- }
650
- return resp.type('application/json').send({ content: buffer.toString('utf8') });
651
- }
652
- catch {
653
- // File no longer exists on disk (deleted).
654
- return resp.type('application/json').send({ content: '' });
655
- }
656
- }
657
- if (ref === 'EMPTY') {
658
- return resp.type('application/json').send({ content: '' });
659
- }
660
- try {
661
- const content = await git.raw(['show', `${ref}:${path}`]);
662
- if (content.includes('\u0000')) {
663
- return resp.type('application/json').send({ binary: true, content: '' });
664
- }
665
- resp.type('application/json').send({ content });
666
- }
667
- catch {
668
- // Path did not exist at that revision (added file).
669
- resp.type('application/json').send({ content: '' });
670
- }
671
- }
672
- catch (err) {
673
- resp.status(400).type('application/json').send({ error: err.message });
674
- }
675
- });
676
- // ==================== Discard ====================
677
- app.post('/api/discard', async (req, resp) => {
678
- try {
679
- const { files } = req.body;
680
- if (!Array.isArray(files) || files.length === 0) {
681
- return resp.status(400).type('application/json').send({ error: 'files required' });
682
- }
683
- const root = await repoRoot();
684
- const status = await git.status();
685
- const untracked = new Set(status.not_added ?? []);
686
- const tracked = files.filter((file) => !untracked.has(file));
687
- const removed = files.filter((file) => untracked.has(file));
688
- if (tracked.length > 0) {
689
- await git.raw(['checkout', 'HEAD', '--', ...tracked]);
690
- }
691
- for (const file of removed) {
692
- await rm(join(root, file), { force: true, recursive: true });
693
- }
694
- resp.type('application/json').send({ success: true });
695
- }
696
- catch (err) {
697
- resp.status(400).type('application/json').send({ error: err.message });
698
- }
699
- });
700
- // ==================== Working Tree Actions ====================
701
- app.post('/api/reset', async (_req, resp) => {
702
- try {
703
- await git.raw(['reset', '--hard', 'HEAD']);
704
- resp.type('application/json').send({ ok: true });
705
- }
706
- catch (err) {
707
- resp.status(400).type('application/json').send({ error: err.message });
708
- }
709
- });
710
- app.post('/api/clean', async (_req, resp) => {
711
- try {
712
- await git.raw(['clean', '-fd']);
713
- resp.type('application/json').send({ ok: true });
714
- }
715
- catch (err) {
716
- resp.status(400).type('application/json').send({ error: err.message });
717
- }
718
- });
719
- // Run the server
720
- app.listen({ port }, (err, address) => {
721
- if (err)
722
- throw err;
723
- console.log(`Visit "${address}" to see git explorer.`);
724
- if (shouldOpenBrowser)
725
- open(address);
726
- });
30
+ catch (error) {
31
+ console.error(error instanceof Error ? error.message : String(error));
32
+ process.exitCode = 1;
33
+ }
727
34
  })();