nexusforge-cli 1.0.6 → 1.0.7

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.
@@ -1,211 +0,0 @@
1
- /**
2
- * NexusForge CLI API Service
3
- *
4
- * Handles all API communication with NexusConnectBridge backend.
5
- */
6
-
7
- import * as config from '../utils/config.js';
8
- import type {
9
- ChatMessage,
10
- ChatResponse,
11
- CLISession,
12
- FileContext,
13
- } from '../types/index.js';
14
-
15
- /**
16
- * Make an authenticated API request
17
- */
18
- async function apiRequest<T>(
19
- endpoint: string,
20
- options: RequestInit = {},
21
- ): Promise<T> {
22
- const apiUrl = config.getApiUrl();
23
- const token = config.getAccessToken();
24
-
25
- const headers: Record<string, string> = {
26
- 'Content-Type': 'application/json',
27
- ...((options.headers as Record<string, string>) || {}),
28
- };
29
-
30
- if (token) {
31
- headers['Authorization'] = `Bearer ${token}`;
32
- }
33
-
34
- const response = await fetch(`${apiUrl}${endpoint}`, {
35
- ...options,
36
- headers,
37
- });
38
-
39
- if (!response.ok) {
40
- const errorText = await response.text();
41
- throw new Error(`API error (${response.status}): ${errorText}`);
42
- }
43
-
44
- return response.json() as Promise<T>;
45
- }
46
-
47
- /**
48
- * Send a chat message and get response
49
- */
50
- export async function sendMessage(
51
- message: string,
52
- conversationId?: string,
53
- model?: string,
54
- fileContext?: FileContext[],
55
- ): Promise<ChatResponse> {
56
- return apiRequest<ChatResponse>('/cli/chat', {
57
- method: 'POST',
58
- body: JSON.stringify({
59
- message,
60
- conversation_id: conversationId,
61
- model,
62
- file_context: fileContext,
63
- }),
64
- });
65
- }
66
-
67
- /**
68
- * Get CLI context data for the backend prompt engine
69
- */
70
- function getCLIContext(): { cwd: string; platform: string; username: string } {
71
- return {
72
- cwd: process.cwd(),
73
- platform: process.platform,
74
- username: config.get('username') || 'user',
75
- };
76
- }
77
-
78
- /**
79
- * Stream a chat message response
80
- */
81
- export async function* streamMessage(
82
- message: string,
83
- conversationId?: string,
84
- model?: string,
85
- fileContext?: FileContext[],
86
- ): AsyncGenerator<string, void, unknown> {
87
- const apiUrl = config.getApiUrl();
88
- const token = config.getAccessToken();
89
- const systemContext = getCLIContext();
90
-
91
- const response = await fetch(`${apiUrl}/ai/chat/stream`, {
92
- method: 'POST',
93
- headers: {
94
- 'Content-Type': 'application/json',
95
- Authorization: token ? `Bearer ${token}` : '',
96
- },
97
- body: JSON.stringify({
98
- message,
99
- conversation_id: conversationId,
100
- model,
101
- system: systemContext,
102
- }),
103
- });
104
-
105
- if (!response.ok) {
106
- throw new Error(`API error: ${response.statusText}`);
107
- }
108
-
109
- const reader = response.body?.getReader();
110
- if (!reader) {
111
- throw new Error('No response body');
112
- }
113
-
114
- const decoder = new TextDecoder();
115
-
116
- while (true) {
117
- const { done, value } = await reader.read();
118
- if (done) break;
119
-
120
- const chunk = decoder.decode(value, { stream: true });
121
- yield chunk;
122
- }
123
- }
124
-
125
- /**
126
- * Create a new CLI session
127
- */
128
- export async function createSession(
129
- conversationId?: string,
130
- workingDirectory?: string,
131
- ): Promise<CLISession> {
132
- return apiRequest<CLISession>('/cli/sessions', {
133
- method: 'POST',
134
- body: JSON.stringify({
135
- conversation_id: conversationId,
136
- working_directory: workingDirectory,
137
- }),
138
- });
139
- }
140
-
141
- /**
142
- * Get session status
143
- */
144
- export async function getSessionStatus(sessionToken: string): Promise<CLISession> {
145
- return apiRequest<CLISession>(`/cli/sessions/${sessionToken}`);
146
- }
147
-
148
- /**
149
- * Log a command execution
150
- */
151
- export async function logCommandExecution(data: {
152
- command: string;
153
- workingDirectory: string;
154
- exitCode?: number;
155
- output?: string;
156
- duration?: number;
157
- }): Promise<void> {
158
- await apiRequest('/cli/execute', {
159
- method: 'POST',
160
- body: JSON.stringify({
161
- command: data.command,
162
- working_directory: data.workingDirectory,
163
- exit_code: data.exitCode,
164
- output: data.output,
165
- duration_ms: data.duration,
166
- }),
167
- });
168
- }
169
-
170
- /**
171
- * Sync conversation from web
172
- */
173
- export async function syncConversation(
174
- conversationId: string,
175
- sinceMessageId?: string,
176
- ): Promise<{
177
- conversation_id: string;
178
- title?: string;
179
- model: string;
180
- messages: ChatMessage[];
181
- has_more: boolean;
182
- }> {
183
- return apiRequest('/cli/sync', {
184
- method: 'POST',
185
- body: JSON.stringify({
186
- conversation_id: conversationId,
187
- since_message_id: sinceMessageId,
188
- }),
189
- });
190
- }
191
-
192
- /**
193
- * Get available models
194
- */
195
- export async function getModels(): Promise<string[]> {
196
- const response = await apiRequest<{ models: Array<{ name: string }> }>('/ai/models');
197
- return response.models.map((m) => m.name);
198
- }
199
-
200
- /**
201
- * Check API health
202
- */
203
- export async function checkHealth(): Promise<boolean> {
204
- try {
205
- const apiUrl = config.getApiUrl();
206
- const response = await fetch(`${apiUrl.replace('/api/v1/bridge', '')}/health`);
207
- return response.ok;
208
- } catch {
209
- return false;
210
- }
211
- }
@@ -1,230 +0,0 @@
1
- /**
2
- * NexusForge CLI Authentication Service
3
- *
4
- * Handles device authorization flow (like Claude Code):
5
- * 1. CLI requests device code
6
- * 2. CLI displays verification URL
7
- * 3. User opens URL in browser and authorizes
8
- * 4. CLI polls for authorization
9
- * 5. CLI saves token on success
10
- */
11
-
12
- import open from 'open';
13
- import ora from 'ora';
14
- import { style, displaySystemMessage } from '../utils/theme.js';
15
- import * as config from '../utils/config.js';
16
- import type { DeviceCodeResponse, DeviceAuthStatus } from '../types/index.js';
17
-
18
- const POLL_INTERVAL_MS = 5000; // 5 seconds
19
- const MAX_POLL_ATTEMPTS = 180; // 15 minutes max
20
-
21
- /**
22
- * Start the device authorization flow
23
- */
24
- export async function startDeviceAuth(): Promise<boolean> {
25
- const apiUrl = config.getApiUrl();
26
-
27
- console.log('');
28
- displaySystemMessage('Starting authentication...', 'info');
29
- console.log('');
30
-
31
- try {
32
- // Step 1: Request device code
33
- const deviceCodeResponse = await requestDeviceCode(apiUrl);
34
-
35
- // Step 2: Display verification URL
36
- console.log(style.divider());
37
- console.log('');
38
- console.log(style.assistant(' To authenticate, open this URL in your browser:'));
39
- console.log('');
40
- console.log(style.prompt(` ${deviceCodeResponse.verification_uri_complete}`));
41
- console.log('');
42
- console.log(style.muted(` Or go to ${deviceCodeResponse.verification_uri}`));
43
- console.log(style.muted(` and enter code: ${style.prompt(deviceCodeResponse.user_code)}`));
44
- console.log('');
45
- console.log(style.divider());
46
- console.log('');
47
-
48
- // Try to open browser automatically
49
- try {
50
- // Use open with wait: false to not block
51
- await open(deviceCodeResponse.verification_uri_complete, { wait: false });
52
- displaySystemMessage('Browser opened automatically', 'info');
53
- } catch (err) {
54
- // Try WSL-specific approach
55
- try {
56
- const { exec } = await import('child_process');
57
- exec(`cmd.exe /c start "" "${deviceCodeResponse.verification_uri_complete}"`);
58
- displaySystemMessage('Browser opened via Windows', 'info');
59
- } catch {
60
- displaySystemMessage('Could not open browser. Please copy the URL above.', 'warning');
61
- }
62
- }
63
-
64
- console.log('');
65
-
66
- // Step 3: Poll for authorization
67
- const spinner = ora({
68
- text: 'Waiting for authorization...',
69
- color: 'cyan',
70
- }).start();
71
-
72
- const authResult = await pollForAuthorization(
73
- apiUrl,
74
- deviceCodeResponse.device_code,
75
- deviceCodeResponse.interval * 1000,
76
- spinner,
77
- );
78
-
79
- if (authResult) {
80
- spinner.succeed(style.success('Authenticated successfully!'));
81
-
82
- // Save auth data
83
- config.saveAuth({
84
- accessToken: authResult.access_token!,
85
- refreshToken: authResult.refresh_token,
86
- userId: authResult.user?.id?.toString(),
87
- username: authResult.user?.username,
88
- email: authResult.user?.email,
89
- });
90
-
91
- console.log('');
92
- displaySystemMessage(`Welcome, ${authResult.user?.username || 'user'}!`, 'success');
93
- console.log('');
94
-
95
- return true;
96
- } else {
97
- spinner.fail(style.error('Authentication failed or timed out'));
98
- return false;
99
- }
100
- } catch (error) {
101
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
102
- displaySystemMessage(`Authentication error: ${errorMessage}`, 'error');
103
- return false;
104
- }
105
- }
106
-
107
- /**
108
- * Request a device code from the API
109
- */
110
- async function requestDeviceCode(apiUrl: string): Promise<DeviceCodeResponse> {
111
- const response = await fetch(`${apiUrl}/cli/auth/device`, {
112
- method: 'POST',
113
- headers: {
114
- 'Content-Type': 'application/json',
115
- },
116
- body: JSON.stringify({ client_id: 'nexusforge-cli' }),
117
- });
118
-
119
- if (!response.ok) {
120
- throw new Error(`Failed to get device code: ${response.statusText}`);
121
- }
122
-
123
- return response.json() as Promise<DeviceCodeResponse>;
124
- }
125
-
126
- /**
127
- * Poll for authorization status
128
- */
129
- async function pollForAuthorization(
130
- apiUrl: string,
131
- deviceCode: string,
132
- intervalMs: number,
133
- spinner: ReturnType<typeof ora>,
134
- ): Promise<DeviceAuthStatus | null> {
135
- let attempts = 0;
136
-
137
- while (attempts < MAX_POLL_ATTEMPTS) {
138
- attempts++;
139
-
140
- await sleep(intervalMs);
141
-
142
- try {
143
- const response = await fetch(`${apiUrl}/cli/auth/device/${deviceCode}`);
144
-
145
- if (!response.ok) {
146
- continue;
147
- }
148
-
149
- const status = await response.json() as DeviceAuthStatus;
150
-
151
- switch (status.status) {
152
- case 'authorized':
153
- return status;
154
-
155
- case 'denied':
156
- spinner.fail(style.error('Authorization denied by user'));
157
- return null;
158
-
159
- case 'expired':
160
- spinner.fail(style.error('Authorization code expired'));
161
- return null;
162
-
163
- case 'pending':
164
- // Keep polling
165
- spinner.text = `Waiting for authorization... (${Math.floor(attempts * intervalMs / 1000)}s)`;
166
- break;
167
- }
168
- } catch {
169
- // Network error, keep trying
170
- spinner.text = 'Waiting for authorization... (connection issue, retrying)';
171
- }
172
- }
173
-
174
- return null;
175
- }
176
-
177
- /**
178
- * Check if current token is valid
179
- */
180
- export async function checkAuth(): Promise<boolean> {
181
- const token = config.getAccessToken();
182
-
183
- if (!token) {
184
- return false;
185
- }
186
-
187
- const apiUrl = config.getApiUrl();
188
-
189
- try {
190
- // Try to get user info with current token
191
- const response = await fetch(`${apiUrl}/auth/me`, {
192
- headers: {
193
- Authorization: `Bearer ${token}`,
194
- },
195
- });
196
-
197
- return response.ok;
198
- } catch {
199
- return false;
200
- }
201
- }
202
-
203
- /**
204
- * Logout - clear stored credentials
205
- */
206
- export function logout(): void {
207
- config.clearAuth();
208
- displaySystemMessage('Logged out successfully', 'success');
209
- }
210
-
211
- /**
212
- * Get current user info
213
- */
214
- export function getCurrentUser(): { username?: string; email?: string } | null {
215
- if (!config.isAuthenticated()) {
216
- return null;
217
- }
218
-
219
- return {
220
- username: config.get('username'),
221
- email: config.get('email'),
222
- };
223
- }
224
-
225
- /**
226
- * Sleep helper
227
- */
228
- function sleep(ms: number): Promise<void> {
229
- return new Promise((resolve) => setTimeout(resolve, ms));
230
- }
@@ -1,250 +0,0 @@
1
- /**
2
- * NexusForge CLI Command Executor
3
- *
4
- * Executes shell commands and streams output.
5
- * Provides Claude Code-like command execution capabilities.
6
- */
7
-
8
- import { spawn, exec } from 'child_process';
9
- import { promisify } from 'util';
10
- import { style, displaySystemMessage } from '../utils/theme.js';
11
- import * as api from './api.js';
12
- import type { CommandResult } from '../types/index.js';
13
-
14
- const execAsync = promisify(exec);
15
-
16
- // Dangerous commands that should be blocked or warned
17
- const DANGEROUS_COMMANDS = [
18
- 'rm -rf /',
19
- 'rm -rf ~',
20
- 'rm -rf *',
21
- 'mkfs',
22
- ':(){:|:&};:',
23
- 'dd if=/dev/zero',
24
- 'chmod -R 777 /',
25
- '> /dev/sda',
26
- ];
27
-
28
- const DANGEROUS_PATTERNS = [
29
- /rm\s+-rf\s+\/(?!\w)/,
30
- /curl.*\|.*(?:bash|sh)/,
31
- /wget.*\|.*(?:bash|sh)/,
32
- />\s*\/dev\/sd/,
33
- ];
34
-
35
- /**
36
- * Check if a command is potentially dangerous
37
- */
38
- export function isDangerous(command: string): boolean {
39
- const normalizedCmd = command.toLowerCase().trim();
40
-
41
- // Check exact matches
42
- if (DANGEROUS_COMMANDS.some((dangerous) => normalizedCmd.includes(dangerous))) {
43
- return true;
44
- }
45
-
46
- // Check patterns
47
- return DANGEROUS_PATTERNS.some((pattern) => pattern.test(command));
48
- }
49
-
50
- /**
51
- * Execute a command and return the result
52
- */
53
- export async function executeCommand(
54
- command: string,
55
- cwd?: string,
56
- ): Promise<CommandResult> {
57
- const startTime = Date.now();
58
- const workingDir = cwd || process.cwd();
59
-
60
- try {
61
- const { stdout, stderr } = await execAsync(command, {
62
- cwd: workingDir,
63
- timeout: 300000, // 5 minute timeout
64
- maxBuffer: 10 * 1024 * 1024, // 10MB buffer
65
- });
66
-
67
- const duration = Date.now() - startTime;
68
-
69
- // Log to API (fire and forget)
70
- api.logCommandExecution({
71
- command,
72
- workingDirectory: workingDir,
73
- exitCode: 0,
74
- output: stdout.substring(0, 1000),
75
- duration,
76
- }).catch(() => {});
77
-
78
- return {
79
- command,
80
- exitCode: 0,
81
- stdout,
82
- stderr,
83
- duration,
84
- };
85
- } catch (error: unknown) {
86
- const duration = Date.now() - startTime;
87
- const execError = error as { code?: number; stdout?: string; stderr?: string };
88
-
89
- // Log to API
90
- api.logCommandExecution({
91
- command,
92
- workingDirectory: workingDir,
93
- exitCode: execError.code || 1,
94
- output: execError.stderr?.substring(0, 1000),
95
- duration,
96
- }).catch(() => {});
97
-
98
- return {
99
- command,
100
- exitCode: execError.code || 1,
101
- stdout: execError.stdout || '',
102
- stderr: execError.stderr || (error instanceof Error ? error.message : 'Unknown error'),
103
- duration,
104
- };
105
- }
106
- }
107
-
108
- /**
109
- * Execute a command with streaming output
110
- */
111
- export async function executeCommandStream(
112
- command: string,
113
- cwd?: string,
114
- onStdout?: (data: string) => void,
115
- onStderr?: (data: string) => void,
116
- ): Promise<CommandResult> {
117
- const startTime = Date.now();
118
- const workingDir = cwd || process.cwd();
119
-
120
- return new Promise((resolve) => {
121
- // Determine shell based on platform
122
- const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/bash';
123
- const shellArgs = process.platform === 'win32' ? ['/c', command] : ['-c', command];
124
-
125
- const child = spawn(shell, shellArgs, {
126
- cwd: workingDir,
127
- env: process.env,
128
- });
129
-
130
- let stdout = '';
131
- let stderr = '';
132
-
133
- child.stdout.on('data', (data: Buffer) => {
134
- const text = data.toString();
135
- stdout += text;
136
- if (onStdout) {
137
- onStdout(text);
138
- }
139
- });
140
-
141
- child.stderr.on('data', (data: Buffer) => {
142
- const text = data.toString();
143
- stderr += text;
144
- if (onStderr) {
145
- onStderr(text);
146
- }
147
- });
148
-
149
- child.on('close', (code) => {
150
- const duration = Date.now() - startTime;
151
- const exitCode = code ?? 0;
152
-
153
- // Log to API
154
- api.logCommandExecution({
155
- command,
156
- workingDirectory: workingDir,
157
- exitCode,
158
- output: stdout.substring(0, 1000),
159
- duration,
160
- }).catch(() => {});
161
-
162
- resolve({
163
- command,
164
- exitCode,
165
- stdout,
166
- stderr,
167
- duration,
168
- });
169
- });
170
-
171
- child.on('error', (error) => {
172
- const duration = Date.now() - startTime;
173
-
174
- resolve({
175
- command,
176
- exitCode: 1,
177
- stdout: '',
178
- stderr: error.message,
179
- duration,
180
- });
181
- });
182
- });
183
- }
184
-
185
- /**
186
- * Display command result in terminal
187
- */
188
- export function displayCommandResult(result: CommandResult): void {
189
- console.log('');
190
- console.log(style.muted(`$ ${result.command}`));
191
- console.log('');
192
-
193
- if (result.stdout) {
194
- console.log(result.stdout);
195
- }
196
-
197
- if (result.stderr && result.exitCode !== 0) {
198
- console.log(style.error(result.stderr));
199
- }
200
-
201
- const statusColor = result.exitCode === 0 ? 'success' : 'error';
202
- displaySystemMessage(
203
- `Exit code: ${result.exitCode} (${result.duration}ms)`,
204
- statusColor,
205
- );
206
- console.log('');
207
- }
208
-
209
- /**
210
- * Parse AI response for executable commands
211
- */
212
- export function parseCommandsFromResponse(content: string): string[] {
213
- const commands: string[] = [];
214
-
215
- // Match code blocks with bash/shell/sh language
216
- const codeBlockRegex = /```(?:bash|shell|sh|zsh|terminal)\n([\s\S]*?)```/g;
217
- let match;
218
-
219
- while ((match = codeBlockRegex.exec(content)) !== null) {
220
- const code = match[1].trim();
221
- // Split by newlines and filter empty lines
222
- const lines = code.split('\n').filter((line) => {
223
- const trimmed = line.trim();
224
- // Skip comments and empty lines
225
- return trimmed && !trimmed.startsWith('#');
226
- });
227
- commands.push(...lines);
228
- }
229
-
230
- return commands;
231
- }
232
-
233
- /**
234
- * Get current working directory
235
- */
236
- export function getCwd(): string {
237
- return process.cwd();
238
- }
239
-
240
- /**
241
- * Change working directory
242
- */
243
- export function changeCwd(path: string): boolean {
244
- try {
245
- process.chdir(path);
246
- return true;
247
- } catch {
248
- return false;
249
- }
250
- }