@cli-use/tui 0.1.4 → 0.1.10
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/dist/bin/{ratatui-demo → cli-use-demo} +0 -0
- package/dist/cli/index.cjs +5 -5
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +5 -5
- package/dist/cli/index.js.map +1 -1
- package/native/Cargo.toml +11 -0
- package/native/src/main.rs +412 -0
- package/package.json +6 -3
- package/scripts/copy-binary.ts +31 -0
- package/scripts/list-models.ts +33 -0
- package/scripts/postinstall.cjs +59 -0
- package/scripts/run-rust-demo.ts +22 -0
- package/scripts/test-ai.ts +33 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use crossterm::{
|
|
3
|
+
event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
|
|
4
|
+
execute,
|
|
5
|
+
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
6
|
+
};
|
|
7
|
+
use ratatui::{
|
|
8
|
+
backend::CrosstermBackend,
|
|
9
|
+
layout::{Alignment, Constraint, Direction, Layout},
|
|
10
|
+
style::{Color, Modifier, Style},
|
|
11
|
+
text::{Line, Span, Text},
|
|
12
|
+
widgets::{Block, BorderType, Borders, List, ListItem, Paragraph},
|
|
13
|
+
Terminal,
|
|
14
|
+
};
|
|
15
|
+
use std::io::{stdout, Stdout};
|
|
16
|
+
use std::process::Stdio;
|
|
17
|
+
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|
18
|
+
use tokio::process::Command;
|
|
19
|
+
use tokio::sync::mpsc;
|
|
20
|
+
use std::time::Duration;
|
|
21
|
+
|
|
22
|
+
// --- Colors & Constants ---
|
|
23
|
+
const CLAUDE_ORANGE: Color = Color::Rgb(217, 119, 87); // #D97757
|
|
24
|
+
const DARK_BG: Color = Color::Rgb(21, 21, 21); // #151515
|
|
25
|
+
const INPUT_BG: Color = Color::Rgb(30, 30, 30); // Slightly lighter than background for input area
|
|
26
|
+
const FOOTER_TEXT: Color = Color::Rgb(112, 128, 144); // Slate Gray
|
|
27
|
+
const FOOTER_HIGHLIGHT: Color = Color::Rgb(255, 255, 255); // White
|
|
28
|
+
|
|
29
|
+
pub const LOGO_TEXT: [&str; 12] = [
|
|
30
|
+
" ██████╗██╗ ██╗ ",
|
|
31
|
+
"██╔════╝██║ ██║ ",
|
|
32
|
+
"██║ ██║ ██║ ",
|
|
33
|
+
"██║ ██║ ██║ ",
|
|
34
|
+
"╚██████╗███████╗██║ ",
|
|
35
|
+
" ╚═════╝╚══════╝╚═╝ ",
|
|
36
|
+
" ██████╗ ██████╗ ██████╗ ███████╗",
|
|
37
|
+
"██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
38
|
+
"██║ ██║ ██║██║ ██║█████╗ ",
|
|
39
|
+
"██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
40
|
+
"╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
41
|
+
" ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// --- Data Structures ---
|
|
45
|
+
|
|
46
|
+
enum AppState {
|
|
47
|
+
Splash,
|
|
48
|
+
Chat,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[derive(Clone)]
|
|
52
|
+
enum MessageType {
|
|
53
|
+
User,
|
|
54
|
+
Thinking,
|
|
55
|
+
#[allow(dead_code)]
|
|
56
|
+
ToolCall,
|
|
57
|
+
Output,
|
|
58
|
+
System,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[derive(Clone)]
|
|
62
|
+
struct Message {
|
|
63
|
+
content: String,
|
|
64
|
+
msg_type: MessageType,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
struct App {
|
|
68
|
+
state: AppState,
|
|
69
|
+
input: String,
|
|
70
|
+
messages: Vec<Message>,
|
|
71
|
+
tx_ai: mpsc::Sender<String>, // Channel to send prompts to AI
|
|
72
|
+
rx_ai: mpsc::Receiver<String>, // Channel to receive responses from AI
|
|
73
|
+
waiting_for_response: bool,
|
|
74
|
+
// Add scroll tracking
|
|
75
|
+
scroll_offset: usize,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
impl App {
|
|
79
|
+
fn new(tx_ai: mpsc::Sender<String>, rx_ai: mpsc::Receiver<String>) -> App {
|
|
80
|
+
App {
|
|
81
|
+
state: AppState::Splash,
|
|
82
|
+
input: String::new(),
|
|
83
|
+
messages: vec![
|
|
84
|
+
Message {
|
|
85
|
+
content: "Welcome to CLI CODE.".to_string(),
|
|
86
|
+
msg_type: MessageType::System,
|
|
87
|
+
},
|
|
88
|
+
Message {
|
|
89
|
+
content: "I am ready to assist. Type anything to chat with Google Gemini.".to_string(),
|
|
90
|
+
msg_type: MessageType::System,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
tx_ai,
|
|
94
|
+
rx_ai,
|
|
95
|
+
waiting_for_response: false,
|
|
96
|
+
scroll_offset: 0,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async fn submit_message(&mut self) {
|
|
101
|
+
if self.input.trim().is_empty() {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let prompt = self.input.trim().to_string();
|
|
106
|
+
|
|
107
|
+
// Add user message to UI
|
|
108
|
+
self.messages.push(Message {
|
|
109
|
+
content: self.input.clone(),
|
|
110
|
+
msg_type: MessageType::User,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Add thinking indicator
|
|
114
|
+
self.messages.push(Message {
|
|
115
|
+
content: "Consulting Gemini...".to_string(),
|
|
116
|
+
msg_type: MessageType::Thinking,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Send to AI worker
|
|
120
|
+
if let Err(e) = self.tx_ai.send(prompt).await {
|
|
121
|
+
self.messages.push(Message {
|
|
122
|
+
content: format!("Error communicating with AI worker: {}", e),
|
|
123
|
+
msg_type: MessageType::System,
|
|
124
|
+
});
|
|
125
|
+
} else {
|
|
126
|
+
self.waiting_for_response = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
self.input.clear();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Main Execution ---
|
|
134
|
+
|
|
135
|
+
#[tokio::main]
|
|
136
|
+
async fn main() -> Result<()> {
|
|
137
|
+
enable_raw_mode()?;
|
|
138
|
+
let mut stdout = stdout();
|
|
139
|
+
execute!(stdout, EnterAlternateScreen)?;
|
|
140
|
+
let backend = CrosstermBackend::new(stdout);
|
|
141
|
+
let mut terminal = Terminal::new(backend)?;
|
|
142
|
+
|
|
143
|
+
// Get worker path from arguments
|
|
144
|
+
let args: Vec<String> = std::env::args().collect();
|
|
145
|
+
let worker_path = if args.len() > 1 {
|
|
146
|
+
args[1].clone()
|
|
147
|
+
} else {
|
|
148
|
+
// Fallback for dev mode
|
|
149
|
+
"scripts/ai-worker.ts".to_string()
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// --- Spawn Node.js AI Worker ---
|
|
153
|
+
let (tx_to_worker, mut rx_from_ui) = mpsc::channel::<String>(100);
|
|
154
|
+
let (tx_to_ui, rx_from_worker) = mpsc::channel::<String>(100);
|
|
155
|
+
|
|
156
|
+
// Determine command based on extension
|
|
157
|
+
let cmd_exec = if worker_path.ends_with(".ts") {
|
|
158
|
+
"tsx"
|
|
159
|
+
} else {
|
|
160
|
+
"node"
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
let mut child = Command::new(cmd_exec)
|
|
164
|
+
.arg(&worker_path)
|
|
165
|
+
.stdin(Stdio::piped())
|
|
166
|
+
.stdout(Stdio::piped())
|
|
167
|
+
.stderr(Stdio::piped())
|
|
168
|
+
.spawn()
|
|
169
|
+
.expect("Failed to spawn Node.js AI worker");
|
|
170
|
+
|
|
171
|
+
let mut stdin = child.stdin.take().expect("Failed to open stdin");
|
|
172
|
+
let stdout = child.stdout.take().expect("Failed to open stdout");
|
|
173
|
+
let stderr = child.stderr.take().expect("Failed to open stderr");
|
|
174
|
+
|
|
175
|
+
// Task: Write to Node process
|
|
176
|
+
tokio::spawn(async move {
|
|
177
|
+
while let Some(prompt) = rx_from_ui.recv().await {
|
|
178
|
+
if let Err(e) = stdin.write_all(format!("{}\n", prompt).as_bytes()).await {
|
|
179
|
+
eprintln!("Failed to write to AI worker: {}", e);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
if let Err(e) = stdin.flush().await {
|
|
183
|
+
eprintln!("Failed to flush to AI worker: {}", e);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Task: Read from Node process
|
|
190
|
+
let tx_to_ui_clone = tx_to_ui.clone();
|
|
191
|
+
tokio::spawn(async move {
|
|
192
|
+
let mut reader = BufReader::new(stdout).lines();
|
|
193
|
+
while let Ok(Some(line)) = reader.next_line().await {
|
|
194
|
+
let _ = tx_to_ui_clone.send(line.replace("\\n", "\n")).await;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Task: Log stderr
|
|
199
|
+
tokio::spawn(async move {
|
|
200
|
+
let mut reader = BufReader::new(stderr).lines();
|
|
201
|
+
while let Ok(Some(_line)) = reader.next_line().await {
|
|
202
|
+
// Ignore stderr for now
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
let mut app = App::new(tx_to_worker, rx_from_worker);
|
|
207
|
+
let res = run_app(&mut terminal, &mut app).await;
|
|
208
|
+
|
|
209
|
+
// Cleanup
|
|
210
|
+
let _ = child.kill().await;
|
|
211
|
+
|
|
212
|
+
disable_raw_mode()?;
|
|
213
|
+
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
214
|
+
terminal.show_cursor()?;
|
|
215
|
+
|
|
216
|
+
if let Err(err) = res {
|
|
217
|
+
println!("{:?}", err);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Ok(())
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>, app: &mut App) -> Result<()> {
|
|
224
|
+
let mut interval = tokio::time::interval(Duration::from_millis(33));
|
|
225
|
+
|
|
226
|
+
loop {
|
|
227
|
+
terminal.draw(|f| {
|
|
228
|
+
let size = f.size();
|
|
229
|
+
let main_block = Block::default().style(Style::default().bg(DARK_BG));
|
|
230
|
+
f.render_widget(main_block, size);
|
|
231
|
+
|
|
232
|
+
match app.state {
|
|
233
|
+
AppState::Splash => {
|
|
234
|
+
let vertical_chunks = Layout::default()
|
|
235
|
+
.direction(Direction::Vertical)
|
|
236
|
+
.constraints([
|
|
237
|
+
Constraint::Percentage(15),
|
|
238
|
+
Constraint::Length(5), // Header
|
|
239
|
+
Constraint::Length(15), // Logo
|
|
240
|
+
Constraint::Min(1), // Spacer
|
|
241
|
+
Constraint::Length(3), // Input
|
|
242
|
+
])
|
|
243
|
+
.margin(2)
|
|
244
|
+
.split(size);
|
|
245
|
+
|
|
246
|
+
let header_area = vertical_chunks[1];
|
|
247
|
+
let header_layout = Layout::default()
|
|
248
|
+
.direction(Direction::Horizontal)
|
|
249
|
+
.constraints([
|
|
250
|
+
Constraint::Fill(1),
|
|
251
|
+
Constraint::Length(54),
|
|
252
|
+
Constraint::Fill(1),
|
|
253
|
+
])
|
|
254
|
+
.split(header_area);
|
|
255
|
+
|
|
256
|
+
let header_text = "Welcome to the Claude Code research preview!";
|
|
257
|
+
let header = Paragraph::new(Line::from(vec![
|
|
258
|
+
Span::styled(header_text, Style::default().fg(CLAUDE_ORANGE).add_modifier(Modifier::BOLD)),
|
|
259
|
+
]))
|
|
260
|
+
.block(
|
|
261
|
+
Block::default()
|
|
262
|
+
.borders(Borders::ALL)
|
|
263
|
+
.border_type(BorderType::Rounded)
|
|
264
|
+
.border_style(Style::default().fg(CLAUDE_ORANGE))
|
|
265
|
+
)
|
|
266
|
+
.alignment(Alignment::Center);
|
|
267
|
+
f.render_widget(header, header_layout[1]);
|
|
268
|
+
|
|
269
|
+
let logo_lines: Vec<Line> = LOGO_TEXT.iter()
|
|
270
|
+
.map(|line| Line::from(Span::styled(*line, Style::default().fg(CLAUDE_ORANGE))))
|
|
271
|
+
.collect();
|
|
272
|
+
let logo = Paragraph::new(Text::from(logo_lines)).alignment(Alignment::Center);
|
|
273
|
+
f.render_widget(logo, vertical_chunks[2]);
|
|
274
|
+
|
|
275
|
+
// Render Input on Splash Screen
|
|
276
|
+
let input_text = format!("> {}_", app.input);
|
|
277
|
+
let input = Paragraph::new(input_text)
|
|
278
|
+
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD)) // Bold White font
|
|
279
|
+
.block(Block::default()
|
|
280
|
+
.borders(Borders::ALL)
|
|
281
|
+
.border_style(Style::default().fg(Color::DarkGray))
|
|
282
|
+
.style(Style::default().bg(INPUT_BG))); // Darker Background
|
|
283
|
+
f.render_widget(input, vertical_chunks[4]);
|
|
284
|
+
}
|
|
285
|
+
AppState::Chat => {
|
|
286
|
+
let chunks = Layout::default()
|
|
287
|
+
.direction(Direction::Vertical)
|
|
288
|
+
.constraints([
|
|
289
|
+
Constraint::Min(1),
|
|
290
|
+
Constraint::Length(3),
|
|
291
|
+
].as_ref())
|
|
292
|
+
.split(size);
|
|
293
|
+
|
|
294
|
+
let messages: Vec<ListItem> = app.messages
|
|
295
|
+
.iter()
|
|
296
|
+
.map(|m| {
|
|
297
|
+
let (symbol, style) = match m.msg_type {
|
|
298
|
+
MessageType::User => (">", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
|
|
299
|
+
MessageType::Thinking => ("⏺", Style::default().fg(Color::Yellow)),
|
|
300
|
+
MessageType::ToolCall => ("⏺ Call", Style::default().fg(Color::Blue)),
|
|
301
|
+
MessageType::Output => ("⎿", Style::default().fg(Color::DarkGray)),
|
|
302
|
+
MessageType::System => ("*", Style::default().fg(Color::Magenta)),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
let mut lines = vec![];
|
|
306
|
+
let first_line_content = m.content.lines().next().unwrap_or("");
|
|
307
|
+
|
|
308
|
+
lines.push(Line::from(vec![
|
|
309
|
+
Span::styled(symbol, style),
|
|
310
|
+
Span::raw(" "),
|
|
311
|
+
Span::styled(first_line_content, if matches!(m.msg_type, MessageType::Output) { Style::default().fg(Color::DarkGray) } else { Style::default() }),
|
|
312
|
+
]));
|
|
313
|
+
|
|
314
|
+
for line in m.content.lines().skip(1) {
|
|
315
|
+
lines.push(Line::from(vec![
|
|
316
|
+
Span::raw(" "),
|
|
317
|
+
Span::styled(line, if matches!(m.msg_type, MessageType::Output) { Style::default().fg(Color::DarkGray) } else { Style::default() }),
|
|
318
|
+
]));
|
|
319
|
+
}
|
|
320
|
+
ListItem::new(lines)
|
|
321
|
+
})
|
|
322
|
+
.collect();
|
|
323
|
+
|
|
324
|
+
let messages_list = List::new(messages)
|
|
325
|
+
.block(Block::default().style(Style::default().bg(DARK_BG)));
|
|
326
|
+
|
|
327
|
+
// Simple auto-scroll
|
|
328
|
+
let mut state = ratatui::widgets::ListState::default();
|
|
329
|
+
if !app.messages.is_empty() {
|
|
330
|
+
state.select(Some(app.messages.len() - 1));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
f.render_stateful_widget(messages_list, chunks[0], &mut state);
|
|
334
|
+
|
|
335
|
+
let input = Paragraph::new(format!("> {}_", app.input))
|
|
336
|
+
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD)) // Bold White font
|
|
337
|
+
.block(Block::default()
|
|
338
|
+
.borders(Borders::TOP) // Separator line
|
|
339
|
+
.border_style(Style::default().fg(Color::DarkGray))
|
|
340
|
+
.style(Style::default().bg(INPUT_BG))); // Darker Background
|
|
341
|
+
f.render_widget(input, chunks[1]);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
})?;
|
|
345
|
+
|
|
346
|
+
// 2. Handle Events (Keyboard + AI Channel)
|
|
347
|
+
tokio::select! {
|
|
348
|
+
_ = interval.tick() => {
|
|
349
|
+
// Just for redraw frequency
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
else => {}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
while let Ok(response) = app.rx_ai.try_recv() {
|
|
356
|
+
// Remove the thinking message if it exists
|
|
357
|
+
if app.waiting_for_response {
|
|
358
|
+
if let Some(last) = app.messages.last() {
|
|
359
|
+
if matches!(last.msg_type, MessageType::Thinking) {
|
|
360
|
+
app.messages.pop();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
app.waiting_for_response = false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
app.messages.push(Message {
|
|
367
|
+
content: response,
|
|
368
|
+
msg_type: MessageType::Output,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if event::poll(Duration::from_millis(10))? {
|
|
373
|
+
if let Event::Key(key) = event::read()? {
|
|
374
|
+
if key.kind == KeyEventKind::Press {
|
|
375
|
+
match app.state {
|
|
376
|
+
AppState::Splash => {
|
|
377
|
+
match key.code {
|
|
378
|
+
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(()),
|
|
379
|
+
KeyCode::Enter => {
|
|
380
|
+
if !app.input.trim().is_empty() {
|
|
381
|
+
app.state = AppState::Chat;
|
|
382
|
+
app.submit_message().await;
|
|
383
|
+
} else {
|
|
384
|
+
app.state = AppState::Chat;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
KeyCode::Char(c) => app.input.push(c),
|
|
388
|
+
KeyCode::Backspace => { app.input.pop(); },
|
|
389
|
+
KeyCode::Esc => return Ok(()),
|
|
390
|
+
_ => {}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
AppState::Chat => {
|
|
394
|
+
match key.code {
|
|
395
|
+
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(()),
|
|
396
|
+
KeyCode::Enter => {
|
|
397
|
+
if app.input.trim().eq_ignore_ascii_case("quit") || app.input.trim().eq_ignore_ascii_case("exit") {
|
|
398
|
+
return Ok(());
|
|
399
|
+
}
|
|
400
|
+
app.submit_message().await;
|
|
401
|
+
}
|
|
402
|
+
KeyCode::Char(c) => app.input.push(c),
|
|
403
|
+
KeyCode::Backspace => { app.input.pop(); },
|
|
404
|
+
_ => {}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cli-use/tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Build beautiful terminal user interfaces with styled components - A powerful TUI framework for creating stunning CLI applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"dist",
|
|
24
24
|
"README.md",
|
|
25
|
-
"LICENSE"
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"scripts",
|
|
27
|
+
"native/src",
|
|
28
|
+
"native/Cargo.toml"
|
|
26
29
|
],
|
|
27
30
|
"scripts": {
|
|
28
31
|
"build": "npm run build:rust && tsup && npm run build:copy",
|
|
@@ -37,7 +40,7 @@
|
|
|
37
40
|
"list:models": "tsx scripts/list-models.ts",
|
|
38
41
|
"build:copy": "tsx scripts/copy-binary.ts",
|
|
39
42
|
"demo:cli-use": "tsx src/examples/cli-use-demo.tsx",
|
|
40
|
-
"postinstall": "
|
|
43
|
+
"postinstall": "node scripts/postinstall.cjs"
|
|
41
44
|
},
|
|
42
45
|
"keywords": [
|
|
43
46
|
"tui",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const isWindows = process.platform === 'win32';
|
|
5
|
+
const extension = isWindows ? '.exe' : '';
|
|
6
|
+
const binaryName = 'cli-use-demo' + extension;
|
|
7
|
+
|
|
8
|
+
const srcPath = path.resolve(process.cwd(), 'native/target/release', binaryName);
|
|
9
|
+
const destDir = path.resolve(process.cwd(), 'dist/bin');
|
|
10
|
+
const destPath = path.resolve(destDir, binaryName);
|
|
11
|
+
|
|
12
|
+
console.log('Copying binary...');
|
|
13
|
+
console.log('Source:', srcPath);
|
|
14
|
+
console.log('Dest:', destPath);
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(srcPath)) {
|
|
17
|
+
console.error('Error: Source binary not found. Did you run npm run build:rust?');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(destDir)) {
|
|
22
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fs.copyFileSync(srcPath, destPath);
|
|
26
|
+
|
|
27
|
+
if (!isWindows) {
|
|
28
|
+
fs.chmodSync(destPath, '755');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log('Binary copied successfully.');
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
|
|
3
|
+
dotenv.config();
|
|
4
|
+
|
|
5
|
+
const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY ||
|
|
6
|
+
process.env.GEMINI_API_KEY ||
|
|
7
|
+
process.env.GOOGLE_API_KEY;
|
|
8
|
+
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
console.error('No API Key');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function listModels() {
|
|
15
|
+
try {
|
|
16
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`;
|
|
17
|
+
console.log('Fetching:', url.replace(apiKey, 'HIDDEN_KEY'));
|
|
18
|
+
|
|
19
|
+
const response = await fetch(url);
|
|
20
|
+
const data = await response.json();
|
|
21
|
+
|
|
22
|
+
if (data.models) {
|
|
23
|
+
console.log('✅ Available Models:');
|
|
24
|
+
data.models.forEach(m => console.log(' - ' + m.name));
|
|
25
|
+
} else {
|
|
26
|
+
console.log('❌ Error:', JSON.stringify(data, null, 2));
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Error listing models:', error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
listModels();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { spawnSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// Use __dirname to find package root (scripts/../)
|
|
6
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
// Only run if cargo is available
|
|
10
|
+
try {
|
|
11
|
+
const result = spawnSync('cargo', ['--version']);
|
|
12
|
+
if (result.error || result.status !== 0) {
|
|
13
|
+
console.log('Cargo not found, skipping native build.');
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.log('Cargo check failed, skipping native build.');
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('Building native binary locally...');
|
|
22
|
+
const nativeDir = path.resolve(packageRoot, 'native');
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(nativeDir)) {
|
|
25
|
+
console.warn('Native directory not found at:', nativeDir);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const build = spawnSync('cargo', ['build', '--release'], {
|
|
30
|
+
cwd: nativeDir,
|
|
31
|
+
stdio: 'inherit',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (build.status === 0) {
|
|
35
|
+
const isWindows = process.platform === 'win32';
|
|
36
|
+
const ext = isWindows ? '.exe' : '';
|
|
37
|
+
const src = path.join(nativeDir, 'target/release/cli-use-demo' + ext);
|
|
38
|
+
|
|
39
|
+
// Install to dist/bin
|
|
40
|
+
const destDir = path.resolve(packageRoot, 'dist/bin');
|
|
41
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
42
|
+
|
|
43
|
+
const dest = path.join(destDir, 'cli-use-demo' + ext);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
fs.copyFileSync(src, dest);
|
|
47
|
+
console.log('Native binary built and installed successfully.');
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('Error copying binary:', err);
|
|
50
|
+
// Don't fail install just because copy failed
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
console.error('Failed to build native binary. You may need to build manually.');
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Postinstall script error:', error);
|
|
57
|
+
// Always exit successfully so npm install doesn't crash
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const rustBinaryPath = path.resolve(process.cwd(), 'native/target/release/cli-use-demo');
|
|
5
|
+
const workerPath = path.resolve(process.cwd(), 'src/ai-worker.ts');
|
|
6
|
+
|
|
7
|
+
console.log('Starting cli-use Demo...');
|
|
8
|
+
console.log(`Binary path: ${rustBinaryPath}`);
|
|
9
|
+
console.log(`Worker path: ${workerPath}`);
|
|
10
|
+
|
|
11
|
+
const child = spawn(rustBinaryPath, [workerPath], {
|
|
12
|
+
stdio: 'inherit',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
child.on('error', (err) => {
|
|
16
|
+
console.error('Failed to start demo:', err);
|
|
17
|
+
console.log('\nMake sure you have built the Rust binary first by running: npm run build:rust');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
child.on('close', (code) => {
|
|
21
|
+
console.log(`Demo exited with code ${code}`);
|
|
22
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
2
|
+
import { generateText } from 'ai';
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
|
|
5
|
+
dotenv.config();
|
|
6
|
+
|
|
7
|
+
async function testWorker() {
|
|
8
|
+
const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY ||
|
|
9
|
+
process.env.GEMINI_API_KEY ||
|
|
10
|
+
process.env.GOOGLE_API_KEY;
|
|
11
|
+
|
|
12
|
+
if (!apiKey) {
|
|
13
|
+
console.error('❌ Error: No API Key found.');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log('✅ API Key found.');
|
|
18
|
+
|
|
19
|
+
const google = createGoogleGenerativeAI({ apiKey });
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
console.log('⏳ Sending test prompt to Gemini 2.0 Flash...');
|
|
23
|
+
const { text } = await generateText({
|
|
24
|
+
model: google('models/gemini-2.0-flash'),
|
|
25
|
+
prompt: 'Say hello',
|
|
26
|
+
});
|
|
27
|
+
console.log('✅ Success! Response:', text);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('❌ API Request Failed:', error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
testWorker();
|