Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bolt cli to work as standalone npm package #893

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 38 additions & 37 deletions app/components/settings/data/DataTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { toast } from 'react-toastify';
import { db, deleteById, getAll } from '~/lib/persistence';
import { logStore } from '~/lib/stores/logs';
import { classNames } from '~/utils/classNames';
import styles from '~/components/settings/Settings.module.scss';

// List of supported providers that can have API keys
const API_KEY_PROVIDERS = [
Expand All @@ -25,8 +24,6 @@ const API_KEY_PROVIDERS = [
'AzureOpenAI',
] as const;

type Provider = typeof API_KEY_PROVIDERS[number];

interface ApiKeys {
[key: string]: string;
}
Expand All @@ -52,6 +49,7 @@ export default function DataTab() {
const error = new Error('Database is not available');
logStore.logError('Failed to export chats - DB unavailable', error);
toast.error('Database is not available');

return;
}

Expand Down Expand Up @@ -83,11 +81,13 @@ export default function DataTab() {
const error = new Error('Database is not available');
logStore.logError('Failed to delete chats - DB unavailable', error);
toast.error('Database is not available');

return;
}

try {
setIsDeleting(true);

const allChats = await getAll(db);
await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
logStore.logSystem('All chats deleted successfully', { count: allChats.length });
Expand Down Expand Up @@ -125,16 +125,22 @@ export default function DataTab() {

const handleImportSettings = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;

if (!file) {
return;
}

const reader = new FileReader();

reader.onload = (e) => {
try {
const settings = JSON.parse(e.target?.result as string);

Object.entries(settings).forEach(([key, value]) => {
if (key === 'bolt_theme') {
if (value) localStorage.setItem(key, value as string);
if (value) {
localStorage.setItem(key, value as string);
}
} else if (value) {
Cookies.set(key, value as string);
}
Expand All @@ -152,32 +158,37 @@ export default function DataTab() {

const handleExportApiKeyTemplate = () => {
const template: ApiKeys = {};
API_KEY_PROVIDERS.forEach(provider => {
API_KEY_PROVIDERS.forEach((provider) => {
template[`${provider}_API_KEY`] = '';
});

template['OPENAI_LIKE_API_BASE_URL'] = '';
template['LMSTUDIO_API_BASE_URL'] = '';
template['OLLAMA_API_BASE_URL'] = '';
template['TOGETHER_API_BASE_URL'] = '';
template.OPENAI_LIKE_API_BASE_URL = '';
template.LMSTUDIO_API_BASE_URL = '';
template.OLLAMA_API_BASE_URL = '';
template.TOGETHER_API_BASE_URL = '';

downloadAsJson(template, 'api-keys-template.json');
toast.success('API keys template exported successfully');
};

const handleImportApiKeys = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;

if (!file) {
return;
}

const reader = new FileReader();

reader.onload = (e) => {
try {
const apiKeys = JSON.parse(e.target?.result as string);
let importedCount = 0;
const consolidatedKeys: Record<string, string> = {};

API_KEY_PROVIDERS.forEach(provider => {
API_KEY_PROVIDERS.forEach((provider) => {
const keyName = `${provider}_API_KEY`;

if (apiKeys[keyName]) {
consolidatedKeys[provider] = apiKeys[keyName];
importedCount++;
Expand All @@ -187,13 +198,14 @@ export default function DataTab() {
if (importedCount > 0) {
// Store all API keys in a single cookie as JSON
Cookies.set('apiKeys', JSON.stringify(consolidatedKeys));

// Also set individual cookies for backward compatibility
Object.entries(consolidatedKeys).forEach(([provider, key]) => {
Cookies.set(`${provider}_API_KEY`, key);
});

toast.success(`Successfully imported ${importedCount} API keys/URLs. Refreshing page to apply changes...`);

// Reload the page after a short delay to allow the toast to be seen
setTimeout(() => {
window.location.reload();
Expand All @@ -203,12 +215,13 @@ export default function DataTab() {
}

// Set base URLs if they exist
['OPENAI_LIKE_API_BASE_URL', 'LMSTUDIO_API_BASE_URL', 'OLLAMA_API_BASE_URL', 'TOGETHER_API_BASE_URL'].forEach(baseUrl => {
if (apiKeys[baseUrl]) {
Cookies.set(baseUrl, apiKeys[baseUrl]);
}
});

['OPENAI_LIKE_API_BASE_URL', 'LMSTUDIO_API_BASE_URL', 'OLLAMA_API_BASE_URL', 'TOGETHER_API_BASE_URL'].forEach(
(baseUrl) => {
if (apiKeys[baseUrl]) {
Cookies.set(baseUrl, apiKeys[baseUrl]);
}
},
);
} catch (error) {
toast.error('Failed to import API keys. Make sure the file is a valid JSON file.');
console.error('Failed to import API keys:', error);
Expand All @@ -226,9 +239,7 @@ export default function DataTab() {
<div className="flex flex-col gap-4">
<div>
<h4 className="text-bolt-elements-textPrimary mb-2">Chat History</h4>
<p className="text-sm text-bolt-elements-textSecondary mb-4">
Export or delete all your chat history.
</p>
<p className="text-sm text-bolt-elements-textSecondary mb-4">Export or delete all your chat history.</p>
<div className="flex gap-4">
<button
onClick={handleExportAllChats}
Expand All @@ -241,7 +252,7 @@ export default function DataTab() {
disabled={isDeleting}
className={classNames(
'px-4 py-2 bg-bolt-elements-button-danger-background hover:bg-bolt-elements-button-danger-backgroundHover text-bolt-elements-button-danger-text rounded-lg transition-colors',
isDeleting ? 'opacity-50 cursor-not-allowed' : ''
isDeleting ? 'opacity-50 cursor-not-allowed' : '',
)}
>
{isDeleting ? 'Deleting...' : 'Delete All Chats'}
Expand All @@ -263,12 +274,7 @@ export default function DataTab() {
</button>
<label className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors cursor-pointer">
Import Settings
<input
type="file"
accept=".json"
onChange={handleImportSettings}
className="hidden"
/>
<input type="file" accept=".json" onChange={handleImportSettings} className="hidden" />
</label>
</div>
</div>
Expand All @@ -287,12 +293,7 @@ export default function DataTab() {
</button>
<label className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors cursor-pointer">
Import API Keys
<input
type="file"
accept=".json"
onChange={handleImportApiKeys}
className="hidden"
/>
<input type="file" accept=".json" onChange={handleImportApiKeys} className="hidden" />
</label>
</div>
</div>
Expand All @@ -301,4 +302,4 @@ export default function DataTab() {
</div>
</div>
);
}
}
176 changes: 176 additions & 0 deletions bin/bolt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#!/usr/bin/env node

// bin/bolt.js
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Find the root directory where the package is installed
const rootDir = join(__dirname, '..');

// Parse command line arguments
const args = process.argv.slice(2);
const options = {
port: 5173, // default port
help: false,
version: false,
};

// Parse arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '--help':
case '-h':
options.help = true;
break;
case '--version':
case '-v':
options.version = true;
break;
case '--port':
case '-p':
const port = parseInt(args[i + 1]);
if (isNaN(port)) {
console.error('Error: Port must be a number');
process.exit(1);
}
options.port = port;
i++; // Skip the next argument since it's the port number
break;
}
}

async function getVersion() {
try {
const fs = await import('fs/promises');
const packageJson = JSON.parse(await fs.readFile(join(rootDir, 'package.json'), 'utf8'));
return packageJson.version;
} catch (e) {
console.error('Error reading package.json:', e);
return 'undefined';
}
}

function showHelp() {
console.log(`
★═══════════════════════════════════════★
B O L T . D I Y
Usage Instructions
★═══════════════════════════════════════★

Options:
-h, --help Show this help message
-v, --version Show the current version
-p, --port Specify a custom port (default: 5173)

Examples:
bolt Start with default settings
bolt --port 3000 Start on port 3000
bolt --help Show this help message
bolt --version Show version information

For more information, visit: https://github.com/stackblitz-labs/bolt.diy
`);
}

async function showVersion() {
const version = await getVersion();
let commitHash;
try {
commitHash = execSync('git rev-parse --short HEAD', { stdio: ['pipe', 'pipe', 'ignore'] })
.toString()
.trim();
} catch (e) {
commitHash = undefined;
}

console.log(`
★═══════════════════════════════════════★
B O L T . D I Y
★═══════════════════════════════════════★
Version: ${version}
${commitHash?`Commit: ${commitHash}\n`:''}`);
}

async function displayBanner() {
const version = await getVersion();
let commitHash;
try {
commitHash = execSync('git rev-parse --short HEAD', { stdio: ['pipe', 'pipe', 'ignore'] })
.toString()
.trim();
} catch (e) {
commitHash = 'unknown';
}

console.log('★═══════════════════════════════════════★');
console.log(' B O L T . D I Y');
console.log(' ⚡️ Welcome ⚡️');
console.log('★═══════════════════════════════════════★');
console.log(`📍 Current Version Tag: v${version}`);
// console.log(`📍 Current Commit Version: "${commitHash}"`);
console.log(`📍 Starting on port: ${options.port}`);
console.log(' Please wait until the URL appears here');
console.log('★═══════════════════════════════════════★');
}


async function startApp() {
if (options.help) {
showHelp();
process.exit(0);
}

if (options.version) {
await showVersion();
process.exit(0);
}

await displayBanner();

try {
// Use the local remix CLI from node_modules
const remixBinPath = join(rootDir, 'node_modules', '.bin', 'remix');

// Then start the development server using the local remix binary
const devProcess = spawn(remixBinPath, ['vite:dev', '--port', options.port.toString()], {
cwd: rootDir,
stdio: 'inherit',
shell: true,
env: {
...process.env,
PORT: options.port.toString(),
PATH: `${join(rootDir, 'node_modules', '.bin')}:${process.env.PATH}`,
},
});

devProcess.on('error', (err) => {
console.error('Failed to start development server:', err);
if (err.code === 'ENOENT') {
console.error('\nError: Required dependencies not found. Please ensure you have run:');
console.error('npm install\n');
}
process.exit(1);
});

// Handle interruption signals
process.on('SIGINT', () => {
devProcess.kill('SIGINT');
process.exit(0);
});

process.on('SIGTERM', () => {
devProcess.kill('SIGTERM');
process.exit(0);
});
} catch (error) {
console.error('Error starting the application:', error);
process.exit(1);
}
}

startApp();
Loading
Loading