diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..b9467de7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,827 @@ +# AI Coding Guidance for APM App Store + +**Repository:** elysia-best/apm-app-store +**Project Type:** Electron + Vue 3 + Vite Desktop Application +**Purpose:** Desktop app store client for APM (AmberPM) package manager +**License:** MulanPSL-2.0 + +--- + +If you are an AI coding agent working on this repo, make sure to follow the guidelines below: + +## ๐Ÿ—๏ธ Project Architecture Overview + +### Technology Stack +- **Frontend Framework:** Vue 3 with Composition API (` +``` + +### Props and Events Pattern + +```typescript +// Props definition +const props = defineProps<{ + app: App | null; + show: boolean; +}>(); + +// Emits definition +const emit = defineEmits<{ + close: []; + install: []; + remove: []; +}>(); + +// Usage +emit('install'); +``` + +### IPC Event Listeners in Vue + +**Always use in `onMounted` for proper cleanup:** + +```typescript +onMounted(() => { + window.ipcRenderer.on('install-complete', (_event: IpcRendererEvent, result: DownloadResult) => { + // Handle event + }); + + window.ipcRenderer.on('install-log', (_event: IpcRendererEvent, log: InstallLog) => { + // Handle log + }); +}); +``` + +--- + +## ๐Ÿ”ง Main Process Patterns + +### Spawning APM Commands + +```typescript +import { spawn } from 'node:child_process'; + +// Check for privilege escalation +const superUserCmd = await checkSuperUserCommand(); +const execCommand = superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH; +const execParams = superUserCmd.length > 0 + ? [SHELL_CALLER_PATH, 'apm', 'install', '-y', pkgname] + : ['apm', 'install', '-y', pkgname]; + +// Spawn process +const child = spawn(execCommand, execParams, { + shell: true, + env: process.env, +}); + +// Stream output +child.stdout.on('data', (data) => { + webContents.send('install-log', { id, time: Date.now(), message: data.toString() }); +}); + +// Handle completion +child.on('close', (code) => { + const success = code === 0; + webContents.send('install-complete', { id, success, exitCode: code, /* ... */ }); +}); +``` + +### Parsing APM Output + +**APM outputs are text-based with specific formats:** + +```typescript +// Installed packages format: "pkgname/repo,version arch [flags]" +// Example: "code/stable,1.108.2 amd64 [installed]" +const parseInstalledList = (output: string) => { + const apps: InstalledAppInfo[] = []; + const lines = output.split('\n'); + for (const line of lines) { + const match = line.trim().match(/^(\S+)\/\S+,\S+\s+(\S+)\s+(\S+)\s+\[(.+)\]$/); + if (match) { + apps.push({ + pkgname: match[1], + version: match[2], + arch: match[3], + flags: match[4], + raw: line.trim(), + }); + } + } + return apps; +}; +``` + +--- + +## ๐ŸŒ API Integration + +### Base Configuration + +```typescript +// src/global/storeConfig.ts +export const APM_STORE_BASE_URL = 'https://erotica.spark-app.store'; + +// URL structure: +// /{arch}/{category}/applist.json - App list +// /{arch}/{category}/{pkgname}/icon.png - App icon +// /{arch}/{category}/{pkgname}/screen_N.png - Screenshots (1-5) +// /{arch}/categories.json - Categories mapping +``` + +### Axios Usage + +```typescript +const axiosInstance = axios.create({ + baseURL: APM_STORE_BASE_URL, + timeout: 1000, // Note: Very short timeout! +}); + +// Loading apps by category +const response = await axiosInstance.get( + `/${window.apm_store.arch}/${category}/applist.json` +); +``` + +**Development Proxy (vite.config.ts):** +```typescript +server: { + proxy: { + '/local_amd64-apm': { + target: 'https://erotica.spark-app.store', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/local_amd64-apm/, ''), + } + } +} +``` + +--- + +## ๐ŸŽฏ Deep Link Protocol + +**URL Scheme:** `apmstore://` + +### Supported Actions + +1. **Install App:** `apmstore://install?pkg=` +2. **Show Updates:** `apmstore://update` +3. **Show Installed:** `apmstore://installed` + +### Implementation Pattern + +```typescript +// electron/main/deeplink.ts - Parse command line +export function handleCommandLine(argv: string[]) { + const deeplinkUrl = argv.find((arg) => arg.startsWith('apmstore://')); + if (!deeplinkUrl) return; + + const url = new URL(deeplinkUrl); + if (url.hostname === 'install') { + const pkg = url.searchParams.get('pkg'); + sendToRenderer('deep-link-install', pkg); + } +} + +// src/App.vue - Handle in renderer +window.ipcRenderer.on('deep-link-install', (_event, pkgname: string) => { + const target = apps.value.find((a) => a.pkgname === pkgname); + if (target) openDetail(target); +}); +``` + +--- + +## ๐Ÿ›ก๏ธ Security Considerations + +### Privilege Escalation + +**Always check for `pkexec` availability:** + +```typescript +const checkSuperUserCommand = async (): Promise => { + if (process.getuid && process.getuid() !== 0) { + const { stdout } = await execAsync('which /usr/bin/pkexec'); + return stdout.trim().length > 0 ? '/usr/bin/pkexec' : ''; + } + return ''; +}; +``` + +### Context Isolation + +**Current Status:** Context isolation is **enabled** (default Electron behavior). + +**IPC Exposed via Preload (Safe):** +```typescript +// electron/preload/index.ts +contextBridge.exposeInMainWorld('ipcRenderer', { + on: (...args) => ipcRenderer.on(...args), + send: (...args) => ipcRenderer.send(...args), + invoke: (...args) => ipcRenderer.invoke(...args), +}); +``` + +**โš ๏ธ Do NOT enable nodeIntegration or disable contextIsolation!** + +--- + +## ๐ŸŽจ UI/UX Patterns + +### Tailwind CSS Usage + +**Dark Mode Support:** +```vue +
+ +
+``` + +**Theme Toggle:** +```typescript +const isDarkTheme = ref(false); + +watch(isDarkTheme, (newVal) => { + localStorage.setItem('theme', newVal ? 'dark' : 'light'); + document.documentElement.classList.toggle('dark', newVal); +}); +``` + +### Modal Pattern + +```vue + +``` + +### Loading States + +```typescript +const loading = ref(true); + +// In template +
Loading...
+
{{ apps.length }} apps
+``` + +--- + +## ๐Ÿงช Testing & Quality + +### ESLint Configuration + +```typescript +// eslint.config.ts +export default defineConfig([ + globalIgnores(['**/3rdparty/**', '**/node_modules/**', '**/dist/**', '**/dist-electron/**']), + tseslint.configs.recommended, + pluginVue.configs['flat/essential'], + eslintConfigPrettier, + eslintPluginPrettierRecommended, +]); +``` + +### TypeScript Configuration + +```json +{ + "compilerOptions": { + "strict": true, // Strict mode enabled + "noEmit": true, // No emit (Vite handles build) + "module": "ESNext", + "target": "ESNext", + "jsx": "preserve", // Vue JSX + "resolveJsonModule": true // Import JSON files + } +} +``` + +### Code Quality Commands + +```bash +npm run lint # Run ESLint +npm run lint:fix # Auto-fix issues +npm run format # Format with Prettier +``` + +--- + +## ๐Ÿš€ Build & Development + +### Development Mode + +```bash +npm run dev # Start dev server (Vite + Electron) +``` + +**Dev Server:** `http://127.0.0.1:3344/` (port from package.json) + +### Production Build + +```bash +npm run build # Build all (deb + rpm) +npm run build:deb # Build Debian package only +npm run build:rpm # Build RPM package only +``` + +**Build Output:** +- `dist-electron/` - Compiled Electron code +- `dist/` - Compiled renderer assets +- Packaged app in project root + +### Build Configuration + +**electron-builder.yml:** +- App ID: `cn.eu.org.simplelinux.apmstore` +- Linux targets: deb, rpm +- Includes extras/ directory in resources +- Auto-update disabled (Linux package manager handles updates) + +--- + +## ๐Ÿ“ฆ Important Files to Understand + +### 1. electron/main/backend/install-manager.ts +**Purpose:** Core package management logic +**Key Responsibilities:** +- Task queue management +- APM command spawning +- Progress reporting +- Installed/upgradable list parsing + +**Critical Functions:** +- `processNextInQueue()` - Task processor +- `parseInstalledList()` - Parse APM output +- `checkSuperUserCommand()` - Privilege escalation + +### 2. src/App.vue +**Purpose:** Root component +**Key Responsibilities:** +- App state management +- Category/app loading +- Modal orchestration +- Deep link handling + +### 3. src/global/downloadStatus.ts +**Purpose:** Download queue state +**Key Features:** +- Reactive download list +- Download item CRUD operations +- Change watchers for UI updates + +### 4. electron/preload/index.ts +**Purpose:** Renderer-Main bridge +**Key Features:** +- IPC API exposure +- Architecture detection +- Loading animation + +### 5. vite.config.ts +**Purpose:** Build configuration +**Key Features:** +- Electron plugin setup +- Dev server proxy +- Tailwind integration + +--- + +## ๐Ÿ› Common Pitfalls & Solutions + +### 1. Duplicate Task Handling + +**Problem:** User clicks install multiple times +**Solution:** +```typescript +if (tasks.has(id) && !download.retry) { + logger.warn('Task already exists, ignoring duplicate'); + return; +} +``` + +### 2. Window Close Behavior + +**Problem:** Closing window while tasks are running +**Solution:** +```typescript +win.on('close', (event) => { + event.preventDefault(); + if (tasks.size > 0) { + win.hide(); // Hide instead of closing + win.setSkipTaskbar(true); + } else { + win.destroy(); // Allow close if no tasks + } +}); +``` + +### 3. App Data Normalization + +**Problem:** API returns PascalCase, app uses camelCase +**Solution:** +```typescript +const normalizedApp: App = { + name: appJson.Name, + pkgname: appJson.Pkgname, + version: appJson.Version, + // ... map all fields +}; +``` + +### 4. Screenshot Loading + +**Problem:** Not all apps have 5 screenshots +**Solution:** +```typescript +for (let i = 1; i <= 5; i++) { + const img = new Image(); + img.src = screenshotUrl; + img.onload = () => screenshots.value.push(screenshotUrl); + // No onerror handler - silently skip missing images +} +``` + +--- + +## ๐Ÿ“š Logging Best Practices + +### Pino Logger Usage + +```typescript +import pino from 'pino'; +const logger = pino({ name: 'module-name' }); + +// Levels: trace, debug, info, warn, error, fatal +logger.info('Application started'); +logger.error({ err }, 'Failed to load apps'); +logger.warn(`Package ${pkgname} not found`); +``` + +### Log Locations + +**Development:** Console with `pino-pretty` +**Production:** Structured JSON to stdout + +--- + +## ๐Ÿ”„ State Management + +### Global State (src/global/storeConfig.ts) + +```typescript +export const currentApp = ref(null); +export const currentAppIsInstalled = ref(false); +``` + +**Usage Pattern:** +```typescript +import { currentApp, currentAppIsInstalled } from '@/global/storeConfig'; + +// Set current app +currentApp.value = selectedApp; + +// Check installation status +window.ipcRenderer.invoke('check-installed', app.pkgname) + .then((isInstalled: boolean) => { + currentAppIsInstalled.value = isInstalled; + }); +``` + +### Download Queue (src/global/downloadStatus.ts) + +```typescript +export const downloads = ref([]); + +// Add download +downloads.value.push(newDownload); + +// Remove download +export const removeDownloadItem = (pkgname: string) => { + const index = downloads.value.findIndex(d => d.pkgname === pkgname); + if (index !== -1) downloads.value.splice(index, 1); +}; + +// Watch changes +export const watchDownloadsChange = (callback: () => void) => { + watch(downloads, callback, { deep: true }); +}; +``` + +--- + +## ๐ŸŽฏ Contribution Guidelines + +### When Adding New Features + +1. **Add TypeScript types first** (src/global/typedefinition.ts) +2. **Update IPC handlers** if main-renderer communication needed +3. **Follow existing component patterns** (props, emits, setup) +4. **Test with actual APM commands** (don't mock in development) +5. **Update README TODO list** when completing tasks + +### Code Style + +- **Use TypeScript strict mode** - no `any` types without `eslint-disable` +- **Prefer Composition API** - `