Compare commits

...

2 Commits

Author SHA1 Message Date
ONLY-yours d2f60c078c feat(cli): add binary release workflow and curl install script
- Add .github/workflows/release-cli.yml: builds standalone binaries via
  bun build --compile on ubuntu/macos runners and uploads to GitHub Release
- Add apps/cli/install.sh: POSIX-compatible curl installer that detects
  OS/arch, installs to /usr/local/bin (or ~/.local/bin fallback), and
  creates lobe + lobehub symlinks pointing to lh
2026-06-09 14:43:02 +08:00
ONLY-yours 96d19fe403 🐛 fix(skill): consolidate add-skill button into header dropdown
Move the standalone 'AddSkillButton' from SkillList sidebar into the
header '+' dropdown, providing a unified entry point for all add-skill
actions (import from URL/GitHub, upload zip, custom connector).
Replace legacy 'Add Custom MCP' with the new Connector flow.
2026-06-09 14:28:52 +08:00
4 changed files with 199 additions and 20 deletions
+75
View File
@@ -0,0 +1,75 @@
name: Release CLI
permissions:
contents: write
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
tag:
description: 'Tag name for the release (e.g. v0.1.0)'
required: true
default: 'v0.0.0'
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
# skip pre-release tags (containing '-') on auto-trigger; always run on workflow_dispatch
if: ${{ github.event_name == 'workflow_dispatch' || !contains(github.ref_name, '-') }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: lobe-linux-x64
- os: macos-latest
target: lobe-macos-arm64
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build binary
run: |
mkdir -p dist
bun build ./apps/cli/src/index.ts --compile --minify --outfile ./dist/${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: ./dist/${{ matrix.target }}
release:
name: Upload to Release
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: ./dist
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
files: |
./dist/lobe-linux-x64/lobe-linux-x64
./dist/lobe-macos-arm64/lobe-macos-arm64
./apps/cli/install.sh
+78
View File
@@ -0,0 +1,78 @@
#!/bin/sh
set -e
REPO="lobehub/lobe-chat"
BIN_NAME="lh"
# Detect OS
case "$(uname -s)" in
Linux) OS="linux" ;;
Darwin) OS="macos" ;;
*)
printf 'Error: Unsupported OS: %s\n' "$(uname -s)" >&2
exit 1
;;
esac
# Detect architecture
case "$(uname -m)" in
x86_64) ARCH="x64" ;;
aarch64|arm64) ARCH="arm64" ;;
*)
printf 'Error: Unsupported architecture: %s\n' "$(uname -m)" >&2
exit 1
;;
esac
BINARY="lobe-${OS}-${ARCH}"
URL="https://github.com/${REPO}/releases/latest/download/${BINARY}"
printf 'Detected: %s/%s\n' "$OS" "$ARCH"
printf 'Downloading %s...\n' "$BINARY"
TMP="$(mktemp)"
trap 'rm -f "$TMP"' EXIT
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$URL" -o "$TMP"
elif command -v wget >/dev/null 2>&1; then
wget -qO "$TMP" "$URL"
else
printf 'Error: curl or wget is required\n' >&2
exit 1
fi
chmod +x "$TMP"
# Choose install directory: prefer /usr/local/bin, fall back to ~/.local/bin
USE_SUDO=0
if [ -w "/usr/local/bin" ]; then
INSTALL_DIR="/usr/local/bin"
elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
INSTALL_DIR="/usr/local/bin"
USE_SUDO=1
else
INSTALL_DIR="${HOME}/.local/bin"
mkdir -p "$INSTALL_DIR"
printf 'Note: No sudo access. Installing to %s\n' "$INSTALL_DIR"
printf 'Add the following to your shell profile if needed:\n'
printf ' export PATH="%s:$PATH"\n' "$INSTALL_DIR"
fi
# Install binary and create symlinks
if [ "$USE_SUDO" = "1" ]; then
sudo cp "$TMP" "${INSTALL_DIR}/${BIN_NAME}"
sudo chmod +x "${INSTALL_DIR}/${BIN_NAME}"
sudo ln -sf "${INSTALL_DIR}/${BIN_NAME}" "${INSTALL_DIR}/lobe"
sudo ln -sf "${INSTALL_DIR}/${BIN_NAME}" "${INSTALL_DIR}/lobehub"
else
cp "$TMP" "${INSTALL_DIR}/${BIN_NAME}"
chmod +x "${INSTALL_DIR}/${BIN_NAME}"
ln -sf "${INSTALL_DIR}/${BIN_NAME}" "${INSTALL_DIR}/lobe"
ln -sf "${INSTALL_DIR}/${BIN_NAME}" "${INSTALL_DIR}/lobehub"
fi
printf '\nInstalled successfully!\n'
printf ' Binary: %s/%s\n' "$INSTALL_DIR" "$BIN_NAME"
printf ' Symlinks: lobe, lobehub -> lh\n\n'
"${INSTALL_DIR}/${BIN_NAME}" --version
@@ -20,7 +20,6 @@ import type React from 'react';
import { memo, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import AddSkillButton from '@/features/SkillStore/SkillList/AddSkillButton';
import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
import { useToolStore } from '@/store/tool';
@@ -333,7 +332,6 @@ const SkillList = memo<SkillListProps>(
return (
<Center className={styles.container} paddingBlock={48}>
<Empty description={t('tab.skillDesc')} icon={SkillsIcon} title={t('tab.skillEmpty')} />
<AddSkillButton />
</Center>
);
}
@@ -561,9 +559,6 @@ const SkillList = memo<SkillListProps>(
renderUserAgentSkills(),
)}
<div style={{ marginTop: 8 }}>
<AddSkillButton />
</div>
</div>
);
},
+46 -15
View File
@@ -1,13 +1,17 @@
'use client';
import { Button, Icon } from '@lobehub/ui';
import { Button, DropdownMenu, Flexbox, Icon, Text } from '@lobehub/ui';
import { GithubIcon } from '@lobehub/ui/icons';
import { createStaticStyles } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { Plus, Store } from 'lucide-react';
import { ChevronDown, FileArchive, Grid2x2Plus, Link, Store } from 'lucide-react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AddConnectorModal } from '@/features/Connectors';
import ImportFromGithubModal from '@/features/SkillStore/SkillList/ImportFromGithubModal';
import ImportFromUrlModal from '@/features/SkillStore/SkillList/ImportFromUrlModal';
import UploadSkillModal from '@/features/SkillStore/SkillList/UploadSkillModal';
import NavHeader from '@/features/NavHeader';
import { createSkillStoreModal } from '@/features/SkillStore';
import { useToolStore } from '@/store/tool';
@@ -85,6 +89,9 @@ const Page = memo(() => {
const [selected, setSelected] = useState<SelectedTool | null>(null);
const [viewMode, setViewMode] = useState<SkillViewMode>('connector');
const [showAddConnector, setShowAddConnector] = useState(false);
const [showUrlModal, setUrlModal] = useState(false);
const [showGithubModal, setGithubModal] = useState(false);
const [showUploadModal, setUploadModal] = useState(false);
// Data sources for auto-select
const builtinTools = useToolStore((s) => s.builtinTools, isEqual);
@@ -93,7 +100,6 @@ const Page = memo(() => {
(s) => builtinToolSelectors.installedAllMetaList(s).map((tool) => tool.identifier),
isEqual,
);
// Auto-select first item when view changes or on load
useEffect(() => {
setSelected(null);
@@ -147,18 +153,40 @@ const Page = memo(() => {
</span>
</div>
<div style={{ display: 'flex', gap: 6 }}>
{viewMode === 'connector' && (
<Button
icon={<Icon icon={Plus} />}
size="small"
title={t('connector.add.title', {
defaultValue: 'Add custom connector',
ns: 'tool',
})}
onClick={() => setShowAddConnector(true)}
/>
)}
<div style={{ display: 'flex', gap: 6 }} onClick={(e) => e.stopPropagation()}>
<DropdownMenu
nativeButton={false}
placement="bottomRight"
items={[
{
icon: <Icon icon={Link} />,
key: 'importUrl',
label: <Flexbox gap={2}><span>{t('tab.importFromUrl')}</span><Text style={{ fontSize: 12 }} type="secondary">{t('tab.importFromUrl.desc')}</Text></Flexbox>,
onClick: () => setUrlModal(true),
},
{
icon: <Icon icon={GithubIcon} />,
key: 'importGithub',
label: <Flexbox gap={2}><span>{t('tab.importFromGithub')}</span><Text style={{ fontSize: 12 }} type="secondary">{t('tab.importFromGithub.desc')}</Text></Flexbox>,
onClick: () => setGithubModal(true),
},
{
icon: <Icon icon={FileArchive} />,
key: 'uploadZip',
label: <Flexbox gap={2}><span>{t('tab.uploadZip')}</span><Text style={{ fontSize: 12 }} type="secondary">{t('tab.uploadZip.desc')}</Text></Flexbox>,
onClick: () => setUploadModal(true),
},
{ type: 'divider' as const },
{
icon: <Icon icon={Grid2x2Plus} />,
key: 'addConnector',
label: <Flexbox gap={2}><span>{t('connector.add.title', { defaultValue: 'Add Custom Connector', ns: 'tool' })}</span></Flexbox>,
onClick: () => setShowAddConnector(true),
},
]}
>
<Button icon={Grid2x2Plus} size="small" />
</DropdownMenu>
<Button icon={<Icon icon={Store} />} size="small" onClick={handleOpenStore} />
</div>
</div>
@@ -179,6 +207,9 @@ const Page = memo(() => {
</div>
)}
</div>
<ImportFromUrlModal open={showUrlModal} onOpenChange={setUrlModal} />
<ImportFromGithubModal open={showGithubModal} onOpenChange={setGithubModal} />
<UploadSkillModal open={showUploadModal} onOpenChange={setUploadModal} />
<AddConnectorModal open={showAddConnector} onClose={() => setShowAddConnector(false)} />
</>
);