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.
Files changed (83) hide show
  1. package/README.md +45 -0
  2. package/bin/cli.js +33 -0
  3. package/package.json +25 -0
  4. package/public/app.js +263 -0
  5. package/public/index.html +123 -0
  6. package/server/api.js +181 -0
  7. package/server/index.js +38 -0
  8. package/templates/proto/.agents/skills/ui-ux-pro-max/SKILL.md +659 -0
  9. package/templates/proto/.agents/skills/ui-ux-pro-max/data/_sync_all.py +414 -0
  10. package/templates/proto/.agents/skills/ui-ux-pro-max/data/app-interface.csv +31 -0
  11. package/templates/proto/.agents/skills/ui-ux-pro-max/data/charts.csv +26 -0
  12. package/templates/proto/.agents/skills/ui-ux-pro-max/data/colors.csv +162 -0
  13. package/templates/proto/.agents/skills/ui-ux-pro-max/data/design.csv +1776 -0
  14. package/templates/proto/.agents/skills/ui-ux-pro-max/data/draft.csv +1779 -0
  15. package/templates/proto/.agents/skills/ui-ux-pro-max/data/google-fonts.csv +1924 -0
  16. package/templates/proto/.agents/skills/ui-ux-pro-max/data/icons.csv +106 -0
  17. package/templates/proto/.agents/skills/ui-ux-pro-max/data/landing.csv +35 -0
  18. package/templates/proto/.agents/skills/ui-ux-pro-max/data/products.csv +162 -0
  19. package/templates/proto/.agents/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  20. package/templates/proto/.agents/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  21. package/templates/proto/.agents/skills/ui-ux-pro-max/data/styles.csv +85 -0
  22. package/templates/proto/.agents/skills/ui-ux-pro-max/data/typography.csv +74 -0
  23. package/templates/proto/.agents/skills/ui-ux-pro-max/data/ui-reasoning.csv +162 -0
  24. package/templates/proto/.agents/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  25. package/templates/proto/.agents/skills/ui-ux-pro-max/scripts/core.py +247 -0
  26. package/templates/proto/.agents/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  27. package/templates/proto/.agents/skills/ui-ux-pro-max/scripts/search.py +114 -0
  28. package/templates/proto/.trae/skills/preview-manager/SKILL.md +37 -0
  29. package/templates/proto/.trae/skills/ui-ux-pro-max/SKILL.md +659 -0
  30. package/templates/proto/.trae/skills/ui-ux-pro-max/data/_sync_all.py +414 -0
  31. package/templates/proto/.trae/skills/ui-ux-pro-max/data/app-interface.csv +31 -0
  32. package/templates/proto/.trae/skills/ui-ux-pro-max/data/charts.csv +26 -0
  33. package/templates/proto/.trae/skills/ui-ux-pro-max/data/colors.csv +162 -0
  34. package/templates/proto/.trae/skills/ui-ux-pro-max/data/design.csv +1776 -0
  35. package/templates/proto/.trae/skills/ui-ux-pro-max/data/draft.csv +1779 -0
  36. package/templates/proto/.trae/skills/ui-ux-pro-max/data/google-fonts.csv +1924 -0
  37. package/templates/proto/.trae/skills/ui-ux-pro-max/data/icons.csv +106 -0
  38. package/templates/proto/.trae/skills/ui-ux-pro-max/data/landing.csv +35 -0
  39. package/templates/proto/.trae/skills/ui-ux-pro-max/data/products.csv +162 -0
  40. package/templates/proto/.trae/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  41. package/templates/proto/.trae/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  42. package/templates/proto/.trae/skills/ui-ux-pro-max/data/styles.csv +85 -0
  43. package/templates/proto/.trae/skills/ui-ux-pro-max/data/typography.csv +74 -0
  44. package/templates/proto/.trae/skills/ui-ux-pro-max/data/ui-reasoning.csv +162 -0
  45. package/templates/proto/.trae/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  46. package/templates/proto/.trae/skills/ui-ux-pro-max/scripts/core.py +247 -0
  47. package/templates/proto/.trae/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  48. package/templates/proto/.trae/skills/ui-ux-pro-max/scripts/search.py +114 -0
  49. package/templates/proto/README.md +77 -0
  50. package/templates/proto/agent.md +361 -0
  51. package/templates/proto/eslint.config.js +23 -0
  52. package/templates/proto/index.html +13 -0
  53. package/templates/proto/package.json +39 -0
  54. package/templates/proto/pnpm-lock.yaml +2401 -0
  55. package/templates/proto/postcss.config.js +6 -0
  56. package/templates/proto/public/favicon.svg +1 -0
  57. package/templates/proto/public/icons.svg +24 -0
  58. package/templates/proto/skills-lock.json +10 -0
  59. package/templates/proto/src/App.css +184 -0
  60. package/templates/proto/src/App.tsx +15 -0
  61. package/templates/proto/src/assets/hero.png +0 -0
  62. package/templates/proto/src/assets/react.svg +1 -0
  63. package/templates/proto/src/assets/vite.svg +1 -0
  64. package/templates/proto/src/components/ui/SampleButton/sampleButton.css +4 -0
  65. package/templates/proto/src/components/ui/SampleButton/sampleButton.tsx +14 -0
  66. package/templates/proto/src/data/mock/products.csv +4 -0
  67. package/templates/proto/src/data/mock/users.json +16 -0
  68. package/templates/proto/src/data/types.ts +17 -0
  69. package/templates/proto/src/index.css +64 -0
  70. package/templates/proto/src/main.tsx +13 -0
  71. package/templates/proto/src/pages/Demo/components/DemoHeader/demoHeader.tsx +20 -0
  72. package/templates/proto/src/pages/Demo/components/FeatureGrid/featureGrid.tsx +18 -0
  73. package/templates/proto/src/pages/Demo/index.tsx +29 -0
  74. package/templates/proto/src/pages/Home/components/FeatureCard/featureCard.tsx +15 -0
  75. package/templates/proto/src/pages/Home/components/HeroSection/heroSection.tsx +28 -0
  76. package/templates/proto/src/pages/Home/components/InteractiveControls/interactiveControls.tsx +27 -0
  77. package/templates/proto/src/pages/Home/index.tsx +39 -0
  78. package/templates/proto/src/utils/format.ts +7 -0
  79. package/templates/proto/tailwind.config.js +11 -0
  80. package/templates/proto/tsconfig.app.json +28 -0
  81. package/templates/proto/tsconfig.json +7 -0
  82. package/templates/proto/tsconfig.node.json +26 -0
  83. 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;