repoburg 0.1.6 → 0.1.8

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/package.json CHANGED
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "name": "repoburg",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Binary distribution for repoburg",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "repoburg": "release/daemon/repoburg-daemon",
7
+ "repoburg": "platform-cli.js",
8
+ "repoburg-daemon": "release/daemon/repoburg-daemon",
8
9
  "repoburg-backend": "release/backend/repoburg-backend"
9
10
  },
10
11
  "files": [
11
12
  "release/daemon/repoburg-daemon",
12
13
  "release/backend/repoburg-backend",
13
14
  "index.js",
15
+ "platform-cli.js",
14
16
  "README.md"
15
17
  ],
16
18
  "scripts": {
17
- "test": "echo \"Error: no test specified\" && exit 1",
18
19
  "publish:patch": "npm version patch && npm publish"
19
20
  },
20
21
  "repository": {
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+ require('dotenv').config();
3
+
4
+ const { Command } = require('commander');
5
+ const axios = require('axios');
6
+ const path = require('path');
7
+ const fs = require('fs/promises');
8
+ const packageJson = require('./package.json');
9
+
10
+ const DAEMON_BASE_URL = 'http://localhost:9998';
11
+
12
+ // Uncomment the next line to use localhost for development
13
+ // const FRONTEND_BASE_URL ='http://localhost:3001';
14
+ const FRONTEND_BASE_URL = 'https://app.repoburg.com';
15
+
16
+
17
+ const program = new Command();
18
+
19
+
20
+ const handleApiError = async (error) => {
21
+ const { default: chalk } = await import('chalk');
22
+ if (error.response) {
23
+ console.error(chalk.red(`Error: ${error.response.status} - ${error.response.statusText}`));
24
+ if (error.response.data && error.response.data.error) {
25
+ console.error(chalk.red.bold('Daemon Error:'), chalk.red(error.response.data.message || error.response.data.error));
26
+ }
27
+ } else if (error.request) {
28
+ console.error(chalk.red('Daemon not reachable. Is it running?'));
29
+ console.error(chalk.yellow(`Attempted to connect to ${DAEMON_BASE_URL}`));
30
+ } else {
31
+ console.error(chalk.red('An unexpected error occurred:'), error.message);
32
+ }
33
+ process.exit(1);
34
+ };
35
+
36
+
37
+ program
38
+ .name('repoburg-platform')
39
+ .description('CLI to manage the Repoburg Platform Daemon and services.')
40
+ .version(packageJson.version);
41
+
42
+ program
43
+ .command('list')
44
+ .alias('ls')
45
+ .description('List all managed repoburg-backend services.')
46
+ .action(async () => {
47
+ const Table = require('cli-table3');
48
+ const { default: chalk } = await import('chalk');
49
+ try {
50
+ const response = await axios.get(`${DAEMON_BASE_URL}/services`);
51
+ const services = response.data;
52
+
53
+ const table = new Table({
54
+ head: ['NAME', 'STATUS', 'PORT', 'PID', 'PATH'].map(h => chalk.cyan(h)),
55
+ colWidths: [20, 12, 8, 8, 60]
56
+ });
57
+
58
+ if (services.length === 0) {
59
+ console.log(chalk.yellow('No services are currently being managed.'));
60
+ return;
61
+ }
62
+
63
+ services.forEach(service => {
64
+ const statusColor = service.status === 'running' ? chalk.green : chalk.yellow;
65
+ table.push([
66
+ service.name,
67
+ statusColor(service.status),
68
+ service.port || '-',
69
+ service.pid || '-',
70
+ service.path
71
+ ]);
72
+ });
73
+
74
+ console.log(table.toString());
75
+ } catch (error) {
76
+ handleApiError(error);
77
+ }
78
+ });
79
+
80
+ program
81
+ .command('start [projectPath]')
82
+ .description('Start a new repoburg-backend instance for a project.')
83
+ .action(async (projectPath) => {
84
+ const { default: chalk } = await import('chalk');
85
+ const { default: open } = await import('open');
86
+
87
+ const pollForAuth = (timeout = 300000) => { // 5 minutes timeout
88
+ process.stdout.write(chalk.yellow('Waiting for authorization...'));
89
+ const spinner = setInterval(() => process.stdout.write('.'), 1000);
90
+
91
+ return new Promise((resolve, reject) => {
92
+ const startTime = Date.now();
93
+ const pollInterval = setInterval(async () => {
94
+ if (Date.now() - startTime > timeout) {
95
+ clearInterval(pollInterval);
96
+ clearInterval(spinner);
97
+ process.stdout.write('\n');
98
+ return reject(new Error('Login timed out. Please run `repoburg-platform login` and try again.'));
99
+ }
100
+
101
+ try {
102
+ const statusResponse = await axios.get(`${DAEMON_BASE_URL}/auth/status`);
103
+ if (statusResponse.data.status === 'authorized') {
104
+ clearInterval(pollInterval);
105
+ clearInterval(spinner);
106
+ process.stdout.write('\n');
107
+ console.log(chalk.green('✓ Authorization successful!'));
108
+ resolve();
109
+ }
110
+ } catch (e) {
111
+ // ignore connection errors during polling
112
+ }
113
+ }, 2000);
114
+ });
115
+ };
116
+
117
+ 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
+
121
+ return new Promise((resolve, reject) => {
122
+ const startTime = Date.now();
123
+ const pollInterval = setInterval(async () => {
124
+ if (Date.now() - startTime > timeout) {
125
+ clearInterval(pollInterval);
126
+ clearInterval(spinner);
127
+ process.stdout.write('\n');
128
+ return reject(new Error('Backend health check timed out. Check logs with `repoburg-platform logs <name>`.'));
129
+ }
130
+ try {
131
+ await axios.get(`http://localhost:${port}/health`);
132
+ clearInterval(pollInterval);
133
+ clearInterval(spinner);
134
+ process.stdout.write('\n');
135
+ console.log(chalk.green(`✓ Backend is healthy.`));
136
+ resolve();
137
+ } catch (e) {
138
+ // ignore until timeout
139
+ }
140
+ }, 1000)
141
+ });
142
+ };
143
+
144
+ try {
145
+ // 1. Check auth status
146
+ let authResponse;
147
+ try {
148
+ authResponse = await axios.get(`${DAEMON_BASE_URL}/auth/status`);
149
+ } catch (error) {
150
+ 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);
155
+ }
156
+ return handleApiError(error);
157
+ }
158
+
159
+ if (authResponse.data.status !== 'authorized') {
160
+ console.log(chalk.yellow(`Authentication required. Status: ${authResponse.data.status}.`));
161
+
162
+ // 2. Initiate login
163
+ console.log(chalk.yellow('Opening your browser to log in...'));
164
+ const initiateResponse = await axios.post(`${DAEMON_BASE_URL}/auth/initiate`);
165
+ const { loginUrl } = initiateResponse.data;
166
+
167
+ if (loginUrl) {
168
+ await open(loginUrl);
169
+ console.log(chalk.green('✓ Please complete the authentication in your browser.'));
170
+ await pollForAuth();
171
+ } else {
172
+ console.error(chalk.red('Could not retrieve login URL from the daemon.'));
173
+ process.exit(1);
174
+ }
175
+ }
176
+
177
+ // 4. Start the service
178
+ console.log(chalk.cyan('Starting backend service...'));
179
+ const targetPath = path.resolve(projectPath || '.');
180
+ const startResponse = await axios.post(`${DAEMON_BASE_URL}/services/start`, { path: targetPath });
181
+ const service = startResponse.data;
182
+ console.log(chalk.green(`✓ Repoburg backend for '${service.name}' started on port ${service.port}.`));
183
+
184
+ // 5. Wait for service to be healthy
185
+ await pollForServiceHealth(service.port);
186
+
187
+ // 6. Open frontend in browser
188
+
189
+ const frontendUrl = `${FRONTEND_BASE_URL}?bport=${service.port}`;
190
+ console.log(chalk.cyan(`Opening frontend at ${frontendUrl}`));
191
+ await open(frontendUrl);
192
+
193
+ } catch (error) {
194
+ handleApiError(error);
195
+ }
196
+ });
197
+
198
+ program
199
+ .command('stop <name>')
200
+ .description('Stop a running repoburg-backend instance.')
201
+ .action(async (name) => {
202
+ const { default: chalk } = await import('chalk');
203
+ try {
204
+ await axios.post(`${DAEMON_BASE_URL}/services/${name}/stop`);
205
+ console.log(chalk.green(`✓ Service '${name}' stopped successfully.`));
206
+ } catch (error) {
207
+ handleApiError(error);
208
+ }
209
+ });
210
+
211
+ program
212
+ .command('restart <name>')
213
+ .description('Restart a repoburg-backend instance.')
214
+ .action(async (name) => {
215
+ const { default: chalk } = await import('chalk');
216
+ try {
217
+ await axios.post(`${DAEMON_BASE_URL}/services/${name}/restart`);
218
+ console.log(chalk.green(`✓ Service '${name}' restarted successfully.`));
219
+ } catch (error) {
220
+ handleApiError(error);
221
+ }
222
+ });
223
+
224
+ program
225
+ .command('remove <name>')
226
+ .alias('rm')
227
+ .description('Stop and remove a service from management.')
228
+ .action(async (name) => {
229
+ const { default: chalk } = await import('chalk');
230
+ try {
231
+ const response = await axios.delete(`${DAEMON_BASE_URL}/services/${name}`);
232
+ console.log(chalk.green(`✓ ${response.data.message}`));
233
+ } catch (error) {
234
+ handleApiError(error);
235
+ }
236
+ });
237
+
238
+ program
239
+ .command('logs <name>')
240
+ .description('Display logs for a specific backend instance.')
241
+ .action(async (name) => {
242
+ const { default: chalk } = await import('chalk');
243
+ try {
244
+ const response = await axios.get(`${DAEMON_BASE_URL}/services/${name}/logs`);
245
+ const { out: outLogPath, err: errLogPath } = response.data;
246
+
247
+ console.log(chalk.cyan(`--- Logs for ${name} ---`));
248
+
249
+ const readAndPrint = async (filePath, logType) => {
250
+ try {
251
+ const content = await fs.readFile(filePath, 'utf-8');
252
+ console.log(chalk.bold.yellow(`\n--- ${logType} Log (${filePath}) ---\n`));
253
+ console.log(content || chalk.gray('(empty)'));
254
+ } catch (e) {
255
+ if (e.code !== 'ENOENT') {
256
+ throw e;
257
+ }
258
+ console.log(chalk.bold.yellow(`\n--- ${logType} Log (${filePath}) ---\n`));
259
+ console.log(chalk.gray('(not found)'));
260
+ }
261
+ };
262
+
263
+ await readAndPrint(outLogPath, 'Output');
264
+ await readAndPrint(errLogPath, 'Error');
265
+
266
+ } catch (error) {
267
+ handleApiError(error);
268
+ }
269
+ });
270
+
271
+ program
272
+ .command('login')
273
+ .description('Initiate the web-based authentication flow.')
274
+ .action(async () => {
275
+ const { default: chalk } = await import('chalk');
276
+ const { default: open } = await import('open');
277
+ try {
278
+ console.log(chalk.yellow('Opening your browser to log in...'));
279
+ const response = await axios.post(`${DAEMON_BASE_URL}/auth/initiate`);
280
+ const { loginUrl } = response.data;
281
+
282
+ if (loginUrl) {
283
+ await open(loginUrl);
284
+ console.log(chalk.green('✓ Please complete the authentication in your browser.'));
285
+ } else {
286
+ console.error(chalk.red('Could not retrieve login URL from the daemon.'));
287
+ }
288
+ } catch (error) {
289
+ handleApiError(error);
290
+ }
291
+ });
292
+
293
+ program
294
+ .command('logout')
295
+ .description('Remove local authentication credentials.')
296
+ .action(async () => {
297
+ const { default: chalk } = await import('chalk');
298
+ try {
299
+ await axios.post(`${DAEMON_BASE_URL}/auth/logout`);
300
+ console.log(chalk.green('✓ Credentials removed. You have been logged out.'));
301
+ } catch (error) {
302
+ handleApiError(error);
303
+ }
304
+ });
305
+
306
+
307
+ program.parse(process.argv);
Binary file
Binary file