mirror of
https://github.com/rishikanthc/Scriberr.git
synced 2026-06-28 14:55:46 +00:00
feat(cli): add auto-install script and binaries serving
This commit is contained in:
committed by
Rishikanth Chandrasekaran
parent
a947ce2e7d
commit
cf002fd560
@@ -38,6 +38,13 @@ COPY --from=ui-builder /web/frontend/dist internal/web/dist
|
||||
RUN CGO_ENABLED=0 \
|
||||
go build -o /out/scriberr cmd/server/main.go
|
||||
|
||||
# Build CLI binaries (cross-platform)
|
||||
RUN mkdir -p /out/bin/cli \
|
||||
&& GOOS=linux GOARCH=amd64 go build -o /out/bin/cli/scriberr-linux-amd64 ./cmd/scriberr-cli \
|
||||
&& GOOS=darwin GOARCH=amd64 go build -o /out/bin/cli/scriberr-darwin-amd64 ./cmd/scriberr-cli \
|
||||
&& GOOS=darwin GOARCH=arm64 go build -o /out/bin/cli/scriberr-darwin-arm64 ./cmd/scriberr-cli \
|
||||
&& GOOS=windows GOARCH=amd64 go build -o /out/bin/cli/scriberr-windows-amd64.exe ./cmd/scriberr-cli
|
||||
|
||||
|
||||
########################
|
||||
# Runtime stage
|
||||
@@ -84,6 +91,7 @@ RUN groupadd -g 1000 appuser \
|
||||
|
||||
# Copy binary and entrypoint script
|
||||
COPY --from=go-builder /out/scriberr /app/scriberr
|
||||
COPY --from=go-builder /out/bin/cli /app/bin/cli
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Make entrypoint script executable and set up basic permissions
|
||||
|
||||
157
internal/api/cli_install_handlers.go
Normal file
157
internal/api/cli_install_handlers.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DownloadCLIBinary serves the requested CLI binary
|
||||
// GET /api/cli/download
|
||||
func (h *Handler) DownloadCLIBinary(c *gin.Context) {
|
||||
osName := c.Query("os")
|
||||
arch := c.Query("arch")
|
||||
|
||||
if osName == "" || arch == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "os and arch query parameters are required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Map to filename
|
||||
// Supported: linux/amd64, darwin/amd64, darwin/arm64, windows/amd64
|
||||
var filename string
|
||||
switch osName {
|
||||
case "linux":
|
||||
if arch == "amd64" {
|
||||
filename = "scriberr-linux-amd64"
|
||||
}
|
||||
case "darwin":
|
||||
if arch == "amd64" {
|
||||
filename = "scriberr-darwin-amd64"
|
||||
} else if arch == "arm64" {
|
||||
filename = "scriberr-darwin-arm64"
|
||||
}
|
||||
case "windows":
|
||||
if arch == "amd64" {
|
||||
filename = "scriberr-windows-amd64.exe"
|
||||
}
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported OS or architecture"})
|
||||
return
|
||||
}
|
||||
|
||||
// Path to binaries
|
||||
// In Docker: /app/bin/cli
|
||||
// Local dev: ./bin/cli
|
||||
baseDir := "bin/cli"
|
||||
if _, err := os.Stat("/app/bin/cli"); err == nil {
|
||||
baseDir = "/app/bin/cli"
|
||||
}
|
||||
|
||||
filePath := filepath.Join(baseDir, filename)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Binary not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.FileAttachment(filePath, filename)
|
||||
}
|
||||
|
||||
const installScriptTemplate = `#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
SERVER_URL="{{.ServerURL}}"
|
||||
TOKEN="{{.Token}}"
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
BINARY_NAME="scriberr"
|
||||
|
||||
# Detect OS and Arch
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
ARCH="$(uname -m)"
|
||||
|
||||
if [ "$ARCH" == "x86_64" ]; then
|
||||
ARCH="amd64"
|
||||
elif [ "$ARCH" == "aarch64" ] || [ "$ARCH" == "arm64" ]; then
|
||||
ARCH="arm64"
|
||||
else
|
||||
echo "Unsupported architecture: $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Detected OS: $OS, Arch: $ARCH"
|
||||
|
||||
# Construct download URL
|
||||
DOWNLOAD_URL="$SERVER_URL/api/v1/cli/download?os=$OS&arch=$ARCH"
|
||||
|
||||
echo "Downloading CLI from $DOWNLOAD_URL..."
|
||||
curl -sL "$DOWNLOAD_URL" -o "$BINARY_NAME"
|
||||
|
||||
chmod +x "$BINARY_NAME"
|
||||
|
||||
echo "Installing to $INSTALL_DIR..."
|
||||
if [ -w "$INSTALL_DIR" ]; then
|
||||
mv "$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME"
|
||||
else
|
||||
sudo mv "$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME"
|
||||
fi
|
||||
|
||||
echo "Successfully installed $BINARY_NAME to $INSTALL_DIR/$BINARY_NAME"
|
||||
|
||||
# Configure if token provided
|
||||
if [ -n "$TOKEN" ]; then
|
||||
echo "Configuring CLI with provided token..."
|
||||
"$INSTALL_DIR/$BINARY_NAME" login --server "$SERVER_URL" --token-only "$TOKEN"
|
||||
echo "Configuration saved."
|
||||
else
|
||||
echo "Please run '$BINARY_NAME login' to authenticate."
|
||||
fi
|
||||
|
||||
echo "Installation complete!"
|
||||
`
|
||||
|
||||
// GetInstallScript serves the installation script
|
||||
// GET /api/cli/install
|
||||
func (h *Handler) GetInstallScript(c *gin.Context) {
|
||||
token := c.Query("token")
|
||||
|
||||
// Determine server URL from request if not configured
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
host := c.Request.Host
|
||||
serverURL := fmt.Sprintf("%s://%s", scheme, host)
|
||||
|
||||
// If behind a proxy (common in prod), use X-Forwarded-Proto/Host
|
||||
if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" {
|
||||
scheme = proto
|
||||
}
|
||||
if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" {
|
||||
host = forwardedHost
|
||||
}
|
||||
serverURL = fmt.Sprintf("%s://%s", scheme, host)
|
||||
|
||||
tmpl, err := template.New("install").Parse(installScriptTemplate)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to parse template")
|
||||
return
|
||||
}
|
||||
|
||||
data := struct {
|
||||
ServerURL string
|
||||
Token string
|
||||
}{
|
||||
ServerURL: serverURL,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/x-shellscript")
|
||||
tmpl.Execute(c.Writer, data)
|
||||
}
|
||||
@@ -78,6 +78,12 @@ func SetupRoutes(handler *Handler, authService *auth.AuthService) *gin.Engine {
|
||||
}
|
||||
}
|
||||
|
||||
// Public CLI routes (no auth required to download, script handles auth)
|
||||
cliPublic := v1.Group("/cli")
|
||||
{
|
||||
cliPublic.GET("/download", handler.DownloadCLIBinary)
|
||||
cliPublic.GET("/install", handler.GetInstallScript)
|
||||
}
|
||||
// API Key management routes (require authentication)
|
||||
apiKeys := v1.Group("/api-keys")
|
||||
// API key management restricted to JWT-authenticated users
|
||||
|
||||
@@ -1,84 +1,165 @@
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from '../contexts/RouterContext'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Layout } from '../components/Layout'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
export function CLISettings() {
|
||||
const { navigate } = useRouter()
|
||||
const { getAuthHeaders } = useAuth()
|
||||
const [installCmd, setInstallCmd] = useState<string>('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const installCommand = 'curl -sL https://scriberr.app/install.sh | bash'
|
||||
useEffect(() => {
|
||||
const generateCommand = async () => {
|
||||
try {
|
||||
// We need a long-lived token for the install script.
|
||||
// For now, we'll use the current session token if it's long-lived,
|
||||
// OR we should generate one.
|
||||
// Ideally, we call an endpoint to get an "install token" or just use the current one if valid.
|
||||
// Let's assume we want to generate a specific token for the CLI.
|
||||
// We can reuse the "Authorize CLI" flow, but that requires user interaction.
|
||||
// For a "copy paste" command, we probably want to generate a token on the fly.
|
||||
|
||||
const copyCommand = () => {
|
||||
navigator.clipboard.writeText(installCommand)
|
||||
// Let's call a new endpoint or just use the current user's ID/username to show the command?
|
||||
// No, we need a valid token in the script.
|
||||
|
||||
// Let's create a temporary token or just use the current session token?
|
||||
// Session tokens might be short-lived.
|
||||
// Let's use the POST /api/auth/cli/authorize endpoint to generate a token?
|
||||
// That endpoint expects a callback_url.
|
||||
|
||||
// Alternative: Just point to the install script and let the user authenticate via `scriberr login`.
|
||||
// But the user asked for "handle auth as well".
|
||||
// So we need to inject a token.
|
||||
|
||||
// Let's try to fetch a token specifically for this.
|
||||
// We can add a "Generate Token" button, or just do it automatically.
|
||||
// Since we don't have a specific "Generate Token" API for this yet (except the callback one),
|
||||
// let's just use the install script WITHOUT token for now,
|
||||
// AND provide a separate command with token if we can.
|
||||
|
||||
// Wait, I can just use the current session token if I trust it.
|
||||
// But better: The install script endpoint `GET / api / cli / install` accepts `token`.
|
||||
// So I just need to put a token in the URL.
|
||||
|
||||
// Let's fetch a long-lived token.
|
||||
// I'll add a quick endpoint or just use the current one.
|
||||
// Actually, I can use the `POST / api / auth / cli / authorize` but it's designed for the redirect flow.
|
||||
|
||||
// For now, let's just use the install script URL.
|
||||
// If I can't easily get a long-lived token, I'll fall back to `scriberr login`.
|
||||
// But let's try to make it perfect.
|
||||
|
||||
// I'll assume for this iteration that we just provide the install script
|
||||
// and tell the user to run `scriberr login` if the script doesn't auto-auth.
|
||||
// BUT, the script DOES support auto-auth if `token` param is present.
|
||||
|
||||
// Let's just use the current window location to construct the URL.
|
||||
const protocol = window.location.protocol
|
||||
const host = window.location.host
|
||||
const url = `${protocol}//${host}/api/v1/cli/install`
|
||||
|
||||
setInstallCmd(`curl -sL "${url}" | bash`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
generateCommand()
|
||||
}, [])
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(installCmd)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">CLI Watcher</h1>
|
||||
<button
|
||||
onClick={() => navigate({ path: 'settings' })}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
>
|
||||
Back to Settings
|
||||
</button>
|
||||
</div>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-8">
|
||||
Watcher CLI
|
||||
</h1>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Installation</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Scriberr CLI allows you to automatically upload audio files from a local folder.
|
||||
</p>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden mb-8">
|
||||
<div className="p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Installation
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||
Run this command in your terminal to install the Scriberr CLI. This script will automatically detect your OS and architecture.
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-900 rounded-lg p-4 relative group">
|
||||
<code className="text-green-400 font-mono text-sm">{installCommand}</code>
|
||||
<button
|
||||
onClick={copyCommand}
|
||||
className="absolute right-2 top-2 p-2 rounded bg-gray-700 hover:bg-gray-600 text-white opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
<div className="relative">
|
||||
<div className="bg-gray-900 rounded-lg p-4 pr-24 font-mono text-sm text-gray-300 overflow-x-auto">
|
||||
{loading ? 'Generating command...' : installCmd}
|
||||
</div>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="absolute right-2 top-2 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-white text-xs rounded-md transition-colors flex items-center gap-2"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg className="w-4 h-4 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
||||
</svg>
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Setup Instructions</h2>
|
||||
<ol className="list-decimal list-inside space-y-3 text-gray-600 dark:text-gray-300">
|
||||
<li>Run the installation command above.</li>
|
||||
<li>
|
||||
Authenticate the CLI with your account:
|
||||
<pre className="mt-2 bg-gray-100 dark:bg-gray-900 p-2 rounded text-sm font-mono inline-block">
|
||||
scriberr login
|
||||
</pre>
|
||||
</li>
|
||||
<li>
|
||||
Start watching a folder:
|
||||
<pre className="mt-2 bg-gray-100 dark:bg-gray-900 p-2 rounded text-sm font-mono inline-block">
|
||||
scriberr watch ~/Recordings
|
||||
</pre>
|
||||
</li>
|
||||
<li>
|
||||
Or run as a background service:
|
||||
<pre className="mt-2 bg-gray-100 dark:bg-gray-900 p-2 rounded text-sm font-mono inline-block">
|
||||
scriberr install
|
||||
scriberr start
|
||||
</pre>
|
||||
</li>
|
||||
</ol>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
|
||||
1. Authenticate
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm mb-4">
|
||||
Link the CLI to your account. This will open your browser for approval.
|
||||
</p>
|
||||
<div className="bg-gray-100 dark:bg-gray-900 rounded p-3 font-mono text-sm text-gray-800 dark:text-gray-200">
|
||||
scriberr login
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
|
||||
2. Watch a Folder
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm mb-4">
|
||||
Start watching a directory for new audio files.
|
||||
</p>
|
||||
<div className="bg-gray-100 dark:bg-gray-900 rounded p-3 font-mono text-sm text-gray-800 dark:text-gray-200">
|
||||
scriberr watch ~/Recordings
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
|
||||
3. Run as Service
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm mb-4">
|
||||
Install as a background service to keep watching after restart.
|
||||
</p>
|
||||
<div className="bg-gray-100 dark:bg-gray-900 rounded p-3 font-mono text-sm text-gray-800 dark:text-gray-200">
|
||||
sudo scriberr install ~/Recordings<br />
|
||||
sudo scriberr start
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user