diff --git a/.gitignore b/.gitignore index dd10ebc50..559e12e40 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ app-update.yml yarn-error.log /test .vscode/settings.json -src/main/lang/index.ts /data package-lock.json +src/helper-go/rsrc_windows_amd64.syso +src/helper-go/rsrc_windows_386.syso +src/helper-go/go.sum diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..3729de5ac --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,322 @@ +# FlyEnv - AI Agent Development Guide + +## Project Overview + +**FlyEnv** is an All-In-One Full-Stack Environment Management Tool built with Electron and Vue 3. It provides a lightweight, modular development environment manager for Windows, macOS, and Linux, allowing developers to install and manage Apache, PHP, Node.js, Python, databases, and more—running natively without Docker. + +- **Version**: 4.13.2 +- **Electron Version**: 35.7.5 +- **License**: MIT +- **Author**: Pengfei Xu +- **Repository**: https://github.com/xpf0000/FlyEnv + +## Technology Stack + +### Core Technologies +- **Frontend Framework**: Vue 3 (Composition API) +- **Desktop Framework**: Electron 35.7.5 +- **Build Tool**: Vite 6.x + esbuild 0.25.x +- **Language**: TypeScript 5.8.x +- **State Management**: Pinia 3.x +- **UI Component Library**: Element Plus 2.11.x +- **Styling**: Tailwind CSS 3.4.x + SCSS +- **Internationalization**: Vue I18n 11.x + +### Additional Libraries +- **Code Editor**: Monaco Editor +- **Terminal**: node-pty + xterm.js +- **HTTP Client**: Axios +- **Process Management**: child_process, node-pty +- **File Operations**: fs-extra +- **Markdown Processing**: markdown-it + Shiki + +## Project Architecture + +FlyEnv follows Electron's multi-process architecture with three main components: + +### 1. Main Process (`/src/main`) +Acts as a command relay station between the renderer and fork processes. + +**Key Files**: +- `index.ts` - Entry point, initializes Launcher +- `Launcher.ts` - Application bootstrap, single-instance lock, event handling +- `Application.ts` - Main application controller, window management, IPC handling + +**Core Managers**: +- `WindowManager` - Browser window creation and management +- `TrayManager` - System tray functionality +- `MenuManager` - Application menus +- `ConfigManager` - Configuration persistence +- `ForkManager` - Forked process management +- `NodePTY` - Terminal PTY handling + +### 2. Forked Asynchronous Process (`/src/fork`) +Executes all heavy commands asynchronously to prevent main thread blocking. + +**Key Files**: +- `index.ts` - Fork process entry point +- `BaseManager.ts` - Command dispatcher +- `module/Base/index.ts` - Base class for all service modules + +**Module Structure** (`/src/fork/module/`): +Each service has its own module (e.g., `Nginx/`, `Php/`, `Mysql/`). A typical module extends the `Base` class and implements: +- `_startServer(version)` - Start the service +- `_stopService(version)` - Stop the service +- `fetchAllOnlineVersion()` - Fetch available versions +- `installSoft()` - Download and install + +### 3. Renderer Process (`/src/render`) +Vue 3-based UI application. + +**Key Files**: +- `main.ts` - Renderer entry point +- `App.vue` - Root component +- `core/type.ts` - Module type definitions + +**Directory Structure**: +- `components/` - Vue components organized by module +- `store/` - Pinia stores +- `util/` - Utility functions +- `style/` - Global styles (SCSS) + +~~### 4. Helper Process (`/src/helper`) +Background helper process for privileged operations.~~ + +Deprecated. Only the Go version is used. + +### 5. Go Helper (`/src/helper-go/`) +Go-based helper binary for platform-specific operations (Windows admin tasks). + +### 6. Shared Code (`/src/shared`) +Shared utilities between all processes: +- `ForkPromise.ts` - Promise with progress callbacks +- `Process.ts` - Process management utilities +- `child-process.ts` - Child process helpers +- `utils.ts` - Platform detection utilities + +### 7. Internationalization (`/src/lang`) +Supports 25+ languages with JSON-based translation files. + +**Supported Languages**: +Arabic, Azerbaijani, Bengali, Czech, Danish, German, Greek, English, Spanish, Finnish, French, Indonesian, Italian, Japanese, Dutch, Norwegian, Polish, Portuguese, Portuguese (Brazil), Romanian, Russian, Swedish, Turkish, Ukrainian, Vietnamese, Chinese. + +## Build System + +### Build Scripts (package.json) + +```bash +# Development +yarn dev # Start development server with hot reload +yarn clean:dev # Clean dist folder +yarn build-dev-runner # Build dev runner script + +# Production Build +yarn build # Build for production (platform-specific) +yarn clean # Clean node-pty build +yarn postinstall # Install Electron app dependencies +``` + +### Build Process Flow + +1. **Development** (`yarn dev`): + - Cleans dist folder + - Builds dev-runner.ts → `electron/dev-runner.mjs` + - Starts Vite dev server + - Watches main/ and fork/ directories for changes + - Restarts Electron on file changes + +2. **Production** (`yarn build`): + - Cleans dist folder and node-pty build + - Builds main process with esbuild + - Builds fork process with esbuild + - Builds renderer with Vite + - Packages with electron-builder + +### esbuild Configuration (`/configs/esbuild.config.ts`) + +| Target | Entry | Output | Minify | +|--------|-------|--------|--------| +| dev | src/main/index.dev.ts | dist/electron/main.mjs | false | +| dist | src/main/index.ts | dist/electron/main.mjs | true | +| devFork | src/fork/index.ts | dist/electron/fork.mjs | false | +| distFork | src/fork/index.ts | dist/electron/fork.mjs | true | +| devHelper | src/helper/index.ts | dist/helper/helper.js | true | +| distHelper | src/helper/index.ts | dist/helper/helper.js | true | + +### Vite Configuration (`/configs/vite.config.ts`) + +**Entry Points**: +- `main` - Main application window +- `tray` - Tray popup window +- `capturer` - Screen capture window + +**Path Aliases**: +- `@` → `src/render/` +- `@shared` → `src/shared/` +- `@lang` → `src/lang/` + +## Code Style Guidelines + +### ESLint Configuration +- Uses flat config format (`eslint.config.mjs`) +- TypeScript ESLint recommended rules +- Vue 3 recommended rules +- Prettier integration for formatting + +### Key Rules +- `@typescript-eslint/no-explicit-any`: error (disabled in practice) +- `vue/multi-word-component-names`: off +- `vue/block-lang`: requires TypeScript in Vue SFCs +- `prettier/prettier`: error (formatting issues treated as errors) + +### Styling +- **Primary**: Tailwind CSS for utility-first styling +- **Secondary**: SCSS for complex styles +- **Dark Mode**: CSS selector-based (`darkMode: 'selector'`) + +### Code Conventions +- Use TypeScript for all new code +- Vue SFCs must use ` + diff --git a/src/render/components/CloudflareTunnel/Logs.vue b/src/render/components/CloudflareTunnel/Logs.vue new file mode 100644 index 000000000..dbfcccb96 --- /dev/null +++ b/src/render/components/CloudflareTunnel/Logs.vue @@ -0,0 +1,54 @@ + + diff --git a/src/render/components/CloudflareTunnel/add.vue b/src/render/components/CloudflareTunnel/add.vue index 17379c94a..61351529e 100644 --- a/src/render/components/CloudflareTunnel/add.vue +++ b/src/render/components/CloudflareTunnel/add.vue @@ -4,6 +4,8 @@ :title="'Cloudflare Tunnel' + ' ' + I18nT('base.add')" class="el-dialog-content-flex-1 h-[75%] dark:bg-[#1d2033]" width="600px" + :close-on-click-modal="false" + :close-on-press-escape="false" @closed="closedFn" > @@ -51,10 +53,14 @@ @@ -73,6 +79,7 @@ import { reactiveBind, uuid } from '@/util/Index' import CloudflareTunnelStore from '@/core/CloudflareTunnel/CloudflareTunnelStore' import { BrewStore } from '@/store/brew' + import { MessageError } from '@/util/Element' const brewStore = BrewStore() @@ -80,6 +87,8 @@ const formRef = ref() + const loading = ref(false) + const zones = ref([]) const form = ref({ @@ -189,7 +198,11 @@ return '' } const domain = `${form.value.subdomain}.${form.value.zoneName}` - const all = CloudflareTunnelStore.items.map((item) => `${item.subdomain}.${item.zoneName}`) + const all = CloudflareTunnelStore.items + .map((item) => { + return item.dns.map((d) => `${d.subdomain}.${d.zoneName}`) + }) + .flat() if (all.includes(domain)) { return I18nT('host.CloudflareTunnel.OnlineDomainExistsTips') } @@ -201,11 +214,39 @@ } const doSubmit = async () => { - const item = reactiveBind(new CloudflareTunnel(form.value)) + if (loading.value) { + return + } + loading.value = true + const obj: any = { + apiToken: form.value.apiToken, + cloudflaredBin: form.value.cloudflaredBin, + accountId: form.value.accountId, + + dns: [ + { + id: uuid(), + subdomain: form.value.subdomain, + localService: form.value.localService, + zoneId: form.value.zoneId, + zoneName: form.value.zoneName + } + ] + } + const item = reactiveBind(new CloudflareTunnel(obj)) item.id = uuid() - CloudflareTunnelStore.items.unshift(item) - CloudflareTunnelStore.save() - onCancel() + item + .fetchTunnel() + .then(() => { + CloudflareTunnelStore.items.unshift(item) + CloudflareTunnelStore.save() + loading.value = false + onCancel() + }) + .catch((error) => { + MessageError(I18nT('host.CloudflareTunnel.TunnelInitFailTips', { error })) + loading.value = false + }) } defineExpose({ diff --git a/src/render/components/CloudflareTunnel/addDNS.vue b/src/render/components/CloudflareTunnel/addDNS.vue new file mode 100644 index 000000000..eedde1a11 --- /dev/null +++ b/src/render/components/CloudflareTunnel/addDNS.vue @@ -0,0 +1,223 @@ + + + diff --git a/src/render/components/CloudflareTunnel/edit.vue b/src/render/components/CloudflareTunnel/edit.vue index c6c98e449..b6b86de85 100644 --- a/src/render/components/CloudflareTunnel/edit.vue +++ b/src/render/components/CloudflareTunnel/edit.vue @@ -2,12 +2,16 @@ + + + + - - - - - - - - - - - - - - - - - - - - -
- -
.
- -
-
- - - -
diff --git a/src/render/components/CloudflareTunnel/setup.ts b/src/render/components/CloudflareTunnel/setup.ts index 9aa1050da..87f04944d 100644 --- a/src/render/components/CloudflareTunnel/setup.ts +++ b/src/render/components/CloudflareTunnel/setup.ts @@ -3,7 +3,12 @@ import { computed, reactive } from 'vue' import CloudflareTunnelStore from '@/core/CloudflareTunnel/CloudflareTunnelStore' import { AppStore } from '@/store/app' import { AsyncComponentShow } from '@/util/AsyncComponent' -import type { ZoneType } from '@/core/CloudflareTunnel/type' +import { CloudflareTunnelDnsRecord, ZoneType } from '@/core/CloudflareTunnel/type' +import Base from '@/core/Base' +import { I18nT } from '@lang/index' +import { clipboard, shell } from '@/util/NodeFn' +import { MessageError, MessageSuccess } from '@/util/Element' +import { SetupStore } from '@/components/Setup/store' export const ZoneDict: Record = reactive({}) @@ -20,6 +25,10 @@ export const Setup = () => { }) function add() { + if (isLocked.value) { + MessageError(I18nT('host.CloudflareTunnel.licenseTips')) + return + } AsyncComponentShow(AddVM).then() } @@ -30,17 +39,40 @@ export const Setup = () => { function edit(item: CloudflareTunnel) { AsyncComponentShow(EditVM, { - item + item: JSON.parse(JSON.stringify(item)) }).then() } - function info(item: CloudflareTunnel) {} + let LogVM: any + import('./Logs.vue').then((res) => { + LogVM = res.default + }) - function del(item: CloudflareTunnel, index: number) {} + function log(item: CloudflareTunnel) { + AsyncComponentShow(LogVM, { + item: JSON.parse(JSON.stringify(item)) + }).then() + } - const openOutUrl = (item: CloudflareTunnel) => {} + function del(item: CloudflareTunnel, index: number) { + Base._Confirm(I18nT('base.areYouSure'), undefined, { + customClass: 'confirm-del', + type: 'warning' + }).then(() => { + CloudflareTunnelStore.items.splice(index, 1) + CloudflareTunnelStore.save() + }) + } + + const openOutUrl = (item: CloudflareTunnelDnsRecord) => { + const url = `http://${item.subdomain}.${item.zoneName}` + shell.openExternal(url).catch() + } - const openLocalUrl = (item: CloudflareTunnel) => {} + const openLocalUrl = (item: CloudflareTunnelDnsRecord) => { + const url = `${item?.protocol || 'http'}://${item.localService}` + shell.openExternal(url).catch() + } const groupTrunOn = (item: CloudflareTunnel) => { const dict = JSON.parse(JSON.stringify(appStore.phpGroupStart)) @@ -55,14 +87,75 @@ export const Setup = () => { appStore.saveConfig().then().catch() } + const copy = (str: string) => { + clipboard.writeText(str).then(() => { + MessageSuccess(I18nT('base.copySuccess')) + }) + } + + let EditDNSVM: any + import('./editDNS.vue').then((res) => { + EditDNSVM = res.default + }) + + const editDNS = (item: CloudflareTunnel, dns: CloudflareTunnelDnsRecord, index: number) => { + AsyncComponentShow(EditDNSVM, { + item: JSON.parse(JSON.stringify(item)), + dns: JSON.parse(JSON.stringify(dns)), + index + }).then() + } + + const delDNS = (item: CloudflareTunnel, dns: CloudflareTunnelDnsRecord, index: number) => { + Base._Confirm(I18nT('base.areYouSure'), undefined, { + customClass: 'confirm-del', + type: 'warning' + }).then(() => { + const find = CloudflareTunnelStore.items.find((i) => i.id === item.id) + if (find) { + find.dns.splice(index, 1) + } + CloudflareTunnelStore.save() + }) + } + + let AddDNSVM: any + import('./addDNS.vue').then((res) => { + AddDNSVM = res.default + }) + + function addDNS(item: CloudflareTunnel) { + if (isLocked.value && item.dns.length > 0) { + MessageError(I18nT('host.CloudflareTunnel.licenseTips')) + return + } + AsyncComponentShow(AddDNSVM, { + item: JSON.parse(JSON.stringify(item)) + }).then() + } + + const setupStore = SetupStore() + const isLocked = computed(() => { + if (setupStore.isActive) { + return false + } + + return CloudflareTunnelStore.items.length > 0 + }) + return { add, edit, - info, del, list, openOutUrl, openLocalUrl, - groupTrunOn + groupTrunOn, + copy, + editDNS, + delDNS, + addDNS, + log, + isLocked } } diff --git a/src/render/components/GoLang/CreateProject.vue b/src/render/components/GoLang/CreateProject.vue new file mode 100644 index 000000000..09440aa8e --- /dev/null +++ b/src/render/components/GoLang/CreateProject.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/render/components/GoLang/Index.vue b/src/render/components/GoLang/Index.vue index 4f8b00bcc..0877c496f 100644 --- a/src/render/components/GoLang/Index.vue +++ b/src/render/components/GoLang/Index.vue @@ -19,7 +19,8 @@ url="https://go.dev/dl/" :has-static="true" > - + +