# 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`
- **Avoid used of eslint-disable directly** - use `undefined` instead if you really do not know its type.
- **Prefer Composition API** - `