ai-agent-router 0.1.0 → 0.1.1
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/.gemini/skills/ui-ux-pro-max/SKILL.md +227 -0
- package/package.json +2 -2
- package/src/app/api/config/route.ts +7 -5
- package/src/app/page.tsx +8 -6
- package/src/cli/index.ts +41 -35
- package/src/server/service-manager.ts +43 -136
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ui-ux-pro-max
|
|
3
|
+
description: "UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# UI/UX Pro Max - Design Intelligence
|
|
7
|
+
|
|
8
|
+
Searchable database of UI styles, color palettes, font pairings, chart types, product recommendations, UX guidelines, and stack-specific best practices.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
Check if Python is installed:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
python3 --version || python --version
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If Python is not installed, instruct the user to install it based on their OS.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## How to Use This Skill
|
|
23
|
+
|
|
24
|
+
When user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow:
|
|
25
|
+
|
|
26
|
+
### Step 1: Analyze User Requirements
|
|
27
|
+
|
|
28
|
+
Extract key information from user request:
|
|
29
|
+
- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.
|
|
30
|
+
- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.
|
|
31
|
+
- **Industry**: healthcare, fintech, gaming, education, etc.
|
|
32
|
+
- **Stack**: React, Vue, Next.js, or default to `html-tailwind`
|
|
33
|
+
|
|
34
|
+
### Step 2: Search Relevant Domains
|
|
35
|
+
|
|
36
|
+
Use `run_shell_command` to execute the `search.py` script multiple times to gather comprehensive information. Search until you have enough context.
|
|
37
|
+
|
|
38
|
+
**Command Format:**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python3 .shared/ui-ux-pro-max/scripts/search.py "<keyword>" --domain <domain>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Recommended search order:**
|
|
45
|
+
|
|
46
|
+
1. **Product** - Get style recommendations for product type
|
|
47
|
+
2. **Style** - Get detailed style guide (colors, effects, frameworks)
|
|
48
|
+
3. **Typography** - Get font pairings with Google Fonts imports
|
|
49
|
+
4. **Color** - Get color palette (Primary, Secondary, CTA, Background, Text, Border)
|
|
50
|
+
5. **Landing** - Get page structure (if landing page)
|
|
51
|
+
6. **Chart** - Get chart recommendations (if dashboard/analytics)
|
|
52
|
+
7. **UX** - Get best practices and anti-patterns
|
|
53
|
+
8. **Stack** - Get stack-specific guidelines (default: html-tailwind)
|
|
54
|
+
|
|
55
|
+
### Step 3: Stack Guidelines (Default: html-tailwind)
|
|
56
|
+
|
|
57
|
+
If user doesn't specify a stack, **default to `html-tailwind`**.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python3 .shared/ui-ux-pro-max/scripts/search.py "<keyword>" --stack html-tailwind
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter`
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Search Reference
|
|
68
|
+
|
|
69
|
+
### Available Domains
|
|
70
|
+
|
|
71
|
+
| Domain | Use For | Example Keywords |
|
|
72
|
+
|--------|---------|------------------|
|
|
73
|
+
| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |
|
|
74
|
+
| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |
|
|
75
|
+
| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |
|
|
76
|
+
| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |
|
|
77
|
+
| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |
|
|
78
|
+
| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |
|
|
79
|
+
| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |
|
|
80
|
+
| `prompt` | AI prompts, CSS keywords | (style name) |
|
|
81
|
+
|
|
82
|
+
### Available Stacks
|
|
83
|
+
|
|
84
|
+
| Stack | Focus |
|
|
85
|
+
|-------|-------|
|
|
86
|
+
| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |
|
|
87
|
+
| `react` | State, hooks, performance, patterns |
|
|
88
|
+
| `nextjs` | SSR, routing, images, API routes |
|
|
89
|
+
| `vue` | Composition API, Pinia, Vue Router |
|
|
90
|
+
| `svelte` | Runes, stores, SvelteKit |
|
|
91
|
+
| `swiftui` | Views, State, Navigation, Animation |
|
|
92
|
+
| `react-native` | Components, Navigation, Lists |
|
|
93
|
+
| `flutter` | Widgets, State, Layout, Theming |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Example Workflow
|
|
98
|
+
|
|
99
|
+
**User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp"
|
|
100
|
+
|
|
101
|
+
**AI should:**
|
|
102
|
+
|
|
103
|
+
1. Search product type:
|
|
104
|
+
```bash
|
|
105
|
+
python3 .shared/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --domain product
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
2. Search style (based on industry: beauty, elegant):
|
|
109
|
+
```bash
|
|
110
|
+
python3 .shared/ui-ux-pro-max/scripts/search.py "elegant minimal soft" --domain style
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
3. Search typography:
|
|
114
|
+
```bash
|
|
115
|
+
python3 .shared/ui-ux-pro-max/scripts/search.py "elegant luxury" --domain typography
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
4. Search color palette:
|
|
119
|
+
```bash
|
|
120
|
+
python3 .shared/ui-ux-pro-max/scripts/search.py "beauty spa wellness" --domain color
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
5. Search landing page structure:
|
|
124
|
+
```bash
|
|
125
|
+
python3 .shared/ui-ux-pro-max/scripts/search.py "hero-centric social-proof" --domain landing
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
6. Search UX guidelines:
|
|
129
|
+
```bash
|
|
130
|
+
python3 .shared/ui-ux-pro-max/scripts/search.py "animation" --domain ux
|
|
131
|
+
python3 .shared/ui-ux-pro-max/scripts/search.py "accessibility" --domain ux
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
7. Search stack guidelines (default: html-tailwind):
|
|
135
|
+
```bash
|
|
136
|
+
python3 .shared/ui-ux-pro-max/scripts/search.py "layout responsive" --stack html-tailwind
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Then:** Synthesize all search results and implement the design.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Tips for Better Results
|
|
144
|
+
|
|
145
|
+
1. **Be specific with keywords** - "healthcare SaaS dashboard" > "app"
|
|
146
|
+
2. **Search multiple times** - Different keywords reveal different insights
|
|
147
|
+
3. **Combine domains** - Style + Typography + Color = Complete design system
|
|
148
|
+
4. **Always check UX** - Search "animation", "z-index", "accessibility" for common issues
|
|
149
|
+
5. **Use stack flag** - Get implementation-specific best practices
|
|
150
|
+
6. **Iterate** - If first search doesn't match, try different keywords
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Common Rules for Professional UI
|
|
155
|
+
|
|
156
|
+
These are frequently overlooked issues that make UI look unprofessional:
|
|
157
|
+
|
|
158
|
+
### Icons & Visual Elements
|
|
159
|
+
|
|
160
|
+
| Rule | Do | Don't |
|
|
161
|
+
|------|----|----- |
|
|
162
|
+
| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |
|
|
163
|
+
| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |
|
|
164
|
+
| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |
|
|
165
|
+
| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |
|
|
166
|
+
|
|
167
|
+
### Interaction & Cursor
|
|
168
|
+
|
|
169
|
+
| Rule | Do | Don't |
|
|
170
|
+
|------|----|----- |
|
|
171
|
+
| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |
|
|
172
|
+
| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |
|
|
173
|
+
| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |
|
|
174
|
+
|
|
175
|
+
### Light/Dark Mode Contrast
|
|
176
|
+
|
|
177
|
+
| Rule | Do | Don't |
|
|
178
|
+
|------|----|----- |
|
|
179
|
+
| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |
|
|
180
|
+
| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |
|
|
181
|
+
| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |
|
|
182
|
+
| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |
|
|
183
|
+
|
|
184
|
+
### Layout & Spacing
|
|
185
|
+
|
|
186
|
+
| Rule | Do | Don't |
|
|
187
|
+
|------|----|----- |
|
|
188
|
+
| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |
|
|
189
|
+
| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |
|
|
190
|
+
| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Pre-Delivery Checklist
|
|
195
|
+
|
|
196
|
+
Before delivering UI code, verify these items:
|
|
197
|
+
|
|
198
|
+
### Visual Quality
|
|
199
|
+
- [ ] No emojis used as icons (use SVG instead)
|
|
200
|
+
- [ ] All icons from consistent icon set (Heroicons, Lucide, Simple Icons)
|
|
201
|
+
- [ ] Brand logos are correct (verified from Simple Icons)
|
|
202
|
+
- [ ] Hover states don't cause layout shift
|
|
203
|
+
- [ ] Use theme colors directly (bg-primary) not var() wrapper
|
|
204
|
+
|
|
205
|
+
### Interaction
|
|
206
|
+
- [ ] All clickable elements have `cursor-pointer`
|
|
207
|
+
- [ ] Hover states provide clear visual feedback
|
|
208
|
+
- [ ] Transitions are smooth (150-300ms)
|
|
209
|
+
- [ ] Focus states visible for keyboard navigation
|
|
210
|
+
|
|
211
|
+
### Light/Dark Mode
|
|
212
|
+
- [ ] Light mode text has sufficient contrast (4.5:1 minimum)
|
|
213
|
+
- [ ] Glass/transparent elements visible in light mode
|
|
214
|
+
- [ ] Borders visible in both modes
|
|
215
|
+
- [ ] Test both modes before delivery
|
|
216
|
+
|
|
217
|
+
### Layout
|
|
218
|
+
- [ ] Floating elements have proper spacing from edges
|
|
219
|
+
- [ ] No content hidden behind fixed navbars
|
|
220
|
+
- [ ] Responsive at 320px, 768px, 1024px, 1440px
|
|
221
|
+
- [ ] No horizontal scroll on mobile
|
|
222
|
+
|
|
223
|
+
### Accessibility
|
|
224
|
+
- [ ] All images have alt text
|
|
225
|
+
- [ ] Form inputs have labels
|
|
226
|
+
- [ ] Color is not the only indicator
|
|
227
|
+
- [ ] `prefers-reduced-motion` respected
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-agent-router",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "A unified API gateway for managing multiple AI model providers (Anthropic, OpenAI, Gemini, etc.)",
|
|
5
5
|
"main": "dist/src/cli/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"aar": "./dist/src/cli/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"dev": "next dev",
|
|
10
|
+
"dev": "next dev -p 9527",
|
|
11
11
|
"build": "next build && tsc",
|
|
12
12
|
"postbuild": "node scripts/fix-esm-imports.js",
|
|
13
13
|
"start": "node dist/src/cli/index.js start",
|
|
@@ -9,7 +9,7 @@ export async function GET(request: NextRequest) {
|
|
|
9
9
|
try {
|
|
10
10
|
// Initialize database on request (safer than module load)
|
|
11
11
|
getDatabase();
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
const { searchParams } = new URL(request.url);
|
|
14
14
|
const key = searchParams.get('key');
|
|
15
15
|
|
|
@@ -39,18 +39,20 @@ export async function POST(request: NextRequest) {
|
|
|
39
39
|
try {
|
|
40
40
|
// Initialize database on request
|
|
41
41
|
getDatabase();
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
const body = await request.json();
|
|
44
44
|
const { key, value } = body;
|
|
45
45
|
|
|
46
|
-
if (!key
|
|
46
|
+
if (!key) {
|
|
47
47
|
return NextResponse.json(
|
|
48
|
-
{ error: 'Key
|
|
48
|
+
{ error: 'Key is required' },
|
|
49
49
|
{ status: 400 }
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
// Allow value to be null, undefined, or empty string (save as empty string)
|
|
54
|
+
const config = setConfig(key, (value === undefined || value === null) ? '' : typeof value === 'string' ? value : JSON.stringify(value));
|
|
55
|
+
|
|
54
56
|
return NextResponse.json(config);
|
|
55
57
|
} catch (error: any) {
|
|
56
58
|
console.error('Config POST API error:', error);
|
package/src/app/page.tsx
CHANGED
|
@@ -93,7 +93,7 @@ export default function Home() {
|
|
|
93
93
|
const res = await fetch('/api/config');
|
|
94
94
|
const data = await res.json();
|
|
95
95
|
setConfig({
|
|
96
|
-
port: data.port || '
|
|
96
|
+
port: data.port || '1357',
|
|
97
97
|
api_key: data.api_key || '',
|
|
98
98
|
});
|
|
99
99
|
} catch (error) {
|
|
@@ -124,17 +124,19 @@ export default function Home() {
|
|
|
124
124
|
|
|
125
125
|
const handleStart = async () => {
|
|
126
126
|
if (starting || serviceStatus.status === 'running') return;
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
setStarting(true);
|
|
129
129
|
try {
|
|
130
|
-
|
|
130
|
+
// Use default port if not configured
|
|
131
|
+
const port = config.port ? parseInt(config.port, 10) : 1357;
|
|
132
|
+
|
|
131
133
|
const res = await fetch('/api/service/start', {
|
|
132
134
|
method: 'POST',
|
|
133
135
|
headers: { 'Content-Type': 'application/json' },
|
|
134
136
|
body: JSON.stringify({ port }),
|
|
135
137
|
});
|
|
136
138
|
const data = await res.json();
|
|
137
|
-
|
|
139
|
+
|
|
138
140
|
if (data.error) {
|
|
139
141
|
showToast(`启动失败: ${data.error}`, 'error');
|
|
140
142
|
} else {
|
|
@@ -382,10 +384,10 @@ export default function Home() {
|
|
|
382
384
|
onChange={(e) => setConfig({ ...config, port: e.target.value })}
|
|
383
385
|
autoComplete="off"
|
|
384
386
|
className="block w-full rounded-lg border border-slate-200 bg-white/80 px-3 py-2 text-xs shadow-sm transition-all duration-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 focus:ring-offset-1"
|
|
385
|
-
placeholder="
|
|
387
|
+
placeholder="1357"
|
|
386
388
|
/>
|
|
387
389
|
<p className="mt-1 text-xs text-slate-400">
|
|
388
|
-
|
|
390
|
+
提示:API 网关默认端口为 1357
|
|
389
391
|
</p>
|
|
390
392
|
</div>
|
|
391
393
|
|
package/src/cli/index.ts
CHANGED
|
@@ -1,60 +1,66 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import { getConfig, setConfig } from '../db/queries';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import path from 'path';
|
|
7
6
|
|
|
8
7
|
const program = new Command();
|
|
9
8
|
|
|
10
9
|
program
|
|
11
10
|
.name('aar')
|
|
12
|
-
.description('AI Agent Router -
|
|
11
|
+
.description('AI Agent Router - Web UI for managing the API gateway')
|
|
13
12
|
.version('0.1.0');
|
|
14
13
|
|
|
15
14
|
program
|
|
16
15
|
.command('start')
|
|
17
|
-
.description('Start the
|
|
18
|
-
.option('-p, --port <port>', 'Port
|
|
19
|
-
.option('--hostname <hostname>', 'Hostname to listen on', 'localhost')
|
|
16
|
+
.description('Start the Web UI management interface')
|
|
17
|
+
.option('-p, --port <port>', 'Port for Web UI', '9527')
|
|
20
18
|
.action(async (options) => {
|
|
21
|
-
const port = parseInt(options.port || '
|
|
22
|
-
const hostname = options.hostname || 'localhost';
|
|
19
|
+
const port = parseInt(options.port || '9527');
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
console.error('Failed to initialize database:', error);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
21
|
+
console.log(`Starting AI Agent Router Web UI`);
|
|
22
|
+
console.log(` Port: ${port}`);
|
|
23
|
+
console.log(` Access the UI at: http://localhost:${port}`);
|
|
24
|
+
console.log('');
|
|
31
25
|
|
|
32
|
-
//
|
|
33
|
-
const
|
|
34
|
-
|
|
26
|
+
// Start Web UI using Next.js
|
|
27
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
28
|
+
let uiProcess: ReturnType<typeof spawn>;
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
if (isDev) {
|
|
31
|
+
// In development mode, use next dev
|
|
32
|
+
uiProcess = spawn('npm', ['run', 'dev', '--', '-p', port.toString()], {
|
|
33
|
+
cwd: process.cwd(),
|
|
34
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
35
|
+
env: { ...process.env, NEXT_TELEMETRY_DISABLED: '1', PORT: port.toString() },
|
|
36
|
+
});
|
|
41
37
|
} else {
|
|
42
|
-
|
|
38
|
+
// In production mode, start the built Next.js app
|
|
39
|
+
const serverPath = path.join(process.cwd(), 'node_modules', 'next', 'dist', 'bin', 'next');
|
|
40
|
+
uiProcess = spawn(process.execPath, [serverPath, 'start', '-p', port.toString()], {
|
|
41
|
+
cwd: process.cwd(),
|
|
42
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
43
|
+
env: { ...process.env, PORT: port.toString(), NODE_ENV: 'production' },
|
|
44
|
+
});
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
apiKey,
|
|
47
|
+
// Handle UI process exit
|
|
48
|
+
uiProcess.on('exit', (code) => {
|
|
49
|
+
console.log(`Web UI process exited with code ${code}`);
|
|
50
|
+
process.exit(code || 0);
|
|
50
51
|
});
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} catch (error: any) {
|
|
55
|
-
console.error(`Failed to start gateway server: ${error.message}`);
|
|
53
|
+
uiProcess.on('error', (error) => {
|
|
54
|
+
console.error(`Failed to start Web UI: ${error.message}`);
|
|
56
55
|
process.exit(1);
|
|
57
|
-
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Keep the process alive
|
|
59
|
+
process.on('SIGINT', () => {
|
|
60
|
+
console.log('\nShutting down...');
|
|
61
|
+
uiProcess.kill('SIGTERM');
|
|
62
|
+
process.exit(0);
|
|
63
|
+
});
|
|
58
64
|
});
|
|
59
65
|
|
|
60
66
|
program
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
|
-
import { getServiceStatus, setServiceStatus, updateServiceStatus,
|
|
4
|
-
import
|
|
5
|
-
import
|
|
3
|
+
import { getServiceStatus, setServiceStatus, updateServiceStatus, getConfig } from '@/db/queries';
|
|
4
|
+
import { getDatabase } from '@/db/database';
|
|
5
|
+
import { GatewayServer } from './gateway-server';
|
|
6
6
|
import net from 'net';
|
|
7
7
|
|
|
8
8
|
const execAsync = promisify(exec);
|
|
@@ -16,11 +16,11 @@ export interface ServiceStatusResponse {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
class ServiceManager {
|
|
19
|
-
private
|
|
19
|
+
private gatewayServer: GatewayServer | null = null;
|
|
20
20
|
private isStarting: boolean = false;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Check if a process with
|
|
23
|
+
* Check if a process with given PID exists
|
|
24
24
|
*/
|
|
25
25
|
private async checkProcessExists(pid: number): Promise<boolean> {
|
|
26
26
|
try {
|
|
@@ -39,12 +39,12 @@ class ServiceManager {
|
|
|
39
39
|
private async checkPortAvailable(port: number): Promise<boolean> {
|
|
40
40
|
return new Promise((resolve) => {
|
|
41
41
|
const server = net.createServer();
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
server.listen(port, () => {
|
|
44
44
|
server.once('close', () => resolve(true));
|
|
45
45
|
server.close();
|
|
46
46
|
});
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
server.on('error', () => {
|
|
49
49
|
resolve(false);
|
|
50
50
|
});
|
|
@@ -57,12 +57,12 @@ class ServiceManager {
|
|
|
57
57
|
private async checkPortInUse(port: number): Promise<{ inUse: boolean; processInfo?: string }> {
|
|
58
58
|
return new Promise((resolve) => {
|
|
59
59
|
const server = net.createServer();
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
server.listen(port, () => {
|
|
62
62
|
server.once('close', () => resolve({ inUse: false }));
|
|
63
63
|
server.close();
|
|
64
64
|
});
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
server.on('error', (err: any) => {
|
|
67
67
|
if (err.code === 'EADDRINUSE') {
|
|
68
68
|
// Try to get process info
|
|
@@ -90,7 +90,7 @@ class ServiceManager {
|
|
|
90
90
|
*/
|
|
91
91
|
async getStatus(): Promise<ServiceStatusResponse> {
|
|
92
92
|
const dbStatus = getServiceStatus();
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
if (!dbStatus) {
|
|
95
95
|
return { status: 'stopped' };
|
|
96
96
|
}
|
|
@@ -114,7 +114,7 @@ class ServiceManager {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* Start
|
|
117
|
+
* Start gateway service
|
|
118
118
|
*/
|
|
119
119
|
async start(port: number): Promise<ServiceStatusResponse> {
|
|
120
120
|
// Prevent concurrent starts
|
|
@@ -128,138 +128,42 @@ class ServiceManager {
|
|
|
128
128
|
return { status: 'running', error: 'Service is already running', port: currentStatus.port, pid: currentStatus.pid };
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
// Check port availability
|
|
131
|
+
// Check port availability
|
|
132
132
|
const portCheck = await this.checkPortInUse(port);
|
|
133
133
|
if (portCheck.inUse) {
|
|
134
|
-
// In development, if port 3000 is in use, suggest using a different port
|
|
135
|
-
const isDev = process.env.NODE_ENV !== 'production';
|
|
136
|
-
if (isDev && port === 3000) {
|
|
137
|
-
return {
|
|
138
|
-
status: 'stopped',
|
|
139
|
-
error: `Port 3000 is already in use (likely by the development server). Please configure a different port (e.g., 3001) in the settings.`
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
134
|
return { status: 'stopped', error: portCheck.processInfo || `Port ${port} is already in use` };
|
|
143
135
|
}
|
|
144
136
|
|
|
145
137
|
this.isStarting = true;
|
|
146
138
|
|
|
147
139
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// Create isolated environment for child process
|
|
154
|
-
// Remove Next.js dev server specific env vars to avoid conflicts
|
|
155
|
-
const childEnv = { ...process.env };
|
|
156
|
-
// Remove PORT if it might conflict
|
|
157
|
-
if (childEnv.PORT && parseInt(childEnv.PORT) === 3000) {
|
|
158
|
-
delete childEnv.PORT;
|
|
159
|
-
}
|
|
160
|
-
// Remove Next.js specific env vars that might cause conflicts
|
|
161
|
-
delete childEnv.NEXT_TELEMETRY_DISABLED;
|
|
162
|
-
// Gateway server doesn't need Next.js, so we can use any NODE_ENV
|
|
163
|
-
childEnv.NODE_ENV = process.env.NODE_ENV || 'production';
|
|
164
|
-
|
|
165
|
-
// Check if compiled code exists
|
|
166
|
-
if (fs.existsSync(cliPath)) {
|
|
167
|
-
// Use compiled JavaScript
|
|
168
|
-
child = spawn(process.execPath, [cliPath, 'start', '-p', port.toString()], {
|
|
169
|
-
detached: false, // Keep attached for proper tracking
|
|
170
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
171
|
-
cwd: process.cwd(),
|
|
172
|
-
env: childEnv,
|
|
173
|
-
});
|
|
174
|
-
} else if (fs.existsSync(tsCliPath)) {
|
|
175
|
-
// In development, try to use tsx via npx
|
|
176
|
-
// Note: tsx should be installed as dev dependency for this to work
|
|
177
|
-
child = spawn('npx', ['--yes', 'tsx', tsCliPath, 'start', '-p', port.toString()], {
|
|
178
|
-
detached: false, // Keep attached for proper tracking
|
|
179
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
180
|
-
cwd: process.cwd(),
|
|
181
|
-
env: childEnv,
|
|
182
|
-
shell: true, // Use shell to resolve npx
|
|
183
|
-
});
|
|
184
|
-
} else {
|
|
140
|
+
// Initialize database
|
|
141
|
+
try {
|
|
142
|
+
getDatabase();
|
|
143
|
+
} catch (error: any) {
|
|
144
|
+
console.error('Failed to initialize database:', error);
|
|
185
145
|
this.isStarting = false;
|
|
186
|
-
return {
|
|
187
|
-
status: 'stopped',
|
|
188
|
-
error: 'CLI not found. Please run "npm run build" first, or ensure src/cli/index.ts exists.'
|
|
189
|
-
};
|
|
146
|
+
return { status: 'stopped', error: 'Failed to initialize database' };
|
|
190
147
|
}
|
|
191
148
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
let errorOutput = '';
|
|
196
|
-
let hasExited = false;
|
|
197
|
-
let exitCode: number | null = null;
|
|
198
|
-
|
|
199
|
-
// Handle process output (optional, for debugging)
|
|
200
|
-
// Use setImmediate to avoid blocking the event loop
|
|
201
|
-
child.stdout?.on('data', (data) => {
|
|
202
|
-
setImmediate(() => {
|
|
203
|
-
const output = data.toString();
|
|
204
|
-
// Only log if it's not empty and not just whitespace
|
|
205
|
-
if (output.trim()) {
|
|
206
|
-
console.log(`[Gateway Service] ${output}`);
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
child.stderr?.on('data', (data) => {
|
|
212
|
-
setImmediate(() => {
|
|
213
|
-
const errorText = data.toString();
|
|
214
|
-
console.error(`[Gateway Service Error] ${errorText}`);
|
|
215
|
-
errorOutput += errorText;
|
|
216
|
-
});
|
|
217
|
-
});
|
|
149
|
+
// Get API key from config if configured
|
|
150
|
+
const apiKeyConfig = getConfig('api_key');
|
|
151
|
+
const apiKey = apiKeyConfig ? apiKeyConfig.value : undefined;
|
|
218
152
|
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
this.childProcess = null;
|
|
225
|
-
this.isStarting = false;
|
|
226
|
-
|
|
227
|
-
// Update database status
|
|
228
|
-
updateServiceStatus({ status: 'stopped', pid: null });
|
|
153
|
+
// Create and start gateway server directly in-process
|
|
154
|
+
const server = new GatewayServer({
|
|
155
|
+
port,
|
|
156
|
+
hostname: 'localhost',
|
|
157
|
+
apiKey,
|
|
229
158
|
});
|
|
230
159
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// Check if process exited during startup
|
|
235
|
-
if (hasExited || child.killed || child.exitCode !== null) {
|
|
236
|
-
this.isStarting = false;
|
|
237
|
-
this.childProcess = null;
|
|
238
|
-
|
|
239
|
-
// Extract meaningful error message
|
|
240
|
-
let errorMessage = 'Service failed to start';
|
|
241
|
-
if (errorOutput) {
|
|
242
|
-
// Try to extract the main error message
|
|
243
|
-
const errorMatch = errorOutput.match(/Error: ([^\n]+)/);
|
|
244
|
-
if (errorMatch) {
|
|
245
|
-
errorMessage = errorMatch[1];
|
|
246
|
-
} else if (errorOutput.includes('production build')) {
|
|
247
|
-
errorMessage = 'Production build not found. Please run "npm run build" first, or use development mode.';
|
|
248
|
-
} else {
|
|
249
|
-
// Use first meaningful line of error
|
|
250
|
-
const lines = errorOutput.split('\n').filter(line => line.trim());
|
|
251
|
-
if (lines.length > 0) {
|
|
252
|
-
errorMessage = lines[0].substring(0, 200); // Limit length
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return { status: 'stopped', error: errorMessage };
|
|
258
|
-
}
|
|
160
|
+
await server.start();
|
|
161
|
+
this.gatewayServer = server;
|
|
259
162
|
|
|
260
|
-
//
|
|
261
|
-
const pid =
|
|
163
|
+
// Use current process PID
|
|
164
|
+
const pid = process.pid;
|
|
262
165
|
const startedAt = new Date().toISOString();
|
|
166
|
+
|
|
263
167
|
setServiceStatus({
|
|
264
168
|
status: 'running',
|
|
265
169
|
port,
|
|
@@ -269,6 +173,8 @@ class ServiceManager {
|
|
|
269
173
|
|
|
270
174
|
this.isStarting = false;
|
|
271
175
|
|
|
176
|
+
console.log(`Gateway server started on port ${port}`);
|
|
177
|
+
|
|
272
178
|
return {
|
|
273
179
|
status: 'running',
|
|
274
180
|
port,
|
|
@@ -277,7 +183,8 @@ class ServiceManager {
|
|
|
277
183
|
};
|
|
278
184
|
} catch (error: any) {
|
|
279
185
|
this.isStarting = false;
|
|
280
|
-
this.
|
|
186
|
+
this.gatewayServer = null;
|
|
187
|
+
console.error(`Failed to start gateway service: ${error.message}`);
|
|
281
188
|
return { status: 'stopped', error: error.message || 'Failed to start service' };
|
|
282
189
|
}
|
|
283
190
|
}
|
|
@@ -287,18 +194,18 @@ class ServiceManager {
|
|
|
287
194
|
*/
|
|
288
195
|
async stop(): Promise<ServiceStatusResponse> {
|
|
289
196
|
const currentStatus = await this.getStatus();
|
|
290
|
-
|
|
197
|
+
|
|
291
198
|
if (currentStatus.status === 'stopped') {
|
|
292
199
|
return { status: 'stopped' };
|
|
293
200
|
}
|
|
294
201
|
|
|
295
202
|
try {
|
|
296
|
-
// If we have a reference to
|
|
297
|
-
if (this.
|
|
298
|
-
this.
|
|
299
|
-
this.
|
|
300
|
-
} else if (currentStatus.pid) {
|
|
301
|
-
//
|
|
203
|
+
// If we have a reference to gateway server, stop it
|
|
204
|
+
if (this.gatewayServer) {
|
|
205
|
+
await this.gatewayServer.stop();
|
|
206
|
+
this.gatewayServer = null;
|
|
207
|
+
} else if (currentStatus.pid && currentStatus.pid !== process.pid) {
|
|
208
|
+
// Only try to kill by PID if it's a different process
|
|
302
209
|
try {
|
|
303
210
|
process.kill(currentStatus.pid, 'SIGTERM');
|
|
304
211
|
} catch (error) {
|