trae-starter 0.0.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/README.md +45 -0
- package/bin/cli.js +33 -0
- package/package.json +25 -0
- package/public/app.js +263 -0
- package/public/index.html +123 -0
- package/server/api.js +181 -0
- package/server/index.js +38 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/SKILL.md +659 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/_sync_all.py +414 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/app-interface.csv +31 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/colors.csv +162 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/design.csv +1776 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/draft.csv +1779 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/google-fonts.csv +1924 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/icons.csv +106 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/landing.csv +35 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/products.csv +162 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/styles.csv +85 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/typography.csv +74 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/ui-reasoning.csv +162 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/scripts/core.py +247 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/templates/proto/.agents/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/templates/proto/.trae/skills/preview-manager/SKILL.md +37 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/SKILL.md +659 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/_sync_all.py +414 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/app-interface.csv +31 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/colors.csv +162 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/design.csv +1776 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/draft.csv +1779 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/google-fonts.csv +1924 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/icons.csv +106 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/landing.csv +35 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/products.csv +162 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/styles.csv +85 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/typography.csv +74 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/ui-reasoning.csv +162 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/scripts/core.py +247 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/templates/proto/.trae/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/templates/proto/README.md +77 -0
- package/templates/proto/agent.md +361 -0
- package/templates/proto/eslint.config.js +23 -0
- package/templates/proto/index.html +13 -0
- package/templates/proto/package.json +39 -0
- package/templates/proto/pnpm-lock.yaml +2401 -0
- package/templates/proto/postcss.config.js +6 -0
- package/templates/proto/public/favicon.svg +1 -0
- package/templates/proto/public/icons.svg +24 -0
- package/templates/proto/skills-lock.json +10 -0
- package/templates/proto/src/App.css +184 -0
- package/templates/proto/src/App.tsx +15 -0
- package/templates/proto/src/assets/hero.png +0 -0
- package/templates/proto/src/assets/react.svg +1 -0
- package/templates/proto/src/assets/vite.svg +1 -0
- package/templates/proto/src/components/ui/SampleButton/sampleButton.css +4 -0
- package/templates/proto/src/components/ui/SampleButton/sampleButton.tsx +14 -0
- package/templates/proto/src/data/mock/products.csv +4 -0
- package/templates/proto/src/data/mock/users.json +16 -0
- package/templates/proto/src/data/types.ts +17 -0
- package/templates/proto/src/index.css +64 -0
- package/templates/proto/src/main.tsx +13 -0
- package/templates/proto/src/pages/Demo/components/DemoHeader/demoHeader.tsx +20 -0
- package/templates/proto/src/pages/Demo/components/FeatureGrid/featureGrid.tsx +18 -0
- package/templates/proto/src/pages/Demo/index.tsx +29 -0
- package/templates/proto/src/pages/Home/components/FeatureCard/featureCard.tsx +15 -0
- package/templates/proto/src/pages/Home/components/HeroSection/heroSection.tsx +28 -0
- package/templates/proto/src/pages/Home/components/InteractiveControls/interactiveControls.tsx +27 -0
- package/templates/proto/src/pages/Home/index.tsx +39 -0
- package/templates/proto/src/utils/format.ts +7 -0
- package/templates/proto/tailwind.config.js +11 -0
- package/templates/proto/tsconfig.app.json +28 -0
- package/templates/proto/tsconfig.json +7 -0
- package/templates/proto/tsconfig.node.json +26 -0
- package/templates/proto/vite.config.ts +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Trae Starter
|
|
2
|
+
|
|
3
|
+
`trae-starter` 是一个用于快速启动和管理 Trae 项目的 CLI 工具。它提供了一个直观的 Web 界面,让你能够轻松管理工作区、创建新项目,并一键在 Trae IDE 中打开它们。
|
|
4
|
+
|
|
5
|
+
## ✨ 主要功能
|
|
6
|
+
|
|
7
|
+
- **可视化项目管理**:启动一个本地 Web 服务,在浏览器中直观地管理你的项目。
|
|
8
|
+
- **工作区管理**:轻松选择和切换当前的工作区目录。
|
|
9
|
+
- **项目创建**:支持基于预设模板(如 `proto` 模板)快速创建新项目。
|
|
10
|
+
- **一键打开 Trae**:通过 `trae-cn://` 自定义协议,一键将项目在 Trae IDE 中打开。
|
|
11
|
+
|
|
12
|
+
## 🚀 快速开始 (Get Started)
|
|
13
|
+
|
|
14
|
+
最简单的使用方式是直接通过 `npx` 运行,无需全局安装:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx trae-starter web
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
启动后,命令行会自动在浏览器中打开 `http://localhost:5050`。
|
|
21
|
+
|
|
22
|
+
### 命令行选项
|
|
23
|
+
|
|
24
|
+
- `-p, --port <number>`: 指定 Web 服务运行的端口(默认:5050)
|
|
25
|
+
|
|
26
|
+
例如,要在 8080 端口启动服务:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx trae-starter web -p 8080
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 📦 安装 (可选)
|
|
33
|
+
|
|
34
|
+
如果你希望经常使用,也可以将其全局安装:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g trae-starter
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
安装后直接运行:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
trae-starter web
|
|
44
|
+
```
|
|
45
|
+
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { startServer } = require('../server/index');
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('trae-starter')
|
|
11
|
+
.description('CLI to start Trae projects with a web UI')
|
|
12
|
+
.version('1.0.0');
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('web')
|
|
16
|
+
.description('Start the web interface')
|
|
17
|
+
.option('-p, --port <number>', 'Port to run the server on', '5050')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
const port = parseInt(options.port, 10);
|
|
20
|
+
try {
|
|
21
|
+
const serverUrl = await startServer(port);
|
|
22
|
+
console.log(`Server started at ${serverUrl}`);
|
|
23
|
+
console.log('Opening browser...');
|
|
24
|
+
|
|
25
|
+
const open = (await import('open')).default;
|
|
26
|
+
await open(serverUrl);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('Failed to start server:', error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trae-starter",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A CLI tool to start Trae projects with a web UI",
|
|
5
|
+
"bin": {
|
|
6
|
+
"trae-starter": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node bin/cli.js web"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"server",
|
|
14
|
+
"public",
|
|
15
|
+
"templates"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"body-parser": "^1.20.2",
|
|
19
|
+
"commander": "^11.1.0",
|
|
20
|
+
"cors": "^2.8.5",
|
|
21
|
+
"express": "^4.18.2",
|
|
22
|
+
"fs-extra": "^11.2.0",
|
|
23
|
+
"open": "^10.0.3"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/public/app.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const API_BASE = '/api';
|
|
2
|
+
|
|
3
|
+
// State
|
|
4
|
+
let workspace = '';
|
|
5
|
+
let projects = [];
|
|
6
|
+
let templates = [];
|
|
7
|
+
|
|
8
|
+
// DOM Elements
|
|
9
|
+
const workspaceEl = document.getElementById('workspace-path');
|
|
10
|
+
const changeWorkspaceBtn = document.getElementById('change-workspace-btn');
|
|
11
|
+
const projectListEl = document.getElementById('project-list');
|
|
12
|
+
const newProjectBtn = document.getElementById('new-project-btn');
|
|
13
|
+
const modal = document.getElementById('modal');
|
|
14
|
+
const closeModalBtn = document.getElementById('close-modal-btn');
|
|
15
|
+
const cancelBtn = document.getElementById('cancel-btn');
|
|
16
|
+
const newProjectForm = document.getElementById('new-project-form');
|
|
17
|
+
const projectNameInput = document.getElementById('project-name');
|
|
18
|
+
const projectTemplateSelect = document.getElementById('project-template');
|
|
19
|
+
|
|
20
|
+
// Initialize
|
|
21
|
+
async function init() {
|
|
22
|
+
await fetchWorkspace();
|
|
23
|
+
await fetchTemplates();
|
|
24
|
+
await fetchProjects();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// API Calls
|
|
28
|
+
async function fetchWorkspace() {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(`${API_BASE}/workspace`);
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
workspace = data.workspace;
|
|
33
|
+
workspaceEl.textContent = workspace;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('Failed to fetch workspace', err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function updateWorkspace(newPath) {
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(`${API_BASE}/workspace`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ workspace: newPath })
|
|
45
|
+
});
|
|
46
|
+
if (res.ok) {
|
|
47
|
+
await fetchWorkspace();
|
|
48
|
+
await fetchProjects();
|
|
49
|
+
} else {
|
|
50
|
+
alert('Failed to update workspace. Check if path exists.');
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error('Failed to update workspace', err);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fetchProjects() {
|
|
58
|
+
try {
|
|
59
|
+
projectListEl.innerHTML = '<div class="col-span-full text-center py-12 text-gray-500">Loading projects...</div>';
|
|
60
|
+
const res = await fetch(`${API_BASE}/projects`);
|
|
61
|
+
const data = await res.json();
|
|
62
|
+
projects = data.projects || []; // Ensure projects is an array
|
|
63
|
+
renderProjects();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('Failed to fetch projects', err);
|
|
66
|
+
projectListEl.innerHTML = '<div class="col-span-full text-center py-12 text-red-500">Error loading projects</div>';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fetchTemplates() {
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(`${API_BASE}/templates`);
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
templates = data.templates || []; // Ensure templates is an array
|
|
75
|
+
renderTemplates();
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('Failed to fetch templates', err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function createProject(name, template) {
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`${API_BASE}/projects`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({ name, template })
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (res.ok) {
|
|
90
|
+
closeModal();
|
|
91
|
+
await fetchProjects();
|
|
92
|
+
// Automatically open the new project? Maybe just list it.
|
|
93
|
+
// Let's ask if user wants to open it or just refresh list.
|
|
94
|
+
// For now, refresh list.
|
|
95
|
+
} else {
|
|
96
|
+
const data = await res.json();
|
|
97
|
+
alert(`Error: ${data.error}`);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error('Failed to create project', err);
|
|
101
|
+
alert('Failed to create project');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function openInTrae(projectPath) {
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetch(`${API_BASE}/open`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
110
|
+
body: JSON.stringify({ projectPath })
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
const data = await res.json();
|
|
115
|
+
alert(`Error: ${data.error}`);
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error('Failed to open project', err);
|
|
119
|
+
alert('Failed to open project in Trae');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Utility to generate a consistent neon color based on string (project name)
|
|
124
|
+
function getNeonColor(str) {
|
|
125
|
+
const colors = [
|
|
126
|
+
{ text: 'text-cyan-400', bg: 'bg-cyan-500/10', border: 'border-cyan-500/20', shadow: 'shadow-[0_0_10px_rgba(34,211,238,0.1)]' },
|
|
127
|
+
{ text: 'text-purple-400', bg: 'bg-purple-500/10', border: 'border-purple-500/20', shadow: 'shadow-[0_0_10px_rgba(168,85,247,0.1)]' },
|
|
128
|
+
{ text: 'text-emerald-400', bg: 'bg-emerald-500/10', border: 'border-emerald-500/20', shadow: 'shadow-[0_0_10px_rgba(52,211,153,0.1)]' },
|
|
129
|
+
{ text: 'text-pink-400', bg: 'bg-pink-500/10', border: 'border-pink-500/20', shadow: 'shadow-[0_0_10px_rgba(244,114,182,0.1)]' },
|
|
130
|
+
{ text: 'text-amber-400', bg: 'bg-amber-500/10', border: 'border-amber-500/20', shadow: 'shadow-[0_0_10px_rgba(251,191,36,0.1)]' },
|
|
131
|
+
{ text: 'text-blue-400', bg: 'bg-blue-500/10', border: 'border-blue-500/20', shadow: 'shadow-[0_0_10px_rgba(96,165,250,0.1)]' },
|
|
132
|
+
{ text: 'text-rose-400', bg: 'bg-rose-500/10', border: 'border-rose-500/20', shadow: 'shadow-[0_0_10px_rgba(251,113,133,0.1)]' }
|
|
133
|
+
];
|
|
134
|
+
let hash = 0;
|
|
135
|
+
for (let i = 0; i < str.length; i++) {
|
|
136
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
137
|
+
}
|
|
138
|
+
return colors[Math.abs(hash) % colors.length];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Rendering
|
|
142
|
+
function renderProjects() {
|
|
143
|
+
if (projects.length === 0) {
|
|
144
|
+
projectListEl.innerHTML = `
|
|
145
|
+
<div class="col-span-full glass-panel rounded-2xl p-12 text-center flex flex-col items-center justify-center border border-dashed border-slate-700/50 relative overflow-hidden">
|
|
146
|
+
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-cyan-900/5 opacity-50"></div>
|
|
147
|
+
<svg class="w-16 h-16 mb-4 text-slate-600 relative z-10 drop-shadow-lg" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>
|
|
148
|
+
<p class="text-slate-400 mb-5 font-medium relative z-10 text-lg">当前工作区暂无项目</p>
|
|
149
|
+
<button onclick="openModal()" class="relative z-10 text-cyan-400 hover:text-cyan-300 neon-text font-semibold flex items-center gap-2 transition-all duration-200 group">
|
|
150
|
+
<svg class="w-5 h-5 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
|
151
|
+
初始化您的第一个项目
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
`;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
projectListEl.innerHTML = projects.map(project => {
|
|
159
|
+
const theme = getNeonColor(project.name);
|
|
160
|
+
return `
|
|
161
|
+
<div class="glass-panel rounded-2xl overflow-hidden hover:-translate-y-1 transition-all duration-300 group hover:shadow-[0_8px_30px_rgba(34,211,238,0.15)] hover:border-cyan-500/30">
|
|
162
|
+
<div class="p-6 relative h-full flex flex-col">
|
|
163
|
+
<!-- Decorative top gradient -->
|
|
164
|
+
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-slate-600 to-transparent group-hover:via-cyan-400 transition-all duration-300 opacity-50"></div>
|
|
165
|
+
|
|
166
|
+
<div class="flex justify-between items-start mb-4">
|
|
167
|
+
<div class="${theme.bg} ${theme.text} ${theme.border} text-[10px] font-bold px-2.5 py-1 rounded-lg uppercase tracking-wider border ${theme.shadow}">项目</div>
|
|
168
|
+
<span class="text-[10px] font-medium text-slate-500 bg-slate-900/80 px-2 py-1 rounded-md border border-slate-800">${new Date(project.modified).toLocaleDateString()}</span>
|
|
169
|
+
</div>
|
|
170
|
+
<h3 class="text-xl font-bold text-slate-100 mb-3 truncate group-hover:text-cyan-300 transition-colors" title="${project.name}">${project.name}</h3>
|
|
171
|
+
|
|
172
|
+
<div class="group/path relative flex items-center text-slate-500 mb-6 bg-slate-900/60 p-2.5 rounded-xl border border-slate-800/80 mt-auto cursor-help overflow-hidden">
|
|
173
|
+
<svg class="w-4 h-4 mr-2.5 flex-shrink-0 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path></svg>
|
|
174
|
+
|
|
175
|
+
<!-- Truncated text (RTL trick to show end of path) -->
|
|
176
|
+
<p class="text-xs font-mono truncate text-slate-400 w-full transition-opacity duration-200 group-hover/path:opacity-0" style="direction: rtl; text-align: left;">
|
|
177
|
+
<span style="direction: ltr; unicode-bidi: embed;">${project.path}</span>
|
|
178
|
+
</p>
|
|
179
|
+
|
|
180
|
+
<!-- Full path on hover (Absolute positioning) -->
|
|
181
|
+
<div class="absolute inset-0 bg-slate-900/95 p-2.5 flex items-center opacity-0 group-hover/path:opacity-100 transition-opacity duration-200 z-10 overflow-x-auto whitespace-nowrap custom-scrollbar">
|
|
182
|
+
<p class="text-[10px] font-mono text-cyan-300">${project.path}</p>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<button onclick="openInTrae('${project.path.replace(/\\/g, '\\\\')}')" class="w-full bg-slate-800/80 hover:bg-cyan-500/10 text-slate-300 hover:text-cyan-400 font-bold py-3 px-4 rounded-xl border border-slate-700 hover:border-cyan-500/40 flex justify-center items-center transition-all duration-200 hover:shadow-[0_0_15px_rgba(34,211,238,0.15)] group/btn">
|
|
187
|
+
<svg class="w-5 h-5 mr-2 text-slate-500 group-hover/btn:text-cyan-400 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
|
|
188
|
+
在 Trae 中打开
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
`;
|
|
193
|
+
}).join('');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderTemplates() {
|
|
197
|
+
// Only show available templates, remove "Empty Project" option
|
|
198
|
+
const templateOptions = templates.map(t => `<option value="${t}">${t}</option>`).join('');
|
|
199
|
+
projectTemplateSelect.innerHTML = templateOptions;
|
|
200
|
+
|
|
201
|
+
// Select the first template by default if available
|
|
202
|
+
if (templates.length > 0) {
|
|
203
|
+
projectTemplateSelect.value = templates[0];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Event Listeners
|
|
208
|
+
changeWorkspaceBtn.addEventListener('click', async () => {
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch(`${API_BASE}/choose-directory`, { method: 'POST' });
|
|
211
|
+
if (res.ok) {
|
|
212
|
+
const data = await res.json();
|
|
213
|
+
if (data.path) {
|
|
214
|
+
updateWorkspace(data.path);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
// Fallback to manual entry if dialog fails or is cancelled
|
|
218
|
+
const newPath = prompt('Enter absolute path to workspace:', workspace);
|
|
219
|
+
if (newPath && newPath !== workspace) {
|
|
220
|
+
updateWorkspace(newPath);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error('Failed to open directory picker', err);
|
|
225
|
+
const newPath = prompt('Enter absolute path to workspace:', workspace);
|
|
226
|
+
if (newPath && newPath !== workspace) {
|
|
227
|
+
updateWorkspace(newPath);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
newProjectBtn.addEventListener('click', openModal);
|
|
233
|
+
closeModalBtn.addEventListener('click', closeModal);
|
|
234
|
+
cancelBtn.addEventListener('click', closeModal);
|
|
235
|
+
|
|
236
|
+
newProjectForm.addEventListener('submit', (e) => {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
const name = projectNameInput.value.trim();
|
|
239
|
+
const template = projectTemplateSelect.value;
|
|
240
|
+
if (name) {
|
|
241
|
+
createProject(name, template);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Modal Helpers
|
|
246
|
+
function openModal() {
|
|
247
|
+
modal.classList.remove('hidden');
|
|
248
|
+
modal.classList.add('flex');
|
|
249
|
+
projectNameInput.focus();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function closeModal() {
|
|
253
|
+
modal.classList.add('hidden');
|
|
254
|
+
modal.classList.remove('flex');
|
|
255
|
+
newProjectForm.reset();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Make functions global for inline onclick handlers
|
|
259
|
+
window.openModal = openModal;
|
|
260
|
+
window.openInTrae = openInTrae;
|
|
261
|
+
|
|
262
|
+
// Start
|
|
263
|
+
init();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Trae 启动器</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<style>
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
|
|
10
|
+
.glass-panel {
|
|
11
|
+
background: rgba(30, 41, 59, 0.7);
|
|
12
|
+
backdrop-filter: blur(12px);
|
|
13
|
+
-webkit-backdrop-filter: blur(12px);
|
|
14
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
15
|
+
}
|
|
16
|
+
.neon-text {
|
|
17
|
+
text-shadow: 0 0 10px rgba(34, 211, 238, 0.5);
|
|
18
|
+
}
|
|
19
|
+
/* Custom Scrollbar for dark theme */
|
|
20
|
+
::-webkit-scrollbar { width: 8px; }
|
|
21
|
+
::-webkit-scrollbar-track { background: #0f172a; }
|
|
22
|
+
::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
|
|
23
|
+
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body class="bg-[#0B0F19] text-slate-200 min-h-screen relative overflow-x-hidden selection:bg-cyan-500/30 selection:text-cyan-200">
|
|
27
|
+
|
|
28
|
+
<!-- Background Effects -->
|
|
29
|
+
<div class="fixed inset-0 z-[-1] bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-cyan-900/20 via-[#0B0F19] to-[#0B0F19]"></div>
|
|
30
|
+
<div class="fixed inset-0 z-[-1] bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMiIgY3k9IjIiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiLz48L3N2Zz4=')] [mask-image:linear-gradient(to_bottom,white,transparent)]"></div>
|
|
31
|
+
|
|
32
|
+
<div id="app" class="container mx-auto px-4 py-12 max-w-5xl">
|
|
33
|
+
|
|
34
|
+
<!-- Header -->
|
|
35
|
+
<header class="flex flex-col md:flex-row justify-between items-start md:items-center mb-12 gap-6">
|
|
36
|
+
<div class="flex items-center gap-3">
|
|
37
|
+
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-cyan-400 to-blue-600 flex items-center justify-center shadow-[0_0_20px_rgba(34,211,238,0.4)]">
|
|
38
|
+
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
|
39
|
+
</div>
|
|
40
|
+
<h1 class="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 to-blue-500 neon-text tracking-tight">Trae 启动器</h1>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="glass-panel px-4 py-3 rounded-2xl flex items-center space-x-4 shadow-lg">
|
|
44
|
+
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider whitespace-nowrap">当前工作区</span>
|
|
45
|
+
<code id="workspace-path" class="bg-slate-900/50 px-3 py-1.5 rounded-lg border border-slate-700/50 text-sm font-mono text-cyan-300 max-w-[200px] md:max-w-xs truncate" style="direction: rtl; text-align: left;">加载中...</code>
|
|
46
|
+
<button id="change-workspace-btn" class="text-cyan-400 hover:text-cyan-300 hover:neon-text text-sm font-medium transition-all duration-200 shrink-0" title="更改工作区">
|
|
47
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
<!-- Actions -->
|
|
53
|
+
<div class="mb-8 flex justify-between items-end border-b border-slate-800 pb-6">
|
|
54
|
+
<div>
|
|
55
|
+
<h2 class="text-xl font-semibold text-slate-100 flex items-center gap-2">
|
|
56
|
+
<svg class="w-5 h-5 text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 002-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
|
|
57
|
+
本地项目
|
|
58
|
+
</h2>
|
|
59
|
+
<p class="text-sm text-slate-500 mt-1">管理和启动您的 Vibe Coding 开发环境</p>
|
|
60
|
+
</div>
|
|
61
|
+
<button id="new-project-btn" class="group relative px-5 py-2.5 bg-cyan-500 hover:bg-cyan-400 text-slate-900 font-bold rounded-xl shadow-[0_0_15px_rgba(34,211,238,0.3)] hover:shadow-[0_0_25px_rgba(34,211,238,0.5)] transition-all duration-300 flex items-center gap-2 overflow-hidden">
|
|
62
|
+
<div class="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300 ease-out"></div>
|
|
63
|
+
<svg class="w-5 h-5 relative z-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
|
64
|
+
<span class="relative z-10">新建项目</span>
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Project List -->
|
|
69
|
+
<div id="project-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
70
|
+
<!-- Projects will be injected here -->
|
|
71
|
+
<div class="col-span-full glass-panel rounded-2xl p-12 text-center text-slate-500 flex flex-col items-center justify-center">
|
|
72
|
+
<svg class="w-12 h-12 mb-4 text-slate-600 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"></path></svg>
|
|
73
|
+
<p>初始化环境列表...</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- New Project Modal -->
|
|
80
|
+
<div id="modal" class="fixed inset-0 bg-[#0B0F19]/80 backdrop-blur-sm hidden items-center justify-center z-50 transition-opacity">
|
|
81
|
+
<div class="glass-panel rounded-2xl shadow-2xl w-full max-w-md p-8 transform transition-all scale-100 border border-slate-700/50 relative overflow-hidden">
|
|
82
|
+
<!-- Modal glow effect -->
|
|
83
|
+
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-1 bg-gradient-to-r from-transparent via-cyan-500 to-transparent opacity-50"></div>
|
|
84
|
+
|
|
85
|
+
<div class="flex justify-between items-center mb-6">
|
|
86
|
+
<h3 class="text-xl font-bold text-slate-100 flex items-center gap-2">
|
|
87
|
+
<svg class="w-5 h-5 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
|
88
|
+
初始化核心项目
|
|
89
|
+
</h3>
|
|
90
|
+
<button id="close-modal-btn" class="text-slate-500 hover:text-slate-300 transition-colors p-1 rounded-lg hover:bg-slate-800">
|
|
91
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<form id="new-project-form" class="space-y-5">
|
|
96
|
+
<div>
|
|
97
|
+
<label class="block text-xs font-medium text-slate-400 mb-1.5 uppercase tracking-wider">项目名称</label>
|
|
98
|
+
<input type="text" id="project-name" required class="w-full px-4 py-2.5 bg-slate-900/50 border border-slate-700 rounded-xl focus:outline-none focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 text-slate-200 placeholder-slate-600 transition-all" placeholder="例如:nexus-ui">
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div>
|
|
102
|
+
<label class="block text-xs font-medium text-slate-400 mb-1.5 uppercase tracking-wider">基础模板</label>
|
|
103
|
+
<div class="relative">
|
|
104
|
+
<select id="project-template" class="w-full px-4 py-2.5 bg-slate-900/50 border border-slate-700 rounded-xl focus:outline-none focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 text-slate-200 appearance-none transition-all">
|
|
105
|
+
<!-- Templates will be injected here -->
|
|
106
|
+
</select>
|
|
107
|
+
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-slate-400">
|
|
108
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div class="pt-6 flex justify-end space-x-3 border-t border-slate-800/50 mt-2">
|
|
114
|
+
<button type="button" id="cancel-btn" class="px-5 py-2.5 text-slate-400 hover:text-slate-200 hover:bg-slate-800 rounded-xl transition-colors font-medium">取消</button>
|
|
115
|
+
<button type="submit" class="px-5 py-2.5 bg-cyan-500 hover:bg-cyan-400 text-slate-900 font-bold rounded-xl shadow-[0_0_15px_rgba(34,211,238,0.2)] hover:shadow-[0_0_20px_rgba(34,211,238,0.4)] transition-all duration-200">创建项目</button>
|
|
116
|
+
</div>
|
|
117
|
+
</form>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<script src="app.js"></script>
|
|
122
|
+
</body>
|
|
123
|
+
</html>
|
package/server/api.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { exec } = require('child_process');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
|
|
8
|
+
// Config file path
|
|
9
|
+
const CONFIG_FILE = path.join(os.homedir(), '.trae-starter-config.json');
|
|
10
|
+
|
|
11
|
+
// Store current workspace in memory for simplicity (or could persist to a config file)
|
|
12
|
+
let currentWorkspace = process.cwd();
|
|
13
|
+
|
|
14
|
+
// Load workspace from config if exists
|
|
15
|
+
try {
|
|
16
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
17
|
+
const config = fs.readJsonSync(CONFIG_FILE);
|
|
18
|
+
if (config.workspace && fs.existsSync(config.workspace)) {
|
|
19
|
+
currentWorkspace = config.workspace;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error('Failed to load config:', err);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Helper to save config
|
|
27
|
+
async function saveConfig(workspace) {
|
|
28
|
+
try {
|
|
29
|
+
await fs.writeJson(CONFIG_FILE, { workspace });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error('Failed to save config:', err);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Get current workspace
|
|
36
|
+
router.get('/workspace', (req, res) => {
|
|
37
|
+
res.json({ workspace: currentWorkspace });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Set workspace
|
|
41
|
+
router.post('/workspace', async (req, res) => {
|
|
42
|
+
const { workspace } = req.body;
|
|
43
|
+
if (!workspace) {
|
|
44
|
+
return res.status(400).json({ error: 'Workspace path is required' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const exists = await fs.pathExists(workspace);
|
|
49
|
+
if (!exists) {
|
|
50
|
+
return res.status(404).json({ error: 'Directory does not exist' });
|
|
51
|
+
}
|
|
52
|
+
currentWorkspace = workspace;
|
|
53
|
+
await saveConfig(workspace);
|
|
54
|
+
res.json({ workspace: currentWorkspace });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
res.status(500).json({ error: error.message });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Choose directory (macOS only)
|
|
61
|
+
router.post('/choose-directory', (req, res) => {
|
|
62
|
+
const command = `osascript -e 'POSIX path of (choose folder with prompt "Select a workspace folder")'`;
|
|
63
|
+
|
|
64
|
+
exec(command, (error, stdout, stderr) => {
|
|
65
|
+
if (error) {
|
|
66
|
+
// User cancelled or other error
|
|
67
|
+
return res.status(400).json({ cancelled: true });
|
|
68
|
+
}
|
|
69
|
+
const selectedPath = stdout.trim();
|
|
70
|
+
if (selectedPath) {
|
|
71
|
+
res.json({ path: selectedPath });
|
|
72
|
+
} else {
|
|
73
|
+
res.status(400).json({ cancelled: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// List projects in workspace
|
|
79
|
+
router.get('/projects', async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const items = await fs.readdir(currentWorkspace, { withFileTypes: true });
|
|
82
|
+
const projects = [];
|
|
83
|
+
|
|
84
|
+
for (const item of items) {
|
|
85
|
+
if (item.isDirectory()) {
|
|
86
|
+
const projectPath = path.join(currentWorkspace, item.name);
|
|
87
|
+
// Simple check if it's a project (e.g., has package.json or just is a dir)
|
|
88
|
+
// For prototype projects, we'll assume any directory is a potential project
|
|
89
|
+
// Or check for specific marker if needed. Let's just list directories.
|
|
90
|
+
projects.push({
|
|
91
|
+
name: item.name,
|
|
92
|
+
path: projectPath,
|
|
93
|
+
modified: (await fs.stat(projectPath)).mtime
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Sort by modified time desc
|
|
99
|
+
projects.sort((a, b) => b.modified - a.modified);
|
|
100
|
+
|
|
101
|
+
res.json({ projects });
|
|
102
|
+
} catch (error) {
|
|
103
|
+
res.status(500).json({ error: error.message });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Create new project
|
|
108
|
+
router.post('/projects', async (req, res) => {
|
|
109
|
+
const { name, template } = req.body;
|
|
110
|
+
|
|
111
|
+
if (!name) {
|
|
112
|
+
return res.status(400).json({ error: 'Project name is required' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const targetPath = path.join(currentWorkspace, name);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (await fs.pathExists(targetPath)) {
|
|
119
|
+
return res.status(400).json({ error: 'Project already exists' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Use template if provided
|
|
123
|
+
if (template) {
|
|
124
|
+
const templatePath = path.join(__dirname, '../templates', template);
|
|
125
|
+
if (await fs.pathExists(templatePath)) {
|
|
126
|
+
await fs.copy(templatePath, targetPath);
|
|
127
|
+
} else {
|
|
128
|
+
return res.status(400).json({ error: 'Template not found' });
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
await fs.ensureDir(targetPath);
|
|
132
|
+
// Create a basic index.html if no template
|
|
133
|
+
await fs.writeFile(path.join(targetPath, 'index.html'), '<h1>Hello Trae!</h1>');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
res.json({ success: true, path: targetPath });
|
|
137
|
+
} catch (error) {
|
|
138
|
+
res.status(500).json({ error: error.message });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Open project in Trae
|
|
143
|
+
router.post('/open', (req, res) => {
|
|
144
|
+
const { projectPath } = req.body;
|
|
145
|
+
|
|
146
|
+
if (!projectPath) {
|
|
147
|
+
return res.status(400).json({ error: 'Project path is required' });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Use the trae-cn:// protocol
|
|
151
|
+
// Format: trae-cn://file/<path>?windowId=_blank
|
|
152
|
+
// Note: <path> should start with / if it's absolute, so it becomes trae-cn://file//path/to/project?windowId=_blank
|
|
153
|
+
|
|
154
|
+
const protocolUrl = `trae-cn://file/${projectPath.startsWith('/') ? '' : '/'}${projectPath}?windowId=_blank`;
|
|
155
|
+
const command = `open "${protocolUrl}"`;
|
|
156
|
+
|
|
157
|
+
exec(command, (error, stdout, stderr) => {
|
|
158
|
+
if (error) {
|
|
159
|
+
console.error(`exec error: ${error}`);
|
|
160
|
+
return res.status(500).json({ error: 'Failed to open Trae protocol URL.' });
|
|
161
|
+
}
|
|
162
|
+
res.json({ success: true });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Get available templates
|
|
167
|
+
router.get('/templates', async (req, res) => {
|
|
168
|
+
const templatesDir = path.join(__dirname, '../templates');
|
|
169
|
+
try {
|
|
170
|
+
await fs.ensureDir(templatesDir);
|
|
171
|
+
const items = await fs.readdir(templatesDir, { withFileTypes: true });
|
|
172
|
+
const templates = items
|
|
173
|
+
.filter(item => item.isDirectory())
|
|
174
|
+
.map(item => item.name);
|
|
175
|
+
res.json({ templates });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
res.status(500).json({ error: error.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
module.exports = router;
|