repoburg 1.0.49 → 1.0.51

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,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const express_1 = require("express");
4
+ const child_process_1 = require("child_process");
5
+ const checkAuth_1 = require("../middleware/checkAuth");
6
+ const router = (0, express_1.Router)();
7
+ router.post('/update', checkAuth_1.checkAuth, (req, res) => {
8
+ try {
9
+ console.log('[DAEMON] Received request to start update process.');
10
+ // Using `spawn` with `detached: true` and `stdio: 'ignore'` allows the parent (daemon)
11
+ // to exit while the child (update script) continues running. `unref()` is crucial.
12
+ const child = (0, child_process_1.spawn)('repoburg', ['update'], {
13
+ detached: true,
14
+ stdio: 'ignore',
15
+ shell: true,
16
+ });
17
+ child.unref();
18
+ res.status(202).json({ message: 'Update process initiated. The daemon and services will restart shortly.' });
19
+ }
20
+ catch (error) {
21
+ console.error('Failed to spawn update process:', error);
22
+ res.status(500).json({ error: 'Failed to start update process.' });
23
+ }
24
+ });
25
+ exports.default = router;
@@ -12,6 +12,7 @@ const serviceManager_1 = require("./serviceManager");
12
12
  const services_1 = __importDefault(require("./api/services"));
13
13
  const auth_1 = __importDefault(require("./api/auth"));
14
14
  const registry_1 = __importDefault(require("./api/registry"));
15
+ const system_1 = __importDefault(require("./api/system"));
15
16
  const PORT = 9998;
16
17
  async function main() {
17
18
  await serviceManager_1.serviceManager.connect();
@@ -28,6 +29,7 @@ async function main() {
28
29
  app.use('/services', services_1.default);
29
30
  app.use('/auth', (0, auth_1.default)(wss));
30
31
  app.use('/registry', registry_1.default);
32
+ app.use('/system', system_1.default);
31
33
  app.get('/health', (req, res) => {
32
34
  res.status(200).json({ status: 'ok' });
33
35
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repoburg",
3
- "version": "1.0.49",
3
+ "version": "1.0.51",
4
4
  "description": "A local AI-powered software developer assistant that runs on your own machine.",
5
5
  "author": "Celal Ertug",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -16,8 +16,7 @@
16
16
  ],
17
17
  "bin": {
18
18
  "repoburg": "./platform-cli.js",
19
- "repoburg-daemon": "daemon/dist/index.js",
20
- "repoburg-backend": "backend/dist/main.js"
19
+ "repoburg-daemon": "daemon/dist/index.js"
21
20
  },
22
21
  "scripts": {
23
22
  "postinstall": "node postinstall.js",
@@ -59,6 +58,7 @@
59
58
  "libphonenumber-js": "^1.12.10",
60
59
  "mermaid": "^11.10.1",
61
60
  "open": "^10.1.0",
61
+ "ora": "^8.0.1",
62
62
  "pm2": "^6.0.8",
63
63
  "reflect-metadata": "^0.1.13",
64
64
  "rxjs": "^7.8.1",
package/platform-cli.js CHANGED
@@ -5,8 +5,11 @@ const axios = require('axios');
5
5
  const path = require('path');
6
6
  const fs = require('fs/promises');
7
7
  const packageJson = require('./package.json');
8
+ const pm2 = require('pm2');
9
+ const { spawn } = require('child_process');
8
10
 
9
11
  const DAEMON_BASE_URL = 'http://localhost:9998';
12
+ const DAEMON_PM2_NAME = 'repoburg-daemon';
10
13
 
11
14
  // Uncomment the next line to use localhost for development
12
15
  // const FRONTEND_BASE_URL ='http://localhost:3001';
@@ -32,6 +35,37 @@ const handleApiError = async (error) => {
32
35
  process.exit(1);
33
36
  };
34
37
 
38
+ const connectToPm2 = () => new Promise((resolve, reject) => {
39
+ pm2.connect(err => {
40
+ if (err) return reject(new Error(`Could not connect to pm2: ${err.message}`));
41
+ resolve();
42
+ });
43
+ });
44
+
45
+ const startDaemon = async () => {
46
+ const { default: ora } = await import('ora');
47
+ const spinner = ora('Starting daemon process...').start();
48
+ try {
49
+ await connectToPm2();
50
+ await new Promise((resolve, reject) => {
51
+ pm2.start({
52
+ name: DAEMON_PM2_NAME,
53
+ script: 'repoburg-daemon',
54
+ }, (err) => {
55
+ pm2.disconnect();
56
+ if (err) {
57
+ const errMsg = err.message || err.msg;
58
+ return reject(new Error(`Error starting daemon with pm2: ${errMsg || err}`));
59
+ }
60
+ resolve();
61
+ });
62
+ });
63
+ spinner.succeed('Daemon process started via pm2.');
64
+ } catch (error) {
65
+ spinner.fail('Failed to start daemon.');
66
+ throw error;
67
+ }
68
+ };
35
69
 
36
70
  program
37
71
  .name('repoburg')
@@ -83,6 +117,7 @@ program
83
117
  .action(async (projectPath) => {
84
118
  const { default: chalk } = await import('chalk');
85
119
  const { default: open } = await import('open');
120
+ const { default: ora } = await import('ora');
86
121
 
87
122
  const pollForAuth = (timeout = 300000) => { // 5 minutes timeout
88
123
  process.stdout.write(chalk.yellow('Waiting for authorization...'));
@@ -115,24 +150,19 @@ program
115
150
  };
116
151
 
117
152
  const pollForServiceHealth = (port, timeout = 30000) => {
118
- process.stdout.write(chalk.yellow(`Waiting for backend on port ${port} to be healthy...`));
119
- const spinner = setInterval(() => process.stdout.write('.'), 1000);
120
-
153
+ const spinner = ora(`Waiting for backend on port ${port} to be healthy...`).start();
121
154
  return new Promise((resolve, reject) => {
122
155
  const startTime = Date.now();
123
156
  const pollInterval = setInterval(async () => {
124
157
  if (Date.now() - startTime > timeout) {
125
158
  clearInterval(pollInterval);
126
- clearInterval(spinner);
127
- process.stdout.write('\n');
159
+ spinner.fail('Backend health check timed out.');
128
160
  return reject(new Error('Backend health check timed out. Check logs with `repoburg logs <name>`.'));
129
161
  }
130
162
  try {
131
163
  await axios.get(`http://localhost:${port}/health`);
132
164
  clearInterval(pollInterval);
133
- clearInterval(spinner);
134
- process.stdout.write('\n');
135
- console.log(chalk.green(`✓ Backend is healthy.`));
165
+ spinner.succeed(`Backend is healthy.`);
136
166
  resolve();
137
167
  } catch (e) {
138
168
  // ignore until timeout
@@ -142,18 +172,49 @@ program
142
172
  };
143
173
 
144
174
  try {
145
- // 1. Check auth status
175
+ // 1. Check daemon status and start if not running
146
176
  let authResponse;
147
177
  try {
148
178
  authResponse = await axios.get(`${DAEMON_BASE_URL}/auth/status`);
149
179
  } catch (error) {
150
180
  if (error.request && !error.response) {
151
- console.error(chalk.red('Daemon not reachable. Is it running?'));
152
- console.error(chalk.yellow(`Attempted to connect to ${DAEMON_BASE_URL}`));
153
- console.log(chalk.cyan('You can typically start the daemon by running `npm run start:daemon` from the project root, or via the launchd service if installed.'));
154
- process.exit(1);
181
+ console.log(chalk.yellow('Daemon not reachable. Attempting to start it now...'));
182
+
183
+ await startDaemon();
184
+
185
+ const pollForDaemonHealth = (timeout = 30000) => {
186
+ const spinner = ora('Waiting for daemon to become healthy...').start();
187
+ return new Promise((resolve, reject) => {
188
+ const startTime = Date.now();
189
+ const pollInterval = setInterval(async () => {
190
+ if (Date.now() - startTime > timeout) {
191
+ clearInterval(pollInterval);
192
+ spinner.fail('Daemon health check timed out.');
193
+ return reject(new Error('Daemon health check timed out. Check logs with `repoburg daemon logs`.'));
194
+ }
195
+ try {
196
+ await axios.get(`${DAEMON_BASE_URL}/health`);
197
+ clearInterval(pollInterval);
198
+ spinner.succeed('Daemon is healthy.');
199
+ resolve();
200
+ } catch (e) {
201
+ // ignore until timeout
202
+ }
203
+ }, 1000)
204
+ });
205
+ };
206
+
207
+ await pollForDaemonHealth();
208
+
209
+ try {
210
+ authResponse = await axios.get(`${DAEMON_BASE_URL}/auth/status`);
211
+ } catch (retryError) {
212
+ console.error(chalk.red('Daemon started but is not responding correctly.'));
213
+ return handleApiError(retryError);
214
+ }
215
+ } else {
216
+ return handleApiError(error);
155
217
  }
156
- return handleApiError(error);
157
218
  }
158
219
 
159
220
  if (authResponse.data.status !== 'authorized') {
@@ -338,5 +399,344 @@ program
338
399
  }
339
400
  });
340
401
 
402
+ program
403
+ .command('update')
404
+ .description('Update the Repoburg platform to the latest version.')
405
+ .action(async () => {
406
+ const { default: chalk } = await import('chalk');
407
+ const { default: ora } = await import('ora');
408
+
409
+ const runCommand = (command, args) => {
410
+ return new Promise((resolve, reject) => {
411
+ const spinner = ora(`Running \`${command} ${args.join(' ')}\`...`).start();
412
+ const child = spawn(command, args, { stdio: 'pipe', shell: true });
413
+ let output = '';
414
+
415
+ child.stdout.on('data', (data) => {
416
+ output += data.toString();
417
+ });
418
+ child.stderr.on('data', (data) => {
419
+ output += data.toString();
420
+ });
421
+
422
+ child.on('close', (code) => {
423
+ if (code === 0) {
424
+ spinner.succeed(`Command \`${command} ${args.join(' ')}\` finished.`);
425
+ resolve();
426
+ } else {
427
+ spinner.fail(`Command \`${command} ${args.join(' ')}\` failed with code ${code}.`);
428
+ console.error(chalk.gray(output));
429
+ reject(new Error(`Command failed`));
430
+ }
431
+ });
432
+ child.on('error', (err) => {
433
+ spinner.fail(`Failed to execute command.`);
434
+ reject(err);
435
+ });
436
+ });
437
+ };
438
+
439
+ const pollForDaemonHealth = (timeout = 30000) => {
440
+ const spinner = ora('Waiting for daemon to become healthy...').start();
441
+ return new Promise((resolve, reject) => {
442
+ const startTime = Date.now();
443
+ const pollInterval = setInterval(async () => {
444
+ if (Date.now() - startTime > timeout) {
445
+ clearInterval(pollInterval);
446
+ spinner.fail('Daemon health check timed out.');
447
+ return reject(new Error('Daemon health check timed out.'));
448
+ }
449
+ try {
450
+ await axios.get(`${DAEMON_BASE_URL}/health`);
451
+ clearInterval(pollInterval);
452
+ spinner.succeed('Daemon is healthy.');
453
+ resolve();
454
+ } catch (e) {
455
+ // ignore until timeout
456
+ }
457
+ }, 1000)
458
+ });
459
+ };
460
+
461
+ let runningServices = [];
462
+
463
+ try {
464
+ // 1. Get list of running services
465
+ const listSpinner = ora('Fetching list of running services...').start();
466
+ try {
467
+ const response = await axios.get(`${DAEMON_BASE_URL}/services`);
468
+ runningServices = response.data.filter(s => s.status === 'running').map(s => s.name);
469
+ listSpinner.succeed(`Found ${runningServices.length} running services to restart later.`);
470
+ } catch (error) {
471
+ if (error.request && !error.response) { // Daemon not running
472
+ listSpinner.warn('Daemon not reachable, assuming no services are running.');
473
+ } else {
474
+ listSpinner.fail('Failed to fetch services.');
475
+ throw error;
476
+ }
477
+ }
478
+
479
+ // 2. Stop all running services
480
+ if (runningServices.length > 0) {
481
+ const stopSpinner = ora('Stopping running backend services...').start();
482
+ for (const name of runningServices) {
483
+ stopSpinner.text = `Stopping ${name}...`;
484
+ await axios.post(`${DAEMON_BASE_URL}/services/${name}/stop`);
485
+ }
486
+ stopSpinner.succeed('All backend services stopped.');
487
+ }
488
+
489
+ // 3. Stop the daemon
490
+ const stopDaemonSpinner = ora('Stopping daemon...').start();
491
+ await new Promise((resolve, reject) => {
492
+ pm2.connect(err => {
493
+ if (err) return reject(err);
494
+ pm2.stop(DAEMON_PM2_NAME, (err) => {
495
+ pm2.disconnect();
496
+ const errMsg = err ? (err.message || err.msg) : null;
497
+ if (err && !(errMsg && errMsg.includes('not found'))) {
498
+ return reject(err);
499
+ }
500
+ resolve();
501
+ });
502
+ });
503
+ });
504
+ stopDaemonSpinner.succeed('Daemon stopped.');
505
+
506
+ // 4. Run update
507
+ await runCommand('npm', ['i', '-g', 'repoburg@latest']);
508
+
509
+ // 5. Start the daemon
510
+ const startDaemonSpinner = ora('Starting daemon...').start();
511
+ await new Promise((resolve, reject) => {
512
+ pm2.connect(err => {
513
+ if (err) return reject(err);
514
+ pm2.start({ name: DAEMON_PM2_NAME, script: 'repoburg-daemon' }, (err) => {
515
+ pm2.disconnect();
516
+ if (err) return reject(err);
517
+ resolve();
518
+ });
519
+ });
520
+ });
521
+ startDaemonSpinner.succeed('Daemon process started.');
522
+ await pollForDaemonHealth();
523
+
524
+ // 6. Restart services
525
+ if (runningServices.length > 0) {
526
+ const restartSpinner = ora('Restarting backend services...').start();
527
+ for (const name of runningServices) {
528
+ restartSpinner.text = `Restarting ${name}...`;
529
+ await axios.post(`${DAEMON_BASE_URL}/services/${name}/restart`);
530
+ }
531
+ restartSpinner.succeed('All backend services restarted.');
532
+ }
533
+
534
+ console.log(chalk.green.bold('\n✓ Repoburg update complete!'));
535
+
536
+ } catch (error) {
537
+ console.error(chalk.red('\nUpdate process failed:'), error.message);
538
+ console.error(chalk.yellow('Your services might be in a stopped state. Please check with `repoburg list` and `repoburg daemon status`.'));
539
+ process.exit(1);
540
+ }
541
+ });
542
+
543
+ // --- Daemon Management ---
544
+ const daemon = program.command('daemon')
545
+ .description('Manage the Repoburg Platform Daemon process (the service running on port 9998).');
546
+
547
+ daemon
548
+ .command('start')
549
+ .description('Start the daemon process in the background using pm2.')
550
+ .action(async () => {
551
+ const { default: chalk } = await import('chalk');
552
+ try {
553
+ await startDaemon();
554
+ console.log(chalk.cyan(`Run 'repoburg daemon status' to check its state.`));
555
+ } catch (error) {
556
+ console.error(chalk.red('An error occurred:'), error.message);
557
+ process.exit(1);
558
+ }
559
+ });
560
+
561
+ daemon
562
+ .command('status')
563
+ .description('Show the status of the daemon process.')
564
+ .action(async () => {
565
+ const { default: chalk } = await import('chalk');
566
+ const Table = require('cli-table3');
567
+ try {
568
+ await connectToPm2();
569
+ pm2.list((err, list) => {
570
+ if (err) {
571
+ console.error(chalk.red('Error listing pm2 processes:'), err.message || err);
572
+ pm2.disconnect();
573
+ process.exit(1);
574
+ }
575
+
576
+ const daemonProcess = list.find(p => p.name === DAEMON_PM2_NAME);
577
+ if (!daemonProcess) {
578
+ console.log(chalk.yellow(`Daemon process "${DAEMON_PM2_NAME}" is not running or not managed by pm2.`));
579
+ } else {
580
+ const table = new Table({
581
+ head: ['NAME', 'STATUS', 'PID', 'CPU', 'MEMORY'].map(h => chalk.cyan(h)),
582
+ colWidths: [20, 12, 8, 8, 15]
583
+ });
584
+ const status = daemonProcess.pm2_env.status;
585
+ const statusColor = status === 'online' ? chalk.green : chalk.yellow;
586
+ table.push([
587
+ daemonProcess.name,
588
+ statusColor(status),
589
+ daemonProcess.pid,
590
+ `${daemonProcess.monit.cpu || 0}%`,
591
+ `${(daemonProcess.monit.memory / 1024 / 1024).toFixed(1)} MB`
592
+ ]);
593
+ console.log(table.toString());
594
+ }
595
+ pm2.disconnect();
596
+ });
597
+ } catch (error) {
598
+ console.error(chalk.red('An error occurred:'), error.message);
599
+ process.exit(1);
600
+ }
601
+ });
602
+
603
+ daemon
604
+ .command('stop')
605
+ .description('Stop the daemon process.')
606
+ .action(async () => {
607
+ const { default: chalk } = await import('chalk');
608
+ try {
609
+ await connectToPm2();
610
+ console.log(chalk.cyan(`Stopping daemon process "${DAEMON_PM2_NAME}"...`));
611
+ pm2.stop(DAEMON_PM2_NAME, (err) => {
612
+ const errMsg = err ? (err.message || err.msg) : null;
613
+ if (err) {
614
+ if (errMsg && errMsg.includes('not found')) {
615
+ console.log(chalk.yellow(`Daemon process "${DAEMON_PM2_NAME}" not found or not running.`));
616
+ } else {
617
+ console.error(chalk.red('Error stopping daemon:'), errMsg || err);
618
+ pm2.disconnect();
619
+ process.exit(1);
620
+ }
621
+ } else {
622
+ console.log(chalk.green('✓ Daemon stopped.'));
623
+ }
624
+ pm2.disconnect();
625
+ });
626
+ } catch (error) {
627
+ console.error(chalk.red('An error occurred:'), error.message);
628
+ process.exit(1);
629
+ }
630
+ });
631
+
632
+ daemon
633
+ .command('restart')
634
+ .description('Restart the daemon process.')
635
+ .action(async () => {
636
+ const { default: chalk } = await import('chalk');
637
+ try {
638
+ await connectToPm2();
639
+ console.log(chalk.cyan(`Restarting daemon process "${DAEMON_PM2_NAME}"...`));
640
+ pm2.restart(DAEMON_PM2_NAME, (err) => {
641
+ const errMsg = err ? (err.message || err.msg) : null;
642
+ if (err) {
643
+ if (errMsg && errMsg.includes('not found')) {
644
+ console.log(chalk.yellow(`Daemon process "${DAEMON_PM2_NAME}" not found. Use 'repoburg daemon start' instead.`));
645
+ } else {
646
+ console.error(chalk.red('Error restarting daemon:'), errMsg || err);
647
+ pm2.disconnect();
648
+ process.exit(1);
649
+ }
650
+ } else {
651
+ console.log(chalk.green('✓ Daemon restarted.'));
652
+ }
653
+ pm2.disconnect();
654
+ });
655
+ } catch (error) {
656
+ console.error(chalk.red('An error occurred:'), error.message);
657
+ process.exit(1);
658
+ }
659
+ });
660
+
661
+ daemon
662
+ .command('delete')
663
+ .alias('rm')
664
+ .description('Stop and delete the daemon process from pm2.')
665
+ .action(async () => {
666
+ const { default: chalk } = await import('chalk');
667
+ try {
668
+ await connectToPm2();
669
+ console.log(chalk.cyan(`Deleting daemon process "${DAEMON_PM2_NAME}" from pm2...`));
670
+ pm2.delete(DAEMON_PM2_NAME, (err) => {
671
+ const errMsg = err ? (err.message || err.msg) : null;
672
+ if (err) {
673
+ if (errMsg && errMsg.includes('not found')) {
674
+ console.log(chalk.yellow(`Daemon process "${DAEMON_PM2_NAME}" not found.`));
675
+ } else {
676
+ console.error(chalk.red('Error deleting daemon:'), errMsg || err);
677
+ pm2.disconnect();
678
+ process.exit(1);
679
+ }
680
+ } else {
681
+ console.log(chalk.green('✓ Daemon process deleted from pm2.'));
682
+ }
683
+ pm2.disconnect();
684
+ });
685
+ } catch (error) {
686
+ console.error(chalk.red('An error occurred:'), error.message);
687
+ process.exit(1);
688
+ }
689
+ });
690
+
691
+ daemon
692
+ .command('logs')
693
+ .description('Display logs for the daemon process.')
694
+ .action(async () => {
695
+ const { default: chalk } = await import('chalk');
696
+ try {
697
+ await connectToPm2();
698
+ pm2.describe(DAEMON_PM2_NAME, async (err, description) => {
699
+ if (err) {
700
+ console.error(chalk.red('Error describing daemon process:'), err.message || err);
701
+ pm2.disconnect();
702
+ process.exit(1);
703
+ }
704
+
705
+ if (!description || description.length === 0) {
706
+ console.log(chalk.yellow(`Daemon process "${DAEMON_PM2_NAME}" not found.`));
707
+ pm2.disconnect();
708
+ return;
709
+ }
710
+
711
+ const proc = description[0];
712
+ const outLogPath = proc.pm2_env.pm_out_log_path;
713
+ const errLogPath = proc.pm2_env.pm_err_log_path;
714
+
715
+ console.log(chalk.cyan(`--- Logs for ${DAEMON_PM2_NAME} ---`));
716
+
717
+ const readAndPrint = async (filePath, logType) => {
718
+ try {
719
+ const content = await fs.readFile(filePath, 'utf-8');
720
+ console.log(chalk.bold.yellow(`\n--- ${logType} Log (${filePath}) ---\n`));
721
+ console.log(content || chalk.gray('(empty)'));
722
+ } catch (e) {
723
+ if (e.code !== 'ENOENT') {
724
+ throw e;
725
+ }
726
+ console.log(chalk.bold.yellow(`\n--- ${logType} Log (${filePath}) ---\n`));
727
+ console.log(chalk.gray('(not found)'));
728
+ }
729
+ };
730
+
731
+ await readAndPrint(outLogPath, 'Output');
732
+ await readAndPrint(errLogPath, 'Error');
733
+
734
+ pm2.disconnect();
735
+ });
736
+ } catch (error) {
737
+ console.error(chalk.red('An error occurred:'), error.message);
738
+ process.exit(1);
739
+ }
740
+ });
341
741
 
342
742
  program.parse(process.argv);