gbos 1.0.0 → 1.1.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/package.json +5 -3
- package/src/cli.js +99 -34
- package/src/commands/auth.js +164 -0
- package/src/commands/connect.js +212 -0
- package/src/commands/logout.js +57 -0
- package/src/lib/api.js +158 -0
- package/src/lib/config.js +156 -0
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gbos",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "GBOS - Command line interface for GBOS services",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"gbos": "
|
|
7
|
+
"gbos": "src/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/index.js",
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"cli",
|
|
20
20
|
"tui",
|
|
21
21
|
"cloud",
|
|
22
|
-
"devtools"
|
|
22
|
+
"devtools",
|
|
23
|
+
"development",
|
|
24
|
+
"ai-agent"
|
|
23
25
|
],
|
|
24
26
|
"author": "Mystro Analytics",
|
|
25
27
|
"license": "MIT",
|
package/src/cli.js
CHANGED
|
@@ -3,69 +3,134 @@
|
|
|
3
3
|
const { Command } = require('commander');
|
|
4
4
|
const program = new Command();
|
|
5
5
|
|
|
6
|
+
const authCommand = require('./commands/auth');
|
|
7
|
+
const connectCommand = require('./commands/connect');
|
|
8
|
+
const logoutCommand = require('./commands/logout');
|
|
9
|
+
const config = require('./lib/config');
|
|
10
|
+
|
|
11
|
+
const VERSION = require('../package.json').version;
|
|
12
|
+
|
|
6
13
|
program
|
|
7
14
|
.name('gbos')
|
|
8
15
|
.description('GBOS - Command line interface for GBOS services')
|
|
9
|
-
.version(
|
|
16
|
+
.version(VERSION);
|
|
10
17
|
|
|
11
18
|
program
|
|
12
19
|
.command('auth')
|
|
13
20
|
.description('Authenticate with GBOS services')
|
|
14
|
-
.option('-
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
.option('-e, --email <email>', 'Email address for authentication')
|
|
22
|
+
.option('-f, --force', 'Force re-authentication even if already authenticated')
|
|
23
|
+
.action(authCommand);
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('connect')
|
|
27
|
+
.description('Connect to a GBOS development node')
|
|
28
|
+
.option('-d, --dir <directory>', 'Working directory (defaults to current directory)')
|
|
29
|
+
.option('-a, --agent <agent>', 'Agent CLI being used (default: claude-code)')
|
|
30
|
+
.option('-f, --force', 'Force reconnect even if already connected')
|
|
31
|
+
.action(connectCommand);
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.command('disconnect')
|
|
35
|
+
.description('Disconnect from the current GBOS node')
|
|
36
|
+
.action(async () => {
|
|
37
|
+
if (!config.isAuthenticated()) {
|
|
38
|
+
console.log('\nNot authenticated.\n');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const connection = config.getConnection();
|
|
43
|
+
if (!connection) {
|
|
44
|
+
console.log('\nNot connected to any node.\n');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const api = require('./lib/api');
|
|
50
|
+
const result = await api.disconnect();
|
|
51
|
+
|
|
52
|
+
config.clearConnection();
|
|
53
|
+
|
|
54
|
+
console.log('\n✓ Disconnected from node.\n');
|
|
55
|
+
|
|
56
|
+
if (result.data) {
|
|
57
|
+
console.log(` Tasks completed: ${result.data.tasks_completed || 0}`);
|
|
58
|
+
console.log(` Tasks failed: ${result.data.tasks_failed || 0}`);
|
|
59
|
+
console.log(` Total time: ${result.data.total_time_minutes || 0} minutes\n`);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
config.clearConnection();
|
|
63
|
+
console.log('\n✓ Disconnected (local session cleared).\n');
|
|
21
64
|
}
|
|
22
|
-
// TODO: Implement authentication logic
|
|
23
65
|
});
|
|
24
66
|
|
|
25
67
|
program
|
|
26
|
-
.command('
|
|
27
|
-
.description('
|
|
28
|
-
.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
console.log(
|
|
68
|
+
.command('status')
|
|
69
|
+
.description('Show current authentication and connection status')
|
|
70
|
+
.action(async () => {
|
|
71
|
+
const session = config.loadSession();
|
|
72
|
+
|
|
73
|
+
if (!session || !session.access_token) {
|
|
74
|
+
console.log('\nStatus: Not authenticated');
|
|
75
|
+
console.log('Run "gbos auth" to authenticate.\n');
|
|
76
|
+
return;
|
|
34
77
|
}
|
|
35
|
-
|
|
36
|
-
|
|
78
|
+
|
|
79
|
+
console.log('\n┌─────────────────────────────────────────────────────────────┐');
|
|
80
|
+
console.log('│ GBOS Status │');
|
|
81
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
82
|
+
console.log(`│ Authenticated: ✓ │`);
|
|
83
|
+
console.log(`│ User ID: ${String(session.user_id).padEnd(42)}│`);
|
|
84
|
+
console.log(`│ Account ID: ${String(session.account_id).padEnd(42)}│`);
|
|
85
|
+
|
|
86
|
+
const connection = session.connection;
|
|
87
|
+
if (connection) {
|
|
88
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
89
|
+
console.log(`│ Connected: ✓ │`);
|
|
90
|
+
console.log(`│ Node: ${(connection.node?.name || 'Unknown').substring(0, 42).padEnd(42)}│`);
|
|
91
|
+
console.log(`│ Node ID: ${String(connection.node?.id || '').padEnd(42)}│`);
|
|
92
|
+
console.log(`│ Connection: ${(connection.connection_id || '').substring(0, 36).padEnd(42)}│`);
|
|
93
|
+
} else {
|
|
94
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
95
|
+
console.log(`│ Connected: ✗ (run "gbos connect") │`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log('└─────────────────────────────────────────────────────────────┘\n');
|
|
99
|
+
|
|
100
|
+
// Show environment variables
|
|
101
|
+
console.log('Environment variables:');
|
|
102
|
+
const envVars = config.getSessionEnv();
|
|
103
|
+
Object.entries(envVars).forEach(([key, value]) => {
|
|
104
|
+
if (value) {
|
|
105
|
+
console.log(` export ${key}="${value}"`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
console.log('');
|
|
37
109
|
});
|
|
38
110
|
|
|
111
|
+
program
|
|
112
|
+
.command('logout')
|
|
113
|
+
.description('Log out from GBOS services and clear credentials')
|
|
114
|
+
.option('-a, --all', 'Clear all stored data including machine ID')
|
|
115
|
+
.action(logoutCommand);
|
|
116
|
+
|
|
39
117
|
program
|
|
40
118
|
.command('help [command]')
|
|
41
119
|
.description('Display help for a specific command')
|
|
42
120
|
.action((command) => {
|
|
43
121
|
if (command) {
|
|
44
|
-
const cmd = program.commands.find(c => c.name() === command);
|
|
122
|
+
const cmd = program.commands.find((c) => c.name() === command);
|
|
45
123
|
if (cmd) {
|
|
46
124
|
cmd.outputHelp();
|
|
47
125
|
} else {
|
|
48
126
|
console.log(`Unknown command: ${command}`);
|
|
49
|
-
console.log('Available commands: auth, connect,
|
|
127
|
+
console.log('Available commands: auth, connect, disconnect, status, logout, help');
|
|
50
128
|
}
|
|
51
129
|
} else {
|
|
52
130
|
program.outputHelp();
|
|
53
131
|
}
|
|
54
132
|
});
|
|
55
133
|
|
|
56
|
-
program
|
|
57
|
-
.command('logout')
|
|
58
|
-
.description('Log out from GBOS services and clear credentials')
|
|
59
|
-
.option('-a, --all', 'Clear all stored credentials')
|
|
60
|
-
.action((options) => {
|
|
61
|
-
console.log('Logging out from GBOS services...');
|
|
62
|
-
if (options.all) {
|
|
63
|
-
console.log('Clearing all stored credentials...');
|
|
64
|
-
}
|
|
65
|
-
console.log('Successfully logged out.');
|
|
66
|
-
// TODO: Implement logout logic
|
|
67
|
-
});
|
|
68
|
-
|
|
69
134
|
// Show help by default if no command is provided
|
|
70
135
|
if (process.argv.length <= 2) {
|
|
71
136
|
program.outputHelp();
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const api = require('../lib/api');
|
|
2
|
+
const config = require('../lib/config');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
|
|
5
|
+
// Simple prompt for email
|
|
6
|
+
async function promptEmail() {
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
rl.question('Enter your email address: ', (answer) => {
|
|
14
|
+
rl.close();
|
|
15
|
+
resolve(answer.trim());
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Sleep helper
|
|
21
|
+
function sleep(ms) {
|
|
22
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Spinner frames
|
|
26
|
+
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
27
|
+
|
|
28
|
+
async function authCommand(options) {
|
|
29
|
+
// Check if already authenticated
|
|
30
|
+
if (config.isAuthenticated() && !options.force) {
|
|
31
|
+
const session = config.loadSession();
|
|
32
|
+
console.log(`\nAlready authenticated as user ID: ${session.user_id}`);
|
|
33
|
+
console.log(`Account ID: ${session.account_id}`);
|
|
34
|
+
console.log(`\nUse --force to re-authenticate or 'gbos logout' first.\n`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Get email from user
|
|
40
|
+
const email = options.email || await promptEmail();
|
|
41
|
+
|
|
42
|
+
if (!email || !email.includes('@')) {
|
|
43
|
+
console.error('Invalid email address.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`\nInitializing authentication for: ${email}`);
|
|
48
|
+
|
|
49
|
+
// Initialize device auth flow
|
|
50
|
+
const initResponse = await api.initAuth({ email });
|
|
51
|
+
const { device_code, verification_code, verification_url_complete, interval, expires_in } = initResponse.data;
|
|
52
|
+
|
|
53
|
+
console.log('\n┌─────────────────────────────────────────────────────────────┐');
|
|
54
|
+
console.log('│ GBOS Authentication │');
|
|
55
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
56
|
+
console.log(`│ Device Code: ${device_code} │`);
|
|
57
|
+
console.log('│ │');
|
|
58
|
+
console.log('│ Please visit the following URL to authorize: │');
|
|
59
|
+
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
60
|
+
console.log(`\n${verification_url_complete}\n`);
|
|
61
|
+
console.log(`Code expires in ${Math.floor(expires_in / 60)} minutes.\n`);
|
|
62
|
+
|
|
63
|
+
// Try to open the URL in the default browser
|
|
64
|
+
const openCommand = process.platform === 'darwin' ? 'open' :
|
|
65
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const { exec } = require('child_process');
|
|
69
|
+
exec(`${openCommand} "${verification_url_complete}"`);
|
|
70
|
+
console.log('Opening browser...\n');
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.log('Please open the URL above in your browser.\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Poll for authorization
|
|
76
|
+
const pollInterval = (interval || 5) * 1000;
|
|
77
|
+
const maxAttempts = Math.ceil((expires_in || 900) / (interval || 5));
|
|
78
|
+
let attempts = 0;
|
|
79
|
+
let frameIndex = 0;
|
|
80
|
+
|
|
81
|
+
process.stdout.write('Waiting for authorization... ');
|
|
82
|
+
|
|
83
|
+
while (attempts < maxAttempts) {
|
|
84
|
+
attempts++;
|
|
85
|
+
|
|
86
|
+
// Show spinner
|
|
87
|
+
process.stdout.write(`\rWaiting for authorization... ${spinnerFrames[frameIndex]} `);
|
|
88
|
+
frameIndex = (frameIndex + 1) % spinnerFrames.length;
|
|
89
|
+
|
|
90
|
+
await sleep(pollInterval);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const statusResponse = await api.checkAuthStatus(verification_code);
|
|
94
|
+
|
|
95
|
+
if (statusResponse.status === 'approved' && statusResponse.data) {
|
|
96
|
+
// Clear spinner line
|
|
97
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
98
|
+
|
|
99
|
+
const { access_token, refresh_token, expires_in: tokenExpires, user_id, account_id, session_id } = statusResponse.data;
|
|
100
|
+
|
|
101
|
+
// Calculate token expiration
|
|
102
|
+
const tokenExpiresAt = new Date(Date.now() + tokenExpires * 1000).toISOString();
|
|
103
|
+
|
|
104
|
+
// Save session
|
|
105
|
+
config.saveSession({
|
|
106
|
+
access_token,
|
|
107
|
+
refresh_token,
|
|
108
|
+
token_expires_at: tokenExpiresAt,
|
|
109
|
+
user_id,
|
|
110
|
+
account_id,
|
|
111
|
+
session_id,
|
|
112
|
+
authenticated_at: new Date().toISOString(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
console.log('\n✓ Authentication successful!\n');
|
|
116
|
+
console.log(` User ID: ${user_id}`);
|
|
117
|
+
console.log(` Account ID: ${account_id}`);
|
|
118
|
+
console.log(` Session: ${session_id}\n`);
|
|
119
|
+
console.log('Run "gbos connect" to connect to a development node.\n');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (statusResponse.status === 'denied') {
|
|
124
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
125
|
+
console.log('\n✗ Authorization denied.\n');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (statusResponse.status === 'expired') {
|
|
130
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
131
|
+
console.log('\n✗ Authorization request expired. Please try again.\n');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Status is pending, continue polling
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error.status === 410) {
|
|
138
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
139
|
+
console.log('\n✗ Authorization request expired. Please try again.\n');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
if (error.status === 403) {
|
|
143
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
144
|
+
console.log('\n✗ Authorization denied.\n');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
// For other errors, continue polling
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
152
|
+
console.log('\n✗ Authorization timed out. Please try again.\n');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(`\nAuthentication failed: ${error.message}\n`);
|
|
157
|
+
if (process.env.DEBUG) {
|
|
158
|
+
console.error(error);
|
|
159
|
+
}
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = authCommand;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const api = require('../lib/api');
|
|
2
|
+
const config = require('../lib/config');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
// Simple selection prompt
|
|
8
|
+
async function selectOption(message, options) {
|
|
9
|
+
const rl = readline.createInterface({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
console.log(`\n${message}\n`);
|
|
15
|
+
|
|
16
|
+
options.forEach((opt, index) => {
|
|
17
|
+
const status = opt.status ? ` [${opt.status}]` : '';
|
|
18
|
+
const connected = opt.is_connected ? ' (connected by another user)' : '';
|
|
19
|
+
console.log(` ${index + 1}. ${opt.name}${status}${connected}`);
|
|
20
|
+
if (opt.description) {
|
|
21
|
+
console.log(` ${opt.description}`);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
console.log('');
|
|
26
|
+
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
rl.question('Enter number (or q to quit): ', (answer) => {
|
|
29
|
+
rl.close();
|
|
30
|
+
if (answer.toLowerCase() === 'q') {
|
|
31
|
+
resolve(null);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const index = parseInt(answer, 10) - 1;
|
|
35
|
+
if (index >= 0 && index < options.length) {
|
|
36
|
+
resolve(options[index]);
|
|
37
|
+
} else {
|
|
38
|
+
resolve(null);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Get git info from current directory
|
|
45
|
+
function getGitInfo() {
|
|
46
|
+
try {
|
|
47
|
+
const gitRepoUrl = execSync('git config --get remote.origin.url', { encoding: 'utf8' }).trim();
|
|
48
|
+
const gitBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
|
49
|
+
return { gitRepoUrl, gitBranch };
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return { gitRepoUrl: null, gitBranch: null };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function connectCommand(options) {
|
|
56
|
+
// Check authentication
|
|
57
|
+
if (!config.isAuthenticated()) {
|
|
58
|
+
console.log('\nNot authenticated. Please run "gbos auth" first.\n');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Check if already connected
|
|
64
|
+
const currentConnection = config.getConnection();
|
|
65
|
+
if (currentConnection && !options.force) {
|
|
66
|
+
console.log(`\nAlready connected to node: ${currentConnection.node?.name}`);
|
|
67
|
+
console.log(`Connection ID: ${currentConnection.connection_id}`);
|
|
68
|
+
console.log(`\nUse --force to reconnect or 'gbos disconnect' first.\n`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('\nFetching available nodes...\n');
|
|
73
|
+
|
|
74
|
+
// Fetch available nodes
|
|
75
|
+
const nodesResponse = await api.listNodes();
|
|
76
|
+
const nodes = nodesResponse.data || [];
|
|
77
|
+
|
|
78
|
+
if (nodes.length === 0) {
|
|
79
|
+
console.log('No development nodes available.');
|
|
80
|
+
console.log('Please create a development node in the GBOS web interface.\n');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Group nodes by application
|
|
85
|
+
const nodesByApp = {};
|
|
86
|
+
nodes.forEach((node) => {
|
|
87
|
+
const appId = node.application_id || 'unassigned';
|
|
88
|
+
if (!nodesByApp[appId]) {
|
|
89
|
+
nodesByApp[appId] = {
|
|
90
|
+
application: node.application,
|
|
91
|
+
nodes: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
nodesByApp[appId].nodes.push(node);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// If multiple applications, let user select one first
|
|
98
|
+
let selectedApp = null;
|
|
99
|
+
const appIds = Object.keys(nodesByApp);
|
|
100
|
+
|
|
101
|
+
if (appIds.length > 1) {
|
|
102
|
+
const appOptions = appIds.map((appId) => ({
|
|
103
|
+
id: appId,
|
|
104
|
+
name: nodesByApp[appId].application?.name || `Application ${appId}`,
|
|
105
|
+
description: `${nodesByApp[appId].nodes.length} node(s) available`,
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
selectedApp = await selectOption('Select an application:', appOptions);
|
|
109
|
+
|
|
110
|
+
if (!selectedApp) {
|
|
111
|
+
console.log('Connection cancelled.\n');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
selectedApp = { id: appIds[0] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Get nodes for selected application
|
|
119
|
+
const appNodes = nodesByApp[selectedApp.id].nodes;
|
|
120
|
+
|
|
121
|
+
// Let user select a node
|
|
122
|
+
const nodeOptions = appNodes.map((node) => ({
|
|
123
|
+
...node,
|
|
124
|
+
name: node.name,
|
|
125
|
+
description: node.node_type || '',
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
const selectedNode = await selectOption('Select a development node:', nodeOptions);
|
|
129
|
+
|
|
130
|
+
if (!selectedNode) {
|
|
131
|
+
console.log('Connection cancelled.\n');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if node is busy
|
|
136
|
+
if (selectedNode.is_connected && selectedNode.active_connection) {
|
|
137
|
+
console.log(`\nNode "${selectedNode.name}" is already connected by another user.`);
|
|
138
|
+
console.log('Please select a different node.\n');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get connection info
|
|
143
|
+
const workingDirectory = options.dir || process.cwd();
|
|
144
|
+
const { gitRepoUrl, gitBranch } = getGitInfo();
|
|
145
|
+
const agentCli = options.agent || 'claude-code';
|
|
146
|
+
|
|
147
|
+
console.log(`\nConnecting to node: ${selectedNode.name}...`);
|
|
148
|
+
|
|
149
|
+
// Connect to node
|
|
150
|
+
const connectResponse = await api.connectToNode(selectedNode.id, {
|
|
151
|
+
working_directory: workingDirectory,
|
|
152
|
+
git_repo_url: gitRepoUrl,
|
|
153
|
+
git_branch: gitBranch,
|
|
154
|
+
agent_cli: agentCli,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const { connection_id, node } = connectResponse.data;
|
|
158
|
+
|
|
159
|
+
// Save connection to session
|
|
160
|
+
config.saveConnection({
|
|
161
|
+
connection_id,
|
|
162
|
+
node: {
|
|
163
|
+
id: node.id,
|
|
164
|
+
uuid: node.uuid,
|
|
165
|
+
name: node.name,
|
|
166
|
+
node_type: node.node_type,
|
|
167
|
+
system_prompt: node.system_prompt,
|
|
168
|
+
application_id: selectedNode.application_id,
|
|
169
|
+
},
|
|
170
|
+
connected_at: new Date().toISOString(),
|
|
171
|
+
working_directory: workingDirectory,
|
|
172
|
+
git_repo_url: gitRepoUrl,
|
|
173
|
+
git_branch: gitBranch,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
console.log('\n┌─────────────────────────────────────────────────────────────┐');
|
|
177
|
+
console.log('│ Connected to GBOS │');
|
|
178
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
179
|
+
console.log(`│ Node: ${node.name.padEnd(42)}│`);
|
|
180
|
+
console.log(`│ Connection ID: ${connection_id.substring(0, 36).padEnd(42)}│`);
|
|
181
|
+
console.log(`│ Working Dir: ${workingDirectory.substring(0, 42).padEnd(42)}│`);
|
|
182
|
+
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
183
|
+
|
|
184
|
+
console.log('\n✓ Successfully connected!\n');
|
|
185
|
+
|
|
186
|
+
// Show session info for other tools
|
|
187
|
+
console.log('Session data stored at: ~/.gbos/session.json');
|
|
188
|
+
console.log('\nEnvironment variables available:');
|
|
189
|
+
const envVars = config.getSessionEnv();
|
|
190
|
+
Object.entries(envVars).forEach(([key, value]) => {
|
|
191
|
+
if (value) {
|
|
192
|
+
console.log(` ${key}=${value}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
console.log('\nOther CLI tools can access this session by reading ~/.gbos/session.json');
|
|
197
|
+
console.log('or by using the GBOS MCP server.\n');
|
|
198
|
+
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error.code === 'NODE_BUSY') {
|
|
201
|
+
console.error(`\nNode is already connected to another CLI session.\n`);
|
|
202
|
+
} else {
|
|
203
|
+
console.error(`\nConnection failed: ${error.message}\n`);
|
|
204
|
+
}
|
|
205
|
+
if (process.env.DEBUG) {
|
|
206
|
+
console.error(error);
|
|
207
|
+
}
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = connectCommand;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const api = require('../lib/api');
|
|
2
|
+
const config = require('../lib/config');
|
|
3
|
+
|
|
4
|
+
async function logoutCommand(options) {
|
|
5
|
+
// Check if authenticated
|
|
6
|
+
if (!config.isAuthenticated()) {
|
|
7
|
+
console.log('\nNot currently authenticated.\n');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const session = config.loadSession();
|
|
13
|
+
|
|
14
|
+
// Disconnect from node if connected
|
|
15
|
+
const connection = config.getConnection();
|
|
16
|
+
if (connection) {
|
|
17
|
+
console.log('Disconnecting from node...');
|
|
18
|
+
try {
|
|
19
|
+
await api.disconnect();
|
|
20
|
+
} catch (e) {
|
|
21
|
+
// Ignore disconnect errors during logout
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Call logout API
|
|
26
|
+
console.log('Logging out...');
|
|
27
|
+
try {
|
|
28
|
+
await api.logout();
|
|
29
|
+
} catch (e) {
|
|
30
|
+
// Ignore API errors - we'll clear local session anyway
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Clear local session
|
|
34
|
+
if (options.all) {
|
|
35
|
+
// Clear everything including machine ID
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const path = require('path');
|
|
38
|
+
const configDir = config.getConfigDir();
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync(configDir)) {
|
|
41
|
+
fs.rmSync(configDir, { recursive: true });
|
|
42
|
+
console.log('Cleared all GBOS data.\n');
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
config.clearSession();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log('\n✓ Successfully logged out.\n');
|
|
49
|
+
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// Clear session anyway on error
|
|
52
|
+
config.clearSession();
|
|
53
|
+
console.log('\n✓ Logged out (session cleared locally).\n');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = logoutCommand;
|
package/src/lib/api.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const config = require('./config');
|
|
2
|
+
|
|
3
|
+
const API_BASE_URL = 'https://gbos-api-579767694933.us-south1.run.app/api/v1';
|
|
4
|
+
|
|
5
|
+
class GbosApiClient {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.baseUrl = API_BASE_URL;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async request(endpoint, options = {}) {
|
|
11
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
12
|
+
const headers = {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
...options.headers,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Add auth header if we have a token
|
|
18
|
+
const token = config.getAccessToken();
|
|
19
|
+
if (token && !options.skipAuth) {
|
|
20
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
...options,
|
|
25
|
+
headers,
|
|
26
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const error = new Error(data.error || data.message || 'API request failed');
|
|
33
|
+
error.status = response.status;
|
|
34
|
+
error.code = data.code;
|
|
35
|
+
error.data = data;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Auth endpoints
|
|
43
|
+
async initAuth(clientInfo) {
|
|
44
|
+
const machineInfo = config.getMachineInfo();
|
|
45
|
+
return this.request('/cli/auth/init', {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
body: {
|
|
48
|
+
client_name: 'gbos-cli',
|
|
49
|
+
client_version: require('../../package.json').version,
|
|
50
|
+
...machineInfo,
|
|
51
|
+
...clientInfo,
|
|
52
|
+
},
|
|
53
|
+
skipAuth: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async checkAuthStatus(verificationCode) {
|
|
58
|
+
return this.request(`/cli/auth/status/${verificationCode}`, {
|
|
59
|
+
method: 'GET',
|
|
60
|
+
skipAuth: true,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async refreshToken(refreshToken) {
|
|
65
|
+
return this.request('/cli/auth/refresh', {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
body: { refresh_token: refreshToken },
|
|
68
|
+
skipAuth: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async logout() {
|
|
73
|
+
return this.request('/cli/auth/logout', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getSession() {
|
|
79
|
+
return this.request('/cli/auth/session', {
|
|
80
|
+
method: 'GET',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Node endpoints
|
|
85
|
+
async listNodes(applicationId = null) {
|
|
86
|
+
let endpoint = '/cli/nodes';
|
|
87
|
+
if (applicationId) {
|
|
88
|
+
endpoint += `?application_id=${applicationId}`;
|
|
89
|
+
}
|
|
90
|
+
return this.request(endpoint, { method: 'GET' });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async connectToNode(nodeId, connectionInfo = {}) {
|
|
94
|
+
return this.request(`/cli/connect/${nodeId}`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
body: connectionInfo,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async disconnect() {
|
|
101
|
+
return this.request('/cli/disconnect', {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getConnectionStatus() {
|
|
107
|
+
return this.request('/cli/connection', {
|
|
108
|
+
method: 'GET',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async sendHeartbeat(taskId = null, progress = null) {
|
|
113
|
+
return this.request('/cli/heartbeat', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: {
|
|
116
|
+
current_task_id: taskId,
|
|
117
|
+
progress,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Task endpoints
|
|
123
|
+
async getNextTask() {
|
|
124
|
+
return this.request('/cli/tasks/next', {
|
|
125
|
+
method: 'GET',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async startTask(taskId) {
|
|
130
|
+
return this.request(`/cli/tasks/${taskId}/start`, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async completeTask(taskId, data = {}) {
|
|
136
|
+
return this.request(`/cli/tasks/${taskId}/complete`, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
body: data,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async failTask(taskId, data = {}) {
|
|
143
|
+
return this.request(`/cli/tasks/${taskId}/fail`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
body: data,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Activity logging
|
|
150
|
+
async logActivity(activity) {
|
|
151
|
+
return this.request('/cli/activity', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
body: activity,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = new GbosApiClient();
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.gbos');
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
|
+
const SESSION_FILE = path.join(CONFIG_DIR, 'session.json');
|
|
8
|
+
|
|
9
|
+
// Ensure config directory exists
|
|
10
|
+
function ensureConfigDir() {
|
|
11
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
12
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Get machine info for device auth
|
|
17
|
+
function getMachineInfo() {
|
|
18
|
+
return {
|
|
19
|
+
machine_id: getMachineId(),
|
|
20
|
+
machine_name: os.hostname(),
|
|
21
|
+
os_type: os.platform(),
|
|
22
|
+
os_version: os.release(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Generate a persistent machine ID
|
|
27
|
+
function getMachineId() {
|
|
28
|
+
ensureConfigDir();
|
|
29
|
+
const machineIdFile = path.join(CONFIG_DIR, '.machine_id');
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(machineIdFile)) {
|
|
32
|
+
return fs.readFileSync(machineIdFile, 'utf8').trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const machineId = `${os.hostname()}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
36
|
+
fs.writeFileSync(machineIdFile, machineId, { mode: 0o600 });
|
|
37
|
+
return machineId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Save session data
|
|
41
|
+
function saveSession(data) {
|
|
42
|
+
ensureConfigDir();
|
|
43
|
+
const session = {
|
|
44
|
+
...data,
|
|
45
|
+
updated_at: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
48
|
+
return session;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Load session data
|
|
52
|
+
function loadSession() {
|
|
53
|
+
try {
|
|
54
|
+
if (fs.existsSync(SESSION_FILE)) {
|
|
55
|
+
const data = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// Ignore parse errors
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Clear session data
|
|
65
|
+
function clearSession() {
|
|
66
|
+
if (fs.existsSync(SESSION_FILE)) {
|
|
67
|
+
fs.unlinkSync(SESSION_FILE);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if authenticated
|
|
72
|
+
function isAuthenticated() {
|
|
73
|
+
const session = loadSession();
|
|
74
|
+
if (!session || !session.access_token) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if token is expired
|
|
79
|
+
if (session.token_expires_at) {
|
|
80
|
+
const expiresAt = new Date(session.token_expires_at);
|
|
81
|
+
if (expiresAt < new Date()) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get access token
|
|
90
|
+
function getAccessToken() {
|
|
91
|
+
const session = loadSession();
|
|
92
|
+
return session?.access_token || null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Get current connection info
|
|
96
|
+
function getConnection() {
|
|
97
|
+
const session = loadSession();
|
|
98
|
+
return session?.connection || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Save connection info
|
|
102
|
+
function saveConnection(connection) {
|
|
103
|
+
const session = loadSession() || {};
|
|
104
|
+
session.connection = connection;
|
|
105
|
+
saveSession(session);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Clear connection info
|
|
109
|
+
function clearConnection() {
|
|
110
|
+
const session = loadSession();
|
|
111
|
+
if (session) {
|
|
112
|
+
delete session.connection;
|
|
113
|
+
saveSession(session);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Export session as environment variables format
|
|
118
|
+
function getSessionEnv() {
|
|
119
|
+
const session = loadSession();
|
|
120
|
+
if (!session) return {};
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
GBOS_ACCESS_TOKEN: session.access_token,
|
|
124
|
+
GBOS_ACCOUNT_ID: session.account_id,
|
|
125
|
+
GBOS_USER_ID: session.user_id,
|
|
126
|
+
GBOS_SESSION_ID: session.session_id,
|
|
127
|
+
GBOS_NODE_ID: session.connection?.node?.id,
|
|
128
|
+
GBOS_NODE_UUID: session.connection?.node?.uuid,
|
|
129
|
+
GBOS_CONNECTION_ID: session.connection?.connection_id,
|
|
130
|
+
GBOS_APPLICATION_ID: session.connection?.node?.application_id,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Get config directory path (for other tools to access)
|
|
135
|
+
function getConfigDir() {
|
|
136
|
+
return CONFIG_DIR;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
CONFIG_DIR,
|
|
141
|
+
CONFIG_FILE,
|
|
142
|
+
SESSION_FILE,
|
|
143
|
+
ensureConfigDir,
|
|
144
|
+
getMachineInfo,
|
|
145
|
+
getMachineId,
|
|
146
|
+
saveSession,
|
|
147
|
+
loadSession,
|
|
148
|
+
clearSession,
|
|
149
|
+
isAuthenticated,
|
|
150
|
+
getAccessToken,
|
|
151
|
+
getConnection,
|
|
152
|
+
saveConnection,
|
|
153
|
+
clearConnection,
|
|
154
|
+
getSessionEnv,
|
|
155
|
+
getConfigDir,
|
|
156
|
+
};
|