Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dce45505c | |||
| 014732788c | |||
| 0e75d838ab | |||
| 8383da8a50 | |||
| 199d173816 | |||
| f2829441f0 | |||
| 21137bd84a | |||
| a05e51a577 | |||
| 09a094629c | |||
| 90de0fbf68 | |||
| c9cdd5109b | |||
| 0e207dc5d2 | |||
| 876a5a98f4 | |||
| 8e82350d66 | |||
| de75ed1551 | |||
| 87266104a3 | |||
| fed8140404 | |||
| e4d83e91bb | |||
| a3153d893a | |||
| be219449f9 | |||
| 06d193f0d9 |
@@ -1,29 +1,71 @@
|
||||
name: Build Electron Desktop App
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version (e.g., v1.6.8)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.validate.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate version format
|
||||
id: validate
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
else
|
||||
VERSION="${{ inputs.version }}"
|
||||
fi
|
||||
|
||||
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Invalid version format. Expected: v1.6.8"
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "✓ Valid version: $VERSION"
|
||||
|
||||
build:
|
||||
name: Build Electron (${{ matrix.os }})
|
||||
name: Build Electron (${{ matrix.platform }})
|
||||
needs: validate
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows
|
||||
- platform: windows
|
||||
runner: windows-latest
|
||||
target: win
|
||||
- os: macos
|
||||
ext: .exe
|
||||
- platform: macos-intel
|
||||
runner: macos-latest
|
||||
target: mac
|
||||
- os: linux
|
||||
ext: .dmg
|
||||
- platform: macos-arm64
|
||||
runner: macos-latest
|
||||
target: mac
|
||||
ext: -arm64.dmg
|
||||
- platform: linux
|
||||
runner: ubuntu-latest
|
||||
target: linux
|
||||
ext: .AppImage
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -53,13 +95,76 @@ jobs:
|
||||
working-directory: electron
|
||||
run: npm install --no-audit --no-fund
|
||||
|
||||
- name: Build Electron for ${{ matrix.target }}
|
||||
- name: Build Electron for ${{ matrix.platform }}
|
||||
working-directory: electron
|
||||
run: npm run build:${{ matrix.target }}
|
||||
|
||||
- name: Upload release assets
|
||||
- name: Collect installers
|
||||
run: |
|
||||
mkdir -p release-assets
|
||||
cd electron/dist-electron
|
||||
# Copy only installer files for this platform
|
||||
for file in *${{ matrix.ext }}; do
|
||||
[ -f "$file" ] && cp "$file" ../../release-assets/
|
||||
done
|
||||
# Windows: also copy portable standalone exe
|
||||
if [ "${{ matrix.platform }}" = "windows" ]; then
|
||||
[ -f "OmniRoute.exe" ] && cp OmniRoute.exe ../../release-assets/
|
||||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: electron-${{ matrix.platform }}
|
||||
path: release-assets/
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
needs: [validate, build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: release-assets
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create source archives
|
||||
run: |
|
||||
# Create source code archives (excluding dev dependencies and build artifacts)
|
||||
export TARBALL="OmniRoute-${{ needs.validate.outputs.version }}.source.tar.gz"
|
||||
export ZIPBALL="OmniRoute-${{ needs.validate.outputs.version }}.source.zip"
|
||||
|
||||
# Use git archive for clean source export
|
||||
git archive --format=tar.gz --prefix=OmniRoute-${{ needs.validate.outputs.version }}/ HEAD -o "release-assets/$TARBALL"
|
||||
git archive --format=zip --prefix=OmniRoute-${{ needs.validate.outputs.version }}/ HEAD -o "release-assets/$ZIPBALL"
|
||||
|
||||
echo "✓ Created source archives:"
|
||||
ls -lh "release-assets/$TARBALL" "release-assets/$ZIPBALL"
|
||||
|
||||
- name: List release files
|
||||
run: ls -la release-assets/
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: electron/dist-electron/**/*
|
||||
tag_name: ${{ needs.validate.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
release-assets/*.dmg
|
||||
release-assets/*-arm64.dmg
|
||||
release-assets/*.exe
|
||||
release-assets/*.AppImage
|
||||
release-assets/*.blockmap
|
||||
release-assets/*.source.tar.gz
|
||||
release-assets/*.source.zip
|
||||
release-assets/OmniRoute.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
+8
-1
@@ -103,6 +103,13 @@ app.log
|
||||
# Backup directories
|
||||
app.__qa_backup/
|
||||
|
||||
# Electron (subproject dependency lock)
|
||||
# Production standalone build (created by scripts/prepublish.mjs)
|
||||
# Conflicts with Next.js App Router detection in dev (root app/ shadows src/app/)
|
||||
# npm publish still includes it via package.json "files" field
|
||||
app/
|
||||
|
||||
# Electron (subproject dependency lock and build artifacts)
|
||||
electron/package-lock.json
|
||||
electron/dist-electron/
|
||||
electron/node_modules/
|
||||
icon.iconset/
|
||||
|
||||
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [1.6.9] — 2026-02-28
|
||||
|
||||
### 🐛 Fixed
|
||||
|
||||
- **Proxy Port Preservation** — `new URL()` silently strips default ports (80/443); proxy connections now extract the port from the raw URL string before parsing, preventing connection timeouts ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
|
||||
- **Proxy Credential Encoding** — URL-encode special characters in proxy username/password; decode during legacy migration ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
|
||||
- **HTTPS Proxy Default Port** — Changed from 8080 to 443 in frontend and migration logic ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
|
||||
- **Proxy Dispatcher Cache** — Invalidate cached dispatchers when proxy config is updated or deleted ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
|
||||
- **Proxy Logger SQLite Type** — Cast `proxyPort` to `Number` for INTEGER column ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
|
||||
- **CopilotToolCard URL** — Use `baseUrl` prop directly instead of redundant `window.location.origin`; filter to chat models only (`!m.type && !m.parent`) ([PR #160](https://github.com/diegosouzapw/OmniRoute/pull/160))
|
||||
|
||||
---
|
||||
|
||||
## [1.6.8] — 2026-02-28
|
||||
|
||||
### 🔧 Improved
|
||||
|
||||
- **Electron Release Workflow** — Refactored CI to trigger on git tags (`v*`) + manual dispatch, with version validation, artifact upload/download pattern across 3 platforms, and a single release job. Only installer files (`.dmg`, `.exe`, `.AppImage`) are uploaded — no more 5K+ unpacked files ([PR #159](https://github.com/diegosouzapw/OmniRoute/pull/159))
|
||||
- **Windows Portable Exe** — Added standalone portable `.exe` build alongside the NSIS installer ([PR #159](https://github.com/diegosouzapw/OmniRoute/pull/159))
|
||||
- **Source Code Archives** — Releases now include `OmniRoute-vX.Y.Z.source.tar.gz` and `.zip` via `git archive` ([PR #159](https://github.com/diegosouzapw/OmniRoute/pull/159))
|
||||
- **Installation Docs** — Added platform-specific installation instructions with macOS Gatekeeper workaround ([PR #159](https://github.com/diegosouzapw/OmniRoute/pull/159))
|
||||
|
||||
### 🐛 Fixed
|
||||
|
||||
- **Next.js App Router Conflict** — Added `app/` (production standalone build) to `.gitignore`. This directory was conflicting with Next.js App Router detection in dev mode, causing all routes to return 404
|
||||
- **Git Tracking** — Added `electron/node_modules/` to `.gitignore`
|
||||
|
||||
---
|
||||
|
||||
## [1.6.7] — 2026-02-28
|
||||
|
||||
### ✨ New Feature
|
||||
|
||||
- **GitHub Copilot Configuration Generator** — New tool on the CLI Tools dashboard page. Select models and generate the `chatLanguageModels.json` config block for VS Code GitHub Copilot using the Azure vendor pattern. Features: bulk model selection from `/v1/models` (includes combos/custom), search/filter, configurable tokens/tool-calling/vision, one-click copy, persistent selection via localStorage. Version compatibility warning for VS Code ≥ 1.109 / Copilot Chat ≥ v0.37 ([#142](https://github.com/diegosouzapw/OmniRoute/issues/142))
|
||||
|
||||
### 🧹 Housekeeping
|
||||
|
||||
- Added `electron/dist-electron/` to `.gitignore` (build artifact)
|
||||
|
||||
---
|
||||
|
||||
## [1.6.6] — 2026-02-28
|
||||
|
||||
### 🔒 Security Fix
|
||||
|
||||
- **Auth bypass after onboarding** — Fixed regression where users could access the dashboard without authentication after upgrading from older versions. The "no password" safeguard (for fresh installs) was incorrectly firing after onboarding was complete, allowing unauthenticated access when `setupComplete=true` but the password DB row was missing ([#151](https://github.com/diegosouzapw/OmniRoute/issues/151))
|
||||
|
||||
---
|
||||
|
||||
## [1.6.5] — 2026-02-28
|
||||
|
||||
### 🖥️ Electron Desktop
|
||||
|
||||
@@ -2,6 +2,7 @@ FROM node:22-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
COPY scripts/postinstall.mjs ./scripts/postinstall.mjs
|
||||
RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi
|
||||
|
||||
COPY . ./
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ General settings, system storage, backup management (export/import database), ap
|
||||
|
||||
## 🔧 CLI Tools
|
||||
|
||||
One-click configuration for AI coding tools: Claude Code, Codex CLI, Gemini CLI, OpenClaw, Kilo Code, and Antigravity.
|
||||
One-click configuration for AI coding tools: Claude Code, Codex CLI, Gemini CLI, OpenClaw, Kilo Code, Antigravity, and **GitHub Copilot** (config generator for `chatLanguageModels.json`).
|
||||
|
||||

|
||||
|
||||
|
||||
+40
-2
@@ -100,10 +100,48 @@ npm run build:linux
|
||||
|
||||
Built applications are placed in `dist-electron/`:
|
||||
|
||||
- Windows: `.exe` installer (NSIS)
|
||||
- macOS: `.dmg` installer (universal)
|
||||
- Windows: `.exe` installer (NSIS) + portable `.exe`
|
||||
- macOS: `.dmg` installer (Intel + Apple Silicon)
|
||||
- Linux: `.AppImage`
|
||||
|
||||
## Installation
|
||||
|
||||
### macOS
|
||||
|
||||
1. Download the latest `.dmg` from the [Releases](https://github.com/diegosouzapw/OmniRoute/releases) page.
|
||||
2. Open the `.dmg` file.
|
||||
3. Drag `OmniRoute.app` to the Applications folder.
|
||||
4. Launch from Applications.
|
||||
|
||||
> ⚠️ **Note:** The app is not signed with an Apple Developer certificate yet. If macOS blocks the app, run:
|
||||
> ```bash
|
||||
> xattr -cr /Applications/OmniRoute.app
|
||||
> ```
|
||||
> Or right-click the app → Open → Open (to bypass Gatekeeper on first launch).
|
||||
|
||||
### Windows
|
||||
|
||||
**Installer (Recommended):**
|
||||
1. Download `OmniRoute.Setup.*.exe` from [Releases](https://github.com/diegosouzapw/OmniRoute/releases).
|
||||
2. Run the installer.
|
||||
3. Launch from Start Menu or Desktop shortcut.
|
||||
|
||||
**Portable (No Installation):**
|
||||
1. Download `OmniRoute.exe` from [Releases](https://github.com/diegosouzapw/OmniRoute/releases).
|
||||
2. Run directly from any folder.
|
||||
|
||||
### Linux
|
||||
|
||||
1. Download the `.AppImage` from [Releases](https://github.com/diegosouzapw/OmniRoute/releases).
|
||||
2. Make it executable:
|
||||
```bash
|
||||
chmod +x OmniRoute-*.AppImage
|
||||
```
|
||||
3. Run:
|
||||
```bash
|
||||
./OmniRoute-*.AppImage
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Server Readiness** — Waits for health check before showing window
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute-desktop",
|
||||
"version": "1.6.4",
|
||||
"version": "1.6.9",
|
||||
"description": "OmniRoute Desktop Application",
|
||||
"main": "main.js",
|
||||
"author": "OmniRoute Team",
|
||||
@@ -63,6 +63,12 @@
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "assets/icon.ico"
|
||||
|
||||
@@ -4,13 +4,39 @@ import { socksDispatcher } from "fetch-socks";
|
||||
const DISPATCHER_CACHE_KEY = Symbol.for("omniroute.proxyDispatcher.cache");
|
||||
const SUPPORTED_PROTOCOLS = new Set(["http:", "https:", "socks5:"]);
|
||||
|
||||
function getDispatcherCache() {
|
||||
function getDispatcherCache(): Map<string, any> {
|
||||
if (!globalThis[DISPATCHER_CACHE_KEY]) {
|
||||
globalThis[DISPATCHER_CACHE_KEY] = new Map();
|
||||
}
|
||||
return globalThis[DISPATCHER_CACHE_KEY];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached proxy dispatchers.
|
||||
* Call this when proxy configuration changes to avoid stale connections.
|
||||
*/
|
||||
export function clearDispatcherCache() {
|
||||
const cache = getDispatcherCache();
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the port from a proxy URL string before URL parsing.
|
||||
* `new URL("http://host:80")` strips port 80 since it's the HTTP default,
|
||||
* but proxy servers commonly listen on port 80/443, so we need to preserve it.
|
||||
*/
|
||||
function extractExplicitPort(urlStr) {
|
||||
try {
|
||||
// Match port in the host portion: "scheme://[user:pass@]host:PORT[/...]"
|
||||
const match = urlStr.match(/:\/\/(?:[^@]*@)?[^:/\s]+:(\d+)/);
|
||||
if (match) {
|
||||
const port = Number(match[1]);
|
||||
if (Number.isInteger(port) && port >= 1 && port <= 65535) return String(port);
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function defaultPortForProtocol(protocol) {
|
||||
if (protocol === "https:" || protocol === "wss:") return "443";
|
||||
if (protocol === "socks5:") return "1080";
|
||||
@@ -26,13 +52,28 @@ function normalizePort(port, protocol) {
|
||||
return String(parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a proxy URL string manually from parsed URL components.
|
||||
* We cannot use URL.toString() because the URL serializer silently strips
|
||||
* default ports (80 for http, 443 for https). Proxy servers commonly
|
||||
* listen on these ports, so we must always include the port explicitly.
|
||||
*/
|
||||
function buildProxyUrlString(parsed, port) {
|
||||
const auth =
|
||||
parsed.username
|
||||
? `${parsed.username}${parsed.password ? `:${parsed.password}` : ""}@`
|
||||
: "";
|
||||
return `${parsed.protocol}//${auth}${parsed.hostname}:${port}`;
|
||||
}
|
||||
|
||||
export function isSocks5ProxyEnabled() {
|
||||
return process.env.ENABLE_SOCKS5_PROXY === "true";
|
||||
}
|
||||
|
||||
export function proxyUrlForLogs(proxyUrl) {
|
||||
const explicitPort = extractExplicitPort(proxyUrl);
|
||||
const parsed = new URL(proxyUrl);
|
||||
const port = parsed.port || defaultPortForProtocol(parsed.protocol);
|
||||
const port = explicitPort || parsed.port || defaultPortForProtocol(parsed.protocol);
|
||||
return `${parsed.protocol}//${parsed.hostname}:${port}`;
|
||||
}
|
||||
|
||||
@@ -41,6 +82,11 @@ export function normalizeProxyUrl(
|
||||
source = "proxy",
|
||||
{ allowSocks5 = isSocks5ProxyEnabled() } = {}
|
||||
) {
|
||||
// Extract the explicit port from the raw URL string BEFORE parsing,
|
||||
// because `new URL()` silently strips default ports (80 for http,
|
||||
// 443 for https), which are valid and common for proxy servers.
|
||||
const explicitPort = extractExplicitPort(proxyUrl);
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(proxyUrl);
|
||||
@@ -62,8 +108,12 @@ export function normalizeProxyUrl(
|
||||
throw new Error(`[ProxyDispatcher] Invalid ${source} host`);
|
||||
}
|
||||
|
||||
parsed.port = normalizePort(parsed.port, parsed.protocol);
|
||||
return parsed.toString();
|
||||
// Use the explicit port from the raw string if present, otherwise apply default.
|
||||
const port = explicitPort || normalizePort(parsed.port, parsed.protocol);
|
||||
|
||||
// Build the URL string manually instead of using parsed.toString(),
|
||||
// which would strip default ports (80/443) and break the proxy connection.
|
||||
return buildProxyUrlString(parsed, port);
|
||||
}
|
||||
|
||||
export function proxyConfigToUrl(proxyConfig, { allowSocks5 = isSocks5ProxyEnabled() } = {}) {
|
||||
@@ -93,14 +143,15 @@ export function proxyConfigToUrl(proxyConfig, { allowSocks5 = isSocks5ProxyEnabl
|
||||
}
|
||||
|
||||
const port = normalizePort(proxyConfig.port, protocol);
|
||||
const proxyUrl = new URL(`${type}://${proxyConfig.host}:${port}`);
|
||||
|
||||
if (proxyConfig.username) {
|
||||
proxyUrl.username = proxyConfig.username;
|
||||
proxyUrl.password = proxyConfig.password || "";
|
||||
}
|
||||
// Build the URL string manually to preserve the port through normalization.
|
||||
const auth = proxyConfig.username
|
||||
? `${encodeURIComponent(proxyConfig.username)}:${proxyConfig.password ? encodeURIComponent(proxyConfig.password) : ""}@`
|
||||
: "";
|
||||
|
||||
return normalizeProxyUrl(proxyUrl.toString(), "context proxy", { allowSocks5 });
|
||||
const proxyUrlStr = `${type}://${auth}${proxyConfig.host}:${port}`;
|
||||
|
||||
return normalizeProxyUrl(proxyUrlStr, "context proxy", { allowSocks5 });
|
||||
}
|
||||
|
||||
export function createProxyDispatcher(proxyUrl) {
|
||||
@@ -111,11 +162,14 @@ export function createProxyDispatcher(proxyUrl) {
|
||||
if (dispatcher) return dispatcher;
|
||||
|
||||
const parsed = new URL(normalizedUrl);
|
||||
const explicitPort = extractExplicitPort(normalizedUrl);
|
||||
const port = explicitPort || normalizePort(parsed.port, parsed.protocol);
|
||||
|
||||
if (parsed.protocol === "socks5:") {
|
||||
const socksOptions: Record<string, any> = {
|
||||
type: 5,
|
||||
host: parsed.hostname,
|
||||
port: Number(normalizePort(parsed.port, parsed.protocol)),
|
||||
port: Number(port),
|
||||
};
|
||||
if (parsed.username) socksOptions.userId = decodeURIComponent(parsed.username);
|
||||
if (parsed.password) socksOptions.password = decodeURIComponent(parsed.password);
|
||||
@@ -126,4 +180,4 @@ export function createProxyDispatcher(proxyUrl) {
|
||||
|
||||
dispatcherCache.set(normalizedUrl, dispatcher);
|
||||
return dispatcher;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "1.6.5",
|
||||
"version": "1.6.9",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
KiloToolCard,
|
||||
DefaultToolCard,
|
||||
AntigravityToolCard,
|
||||
CopilotToolCard,
|
||||
} from "./components";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -32,7 +33,6 @@ export default function CLIToolsPageClient({ machineId }) {
|
||||
const [apiKeys, setApiKeys] = useState([]);
|
||||
const [toolStatuses, setToolStatuses] = useState({});
|
||||
const [statusesLoaded, setStatusesLoaded] = useState(false);
|
||||
const [apiBaseUrl, setApiBaseUrl] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
fetchConnections();
|
||||
@@ -47,12 +47,6 @@ export default function CLIToolsPageClient({ machineId }) {
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setCloudEnabled(data.cloudEnabled || false);
|
||||
if (typeof window !== "undefined") {
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
const apiPort = data?.apiPort || 20128;
|
||||
setApiBaseUrl(`${protocol}//${hostname}:${apiPort}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error loading cloud settings:", error);
|
||||
@@ -154,9 +148,8 @@ export default function CLIToolsPageClient({ machineId }) {
|
||||
if (cloudEnabled && CLOUD_URL) {
|
||||
return CLOUD_URL;
|
||||
}
|
||||
if (apiBaseUrl) {
|
||||
return apiBaseUrl;
|
||||
}
|
||||
// Use window.location.origin directly — works correctly in Docker/reverse-proxy
|
||||
// Per @alpgul feedback: don't use baseUrl prop (has port duplication issues)
|
||||
if (typeof window !== "undefined") {
|
||||
return window.location.origin;
|
||||
}
|
||||
@@ -263,6 +256,16 @@ export default function CLIToolsPageClient({ machineId }) {
|
||||
cloudEnabled={cloudEnabled}
|
||||
/>
|
||||
);
|
||||
case "copilot":
|
||||
return (
|
||||
<CopilotToolCard
|
||||
key={toolId}
|
||||
{...commonProps}
|
||||
activeProviders={getActiveProviders()}
|
||||
hasActiveProviders={hasActiveProviders}
|
||||
cloudEnabled={cloudEnabled}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<DefaultToolCard
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* GitHub Copilot Configuration Generator
|
||||
*
|
||||
* Generates the chatLanguageModels.json block for VS Code GitHub Copilot
|
||||
* using the Azure vendor pattern as required by Copilot's architecture.
|
||||
*
|
||||
* Feature request: https://github.com/diegosouzapw/OmniRoute/issues/142
|
||||
*/
|
||||
export default function CopilotToolCard({
|
||||
tool,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
baseUrl,
|
||||
apiKeys,
|
||||
activeProviders = [],
|
||||
hasActiveProviders = false,
|
||||
cloudEnabled = false,
|
||||
batchStatus,
|
||||
}) {
|
||||
const t = useTranslations("cliTools");
|
||||
const [copiedField, setCopiedField] = useState<string | null>(null);
|
||||
const [selectedModels, setSelectedModels] = useState<Set<string>>(() => {
|
||||
if (typeof window === "undefined") return new Set<string>();
|
||||
try {
|
||||
const saved = localStorage.getItem("omniroute-copilot-selected-models");
|
||||
return saved ? new Set<string>(JSON.parse(saved)) : new Set<string>();
|
||||
} catch {
|
||||
return new Set<string>();
|
||||
}
|
||||
});
|
||||
const [selectedApiKey, setSelectedApiKey] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const savedKey = localStorage.getItem("omniroute-cli-key-copilot");
|
||||
if (savedKey && apiKeys?.some((k: any) => k.key === savedKey)) return savedKey;
|
||||
}
|
||||
return apiKeys?.length > 0 ? apiKeys[0].key : "";
|
||||
});
|
||||
const [maxInputTokens, setMaxInputTokens] = useState(128000);
|
||||
const [maxOutputTokens, setMaxOutputTokens] = useState(16000);
|
||||
const [toolCalling, setToolCalling] = useState(true);
|
||||
const [vision, setVision] = useState(false);
|
||||
const [allModels, setAllModels] = useState<Array<{ value: string; label: string }>>([]);
|
||||
const [modelsLoaded, setModelsLoaded] = useState(false);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
|
||||
// Fetch ALL models dynamically from /v1/models (includes combos, custom, aliased)
|
||||
// Per @alpgul feedback: /api/models/alias doesn't include combo definitions
|
||||
useEffect(() => {
|
||||
if (!isExpanded || modelsLoaded) return;
|
||||
let cancelled = false;
|
||||
fetch("/v1/models")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (cancelled) return;
|
||||
const modelList = (data.data || [])
|
||||
.filter((m: any) => m && !m.type && !m.parent && m.id) // Only chat models with valid IDs
|
||||
.map((m: any) => ({
|
||||
value: m.id,
|
||||
label: m.id,
|
||||
}));
|
||||
setAllModels(modelList);
|
||||
setModelsLoaded(true);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setModelsLoaded(true);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isExpanded, modelsLoaded]);
|
||||
|
||||
// Filter models by search
|
||||
const availableModels = searchFilter
|
||||
? allModels.filter((m) => m.label.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||
: allModels;
|
||||
|
||||
// Persist selection
|
||||
useEffect(() => {
|
||||
if (selectedModels.size > 0) {
|
||||
localStorage.setItem(
|
||||
"omniroute-copilot-selected-models",
|
||||
JSON.stringify([...selectedModels])
|
||||
);
|
||||
}
|
||||
}, [selectedModels]);
|
||||
|
||||
const toggleModel = (modelValue: string) => {
|
||||
setSelectedModels((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(modelValue)) {
|
||||
next.delete(modelValue);
|
||||
} else {
|
||||
next.add(modelValue);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
setSelectedModels(new Set(allModels.map((m) => m.value)));
|
||||
};
|
||||
|
||||
const deselectAll = () => {
|
||||
setSelectedModels(new Set());
|
||||
};
|
||||
|
||||
const getBaseUrlForConfig = () => {
|
||||
const url = baseUrl;
|
||||
return `${url}/v1/chat/completions`;
|
||||
};
|
||||
|
||||
// Generate the Copilot chatLanguageModels.json config
|
||||
const generateConfig = () => {
|
||||
const models = [...selectedModels].map((modelId) => ({
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
url: `${getBaseUrlForConfig()}#models.ai.azure.com`,
|
||||
toolCalling,
|
||||
vision,
|
||||
maxInputTokens,
|
||||
maxOutputTokens,
|
||||
}));
|
||||
|
||||
const config = {
|
||||
name: "OmniRoute",
|
||||
vendor: "azure",
|
||||
apiKey: `\${input:chat.lm.secret.omniroute}`,
|
||||
models,
|
||||
};
|
||||
|
||||
return JSON.stringify(config, null, 2);
|
||||
};
|
||||
|
||||
const handleCopy = async (text: string, field: string) => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopiedField(field);
|
||||
setTimeout(() => setCopiedField(null), 2000);
|
||||
};
|
||||
|
||||
const handleApiKeyChange = (value: string) => {
|
||||
setSelectedApiKey(value);
|
||||
if (value) localStorage.setItem("omniroute-cli-key-copilot", value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card padding="sm" className="overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 rounded-lg flex items-center justify-center shrink-0">
|
||||
<Image
|
||||
src={tool.image || "/providers/copilot.png"}
|
||||
alt={tool.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain rounded-lg"
|
||||
sizes="32px"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 text-[11px] font-medium rounded-full bg-blue-500/10 text-blue-600 dark:text-blue-400">
|
||||
<span className="size-1.5 rounded-full bg-blue-500" />
|
||||
{t("guide")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`material-symbols-outlined text-text-muted text-[20px] transition-transform ${isExpanded ? "rotate-180" : ""}`}
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Expanded content */}
|
||||
{isExpanded && (
|
||||
<div className="mt-6 pt-6 border-t border-border">
|
||||
<div className="flex flex-col gap-5">
|
||||
{/* Info box */}
|
||||
<div className="flex items-start gap-3 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<span className="material-symbols-outlined text-blue-500 text-lg">info</span>
|
||||
<div className="text-sm text-blue-700 dark:text-blue-300">
|
||||
<p className="font-medium">GitHub Copilot Config Generator</p>
|
||||
<p className="mt-1 text-xs opacity-80">
|
||||
Generates the{" "}
|
||||
<code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/10">
|
||||
chatLanguageModels.json
|
||||
</code>{" "}
|
||||
block for VS Code GitHub Copilot using the Azure vendor pattern. Select the models
|
||||
you want, then copy the JSON into your config file.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version compatibility warning */}
|
||||
<div className="flex items-start gap-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<span className="material-symbols-outlined text-yellow-500 text-lg">warning</span>
|
||||
<p className="text-xs text-yellow-600 dark:text-yellow-400">
|
||||
This configuration uses the Azure vendor workaround for custom model lists. Tested
|
||||
with <strong>VS Code ≥ 1.109</strong> and{" "}
|
||||
<strong>GitHub Copilot Chat ≥ v0.37</strong>. Future extension updates may change
|
||||
this behavior.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Step 2: API Key (if cloud enabled) */}
|
||||
{cloudEnabled && apiKeys?.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div
|
||||
className="size-6 rounded-full flex items-center justify-center text-xs font-semibold text-white"
|
||||
style={{ backgroundColor: tool.color }}
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<span className="font-medium text-sm">API Key</span>
|
||||
</div>
|
||||
<select
|
||||
value={selectedApiKey}
|
||||
onChange={(e) => handleApiKeyChange(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
>
|
||||
{apiKeys.map((key: any) => (
|
||||
<option key={key.id} value={key.key}>
|
||||
{key.key}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Model Selection */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="size-6 rounded-full flex items-center justify-center text-xs font-semibold text-white"
|
||||
style={{ backgroundColor: tool.color }}
|
||||
>
|
||||
{cloudEnabled && apiKeys?.length > 0 ? "2" : "1"}
|
||||
</div>
|
||||
<span className="font-medium text-sm">
|
||||
Select Models ({selectedModels.size}/{availableModels.length})
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={selectAll}
|
||||
className="px-2 py-1 text-xs bg-bg-secondary hover:bg-bg-tertiary rounded border border-border transition-colors"
|
||||
>
|
||||
Select All
|
||||
</button>
|
||||
<button
|
||||
onClick={deselectAll}
|
||||
className="px-2 py-1 text-xs bg-bg-secondary hover:bg-bg-tertiary rounded border border-border transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search filter */}
|
||||
<div className="mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
placeholder="Filter models..."
|
||||
className="w-full px-3 py-1.5 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!modelsLoaded && allModels.length === 0 ? (
|
||||
<div className="flex items-center gap-2 p-3 text-text-muted text-sm">
|
||||
<span className="material-symbols-outlined animate-spin text-base">
|
||||
progress_activity
|
||||
</span>
|
||||
<span>Loading models...</span>
|
||||
</div>
|
||||
) : availableModels.length === 0 && allModels.length === 0 ? (
|
||||
<div className="flex items-center gap-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<span className="material-symbols-outlined text-yellow-500 text-lg">warning</span>
|
||||
<p className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
{t("noActiveProviders")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-64 overflow-y-auto rounded-lg border border-border bg-bg-secondary">
|
||||
{availableModels.map((model) => (
|
||||
<label
|
||||
key={model.value}
|
||||
className="flex items-center gap-3 px-3 py-2 hover:bg-bg-tertiary cursor-pointer border-b border-border last:border-0 transition-colors"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedModels.has(model.value)}
|
||||
onChange={() => toggleModel(model.value)}
|
||||
className="rounded border-border text-primary accent-[#1F6FEB]"
|
||||
/>
|
||||
<span className="text-sm font-mono truncate">{model.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step 4: Advanced options (collapsible) */}
|
||||
<details className="group">
|
||||
<summary className="flex items-center gap-2 cursor-pointer text-sm text-text-muted hover:text-text-main transition-colors">
|
||||
<span className="material-symbols-outlined text-base group-open:rotate-90 transition-transform">
|
||||
chevron_right
|
||||
</span>
|
||||
Advanced Options
|
||||
</summary>
|
||||
<div className="mt-3 grid grid-cols-2 gap-3 pl-6">
|
||||
<div>
|
||||
<label className="text-xs text-text-muted block mb-1">Max Input Tokens</label>
|
||||
<input
|
||||
type="number"
|
||||
value={maxInputTokens}
|
||||
onChange={(e) => setMaxInputTokens(Number(e.target.value) || 128000)}
|
||||
className="w-full px-3 py-1.5 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-text-muted block mb-1">Max Output Tokens</label>
|
||||
<input
|
||||
type="number"
|
||||
value={maxOutputTokens}
|
||||
onChange={(e) => setMaxOutputTokens(Number(e.target.value) || 16000)}
|
||||
className="w-full px-3 py-1.5 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={toolCalling}
|
||||
onChange={(e) => setToolCalling(e.target.checked)}
|
||||
className="rounded border-border accent-[#1F6FEB]"
|
||||
/>
|
||||
<span className="text-sm">Tool Calling</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={vision}
|
||||
onChange={(e) => setVision(e.target.checked)}
|
||||
className="rounded border-border accent-[#1F6FEB]"
|
||||
/>
|
||||
<span className="text-sm">Vision</span>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* Step 5: Generated config */}
|
||||
{selectedModels.size > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="size-6 rounded-full flex items-center justify-center text-xs font-semibold text-white"
|
||||
style={{ backgroundColor: tool.color }}
|
||||
>
|
||||
{cloudEnabled && apiKeys?.length > 0 ? "3" : "2"}
|
||||
</div>
|
||||
<span className="font-medium text-sm">
|
||||
Copy Config ({selectedModels.size} model{selectedModels.size !== 1 ? "s" : ""}
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => handleCopy(generateConfig(), "config")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">
|
||||
{copiedField === "config" ? "check" : "content_copy"}
|
||||
</span>
|
||||
{copiedField === "config" ? t("copied") : t("copyConfig")}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="p-4 bg-bg-secondary rounded-lg border border-border overflow-x-auto max-h-80">
|
||||
<code className="text-xs font-mono whitespace-pre text-text-main">
|
||||
{generateConfig()}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
{/* Usage instructions */}
|
||||
<div className="mt-3 p-3 bg-bg-secondary rounded-lg border border-border">
|
||||
<p className="text-xs text-text-muted">
|
||||
<span className="font-medium text-text-main">Paste into: </span>
|
||||
<code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/10">
|
||||
~/.config/Code/User/chatLanguageModels.json
|
||||
</code>
|
||||
</p>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Then reload VS Code and set the API key in the input prompt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -6,3 +6,4 @@ export { default as ClineToolCard } from "./ClineToolCard";
|
||||
export { default as KiloToolCard } from "./KiloToolCard";
|
||||
export { default as DefaultToolCard } from "./DefaultToolCard";
|
||||
export { default as AntigravityToolCard } from "./AntigravityToolCard";
|
||||
export { default as CopilotToolCard } from "./CopilotToolCard";
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
deleteProxyForLevel,
|
||||
resolveProxyForConnection,
|
||||
} from "../../../../lib/localDb";
|
||||
import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher";
|
||||
|
||||
const BASE_SUPPORTED_PROXY_TYPES = new Set(["http", "https"]);
|
||||
|
||||
@@ -129,6 +130,7 @@ export async function PUT(request) {
|
||||
const body = await request.json();
|
||||
const normalizedBody = normalizeProxyPayload(body);
|
||||
const updated = await setProxyConfig(normalizedBody);
|
||||
clearDispatcherCache();
|
||||
return Response.json(updated);
|
||||
} catch (error) {
|
||||
const status = Number(error?.status) || 500;
|
||||
@@ -155,6 +157,7 @@ export async function DELETE(request) {
|
||||
}
|
||||
|
||||
const updated = await deleteProxyForLevel(level, id);
|
||||
clearDispatcherCache();
|
||||
return Response.json(updated);
|
||||
} catch (error) {
|
||||
return Response.json(
|
||||
|
||||
@@ -211,11 +211,11 @@ function migrateProxyEntry(value: any) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return {
|
||||
type: url.protocol.replace(":", "").replace("//", "") || "http",
|
||||
type: url.protocol.replace(":", "") || "http",
|
||||
host: url.hostname,
|
||||
port: url.port || (url.protocol === "socks5:" ? "1080" : "8080"),
|
||||
username: url.username || "",
|
||||
password: url.password || "",
|
||||
port: url.port || (url.protocol === "socks5:" ? "1080" : url.protocol === "https:" ? "443" : "8080"),
|
||||
username: url.username ? decodeURIComponent(url.username) : "",
|
||||
password: url.password ? decodeURIComponent(url.password) : "",
|
||||
};
|
||||
} catch {
|
||||
const parts = value.split(":");
|
||||
|
||||
@@ -134,7 +134,7 @@ export function logProxyEvent(entry: Partial<ProxyLogEntry>) {
|
||||
status: log.status,
|
||||
proxyType: log.proxy?.type || null,
|
||||
proxyHost: log.proxy?.host || null,
|
||||
proxyPort: log.proxy?.port || null,
|
||||
proxyPort: log.proxy?.port ? Number(log.proxy.port) : null,
|
||||
level: log.level,
|
||||
levelId: log.levelId,
|
||||
provider: log.provider,
|
||||
|
||||
+3
-3
@@ -131,9 +131,9 @@ export async function proxy(request) {
|
||||
if (settings.requireLogin === false) {
|
||||
return response;
|
||||
}
|
||||
// Skip auth if no password has been set yet (fresh install with no env override)
|
||||
// This prevents an unresolvable loop where requireLogin=true but no password exists
|
||||
if (!settings.password && !process.env.INITIAL_PASSWORD) {
|
||||
// Skip auth ONLY for fresh installs (before onboarding) where no password exists yet.
|
||||
// Once setupComplete is true, always require auth — prevents bypass if password row is lost (#151)
|
||||
if (!settings.setupComplete && !settings.password && !process.env.INITIAL_PASSWORD) {
|
||||
return response;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -48,7 +48,11 @@ export default function ProxyConfigModal({ isOpen, onClose, level, levelId, leve
|
||||
const [hasOwnProxy, setHasOwnProxy] = useState(false);
|
||||
const [formError, setFormError] = useState(null);
|
||||
|
||||
const getDefaultPort = (type) => (type === "socks5" ? "1080" : "8080");
|
||||
const getDefaultPort = (type) => {
|
||||
if (type === "socks5") return "1080";
|
||||
if (type === "https") return "443";
|
||||
return "8080";
|
||||
};
|
||||
|
||||
// Load existing proxy config when modal opens
|
||||
useEffect(() => {
|
||||
|
||||
@@ -164,6 +164,14 @@ export const CLI_TOOLS = {
|
||||
{ id: "gpt-oss-120b-medium", name: "GPT OSS 120B Medium", alias: "gpt-oss-120b-medium" },
|
||||
],
|
||||
},
|
||||
copilot: {
|
||||
id: "copilot",
|
||||
name: "GitHub Copilot",
|
||||
image: "/providers/copilot.png",
|
||||
color: "#1F6FEB",
|
||||
description: "GitHub Copilot Chat — VS Code Extension",
|
||||
configType: "custom",
|
||||
},
|
||||
// HIDDEN: gemini-cli
|
||||
// "gemini-cli": {
|
||||
// id: "gemini-cli",
|
||||
|
||||
@@ -134,8 +134,10 @@ export async function isAuthRequired(): Promise<boolean> {
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
if (settings.requireLogin === false) return false;
|
||||
// If no password set and no env override, don't require auth (fresh install)
|
||||
if (!settings.password && !process.env.INITIAL_PASSWORD) return false;
|
||||
// Only skip auth for fresh installs (not yet onboarded) with no password.
|
||||
// Once setupComplete is true, always require auth — prevents bypass if password row is lost (#151)
|
||||
if (!settings.setupComplete && !settings.password && !process.env.INITIAL_PASSWORD)
|
||||
return false;
|
||||
return true;
|
||||
} catch {
|
||||
// On error, require auth (secure by default)
|
||||
|
||||
Reference in New Issue
Block a user