@booncli/booncli 1.0.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/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import Pastel from 'pastel';
3
+ import { ensureConfigExists } from './config.js';
4
+ await ensureConfigExists();
5
+ const app = new Pastel({
6
+ importMeta: import.meta,
7
+ });
8
+ await app.run();
@@ -0,0 +1,16 @@
1
+ export declare const commandActions: readonly [{
2
+ readonly command: "/login";
3
+ readonly result: "Login";
4
+ }, {
5
+ readonly command: "/loginout";
6
+ readonly result: "Loginout";
7
+ }, {
8
+ readonly command: "/log";
9
+ readonly result: "Log";
10
+ }, {
11
+ readonly command: "/clear";
12
+ readonly result: "Cleared";
13
+ }];
14
+ export type SlashCommand = (typeof commandActions)[number]['command'];
15
+ export declare function isSlashCommand(value: string): value is SlashCommand;
16
+ export declare function runCommand(value: string): string;
@@ -0,0 +1,15 @@
1
+ export const commandActions = [
2
+ { command: '/login', result: 'Login' },
3
+ { command: '/loginout', result: 'Loginout' },
4
+ { command: '/log', result: 'Log' },
5
+ { command: '/clear', result: 'Cleared' },
6
+ ];
7
+ const slashCommandSet = new Set(commandActions.map(({ command }) => command));
8
+ export function isSlashCommand(value) {
9
+ return slashCommandSet.has(value);
10
+ }
11
+ export function runCommand(value) {
12
+ const command = value.trim().toLowerCase();
13
+ const action = commandActions.find(action => action.command === command);
14
+ return action?.result ?? value.trim();
15
+ }
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export default function Clear(): React.JSX.Element;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
+ import { runCommand } from '../command-actions.js';
4
+ export default function Clear() {
5
+ return React.createElement(Text, null, runCommand('/clear'));
6
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ export declare const slashCommands: readonly ["/login", "/loginout", "/log", "/clear"];
3
+ export declare function getAvailableSlashCommands(isLoggedIn: boolean): ("/login" | "/loginout" | "/log" | "/clear")[];
4
+ export declare function getCommandSuggestions(input: string, isLoggedIn?: boolean): ("/login" | "/loginout" | "/log" | "/clear")[];
5
+ export declare function getDefaultSuggestionIndex(input: string, isLoggedIn?: boolean): number;
6
+ export default function Index(): React.JSX.Element;
@@ -0,0 +1,251 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import BigText from 'ink-big-text';
5
+ import { getProviderLoginUsername, isLoginMethod, isAccountPasswordLoginMethod, loginMethods, } from '../login-methods.js';
6
+ import Radio from '../modules/radio.js';
7
+ import { runCommand } from '../command-actions.js';
8
+ export const slashCommands = ['/login', '/loginout', '/log', '/clear'];
9
+ export function getAvailableSlashCommands(isLoggedIn) {
10
+ return slashCommands.filter(command => {
11
+ if (command === '/login') {
12
+ return !isLoggedIn;
13
+ }
14
+ if (command === '/loginout') {
15
+ return isLoggedIn;
16
+ }
17
+ return true;
18
+ });
19
+ }
20
+ export function getCommandSuggestions(input, isLoggedIn = false) {
21
+ const query = input.trim().toLowerCase();
22
+ if (!query.startsWith('/')) {
23
+ return [];
24
+ }
25
+ return getAvailableSlashCommands(isLoggedIn).filter(command => command.startsWith(query));
26
+ }
27
+ export function getDefaultSuggestionIndex(input, isLoggedIn = false) {
28
+ const query = input.trim().toLowerCase();
29
+ const suggestions = getCommandSuggestions(input, isLoggedIn);
30
+ const exactMatchIndex = suggestions.indexOf(query);
31
+ return Math.max(0, exactMatchIndex);
32
+ }
33
+ function getInputPlaceholder(pendingFlow) {
34
+ if (pendingFlow?.type === 'loginMethod') {
35
+ return 'Select login method';
36
+ }
37
+ if (pendingFlow?.type === 'loginUsername') {
38
+ return 'username';
39
+ }
40
+ if (pendingFlow?.type === 'loginPassword') {
41
+ return 'password';
42
+ }
43
+ if (pendingFlow?.type === 'loginoutConfirm') {
44
+ return 'yes/no';
45
+ }
46
+ return 'Enter your query';
47
+ }
48
+ export default function Index() {
49
+ const [input, setInput] = useState('');
50
+ const [selectedIndex, setSelectedIndex] = useState(0);
51
+ const [pendingFlow, setPendingFlow] = useState();
52
+ const [username, setUsername] = useState();
53
+ const [sentMessages, setSentMessages] = useState([]);
54
+ const [nextMessageId, setNextMessageId] = useState(1);
55
+ const isLoggedIn = username !== undefined;
56
+ const suggestions = useMemo(() => (pendingFlow ? [] : getCommandSuggestions(input, isLoggedIn)), [input, isLoggedIn, pendingFlow]);
57
+ useEffect(() => {
58
+ if (selectedIndex >= suggestions.length) {
59
+ setSelectedIndex(0);
60
+ }
61
+ }, [selectedIndex, suggestions.length]);
62
+ const addMessage = (command, message) => {
63
+ setSentMessages(messages => [
64
+ ...messages.slice(-4),
65
+ { id: nextMessageId, command, message },
66
+ ]);
67
+ setNextMessageId(id => id + 1);
68
+ };
69
+ const resetInput = () => {
70
+ setInput('');
71
+ setSelectedIndex(0);
72
+ };
73
+ const clearPanel = () => {
74
+ setSentMessages([]);
75
+ setPendingFlow(undefined);
76
+ resetInput();
77
+ };
78
+ const confirmLoginout = (value) => {
79
+ const answer = value.trim().toLowerCase();
80
+ if (answer === 'yes') {
81
+ setUsername(undefined);
82
+ addMessage('/loginout', 'Logged out');
83
+ setPendingFlow(undefined);
84
+ resetInput();
85
+ return;
86
+ }
87
+ if (answer === 'no') {
88
+ addMessage('/loginout', 'Cancelled');
89
+ setPendingFlow(undefined);
90
+ resetInput();
91
+ return;
92
+ }
93
+ setInput('');
94
+ };
95
+ const submitLoginUsername = (value) => {
96
+ setPendingFlow({
97
+ type: 'loginPassword',
98
+ username: value.trim() || 'anonymous',
99
+ });
100
+ resetInput();
101
+ };
102
+ const submitLoginPassword = () => {
103
+ if (pendingFlow?.type !== 'loginPassword') {
104
+ resetInput();
105
+ return;
106
+ }
107
+ setUsername(pendingFlow.username);
108
+ addMessage('/login', `Logged in as ${pendingFlow.username}`);
109
+ setPendingFlow(undefined);
110
+ resetInput();
111
+ };
112
+ const selectLoginMethod = (method) => {
113
+ if (!isLoginMethod(method)) {
114
+ return;
115
+ }
116
+ if (isAccountPasswordLoginMethod(method)) {
117
+ setPendingFlow({ type: 'loginUsername' });
118
+ resetInput();
119
+ return;
120
+ }
121
+ const providerUsername = getProviderLoginUsername(method);
122
+ if (providerUsername) {
123
+ setUsername(providerUsername);
124
+ addMessage('/login', `Logged in as ${providerUsername}`);
125
+ setPendingFlow(undefined);
126
+ resetInput();
127
+ }
128
+ };
129
+ const sendMessage = (value) => {
130
+ if (pendingFlow?.type === 'loginMethod') {
131
+ resetInput();
132
+ return;
133
+ }
134
+ if (pendingFlow?.type === 'loginUsername') {
135
+ submitLoginUsername(value);
136
+ return;
137
+ }
138
+ if (pendingFlow?.type === 'loginPassword') {
139
+ submitLoginPassword();
140
+ return;
141
+ }
142
+ if (pendingFlow?.type === 'loginoutConfirm') {
143
+ confirmLoginout(value);
144
+ return;
145
+ }
146
+ const currentSuggestions = getCommandSuggestions(value, isLoggedIn);
147
+ const selectedSuggestion = currentSuggestions[selectedIndex];
148
+ const command = selectedSuggestion ?? value.trim();
149
+ const normalizedCommand = command.toLowerCase();
150
+ if (normalizedCommand === '/clear') {
151
+ clearPanel();
152
+ return;
153
+ }
154
+ if (normalizedCommand === '/login') {
155
+ if (isLoggedIn) {
156
+ addMessage(command, `Already logged in as ${username}`);
157
+ resetInput();
158
+ return;
159
+ }
160
+ setPendingFlow({ type: 'loginMethod' });
161
+ resetInput();
162
+ return;
163
+ }
164
+ if (normalizedCommand === '/loginout') {
165
+ if (!isLoggedIn) {
166
+ addMessage(command, 'Not logged in');
167
+ resetInput();
168
+ return;
169
+ }
170
+ setPendingFlow({ type: 'loginoutConfirm' });
171
+ resetInput();
172
+ return;
173
+ }
174
+ const message = runCommand(command);
175
+ if (message.length > 0) {
176
+ addMessage(command, message);
177
+ }
178
+ resetInput();
179
+ };
180
+ useInput((_value, key) => {
181
+ if (key.upArrow && suggestions.length > 0) {
182
+ setSelectedIndex(index => Math.max(0, index - 1));
183
+ return;
184
+ }
185
+ if (key.downArrow && suggestions.length > 0) {
186
+ setSelectedIndex(index => Math.min(suggestions.length - 1, index + 1));
187
+ return;
188
+ }
189
+ if (key.tab && suggestions[selectedIndex]) {
190
+ const suggestion = suggestions[selectedIndex];
191
+ setInput(suggestion);
192
+ setSelectedIndex(getDefaultSuggestionIndex(suggestion, isLoggedIn));
193
+ return;
194
+ }
195
+ if (key.escape) {
196
+ setInput('');
197
+ setSelectedIndex(0);
198
+ setPendingFlow(undefined);
199
+ }
200
+ });
201
+ const inputPlaceholder = getInputPlaceholder(pendingFlow);
202
+ const isLoginMethodSelection = pendingFlow?.type === 'loginMethod';
203
+ return (React.createElement(Box, { flexDirection: "column" },
204
+ React.createElement(BigText, { text: "BOONCLI" }),
205
+ sentMessages.length > 0 && (React.createElement(Box, { flexDirection: "column" }, sentMessages.map(({ id, command, message }) => (React.createElement(Text, { key: id },
206
+ command.startsWith('/') ? 'Run' : 'Sent',
207
+ ":",
208
+ ' ',
209
+ React.createElement(Text, { color: "cyan" }, command),
210
+ " -",
211
+ ' ',
212
+ React.createElement(Text, { color: "green" }, message)))))),
213
+ isLoginMethodSelection && (React.createElement(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1 },
214
+ React.createElement(Text, null, "Login: select a method."),
215
+ React.createElement(Radio, { isActive: isLoginMethodSelection, items: [...loginMethods], onSelect: selectLoginMethod }))),
216
+ pendingFlow?.type === 'loginUsername' && (React.createElement(Box, { marginLeft: 2, marginTop: 1 },
217
+ React.createElement(Text, null,
218
+ "Login: enter any ",
219
+ React.createElement(Text, { color: "cyan" }, "username"),
220
+ "."))),
221
+ pendingFlow?.type === 'loginPassword' && (React.createElement(Box, { marginLeft: 2, marginTop: 1 },
222
+ React.createElement(Text, null,
223
+ "Login: enter any ",
224
+ React.createElement(Text, { color: "cyan" }, "password"),
225
+ "."))),
226
+ pendingFlow?.type === 'loginoutConfirm' && (React.createElement(Box, { marginLeft: 2, marginTop: 1 },
227
+ React.createElement(Text, null,
228
+ "Confirm ",
229
+ React.createElement(Text, { color: "cyan" }, "/loginout"),
230
+ "? Type",
231
+ ' ',
232
+ React.createElement(Text, { color: "green" }, "yes"),
233
+ " or ",
234
+ React.createElement(Text, { color: "red" }, "no"),
235
+ "."))),
236
+ React.createElement(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, paddingY: 0, borderLeft: false, borderRight: false, marginTop: 1 },
237
+ React.createElement(Text, { color: "cyan" }, "\u276F "),
238
+ React.createElement(TextInput, { focus: !isLoginMethodSelection, placeholder: inputPlaceholder, value: input, ...(pendingFlow?.type === 'loginPassword' ? { mask: '*' } : {}), onChange: value => {
239
+ setInput(value);
240
+ if (!pendingFlow) {
241
+ setSelectedIndex(getDefaultSuggestionIndex(value, isLoggedIn));
242
+ }
243
+ }, onSubmit: sendMessage })),
244
+ suggestions.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1 }, suggestions.map((command, index) => {
245
+ const isSelected = index === selectedIndex;
246
+ return (React.createElement(Text, { key: command, ...(isSelected ? { color: 'yellow' } : {}) },
247
+ isSelected ? '>' : ' ',
248
+ " ",
249
+ command));
250
+ })))));
251
+ }
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export default function Log(): React.JSX.Element;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
+ import { runCommand } from '../command-actions.js';
4
+ export default function Log() {
5
+ return React.createElement(Text, null, runCommand('/log'));
6
+ }
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export default function Login(): React.JSX.Element;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
+ import { runCommand } from '../command-actions.js';
4
+ export default function Login() {
5
+ return React.createElement(Text, null, runCommand('/login'));
6
+ }
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export default function Loginout(): React.JSX.Element;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
+ import { runCommand } from '../command-actions.js';
4
+ export default function Loginout() {
5
+ return React.createElement(Text, null, runCommand('/loginout'));
6
+ }
@@ -0,0 +1,27 @@
1
+ export type ModelProviderConfig = {
2
+ readonly name: string;
3
+ readonly baseUrl: string;
4
+ readonly wireApi: string;
5
+ readonly envKey: string;
6
+ };
7
+ export type ResolvedModelProviderConfig = ModelProviderConfig & {
8
+ readonly apiKey: string | undefined;
9
+ };
10
+ export type BoonCliConfig = {
11
+ readonly modelProviders: {
12
+ readonly deepseek: ModelProviderConfig;
13
+ };
14
+ };
15
+ export declare const defaultConfig: BoonCliConfig;
16
+ export declare function getConfigDirectory(): string;
17
+ export declare function getConfigFilePath(): string;
18
+ export declare function ensureConfigExists(): Promise<string>;
19
+ export declare function readConfig(): Promise<BoonCliConfig>;
20
+ export declare function resolveProviderApiKey(provider: ModelProviderConfig): string | undefined;
21
+ export declare function readResolvedDeepseekProvider(): Promise<{
22
+ apiKey: string | undefined;
23
+ name: string;
24
+ baseUrl: string;
25
+ wireApi: string;
26
+ envKey: string;
27
+ }>;
package/dist/config.js ADDED
@@ -0,0 +1,90 @@
1
+ import { constants } from 'node:fs';
2
+ import { access, chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ const defaultConfigFileContent = `[model_providers.deepseek]
7
+ name = "DeepSeek"
8
+ base_url = "https://api.deepseek.com/v1"
9
+ wire_api = "responses"
10
+ env_key = "DEEPSEEK_API_KEY"
11
+ `;
12
+ export const defaultConfig = {
13
+ modelProviders: {
14
+ deepseek: {
15
+ name: 'DeepSeek',
16
+ baseUrl: 'https://api.deepseek.com/v1',
17
+ wireApi: 'responses',
18
+ envKey: 'DEEPSEEK_API_KEY',
19
+ },
20
+ },
21
+ };
22
+ export function getConfigDirectory() {
23
+ return (process.env['BOONCLI_CONFIG_DIR'] ?? path.join(os.homedir(), '.booncli'));
24
+ }
25
+ export function getConfigFilePath() {
26
+ return path.join(getConfigDirectory(), 'config.toml');
27
+ }
28
+ async function pathExists(filePath) {
29
+ try {
30
+ await access(filePath, constants.F_OK);
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ function readTomlValue(rawConfig, key) {
38
+ const match = new RegExp(`^${key}\\s*=\\s*"(.*)"$`, 'm').exec(rawConfig);
39
+ return match?.[1];
40
+ }
41
+ function parseConfig(rawConfig) {
42
+ return {
43
+ modelProviders: {
44
+ deepseek: {
45
+ name: readTomlValue(rawConfig, 'name') ??
46
+ defaultConfig.modelProviders.deepseek.name,
47
+ baseUrl: readTomlValue(rawConfig, 'base_url') ??
48
+ defaultConfig.modelProviders.deepseek.baseUrl,
49
+ wireApi: readTomlValue(rawConfig, 'wire_api') ??
50
+ defaultConfig.modelProviders.deepseek.wireApi,
51
+ envKey: readTomlValue(rawConfig, 'env_key') ??
52
+ defaultConfig.modelProviders.deepseek.envKey,
53
+ },
54
+ },
55
+ };
56
+ }
57
+ export async function ensureConfigExists() {
58
+ const configDirectory = getConfigDirectory();
59
+ const configFilePath = getConfigFilePath();
60
+ if (await pathExists(configFilePath)) {
61
+ return configFilePath;
62
+ }
63
+ await mkdir(configDirectory, { recursive: true });
64
+ await writeFile(configFilePath, defaultConfigFileContent, { mode: 0o600 });
65
+ await chmod(configFilePath, 0o600);
66
+ return configFilePath;
67
+ }
68
+ export async function readConfig() {
69
+ const configFilePath = await ensureConfigExists();
70
+ try {
71
+ const rawConfig = await readFile(configFilePath, 'utf8');
72
+ return parseConfig(rawConfig);
73
+ }
74
+ catch (error) {
75
+ throw new Error(`Failed to read config file: ${configFilePath}`, {
76
+ cause: error,
77
+ });
78
+ }
79
+ }
80
+ export function resolveProviderApiKey(provider) {
81
+ return process.env[provider.envKey];
82
+ }
83
+ export async function readResolvedDeepseekProvider() {
84
+ const config = await readConfig();
85
+ const provider = config.modelProviders.deepseek;
86
+ return {
87
+ ...provider,
88
+ apiKey: resolveProviderApiKey(provider),
89
+ };
90
+ }
@@ -0,0 +1,6 @@
1
+ export declare const accountPasswordLoginMethod = "\u8D26\u53F7\u5BC6\u7801\u767B\u5F55";
2
+ export declare const loginMethods: readonly ["google", "github", "apple", "账号密码登录"];
3
+ export type LoginMethod = (typeof loginMethods)[number];
4
+ export declare function isLoginMethod(method: string): method is LoginMethod;
5
+ export declare function isAccountPasswordLoginMethod(method: string): method is typeof accountPasswordLoginMethod;
6
+ export declare function getProviderLoginUsername(method: LoginMethod): "google" | "github" | "apple" | undefined;
@@ -0,0 +1,17 @@
1
+ export const accountPasswordLoginMethod = '账号密码登录';
2
+ export const loginMethods = [
3
+ 'google',
4
+ 'github',
5
+ 'apple',
6
+ accountPasswordLoginMethod,
7
+ ];
8
+ const loginMethodSet = new Set(loginMethods);
9
+ export function isLoginMethod(method) {
10
+ return loginMethodSet.has(method);
11
+ }
12
+ export function isAccountPasswordLoginMethod(method) {
13
+ return method === accountPasswordLoginMethod;
14
+ }
15
+ export function getProviderLoginUsername(method) {
16
+ return isAccountPasswordLoginMethod(method) ? undefined : method;
17
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ type CheckboxProps = {
3
+ readonly items: readonly string[];
4
+ readonly defaultChecked?: readonly number[];
5
+ readonly isActive?: boolean;
6
+ readonly onToggle?: (item: string, index: number, checked: boolean) => void;
7
+ readonly onSubmit?: (checkedItems: string[]) => void;
8
+ readonly onBack?: () => void;
9
+ };
10
+ export default function Checkbox({ items, defaultChecked, isActive, onToggle, onSubmit, onBack, }: CheckboxProps): React.JSX.Element;
11
+ export {};
@@ -0,0 +1,47 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ export default function Checkbox({ items, defaultChecked = [], isActive = true, onToggle, onSubmit, onBack, }) {
4
+ const [cursor, setCursor] = useState(0);
5
+ const [checkedSet, setCheckedSet] = useState(new Set(defaultChecked));
6
+ useInput((_input, key) => {
7
+ if (key.upArrow) {
8
+ setCursor(index => Math.max(0, index - 1));
9
+ }
10
+ if (key.downArrow) {
11
+ setCursor(index => Math.min(items.length - 1, index + 1));
12
+ }
13
+ // 空格切换选中
14
+ if (!key.return && _input === ' ') {
15
+ const checked = !checkedSet.has(cursor);
16
+ setCheckedSet(previous => {
17
+ const next = new Set(previous);
18
+ if (checked)
19
+ next.add(cursor);
20
+ else
21
+ next.delete(cursor);
22
+ return next;
23
+ });
24
+ onToggle?.(items[cursor], cursor, checked);
25
+ }
26
+ // Esc 返回
27
+ if (key.escape) {
28
+ onBack?.();
29
+ }
30
+ // 回车提交
31
+ if (key.return) {
32
+ const checkedItems = items.filter((_, index) => checkedSet.has(index));
33
+ onSubmit?.(checkedItems);
34
+ }
35
+ }, { isActive });
36
+ return (React.createElement(Box, { flexDirection: "column" }, items.map((item, i) => {
37
+ const isCursor = i === cursor;
38
+ const isChecked = checkedSet.has(i);
39
+ // 选中状态用 ■(实心方块),未选中用 □(空心方块),样式与 Radio 完全对齐
40
+ return (React.createElement(Text, { key: item, color: isCursor ? 'yellow' : 'white' },
41
+ isCursor ? '❯' : ' ',
42
+ " ",
43
+ isChecked ? '◉' : '○',
44
+ " ",
45
+ item));
46
+ })));
47
+ }
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export default function Loading(): React.JSX.Element;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ export default function Loading() {
5
+ return (React.createElement(Box, null,
6
+ React.createElement(Text, null,
7
+ React.createElement(Spinner, null),
8
+ " Thinking...")));
9
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ type RadioProps = {
3
+ readonly items: readonly string[];
4
+ readonly defaultIndex?: number;
5
+ readonly isActive?: boolean;
6
+ readonly onSelect?: (item: string, index: number) => void;
7
+ };
8
+ export default function Radio({ items, defaultIndex, isActive, onSelect, }: RadioProps): React.JSX.Element;
9
+ export {};
@@ -0,0 +1,28 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ export default function Radio({ items, defaultIndex = 0, isActive = true, onSelect, }) {
4
+ const [index, setIndex] = useState(defaultIndex);
5
+ useInput((_input, key) => {
6
+ if (key.upArrow) {
7
+ setIndex(index => Math.max(0, index - 1));
8
+ }
9
+ if (key.downArrow) {
10
+ setIndex(index => Math.min(items.length - 1, index + 1));
11
+ }
12
+ if (key.return) {
13
+ const selectedItem = items[index];
14
+ if (selectedItem) {
15
+ onSelect?.(selectedItem, index);
16
+ }
17
+ }
18
+ }, { isActive });
19
+ return (React.createElement(Box, { flexDirection: "column" }, items.map((item, i) => {
20
+ const isSelected = i === index;
21
+ return (React.createElement(Text, { key: item, ...(isSelected ? { color: 'yellow' } : {}) },
22
+ isSelected ? '❯' : ' ',
23
+ " ",
24
+ isSelected ? '◉' : '○',
25
+ " ",
26
+ item));
27
+ })));
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { chmod, rm } from 'node:fs/promises';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ const execFileAsync = promisify(execFile);
5
+ await rm('dist', { force: true, recursive: true });
6
+ await execFileAsync('tsc');
7
+ await chmod('dist/cli.js', 0o755);
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@booncli/booncli",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "bin": {
6
+ "booncli": "dist/cli.js"
7
+ },
8
+ "type": "module",
9
+ "engines": {
10
+ "node": ">=16"
11
+ },
12
+ "scripts": {
13
+ "build": "TS_NODE_TRANSPILE_ONLY=true node --no-warnings --loader=ts-node/esm source/scripts/build.ts",
14
+ "dev": "TS_NODE_TRANSPILE_ONLY=true node --no-warnings --loader=ts-node/esm source/cli.tsx",
15
+ "watch": "tsc --watch",
16
+ "test": "prettier --check . && xo && TS_NODE_TRANSPILE_ONLY=true ava"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "dependencies": {
22
+ "ink": "^4.1.0",
23
+ "ink-big-text": "^2.0.0",
24
+ "ink-spinner": "^5.0.0",
25
+ "ink-text-input": "^5.0.1",
26
+ "pastel": "^2.0.0",
27
+ "react": "^18.2.0",
28
+ "zod": "^3.21.4"
29
+ },
30
+ "devDependencies": {
31
+ "@sindresorhus/tsconfig": "^3.0.1",
32
+ "@types/react": "^18.0.32",
33
+ "@vdemedes/prettier-config": "^2.0.1",
34
+ "ava": "^5.2.0",
35
+ "chalk": "^5.2.0",
36
+ "eslint-config-xo-react": "^0.27.0",
37
+ "eslint-plugin-react": "^7.32.2",
38
+ "eslint-plugin-react-hooks": "^4.6.0",
39
+ "ink-testing-library": "^3.0.0",
40
+ "prettier": "^2.8.7",
41
+ "ts-node": "^10.9.1",
42
+ "typescript": "^5.0.3",
43
+ "xo": "^0.54.2"
44
+ },
45
+ "ava": {
46
+ "extensions": {
47
+ "ts": "module",
48
+ "tsx": "module"
49
+ },
50
+ "nodeArguments": [
51
+ "--loader=ts-node/esm"
52
+ ]
53
+ },
54
+ "xo": {
55
+ "extends": "xo-react",
56
+ "prettier": true,
57
+ "rules": {
58
+ "react/prop-types": "off"
59
+ }
60
+ },
61
+ "prettier": "@vdemedes/prettier-config"
62
+ }
package/readme.md ADDED
@@ -0,0 +1,70 @@
1
+ # booncli
2
+
3
+ > This readme is automatically generated by [create-pastel-app](https://github.com/vadimdemedes/create-pastel-app)
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ $ npm install --global booncli
9
+ ```
10
+
11
+ ## CLI
12
+
13
+ ```
14
+ $ booncli --help
15
+
16
+ Usage
17
+ $ booncli
18
+
19
+ Options
20
+ --name Your name
21
+
22
+ Commands
23
+ clear
24
+ log
25
+ login
26
+ loginout
27
+
28
+ Examples
29
+ $ booncli --name=Jane
30
+ Hello, Jane
31
+
32
+ $ booncli login
33
+ Login
34
+
35
+ $ booncli loginout
36
+ Loginout
37
+
38
+ $ booncli log
39
+ Log
40
+
41
+ $ booncli clear
42
+ Cleared
43
+
44
+ $ booncli
45
+ Opens an interactive command dialog with /login, /loginout, /log and /clear suggestions.
46
+ ```
47
+
48
+ 单选框组件: /Users/zhuxwz/webSpace/space/CLI/booncli/source/modules/radio.tsx
49
+ 复选框组件:/Users/zhuxwz/webSpace/space/CLI/booncli/source/modules/checkbox.tsx
50
+ Loading 组件:/Users/zhuxwz/webSpace/space/CLI/booncli/source/modules/loading.tsx
51
+
52
+ ## Local Config
53
+
54
+ 首次运行 CLI 时会自动创建本地配置文件:
55
+
56
+ ```bash
57
+ ~/.booncli/config.toml
58
+ ```
59
+
60
+ 默认内容:
61
+
62
+ ```toml
63
+ [model_providers.deepseek]
64
+ name = "DeepSeek"
65
+ base_url = "https://api.deepseek.com/v1"
66
+ wire_api = "responses"
67
+ env_key = "DEEPSEEK_API_KEY"
68
+ ```
69
+
70
+ 配置文件权限会设置为仅当前用户可读写。配置文件只保存环境变量名称,真实 API key 从 `DEEPSEEK_API_KEY` 环境变量读取。