# 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 ### Home (ไธป้กต) ้กต้ขๆ•ฐๆฎ The store may provide a special `home` directory under `{arch}` for a localized homepage. Two JSON files are expected: - `homelinks.json` โ€” ็”จไบŽๆž„ๅปบ้ฆ–้กต็š„่ฝฎๆ’ญๆˆ–้“พๆŽฅๅ—ใ€‚ๆฏไธชๆก็›ฎ็คบไพ‹: ```json { "name": "ไบคๆตๅ้ฆˆ", "more": "ๅ‰ๅพ€่ฎบๅ›ไบคๆต่ฎจ่ฎบ", "imgUrl": "/home/links/bbs.png", "type": "_blank", "url": "https://bbs.spark-app.store/" } ``` - `homelist.json` โ€” ๆ่ฟฐ่‹ฅๅนฒๆŽจ่ๅบ”็”จๅˆ—่กจ๏ผŒๆฏ้กนๅผ•็”จไธ€ไธช JSON ๅˆ—่กจ๏ผˆ`jsonUrl`๏ผ‰: ```json [ { "name":"่ฃ…ๆœบๅฟ…ๅค‡", "type":"appList", "jsonUrl":"/home/lists/NecessaryforInstallation.json" } ] ``` Parsing rules used by the app: - Resolve `imgUrl` by prefixing: `${APM_STORE_BASE_URL}/{arch}${imgUrl}`. - `type: _blank` โ†’ ไฝฟ็”จ็ณป็ปŸๆต่งˆๅ™จๆ‰“ๅผ€้“พๆŽฅ๏ผ›`type: _self` โ†’ ๅœจๅฝ“ๅ‰้กต้ขๆ‰“ๅผ€ใ€‚ - For `homelist.json` entries with `type: "appList"`, fetch the referenced `jsonUrl` and map each item to the app shape used by the UI: - `Name` โ†’ `name` - `Pkgname` โ†’ `pkgname` - `Category` โ†’ `category` - `More` โ†’ `more` Where to implement: - Renderer: `src/App.vue` loads and normalizes `homelinks.json` and `homelist.json` on selecting the `home` category and exposes data to a new `HomeView` component. - Component: `src/components/HomeView.vue` renders link cards and recommended app sections (re-uses `AppCard.vue`). Notes: - The `home` directory path is: `/{arch}/home/` under the configured `APM_STORE_BASE_URL`. - Missing or partially invalid files are handled gracefully โ€” individual failures don't block showing other home sections. ``` ### 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-store': { target: 'https://erotica.spark-app.store', changeOrigin: true, rewrite: (path) => path.replace(/^\/local_amd64-store/, ''), } } } ``` --- ## ๐ŸŽฏ Deep Link Protocol (SPK URI Scheme) **URL Scheme:** `spk://` ### Supported SPK URI Format Format: `spk://search/{pkgname}` **Examples:** - `spk://search/code` - Search for and open "code" app - `spk://search/steam` - Search for and open "steam" app - `spk://search/store.spark-app.hmcl` - Search for and open "HMCL" game ### Implementation Pattern ```typescript // electron/main/deeplink.ts - Parse command line and route export function handleCommandLine(commandLine: string[]) { const deeplinkUrl = commandLine.find((arg) => arg.startsWith('spk://') ); if (!deeplinkUrl) return; try { const url = new URL(deeplinkUrl); const action = url.hostname; // 'search' if (action === 'search') { // Format: spk://search/pkgname // url.pathname will be '/pkgname' const pkgname = url.pathname.split('/').filter(Boolean)[0]; if (pkgname) { listeners.emit('search', { pkgname }); } } } catch (error) { logger.error({ err: error }, 'Error parsing SPK URI'); } } // src/App.vue - Handle in renderer window.ipcRenderer.on( 'deep-link-search', (_event: IpcRendererEvent, data: { pkgname: string }) => { // Trigger search with the pkgname searchQuery.value = data.pkgname; } ); ``` --- ## ๐Ÿ›ก๏ธ 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` - **Avoid used of eslint-disable directly** - use `undefined` instead if you really do not know its type. - **Prefer Composition API** - `